a0bdd2b5b4
Replace all corruption variants with HTML entities across 226 view files: - 3-char UTF-8-as-Win1252 sequences (ae-corruption) - Standalone smart/curly quotes that break C# Razor expressions - Partially re-corrupted variants where the 3rd byte was normalised to ASCII tools/Fix-Encoding.ps1: re-runnable sweep; uses [char] code points so the script itself never contains a literal non-ASCII character; supports -DryRun .githooks/pre-commit: blocks commits containing the ae-corruption byte signature (xc3xa2xe2x82xac); git core.hooksPath = .githooks so the hook is repo-committed and active for all future work on this machine. Build clean; 225 unit tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
355 lines
19 KiB
Plaintext
355 lines
19 KiB
Plaintext
@using PowderCoating.Web.Controllers
|
|
@model List<CompanyExportSummary>
|
|
@{
|
|
ViewData["Title"] = "Data Export";
|
|
}
|
|
|
|
@section Styles {
|
|
<style>
|
|
[data-bs-theme="dark"] .table-light th,
|
|
[data-bs-theme="dark"] .table-light td {
|
|
background-color: var(--bs-secondary-bg);
|
|
color: var(--bs-body-color);
|
|
}
|
|
[data-bs-theme="dark"] .card {
|
|
border-color: var(--bs-border-color) !important;
|
|
}
|
|
[data-bs-theme="dark"] .card-header {
|
|
border-color: var(--bs-border-color) !important;
|
|
}
|
|
[data-bs-theme="dark"] .alert-light {
|
|
background-color: var(--bs-secondary-bg);
|
|
border-color: var(--bs-border-color);
|
|
color: var(--bs-body-color);
|
|
}
|
|
[data-bs-theme="dark"] .border-top {
|
|
border-color: var(--bs-border-color) !important;
|
|
}
|
|
</style>
|
|
}
|
|
|
|
<div class="container-fluid py-3">
|
|
<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>
|
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
|
<div>
|
|
<h4 class="mb-0"><i class="bi bi-file-earmark-arrow-down me-2 text-primary"></i>Data Export</h4>
|
|
<small class="text-muted">Export company data to Excel for offboarding, audits, GDPR requests, or migration</small>
|
|
</div>
|
|
</div>
|
|
|
|
@if (TempData["Error"] != null)
|
|
{
|
|
<div class="alert alert-danger alert-permanent mb-3"><i class="bi bi-exclamation-triangle me-2"></i>@TempData["Error"]</div>
|
|
}
|
|
|
|
<div class="row g-3">
|
|
@* Left: company list *@
|
|
<div class="col-lg-7">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-transparent border-bottom py-2">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<h6 class="mb-0">Select a Company</h6>
|
|
<input type="text" id="companySearch" class="form-control form-control-sm w-auto"
|
|
placeholder="Search…" style="min-width:180px" />
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive" style="max-height:520px;overflow-y:auto">
|
|
<table class="table table-hover table-sm align-middle mb-0 small">
|
|
<thead class="table-light sticky-top">
|
|
<tr>
|
|
<th>Company</th>
|
|
<th style="width:80px">Plan</th>
|
|
<th style="width:70px">Status</th>
|
|
<th style="width:100px">Created</th>
|
|
<th style="width:60px"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="companyTable">
|
|
@foreach (var c in Model)
|
|
{
|
|
<tr class="company-row" data-name="@c.CompanyName.ToLower()">
|
|
<td>
|
|
<div class="fw-medium">@c.CompanyName</div>
|
|
</td>
|
|
<td class="text-muted">@c.Plan</td>
|
|
<td>
|
|
@if (c.IsActive)
|
|
{
|
|
<span class="badge bg-success-subtle text-success">Active</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="badge bg-secondary">Inactive</span>
|
|
}
|
|
</td>
|
|
<td class="text-muted">@c.CreatedAt.ToString("MM/dd/yyyy")</td>
|
|
<td>
|
|
<button type="button"
|
|
class="btn btn-sm btn-outline-primary py-0 px-2 select-company-btn"
|
|
data-id="@c.Id"
|
|
data-name="@c.CompanyName">
|
|
Select
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Mobile card view — shown on screens < 992px -->
|
|
<div class="mobile-card-view">
|
|
<div class="mobile-card-list">
|
|
@foreach (var c in Model)
|
|
{
|
|
<div class="mobile-data-card" data-name="@c.CompanyName.ToLower()">
|
|
<div class="mobile-card-header">
|
|
<div class="mobile-card-icon bg-primary"><i class="bi bi-building"></i></div>
|
|
<div class="mobile-card-title">
|
|
<h6>@c.CompanyName</h6>
|
|
<small>Created @c.CreatedAt.ToString("MM/dd/yyyy")</small>
|
|
</div>
|
|
</div>
|
|
<div class="mobile-card-body">
|
|
<div class="mobile-card-row">
|
|
<span class="mobile-card-label">Plan</span>
|
|
<span class="mobile-card-value">@c.Plan</span>
|
|
</div>
|
|
<div class="mobile-card-row">
|
|
<span class="mobile-card-label">Status</span>
|
|
<span class="mobile-card-value">
|
|
@if (c.IsActive)
|
|
{
|
|
<span class="badge bg-success-subtle text-success">Active</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="badge bg-secondary">Inactive</span>
|
|
}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="mobile-card-footer">
|
|
<button type="button"
|
|
class="btn btn-sm btn-outline-primary select-company-btn"
|
|
data-id="@c.Id"
|
|
data-name="@c.CompanyName">
|
|
Select for Export
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@* Right: export options — always visible *@
|
|
<div class="col-lg-5">
|
|
<div class="card border-0 shadow-sm" style="position:sticky;top:1rem">
|
|
<div class="card-header bg-primary text-white py-2">
|
|
<h6 class="mb-0"><i class="bi bi-file-earmark-spreadsheet me-2"></i>Export Options</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
|
|
<!-- Company selection banner — shown/hidden by JS -->
|
|
<div id="noCompanyBanner" class="alert alert-light alert-permanent border d-flex align-items-center gap-2 mb-3 small">
|
|
<i class="bi bi-arrow-left-circle fs-5 text-muted"></i>
|
|
<span>Select a company from the list to begin.</span>
|
|
</div>
|
|
<div id="selectedBanner" class="alert alert-info alert-permanent py-2 mb-3 small" style="display:none">
|
|
<i class="bi bi-building me-1"></i>
|
|
Exporting: <strong id="selectedCompanyName">—</strong>
|
|
</div>
|
|
|
|
<form method="post" asp-action="Export" id="exportForm">
|
|
@Html.AntiForgeryToken()
|
|
<input type="hidden" name="companyId" id="companyIdInput" value="0" />
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label fw-semibold">Data to export</label>
|
|
|
|
<div class="form-check mb-1">
|
|
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
|
|
value="Customers" id="chkCustomers" checked />
|
|
<label class="form-check-label" for="chkCustomers">
|
|
<i class="bi bi-people me-1 text-muted"></i>Customers
|
|
</label>
|
|
</div>
|
|
<div class="form-check mb-1">
|
|
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
|
|
value="Jobs" id="chkJobs" checked />
|
|
<label class="form-check-label" for="chkJobs">
|
|
<i class="bi bi-briefcase me-1 text-muted"></i>Jobs
|
|
</label>
|
|
</div>
|
|
<div class="form-check mb-1">
|
|
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
|
|
value="Quotes" id="chkQuotes" checked />
|
|
<label class="form-check-label" for="chkQuotes">
|
|
<i class="bi bi-file-earmark-text me-1 text-muted"></i>Quotes
|
|
</label>
|
|
</div>
|
|
<div class="form-check mb-1">
|
|
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
|
|
value="Inventory" id="chkInventory" />
|
|
<label class="form-check-label" for="chkInventory">
|
|
<i class="bi bi-boxes me-1 text-muted"></i>Inventory
|
|
</label>
|
|
</div>
|
|
<div class="form-check mb-1">
|
|
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
|
|
value="Equipment" id="chkEquipment" />
|
|
<label class="form-check-label" for="chkEquipment">
|
|
<i class="bi bi-tools me-1 text-muted"></i>Equipment
|
|
</label>
|
|
</div>
|
|
<div class="form-check mb-1">
|
|
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
|
|
value="Vendors" id="chkVendors" />
|
|
<label class="form-check-label" for="chkVendors">
|
|
<i class="bi bi-truck me-1 text-muted"></i>Vendors
|
|
</label>
|
|
</div>
|
|
<div class="form-check mb-1">
|
|
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
|
|
value="ShopWorkers" id="chkShopWorkers" />
|
|
<label class="form-check-label" for="chkShopWorkers">
|
|
<i class="bi bi-person-badge me-1 text-muted"></i>Shop Workers
|
|
</label>
|
|
</div>
|
|
<div class="form-check mb-1">
|
|
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
|
|
value="Invoices" id="chkInvoices" />
|
|
<label class="form-check-label" for="chkInvoices">
|
|
<i class="bi bi-receipt me-1 text-muted"></i>Invoices
|
|
</label>
|
|
</div>
|
|
<div class="form-check mb-2">
|
|
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
|
|
value="Users" id="chkUsers" />
|
|
<label class="form-check-label" for="chkUsers">
|
|
<i class="bi bi-person-lock me-1 text-muted"></i>Users
|
|
<span class="badge bg-warning text-dark ms-1" style="font-size:0.65rem">GDPR</span>
|
|
</label>
|
|
</div>
|
|
<div class="border-top pt-2 mt-1">
|
|
<div class="small text-muted mb-1">
|
|
<i class="bi bi-shield-check me-1 text-success"></i>
|
|
The <strong>Users</strong> sheet contains personal data (names, emails, login history).
|
|
Include only when required for a data access request or offboarding.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-check mb-3">
|
|
<input class="form-check-input" type="checkbox" id="selectAllSheets">
|
|
<label class="form-check-label text-muted small" for="selectAllSheets">
|
|
Select / deselect all
|
|
</label>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label fw-semibold">Format</label>
|
|
<div class="d-flex gap-3">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="radio" name="format" id="fmtXlsx" value="xlsx" checked />
|
|
<label class="form-check-label" for="fmtXlsx">
|
|
<i class="bi bi-file-earmark-spreadsheet me-1 text-success"></i>Excel (.xlsx)
|
|
</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="radio" name="format" id="fmtCsv" value="csv" />
|
|
<label class="form-check-label" for="fmtCsv">
|
|
<i class="bi bi-filetype-csv me-1 text-secondary"></i>CSV (.zip)
|
|
<span class="text-muted small">— one file per sheet</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="noCompanyHint" class="alert alert-warning alert-permanent py-2 small mb-2" style="display:none">
|
|
<i class="bi bi-exclamation-triangle me-1"></i>Please select a company from the list before downloading.
|
|
</div>
|
|
<button type="submit" class="btn btn-primary w-100" id="exportBtn">
|
|
<i class="bi bi-file-earmark-arrow-down me-2" id="exportBtnIcon"></i><span id="exportBtnLabel">Download Excel (.xlsx)</span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@section Scripts {
|
|
<script>
|
|
(function () {
|
|
var selectedId = null;
|
|
|
|
// ── Company selection ────────────────────────────────────────────────────
|
|
document.querySelectorAll('.select-company-btn').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
// Reset all buttons
|
|
document.querySelectorAll('.select-company-btn').forEach(function (b) {
|
|
b.textContent = 'Select';
|
|
b.className = 'btn btn-sm btn-outline-primary py-0 px-2 select-company-btn';
|
|
});
|
|
|
|
// Mark this one selected
|
|
btn.textContent = 'Selected';
|
|
btn.className = 'btn btn-sm btn-primary py-0 px-2 select-company-btn';
|
|
|
|
selectedId = btn.getAttribute('data-id');
|
|
var name = btn.getAttribute('data-name');
|
|
|
|
document.getElementById('companyIdInput').value = selectedId;
|
|
document.getElementById('selectedCompanyName').textContent = name;
|
|
document.getElementById('noCompanyBanner').style.display = 'none';
|
|
document.getElementById('selectedBanner').style.display = '';
|
|
document.getElementById('noCompanyHint').style.display = 'none';
|
|
});
|
|
});
|
|
|
|
// ── Company search ────────────────────────────────────────────────────────
|
|
document.getElementById('companySearch').addEventListener('input', function () {
|
|
var q = this.value.toLowerCase();
|
|
document.querySelectorAll('.company-row').forEach(function (row) {
|
|
row.style.display = row.getAttribute('data-name').indexOf(q) !== -1 ? '' : 'none';
|
|
});
|
|
document.querySelectorAll('.mobile-card-list .mobile-data-card').forEach(function (card) {
|
|
card.style.display = card.getAttribute('data-name').indexOf(q) !== -1 ? '' : 'none';
|
|
});
|
|
});
|
|
|
|
// ── Select all sheets ─────────────────────────────────────────────────────
|
|
document.getElementById('selectAllSheets').addEventListener('change', function () {
|
|
var checked = this.checked;
|
|
document.querySelectorAll('.sheet-check').forEach(function (cb) {
|
|
cb.checked = checked;
|
|
});
|
|
});
|
|
|
|
// ── Format toggle — update button label ──────────────────────────────────
|
|
document.querySelectorAll('input[name="format"]').forEach(function (radio) {
|
|
radio.addEventListener('change', function () {
|
|
var isCsv = this.value === 'csv';
|
|
document.getElementById('exportBtnLabel').textContent = isCsv ? 'Download CSV (.zip)' : 'Download Excel (.xlsx)';
|
|
document.getElementById('exportBtnIcon').className = isCsv ? 'bi bi-file-zip me-2' : 'bi bi-file-earmark-arrow-down me-2';
|
|
});
|
|
});
|
|
|
|
// ── Guard: require company before submit ──────────────────────────────────
|
|
document.getElementById('exportForm').addEventListener('submit', function (e) {
|
|
if (!selectedId) {
|
|
e.preventDefault();
|
|
document.getElementById('noCompanyHint').style.display = '';
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
}
|