PR 5 follow-up: boolean radio buttons and seed missing settings

- Replace true/false text display with Yes/No radio button groups for
  boolean platform settings; toggling auto-submits the form so no Edit
  modal is needed for flags
- IsBool() helper detects *Enabled, *AppliesToTrials, *TrialsEnabled keys
- Hide Edit button for boolean settings (radio buttons are the control)
- Add AI group icon (bi-robot) and description to the group header switch
- Add Max key detection to InputType/InputHint for number inputs
- Migration AddMissingPlatformSettings seeds 6 previously missing rows
  (SmsEnabled, TrialsEnabled, GracePeriodDays, GracePeriodAppliesToTrials,
  MaxTenants, AiCatalogPriceCheckEnabled) using IF NOT EXISTS guards

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-12 21:40:16 -04:00
parent fb31fa7eb3
commit cb1b6dceb6
4 changed files with 10730 additions and 16 deletions
@@ -12,23 +12,31 @@
"Subscriptions" => "bi-credit-card",
"Quotes" => "bi-file-earmark-text",
"Data Retention" => "bi-clock-history",
"AI" => "bi-robot",
_ => "bi-globe"
};
static string GroupDescription(string group) => group switch
{
"Notifications" => "Controls where platform event alerts (signups, bug reports, billing events) are sent.",
"Subscriptions" => "Trial and billing defaults applied to new tenant companies at registration.",
"Notifications" => "Controls where platform event alerts (signups, bug reports, billing events) are sent, and whether SMS is active.",
"Subscriptions" => "Trial and billing defaults applied to new tenant companies at registration, including grace periods and tenant limits.",
"Quotes" => "Default validity windows and token expiry for customer-facing quote links.",
"Data Retention" => "How long audit and webhook records are kept before the nightly purge removes them.",
"AI" => "Platform-level feature flags for AI-powered features. These override any company-level settings.",
_ => "Core platform configuration values used across features and email links."
};
static bool IsBool(string key) =>
key.EndsWith("Enabled", StringComparison.OrdinalIgnoreCase) ||
key.EndsWith("AppliesToTrials", StringComparison.OrdinalIgnoreCase) ||
key.EndsWith("TrialsEnabled", StringComparison.OrdinalIgnoreCase);
static string InputType(string key) => key switch
{
var k when k.Contains("Email", StringComparison.OrdinalIgnoreCase) => "email",
var k when k.Contains("Url", StringComparison.OrdinalIgnoreCase) => "url",
var k when k.Contains("Days", StringComparison.OrdinalIgnoreCase) => "number",
var k when k.Contains("Max", StringComparison.OrdinalIgnoreCase) => "number",
_ => "text"
};
@@ -37,6 +45,7 @@
var k when k.Contains("Email", StringComparison.OrdinalIgnoreCase) => "e.g. admin@example.com",
var k when k.Contains("Url", StringComparison.OrdinalIgnoreCase) => "e.g. https://app.yourdomain.com",
var k when k.Contains("Days", StringComparison.OrdinalIgnoreCase) => "Enter a whole number of days",
var k when k.Contains("Max", StringComparison.OrdinalIgnoreCase) => "Enter a whole number, or -1 for unlimited",
_ => ""
};
}
@@ -106,7 +115,27 @@
<div class="setting-meta font-monospace mt-1 text-muted">@s.Key</div>
</td>
<td class="align-middle">
@if (string.IsNullOrWhiteSpace(s.Value))
@if (IsBool(s.Key))
{
var isOn = string.Equals(s.Value, "true", StringComparison.OrdinalIgnoreCase);
<form asp-action="Save" method="post" class="d-inline">
@Html.AntiForgeryToken()
<input type="hidden" name="key" value="@s.Key" />
<div class="btn-group btn-group-sm" role="group">
<input type="radio" class="btn-check" name="value"
id="@(s.Key)_yes" value="true"
@(isOn ? "checked" : "")
onchange="this.form.submit()" />
<label class="btn @(isOn ? "btn-success" : "btn-outline-success")" for="@(s.Key)_yes">Yes</label>
<input type="radio" class="btn-check" name="value"
id="@(s.Key)_no" value="false"
@(!isOn ? "checked" : "")
onchange="this.form.submit()" />
<label class="btn @(!isOn ? "btn-danger" : "btn-outline-danger")" for="@(s.Key)_no">No</label>
</div>
</form>
}
else if (string.IsNullOrWhiteSpace(s.Value))
{
<span class="text-muted fst-italic">Not set</span>
}
@@ -126,16 +155,19 @@
}
</td>
<td class="text-end pe-4 align-middle">
<button class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#editModal"
data-key="@s.Key"
data-label="@(s.Label ?? s.Key)"
data-value="@(s.Value ?? "")"
data-type="@InputType(s.Key)"
data-hint="@InputHint(s.Key)">
<i class="bi bi-pencil me-1"></i>Edit
</button>
@if (!IsBool(s.Key))
{
<button class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#editModal"
data-key="@s.Key"
data-label="@(s.Label ?? s.Key)"
data-value="@(s.Value ?? "")"
data-type="@InputType(s.Key)"
data-hint="@InputHint(s.Key)">
<i class="bi bi-pencil me-1"></i>Edit
</button>
}
</td>
</tr>
}