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:
Generated
+10594
File diff suppressed because it is too large
Load Diff
+88
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace PowderCoating.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMissingPlatformSettings : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Conditional inserts — safe to run against a DB that already has some of these keys set manually.
|
||||
migrationBuilder.Sql(@"
|
||||
IF NOT EXISTS (SELECT 1 FROM PlatformSettings WHERE [Key] = 'SmsEnabled')
|
||||
INSERT INTO PlatformSettings ([Key],[Value],[Label],[Description],[GroupName])
|
||||
VALUES ('SmsEnabled','false','SMS Enabled','Platform-level switch for outbound SMS. When off, no SMS messages are sent regardless of company settings.','Notifications');
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM PlatformSettings WHERE [Key] = 'TrialsEnabled')
|
||||
INSERT INTO PlatformSettings ([Key],[Value],[Label],[Description],[GroupName])
|
||||
VALUES ('TrialsEnabled','true','Trials Enabled','Allow new companies to register with a free trial period. When off, registration requires a paid plan immediately.','Subscriptions');
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM PlatformSettings WHERE [Key] = 'GracePeriodDays')
|
||||
INSERT INTO PlatformSettings ([Key],[Value],[Label],[Description],[GroupName])
|
||||
VALUES ('GracePeriodDays','14','Grace Period (days)','Days after subscription expiry before access is fully cut off. Gives companies time to renew without an abrupt lockout.','Subscriptions');
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM PlatformSettings WHERE [Key] = 'GracePeriodAppliesToTrials')
|
||||
INSERT INTO PlatformSettings ([Key],[Value],[Label],[Description],[GroupName])
|
||||
VALUES ('GracePeriodAppliesToTrials','false','Grace Period Applies to Trials','When enabled, trial companies also receive the grace period after expiry rather than being cut off immediately.','Subscriptions');
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM PlatformSettings WHERE [Key] = 'MaxTenants')
|
||||
INSERT INTO PlatformSettings ([Key],[Value],[Label],[Description],[GroupName])
|
||||
VALUES ('MaxTenants','-1','Max Tenants','Maximum number of active tenant companies allowed on the platform. Set to -1 for no limit.','Subscriptions');
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM PlatformSettings WHERE [Key] = 'AiCatalogPriceCheckEnabled')
|
||||
INSERT INTO PlatformSettings ([Key],[Value],[Label],[Description],[GroupName])
|
||||
VALUES ('AiCatalogPriceCheckEnabled','true','AI Catalog Price Check','Platform-level switch for the AI catalog price review feature. When off, the feature is disabled for all companies regardless of their settings.','AI');
|
||||
");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 1,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 5, 13, 1, 34, 45, 450, DateTimeKind.Utc).AddTicks(8377));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 2,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 5, 13, 1, 34, 45, 450, DateTimeKind.Utc).AddTicks(8383));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 3,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 5, 13, 1, 34, 45, 450, DateTimeKind.Utc).AddTicks(8385));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 1,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 5, 11, 14, 23, 23, 221, DateTimeKind.Utc).AddTicks(5837));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 2,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 5, 11, 14, 23, 23, 221, DateTimeKind.Utc).AddTicks(5846));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 3,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 5, 11, 14, 23, 23, 221, DateTimeKind.Utc).AddTicks(5847));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6574,7 +6574,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
||||
{
|
||||
Id = 1,
|
||||
CompanyId = 0,
|
||||
CreatedAt = new DateTime(2026, 5, 11, 14, 23, 23, 221, DateTimeKind.Utc).AddTicks(5837),
|
||||
CreatedAt = new DateTime(2026, 5, 13, 1, 34, 45, 450, DateTimeKind.Utc).AddTicks(8377),
|
||||
Description = "Standard pricing for regular customers",
|
||||
DiscountPercent = 0m,
|
||||
IsActive = true,
|
||||
@@ -6585,7 +6585,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
||||
{
|
||||
Id = 2,
|
||||
CompanyId = 0,
|
||||
CreatedAt = new DateTime(2026, 5, 11, 14, 23, 23, 221, DateTimeKind.Utc).AddTicks(5846),
|
||||
CreatedAt = new DateTime(2026, 5, 13, 1, 34, 45, 450, DateTimeKind.Utc).AddTicks(8383),
|
||||
Description = "5% discount for preferred customers",
|
||||
DiscountPercent = 5m,
|
||||
IsActive = true,
|
||||
@@ -6596,7 +6596,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
||||
{
|
||||
Id = 3,
|
||||
CompanyId = 0,
|
||||
CreatedAt = new DateTime(2026, 5, 11, 14, 23, 23, 221, DateTimeKind.Utc).AddTicks(5847),
|
||||
CreatedAt = new DateTime(2026, 5, 13, 1, 34, 45, 450, DateTimeKind.Utc).AddTicks(8385),
|
||||
Description = "10% discount for premium customers",
|
||||
DiscountPercent = 10m,
|
||||
IsActive = true,
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user