Files
PowderCoatingLogix/src/PowderCoating.Web/Views/SeedData/Index.cshtml
T
spouliot 637be701ea PR 4: Production guardrails for Seed Data and Storage Migration
- SeedData/Index: added prominent danger banner when running in Production (environment include="Production") so operators are clearly warned before writing to the live database; page remains accessible since seeding is occasionally valid in prod for new company onboarding
- StorageMigration/Index: added warning banner in Production explaining the tool is not needed now that Azure Blob migration is complete
- PlatformAdminController: hide Storage Migration hub card in Production via ShowStorageMigration flag (same WEBSITE_SITE_NAME pattern as ShowRawLogFiles); Seed Data card remains visible so prod onboarding stays reachable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:08:14 -04:00

412 lines
21 KiB
Plaintext

@model List<PowderCoating.Core.Entities.Company>
@{
ViewData["Title"] = "Seed Data Management";
ViewData["PageIcon"] = "bi-database-fill-gear";
}
@section Styles {
<style>
/* Dark mode: modal header with bg-warning */
[data-bs-theme="dark"] .modal-header.bg-warning {
background-color: #664d03 !important;
color: #fff !important;
}
[data-bs-theme="dark"] .modal-header.bg-warning .btn-close {
filter: invert(1);
}
</style>
}
<div class="container-fluid">
<div class="mb-2">
<a asp-controller="PlatformAdmin" asp-action="Maintenance" class="text-muted small text-decoration-none">
<i class="bi bi-arrow-left me-1"></i>Maintenance
</a>
</div>
<environment include="Production">
<div class="alert alert-danger alert-permanent d-flex gap-3 align-items-start mb-4">
<i class="bi bi-exclamation-octagon-fill fs-4 flex-shrink-0 mt-1 text-danger"></i>
<div>
<div class="fw-semibold">You are running in Production</div>
<div class="small mt-1">Seed operations write data directly to the live database. Only proceed if you have a specific, intentional reason — for example, onboarding a new company. Do not seed the default demo company in production.</div>
</div>
</div>
</environment>
@if (TempData["SuccessMessage"] != null)
{
<div class="alert alert-success alert-dismissible alert-permanent fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i>
<strong>Success!</strong> @TempData["SuccessMessage"]
@if (TempData["SeedDetails"] != null)
{
var details = TempData["SeedDetails"].ToString()?.Split('|');
if (details != null && details.Length > 0)
{
<div class="mt-2">
@foreach (var detail in details)
{
<div class="small">@detail</div>
}
</div>
}
}
@if (TempData["ItemsSeeded"] != null)
{
<div class="mt-2">
<span class="badge bg-success">@TempData["ItemsSeeded"] items seeded</span>
</div>
}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
@if (TempData["WarningMessage"] != null)
{
<div class="alert alert-warning alert-dismissible alert-permanent fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>Warning!</strong> @TempData["WarningMessage"]
@if (TempData["SeedWarnings"] != null)
{
var warnings = TempData["SeedWarnings"].ToString()?.Split('|');
if (warnings != null && warnings.Length > 0)
{
<div class="mt-2">
<details>
<summary class="small text-decoration-underline" style="cursor: pointer;">View skipped items</summary>
<div class="mt-2">
@foreach (var warning in warnings)
{
<div class="small">@warning</div>
}
</div>
</details>
</div>
}
}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
@if (TempData["ErrorMessage"] != null)
{
<div class="alert alert-danger alert-dismissible alert-permanent fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>Error!</strong> @TempData["ErrorMessage"]
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
<!-- System Data Card -->
<div class="card mb-4 border-primary">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-gear-fill me-2"></i>System Data</h5>
</div>
<div class="card-body">
<p class="card-text">
Seed system-level data including:
</p>
<ul class="mb-3">
<li><strong>Roles:</strong> SuperAdmin, Administrator, Manager, Employee, ShopFloor, ReadOnly</li>
<li><strong>Default Company:</strong> Demo Company (DEMO)</li>
<li><strong>SuperAdmin User:</strong> artemis@("@")powdercoatinglogix.com</li>
</ul>
<div class="alert alert-info alert-permanent">
<i class="bi bi-info-circle-fill me-2"></i>
<strong>Note:</strong> This operation is idempotent. If data already exists, it will be skipped.
</div>
<form asp-action="SeedSystem" method="post" onsubmit="return confirm('Are you sure you want to seed system data? This will create default roles, companies, and SuperAdmin users.');">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-primary">
<i class="bi bi-database-fill-add me-2"></i>Seed System Data
</button>
</form>
</div>
</div>
<!-- Company Data Card -->
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="bi bi-building me-2"></i>Company Data</h5>
</div>
<div class="card-body">
<p class="card-text">
Seed company-specific data including:
</p>
<ul class="mb-3">
<li><strong>Inventory Items:</strong> 8 powder coating colors + 2 consumables</li>
<li><strong>Operating Costs:</strong> Default labor, equipment, and overhead rates</li>
<li><strong>Demo Users:</strong> Company Admin and Manager (only for Demo Company)</li>
</ul>
<div class="alert alert-info alert-permanent mb-3">
<i class="bi bi-info-circle-fill me-2"></i>
<strong>Note:</strong> Select a company below to seed demo data. Existing data will not be overwritten.
</div>
@if (Model == null || !Model.Any())
{
<div class="alert alert-warning alert-permanent">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
No companies found. Please seed system data first to create the default company.
</div>
}
else
{
<!-- Desktop table -->
<div class="table-responsive d-none d-lg-block">
<table class="table table-hover">
<thead>
<tr>
<th>Company Name</th>
<th>Code</th>
<th>Status</th>
<th>Subscription Plan</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var company in Model)
{
<tr>
<td>
<strong>@company.CompanyName</strong>
@if (company.CompanyCode == "DEMO")
{
<span class="badge bg-info ms-2">Default</span>
}
</td>
<td><code>@company.CompanyCode</code></td>
<td>
@if (company.IsActive)
{
<span class="badge bg-success">Active</span>
}
else
{
<span class="badge bg-secondary">Inactive</span>
}
</td>
<td>@company.SubscriptionPlan</td>
<td class="text-center">
<div class="d-flex gap-2 justify-content-center">
<form asp-action="SeedCompany" method="post" style="display: inline;"
onsubmit="return confirm('Are you sure you want to seed data for @company.CompanyName? Existing data will not be overwritten.');">
@Html.AntiForgeryToken()
<input type="hidden" name="companyId" value="@company.Id" />
<button type="submit" class="btn btn-success btn-sm">
<i class="bi bi-database-fill-add me-1"></i>Seed Data
</button>
</form>
<button type="button" class="btn btn-warning btn-sm"
onclick="openUnseedModal(@company.Id, '@Html.Raw(Html.Encode(company.CompanyName))')">
<i class="bi bi-database-fill-dash me-1"></i>Unseed Data
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
<!-- Mobile card view — shown on screens < 992px -->
<div class="mobile-card-view d-lg-none">
<div class="mobile-card-list">
@foreach (var company in Model)
{
<div class="mobile-data-card">
<div class="mobile-card-header">
<div class="mobile-card-icon bg-success"><i class="bi bi-building"></i></div>
<div class="mobile-card-title">
<h6>
@company.CompanyName
@if (company.CompanyCode == "DEMO")
{
<span class="badge bg-info ms-1">Default</span>
}
</h6>
<small><code>@company.CompanyCode</code></small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value">
@if (company.IsActive)
{
<span class="badge bg-success">Active</span>
}
else
{
<span class="badge bg-secondary">Inactive</span>
}
</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Plan</span>
<span class="mobile-card-value">@company.SubscriptionPlan</span>
</div>
</div>
<div class="mobile-card-footer">
<form asp-action="SeedCompany" method="post" style="display: inline;"
onsubmit="return confirm('Are you sure you want to seed data for @company.CompanyName? Existing data will not be overwritten.');">
@Html.AntiForgeryToken()
<input type="hidden" name="companyId" value="@company.Id" />
<button type="submit" class="btn btn-success btn-sm me-1">
<i class="bi bi-database-fill-add me-1"></i>Seed
</button>
</form>
<button type="button" class="btn btn-warning btn-sm"
onclick="openUnseedModal(@company.Id, '@Html.Raw(Html.Encode(company.CompanyName))')">
<i class="bi bi-database-fill-dash me-1"></i>Unseed
</button>
</div>
</div>
}
</div>
</div>
}
</div>
</div>
<environment exclude="Production">
<!-- Documentation Card -->
<div class="card mt-4 border-info">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-book me-2"></i>Default Credentials</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6 class="fw-bold">SuperAdmin (Platform Access)</h6>
<ul class="list-unstyled">
<li class="mb-2">
<strong>SuperAdmin:</strong><br />
Email: <code>artemis@("@")powdercoatinglogix.com</code><br />
Password: <code>SuperAdmin123!</code>
</li>
</ul>
</div>
<div class="col-md-6">
<h6 class="fw-bold">Demo Company Users</h6>
<ul class="list-unstyled">
<li class="mb-2">
<strong>Company Admin:</strong><br />
Email: <code>demo@("@")powdercoatinglogix.com</code><br />
Password: <code>CompanyAdmin123!</code>
</li>
<li>
<strong>Manager:</strong><br />
Email: <code>manager@("@")demo.com</code><br />
Password: <code>Manager123!</code>
</li>
</ul>
</div>
</div>
</div>
</div>
</environment>
</div>
<!-- Unseed Data Modal -->
<div class="modal fade" id="unseedModal" tabindex="-1" aria-labelledby="unseedModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-warning text-dark">
<h5 class="modal-title" id="unseedModalLabel">
<i class="bi bi-database-fill-dash me-2"></i>Unseed Data
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form asp-action="RemoveSeedData" method="post" id="unseedForm">
@Html.AntiForgeryToken()
<input type="hidden" name="companyId" id="unseedCompanyId" />
<div class="modal-body">
<p class="mb-1">Select the data types to remove for <strong id="unseedCompanyName"></strong>.</p>
<p class="text-muted small mb-3">Only records that match the original seed values will be removed. Any data you have added or modified will not be affected.</p>
<div class="list-group">
<label class="list-group-item list-group-item-action d-flex gap-3">
<input class="form-check-input flex-shrink-0 mt-1" type="checkbox" name="options.Customers" value="true" id="chkCustomers" />
<span>
<strong>Customers</strong>
<span class="d-block text-muted small">Also removes associated jobs, quotes, and their line items (25 seeded customers)</span>
</span>
</label>
<label class="list-group-item list-group-item-action d-flex gap-3">
<input class="form-check-input flex-shrink-0 mt-1" type="checkbox" name="options.InventoryItems" value="true" id="chkInventory" />
<span>
<strong>Inventory Items</strong>
<span class="d-block text-muted small">10 seeded items (8 powder colors, cleaner, masking tape) and their transactions</span>
</span>
</label>
<label class="list-group-item list-group-item-action d-flex gap-3">
<input class="form-check-input flex-shrink-0 mt-1" type="checkbox" name="options.Equipment" value="true" id="chkEquipment" />
<span>
<strong>Equipment</strong>
<span class="d-block text-muted small">10 seeded equipment records and their maintenance history</span>
</span>
</label>
<label class="list-group-item list-group-item-action d-flex gap-3">
<input class="form-check-input flex-shrink-0 mt-1" type="checkbox" name="options.Catalog" value="true" id="chkCatalog" />
<span>
<strong>Catalog Items &amp; Categories</strong>
<span class="d-block text-muted small">7 categories and all their catalog items</span>
</span>
</label>
<label class="list-group-item list-group-item-action d-flex gap-3">
<input class="form-check-input flex-shrink-0 mt-1" type="checkbox" name="options.PricingTiers" value="true" id="chkPricingTiers" />
<span>
<strong>Pricing Tiers</strong>
<span class="d-block text-muted small">Standard, Silver, Gold, Platinum tiers</span>
</span>
</label>
<label class="list-group-item list-group-item-action d-flex gap-3">
<input class="form-check-input flex-shrink-0 mt-1" type="checkbox" name="options.OperatingCosts" value="true" id="chkOperatingCosts" />
<span>
<strong>Operating Costs</strong>
<span class="d-block text-muted small">Default labor, equipment, and overhead rates</span>
</span>
</label>
</div>
<div class="alert alert-warning alert-permanent mt-3 mb-0">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>This action permanently deletes records and cannot be undone.</strong>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-warning" id="unseedSubmitBtn" disabled>
<i class="bi bi-database-fill-dash me-1"></i>Remove Selected Data
</button>
</div>
</form>
</div>
</div>
</div>
@section Scripts {
<script>
function openUnseedModal(companyId, companyName) {
document.getElementById('unseedCompanyId').value = companyId;
document.getElementById('unseedCompanyName').textContent = companyName;
// Reset checkboxes
document.querySelectorAll('#unseedForm input[type="checkbox"]').forEach(cb => cb.checked = false);
document.getElementById('unseedSubmitBtn').disabled = true;
new bootstrap.Modal(document.getElementById('unseedModal')).show();
}
// Enable submit only when at least one checkbox is checked
document.querySelectorAll('#unseedForm input[type="checkbox"]').forEach(cb => {
cb.addEventListener('change', function () {
const anyChecked = [...document.querySelectorAll('#unseedForm input[type="checkbox"]')].some(c => c.checked);
document.getElementById('unseedSubmitBtn').disabled = !anyChecked;
});
});
</script>
}