Initial commit
This commit is contained in:
@@ -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 & 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>
|
||||
Reference in New Issue
Block a user