Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,214 @@
@using PowderCoating.Application.DTOs.Subscription
@model UpdateSubscriptionPlanConfigDto
@{
ViewData["Title"] = $"Edit {ViewBag.PlanName} Plan";
ViewData["PageIcon"] = "bi-pencil-square";
}
<div class="d-flex justify-content-start mb-4">
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i>
</a>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card border-0 shadow-sm">
<div class="card-body">
<form asp-action="Edit" method="post">
@Html.AntiForgeryToken()
<input type="hidden" asp-for="Id" />
<h5 class="mb-3 pb-2 border-bottom">Plan Details</h5>
<div class="mb-3">
<label asp-for="Description" class="form-label">Description</label>
<textarea asp-for="Description" class="form-control" rows="2"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="mb-3">
<div class="form-check">
<input asp-for="IsActive" class="form-check-input" type="checkbox" />
<label asp-for="IsActive" class="form-check-label">Available for new signups &amp; upgrades</label>
</div>
<div class="form-text text-muted">
Uncheck to retire this plan. It will be hidden from registration and upgrade options,
but companies already on this plan will continue to see it in their billing page and
their subscription will not be affected.
</div>
</div>
<h5 class="mb-3 pb-2 border-bottom mt-4">Usage Limits</h5>
<p class="text-muted small">Use <strong>-1</strong> for unlimited.</p>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label asp-for="MaxUsers" class="form-label">Max Users</label>
<input asp-for="MaxUsers" class="form-control" type="number" min="-1" />
<span asp-validation-for="MaxUsers" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="MaxActiveJobs" class="form-label">Max Active Jobs</label>
<input asp-for="MaxActiveJobs" class="form-control" type="number" min="-1" />
<span asp-validation-for="MaxActiveJobs" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="MaxCustomers" class="form-label">Max Customers</label>
<input asp-for="MaxCustomers" class="form-control" type="number" min="-1" />
<span asp-validation-for="MaxCustomers" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="MaxQuotes" class="form-label">Max Quotes Per Month</label>
<input asp-for="MaxQuotes" class="form-control" type="number" min="-1" />
<span asp-validation-for="MaxQuotes" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="MaxCatalogItems" class="form-label">Max Catalog Items</label>
<input asp-for="MaxCatalogItems" class="form-control" type="number" min="-1" />
<span asp-validation-for="MaxCatalogItems" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="MaxJobPhotos" class="form-label">Max Photos Per Job</label>
<input asp-for="MaxJobPhotos" class="form-control" type="number" min="-1" />
<span asp-validation-for="MaxJobPhotos" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="MaxQuotePhotos" class="form-label">Max Photos Per Quote</label>
<input asp-for="MaxQuotePhotos" class="form-control" type="number" min="-1" />
<span asp-validation-for="MaxQuotePhotos" class="text-danger"></span>
<div class="form-text">-1 = unlimited. 0 = disabled for this plan.</div>
</div>
<div class="col-md-4">
<label asp-for="MaxAiPhotoQuotesPerMonth" class="form-label">AI Photo Quotes / Month</label>
<input asp-for="MaxAiPhotoQuotesPerMonth" class="form-control" type="number" min="-1" />
<span asp-validation-for="MaxAiPhotoQuotesPerMonth" class="text-danger"></span>
<div class="form-text">-1 = unlimited. 0 = disabled for this plan.</div>
</div>
</div>
<h5 class="mb-3 pb-2 border-bottom mt-4">Pricing</h5>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label asp-for="MonthlyPrice" class="form-label">Monthly Price ($)</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="MonthlyPrice" class="form-control" type="number" min="0" step="0.01" />
</div>
<span asp-validation-for="MonthlyPrice" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="AnnualPrice" class="form-label">Annual Price ($)</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="AnnualPrice" class="form-control" type="number" min="0" step="0.01" />
</div>
<span asp-validation-for="AnnualPrice" class="text-danger"></span>
</div>
</div>
<h5 class="mb-3 pb-2 border-bottom mt-4">Online Payments</h5>
<div class="mb-4">
<div class="form-check form-switch">
<input asp-for="AllowOnlinePayments" class="form-check-input" type="checkbox" role="switch" />
<label asp-for="AllowOnlinePayments" class="form-check-label fw-medium">Allow Online Payments</label>
</div>
<div class="form-text">
When enabled, companies on this plan can connect Stripe and accept online invoice payments via payment link.
</div>
</div>
<h5 class="mb-3 pb-2 border-bottom mt-4">Accounting</h5>
<div class="mb-4">
<div class="form-check form-switch">
<input asp-for="AllowAccounting" class="form-check-input" type="checkbox" role="switch" />
<label asp-for="AllowAccounting" class="form-check-label fw-medium">Allow Accounting Features</label>
</div>
<div class="form-text">
When enabled, companies on this plan can access Chart of Accounts, Bills, Expenses, and Accounting Export.
</div>
</div>
<h5 class="mb-3 pb-2 border-bottom mt-4">AI Features</h5>
<div class="mb-3">
<div class="form-check form-switch">
<input asp-for="AllowAiPhotoQuotes" class="form-check-input" type="checkbox" role="switch" />
<label asp-for="AllowAiPhotoQuotes" class="form-check-label fw-medium">Allow AI Photo Quotes</label>
</div>
<div class="form-text">
When enabled, companies on this plan can use AI photo-based quote analysis (subject to the monthly quota above).
</div>
</div>
<div class="mb-4">
<div class="form-check form-switch">
<input asp-for="AllowAiInventoryAssist" class="form-check-input" type="checkbox" role="switch" />
<label asp-for="AllowAiInventoryAssist" class="form-check-label fw-medium">Allow AI Inventory Assist</label>
</div>
<div class="form-text">
When enabled, companies on this plan can use AI-powered product lookup when creating or editing inventory items.
</div>
</div>
<h5 class="mb-3 pb-2 border-bottom mt-4">Stripe Integration</h5>
<div class="alert alert-info small mb-3" role="alert">
<i class="bi bi-info-circle me-2"></i>
<strong>Where to find price IDs:</strong>
In your <a href="https://dashboard.stripe.com/products" target="_blank" class="alert-link">Stripe Dashboard</a>,
open the product, then look in the <strong>Pricing</strong> section for the specific price.
The ID starts with <code>price_</code> — <em>not</em> the product ID which starts with <code>prod_</code>.
</div>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label asp-for="StripePriceIdMonthly" class="form-label">Stripe Price ID (Monthly)</label>
<input asp-for="StripePriceIdMonthly" class="form-control font-monospace" placeholder="price_..." />
@{
var monthlyVal = Model.StripePriceIdMonthly;
if (!string.IsNullOrWhiteSpace(monthlyVal) && !monthlyVal.StartsWith("price_"))
{
<div class="text-danger small mt-1">
<i class="bi bi-exclamation-triangle me-1"></i>
This looks like a product ID (<code>@monthlyVal[..Math.Min(12, monthlyVal.Length)]...</code>). Price IDs must start with <code>price_</code>.
</div>
}
}
<span asp-validation-for="StripePriceIdMonthly" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="StripePriceIdAnnual" class="form-label">Stripe Price ID (Annual)</label>
<input asp-for="StripePriceIdAnnual" class="form-control font-monospace" placeholder="price_..." />
@{
var annualVal = Model.StripePriceIdAnnual;
if (!string.IsNullOrWhiteSpace(annualVal) && !annualVal.StartsWith("price_"))
{
<div class="text-danger small mt-1">
<i class="bi bi-exclamation-triangle me-1"></i>
This looks like a product ID (<code>@annualVal[..Math.Min(12, annualVal.Length)]...</code>). Price IDs must start with <code>price_</code>.
</div>
}
}
<span asp-validation-for="StripePriceIdAnnual" class="text-danger"></span>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg me-1"></i>Save Changes
</button>
<a asp-action="Index" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
@@ -0,0 +1,185 @@
@using PowderCoating.Application.DTOs.Subscription
@model IEnumerable<SubscriptionPlanConfigDto>
@{
ViewData["Title"] = "Subscription Plan Configuration";
ViewData["PageIcon"] = "bi-layers";
// Badge color by position in the sorted list (SortOrder already applied in controller)
var planList = Model.ToList();
string PlanBadge(int index) => index switch {
0 => "bg-secondary",
1 => "bg-primary",
2 => "bg-info",
_ => "bg-success"
};
}
@section Styles {
<style>
/* Dark mode: table-light section headers inside plan cards */
[data-bs-theme="dark"] .card .table .table-light td {
--bs-table-bg: var(--bs-tertiary-bg);
color: var(--bs-secondary-color);
}
/* Dark mode: card headers */
[data-bs-theme="dark"] .card .card-header {
background-color: var(--bs-tertiary-bg);
border-color: var(--bs-border-color);
}
</style>
}
<div class="mb-4"></div>
@if (TempData["Success"] != null)
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle me-2"></i>@TempData["Success"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
<div class="row g-4">
@for (int i = 0; i < planList.Count; i++)
{
var plan = planList[i];
<div class="col-xl-3 col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span class="badge @PlanBadge(i) fs-6">@plan.DisplayName</span>
@if (!plan.IsActive)
{
<span class="badge bg-danger">Inactive</span>
}
</div>
<div class="card-body">
@if (!string.IsNullOrEmpty(plan.Description))
{
<p class="text-muted small mb-3">@plan.Description</p>
}
<table class="table table-sm">
<tbody>
<tr class="table-light">
<td colspan="2" class="fw-semibold small text-uppercase text-muted py-1">Limits</td>
</tr>
<tr>
<td class="text-muted">Users</td>
<td class="fw-semibold">@(plan.MaxUsers == -1 ? "Unlimited" : plan.MaxUsers.ToString())</td>
</tr>
<tr>
<td class="text-muted">Active Jobs</td>
<td class="fw-semibold">@(plan.MaxActiveJobs == -1 ? "Unlimited" : plan.MaxActiveJobs.ToString())</td>
</tr>
<tr>
<td class="text-muted">Customers</td>
<td class="fw-semibold">@(plan.MaxCustomers == -1 ? "Unlimited" : plan.MaxCustomers.ToString())</td>
</tr>
<tr>
<td class="text-muted">Quotes / Month</td>
<td class="fw-semibold">@(plan.MaxQuotes == -1 ? "Unlimited" : plan.MaxQuotes.ToString())</td>
</tr>
<tr>
<td class="text-muted">Catalog Items</td>
<td class="fw-semibold">@(plan.MaxCatalogItems == -1 ? "Unlimited" : plan.MaxCatalogItems.ToString())</td>
</tr>
<tr>
<td class="text-muted">Photos / Job</td>
<td class="fw-semibold">@(plan.MaxJobPhotos == -1 ? "Unlimited" : plan.MaxJobPhotos == 0 ? "None" : plan.MaxJobPhotos.ToString())</td>
</tr>
<tr>
<td class="text-muted">Photos / Quote</td>
<td class="fw-semibold">@(plan.MaxQuotePhotos == -1 ? "Unlimited" : plan.MaxQuotePhotos == 0 ? "None" : plan.MaxQuotePhotos.ToString())</td>
</tr>
<tr>
<td class="text-muted">AI Photo Quotes / Month</td>
<td class="fw-semibold">@(plan.MaxAiPhotoQuotesPerMonth == -1 ? "Unlimited" : plan.MaxAiPhotoQuotesPerMonth == 0 ? "Disabled" : plan.MaxAiPhotoQuotesPerMonth.ToString())</td>
</tr>
<tr class="table-light">
<td colspan="2" class="fw-semibold small text-uppercase text-muted py-1">Pricing</td>
</tr>
<tr>
<td class="text-muted">Monthly</td>
<td class="fw-semibold">$@plan.MonthlyPrice.ToString("F2")</td>
</tr>
<tr>
<td class="text-muted">Annual</td>
<td class="fw-semibold">$@plan.AnnualPrice.ToString("F2")</td>
</tr>
<tr>
<td class="text-muted">Online Payments</td>
<td>
@if (plan.AllowOnlinePayments)
{
<span class="badge bg-success">Enabled</span>
}
else
{
<span class="badge bg-secondary">Disabled</span>
}
</td>
</tr>
<tr>
<td class="text-muted">Accounting</td>
<td>
@if (plan.AllowAccounting)
{
<span class="badge bg-success">Enabled</span>
}
else
{
<span class="badge bg-secondary">Disabled</span>
}
</td>
</tr>
<tr>
<td class="text-muted">AI Photo Quotes</td>
<td>
@if (plan.AllowAiPhotoQuotes)
{
<span class="badge bg-success">Enabled</span>
}
else
{
<span class="badge bg-secondary">Disabled</span>
}
</td>
</tr>
<tr>
<td class="text-muted">AI Inventory Assist</td>
<td>
@if (plan.AllowAiInventoryAssist)
{
<span class="badge bg-success">Enabled</span>
}
else
{
<span class="badge bg-secondary">Disabled</span>
}
</td>
</tr>
<tr class="table-light">
<td colspan="2" class="fw-semibold small text-uppercase text-muted py-1">Stripe</td>
</tr>
<tr>
<td class="text-muted">Monthly ID</td>
<td class="font-monospace small text-truncate" style="max-width: 120px;" title="@plan.StripePriceIdMonthly">
@(string.IsNullOrEmpty(plan.StripePriceIdMonthly) ? "—" : plan.StripePriceIdMonthly)
</td>
</tr>
<tr>
<td class="text-muted">Annual ID</td>
<td class="font-monospace small text-truncate" style="max-width: 120px;" title="@plan.StripePriceIdAnnual">
@(string.IsNullOrEmpty(plan.StripePriceIdAnnual) ? "—" : plan.StripePriceIdAnnual)
</td>
</tr>
</tbody>
</table>
</div>
<div class="card-footer bg-transparent">
<a asp-action="Edit" asp-route-id="@plan.Id" class="btn btn-outline-primary btn-sm w-100">
<i class="bi bi-pencil me-1"></i>Edit Configuration
</a>
</div>
</div>
</div>
}
</div>