Restore all zeroed views + add bulk gift certificate creation

The HTML entity sweep script had a bug where it wrote empty files for any
view that contained no target Unicode characters, zeroing out 215 view files.
All views restored from the pre-sweep commit (cefdf3e).

Bulk gift certificate feature:
- BulkCreateGiftCertificateDto with Quantity (1-500), Amount, Reason, Expiry, Notes
- GenerateBulkGiftCertificatePdfAsync on IPdfService / PdfService: one Letter page
  per cert, reusing the same purple/gold branded ComposeGiftCertificateContent helper
- GiftCertificatesController: BulkCreate GET/POST, BulkResult GET, BulkDownloadPdf POST
- Views: BulkCreate.cshtml (form with live total preview), BulkResult.cshtml (table +
  Download All PDF button that POSTs cert IDs to avoid URL length limits)
- gift-certificate-bulk.js: live preview + spinner/disable on submit
- Index.cshtml: Bulk Create button added alongside New Certificate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-14 20:09:22 -04:00
parent 3eda91f170
commit 4ec55e7290
240 changed files with 73116 additions and 0 deletions
@@ -0,0 +1,227 @@
@model PowderCoating.Application.DTOs.Vendor.CreateVendorDto
@{
ViewData["Title"] = "Add New Vendor";
ViewData["PageIcon"] = "bi-truck";
ViewData["PageHelpTitle"] = "Add New Vendor";
ViewData["PageHelpContent"] = "Add a supplier you purchase materials from. Mark as Preferred to prioritize them in purchase order pickers. Set Default Expense Account and Payment Terms to pre-fill bills created from this vendor's POs. Account Number is your customer account number at the vendor — useful on purchase orders.";
}
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="d-flex justify-content-end align-items-center mb-4">
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>Back to List
</a>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<form asp-action="Create" method="post">
<partial name="_ValidationSummary" />
<!-- Company Information Section -->
<div class="mb-4">
<div class="d-flex align-items-center gap-2 border-bottom pb-2 mb-3">
<h5 class="mb-0">
<i class="bi bi-truck me-2 text-primary"></i>Vendor Information
</h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Vendor Information"
data-bs-content="Vendor Name is required — use the official company name as it appears on invoices. Preferred Vendor marks this supplier as a go-to source; preferred vendors are listed first in purchase order dropdowns.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="row g-3">
<div class="col-md-8">
<label asp-for="CompanyName" class="form-label">Vendor Name</label>
<input asp-for="CompanyName" class="form-control" placeholder="Enter vendor name" />
<span asp-validation-for="CompanyName" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="IsPreferred" class="form-label">Status</label>
<div class="form-check form-switch mt-2">
<input asp-for="IsPreferred" class="form-check-input" type="checkbox" />
<label asp-for="IsPreferred" class="form-check-label">Preferred Vendor</label>
</div>
</div>
</div>
</div>
<!-- Contact Information Section -->
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">
<i class="bi bi-person me-2 text-primary"></i>Contact Information
</h5>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="ContactName" class="form-label">Contact Name</label>
<input asp-for="ContactName" class="form-control" placeholder="Enter contact name" />
<span asp-validation-for="ContactName" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Email" class="form-label">Email</label>
<input asp-for="Email" type="email" class="form-control" placeholder="name@example.com" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Phone" class="form-label">Phone</label>
<input asp-for="Phone" type="tel" class="form-control" placeholder="(555) 123-4567" />
<span asp-validation-for="Phone" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Website" class="form-label">Website</label>
<input asp-for="Website" type="url" class="form-control" placeholder="https://www.example.com" />
<span asp-validation-for="Website" class="text-danger"></span>
</div>
</div>
</div>
<!-- Address Section -->
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">
<i class="bi bi-geo-alt me-2 text-primary"></i>Address
</h5>
<div class="row g-3">
<div class="col-12">
<label asp-for="Address" class="form-label">Street Address</label>
<input asp-for="Address" class="form-control" placeholder="Enter street address" />
<span asp-validation-for="Address" class="text-danger"></span>
</div>
<div class="col-md-5">
<label asp-for="City" class="form-label">City</label>
<input asp-for="City" class="form-control" placeholder="Enter city" />
<span asp-validation-for="City" class="text-danger"></span>
</div>
<div class="col-md-3">
<label asp-for="State" class="form-label">State</label>
<input asp-for="State" class="form-control" placeholder="Enter state" maxlength="2" />
<span asp-validation-for="State" class="text-danger"></span>
</div>
<div class="col-md-2">
<label asp-for="ZipCode" class="form-label">Zip Code</label>
<input asp-for="ZipCode" class="form-control" placeholder="12345" />
<span asp-validation-for="ZipCode" class="text-danger"></span>
</div>
<div class="col-md-2">
<label asp-for="Country" class="form-label">Country</label>
<select asp-for="Country" class="form-select">
<option value="">-- Select --</option>
<option value="USA">USA</option>
<option value="Canada">Canada</option>
<option value="Mexico">Mexico</option>
<option value="United Kingdom">United Kingdom</option>
<option value="China">China</option>
<option value="Germany">Germany</option>
<option value="Japan">Japan</option>
<option value="India">India</option>
<option value="Australia">Australia</option>
<option value="France">France</option>
<option value="Italy">Italy</option>
<option value="Spain">Spain</option>
<option value="Brazil">Brazil</option>
<option value="South Korea">South Korea</option>
<option value="Netherlands">Netherlands</option>
<option value="Switzerland">Switzerland</option>
</select>
<span asp-validation-for="Country" class="text-danger"></span>
</div>
</div>
</div>
<!-- Business Information Section -->
<div class="mb-4">
<div class="d-flex align-items-center gap-2 border-bottom pb-2 mb-3">
<h5 class="mb-0">
<i class="bi bi-briefcase me-2 text-primary"></i>Business Information
</h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Business Information"
data-bs-content="Account Number is your customer account number at the vendor (printed on POs and bills). Default Expense Account pre-fills the expense category when creating bills from this vendor's purchase orders. Payment Terms sets the expected due date on bills (e.g. Net 30 = due 30 days after receipt). Credit Limit is informational only.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="AccountNumber" class="form-label">Account Number</label>
<input asp-for="AccountNumber" class="form-control" placeholder="Enter account number" />
<span asp-validation-for="AccountNumber" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="TaxId" class="form-label">Tax ID / EIN</label>
<input asp-for="TaxId" class="form-control" placeholder="Enter tax ID" />
<span asp-validation-for="TaxId" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="DefaultExpenseAccountId" class="form-label">Default Expense Account</label>
<select asp-for="DefaultExpenseAccountId" asp-items="@ViewBag.ExpenseAccounts" class="form-select"></select>
<div class="form-text text-muted">Used to pre-fill bills created from this vendor's POs.</div>
</div>
<div class="col-md-6">
<label asp-for="PaymentTerms" class="form-label">Payment Terms</label>
<select asp-for="PaymentTerms" class="form-select">
<option value="">Select payment terms</option>
<option value="Net 15">Net 15</option>
<option value="Net 30">Net 30</option>
<option value="Net 45">Net 45</option>
<option value="Net 60">Net 60</option>
<option value="Due on Receipt">Due on Receipt</option>
<option value="Cash on Delivery">Cash on Delivery</option>
<option value="Prepaid">Prepaid</option>
</select>
<span asp-validation-for="PaymentTerms" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="CreditLimit" class="form-label">Credit Limit</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="CreditLimit" type="number" step="0.01" min="0" class="form-control" placeholder="0.00" />
</div>
<span asp-validation-for="CreditLimit" class="text-danger"></span>
</div>
<div class="col-md-6">
<div class="form-check form-switch mt-4">
<input asp-for="IsActive" class="form-check-input" type="checkbox" checked />
<label asp-for="IsActive" class="form-check-label">Active Vendor</label>
</div>
<div class="form-check form-switch mt-2">
<input asp-for="Is1099Vendor" class="form-check-input" type="checkbox" />
<label asp-for="Is1099Vendor" class="form-check-label">1099 Vendor</label>
</div>
<div class="form-text">Check if this vendor requires a 1099-NEC at year end (typically non-incorporated service providers paid ≥ $600).</div>
</div>
</div>
</div>
<!-- Notes Section -->
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">
<i class="bi bi-journal-text me-2 text-primary"></i>Notes
</h5>
<div class="row g-3">
<div class="col-12">
<label asp-for="Notes" class="form-label">Notes</label>
<textarea asp-for="Notes" class="form-control" rows="4" placeholder="Enter any additional notes about this vendor"></textarea>
<span asp-validation-for="Notes" class="text-danger"></span>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="d-flex gap-2 justify-content-end pt-3 border-top">
<a asp-action="Index" class="btn btn-outline-secondary px-4">Cancel</a>
<button type="submit" class="btn btn-primary px-4">
<i class="bi bi-check-circle me-2"></i>Create Vendor
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
@@ -0,0 +1,299 @@
@model PowderCoating.Application.DTOs.Vendor.VendorDto
@{
ViewData["Title"] = Model.CompanyName;
ViewData["PageIcon"] = "bi-truck";
ViewData["PageHelpTitle"] = "Vendor Details";
ViewData["PageHelpContent"] = "Full detail for this vendor. Use Quick Actions to view or add inventory items assigned to this supplier, or create a purchase order. Business Information shows your account number at the vendor and their payment terms — these pre-fill on bills and purchase orders.";
}
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="d-flex justify-content-end gap-2 mb-4">
<a asp-action="Statement" asp-route-id="@Model.Id" class="btn btn-outline-secondary">
<i class="bi bi-journal-text me-2"></i>Statement
</a>
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-warning">
<i class="bi bi-pencil me-2"></i>Edit
</a>
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>Back to List
</a>
</div>
<!-- Status Banner -->
<div class="alert @(Model.IsActive ? "alert-success" : "alert-danger") alert-permanent d-flex align-items-center mb-4">
<i class="bi @(Model.IsActive ? "bi-check-circle" : "bi-x-circle") me-2" style="font-size: 1.5rem;"></i>
<div>
<strong>Status:</strong> @(Model.IsActive ? "Active Vendor" : "Inactive Vendor")
@if (Model.IsPreferred)
{
<span class="badge bg-warning text-dark ms-3">
<i class="bi bi-star-fill"></i> Preferred Vendor
</span>
}
</div>
</div>
<div class="row g-4">
<!-- Left Column -->
<div class="col-lg-8">
<!-- Vendor Information -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-0 py-3">
<h5 class="mb-0 fw-semibold">
<i class="bi bi-truck me-2 text-primary"></i>Vendor Information
</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="text-muted small mb-1">Vendor Name</label>
<p class="fw-semibold mb-0">@Model.CompanyName</p>
</div>
</div>
</div>
</div>
<!-- Contact Information -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-0 py-3">
<h5 class="mb-0 fw-semibold">
<i class="bi bi-person me-2 text-primary"></i>Contact Information
</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="text-muted small mb-1">Contact Name</label>
<p class="mb-0">
@if (!string.IsNullOrEmpty(Model.ContactName))
{
<span>@Model.ContactName</span>
}
else
{
<span class="text-muted">Not provided</span>
}
</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Email</label>
<p class="mb-0">
@if (!string.IsNullOrEmpty(Model.Email))
{
<a href="mailto:@Model.Email" class="text-decoration-none">
<i class="bi bi-envelope me-1"></i>@Model.Email
</a>
}
else
{
<span class="text-muted">Not provided</span>
}
</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Phone</label>
<p class="mb-0">
@if (!string.IsNullOrEmpty(Model.Phone))
{
<a href="tel:@Model.Phone" class="text-decoration-none">
<i class="bi bi-telephone me-1"></i>@Model.Phone
</a>
}
else
{
<span class="text-muted">Not provided</span>
}
</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Website</label>
<p class="mb-0">
@if (!string.IsNullOrEmpty(Model.Website))
{
<a href="@Model.Website" target="_blank" class="text-decoration-none">
<i class="bi bi-globe me-1"></i>@Model.Website
<i class="bi bi-box-arrow-up-right ms-1 small"></i>
</a>
}
else
{
<span class="text-muted">Not provided</span>
}
</p>
</div>
</div>
</div>
</div>
<!-- Address Information -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-0 py-3">
<h5 class="mb-0 fw-semibold">
<i class="bi bi-geo-alt me-2 text-primary"></i>Address
</h5>
</div>
<div class="card-body">
@if (!string.IsNullOrEmpty(Model.Address))
{
<p class="mb-2">@Model.Address</p>
<p class="mb-0">
@if (!string.IsNullOrEmpty(Model.City))
{
<span>@Model.City</span>
}
@if (!string.IsNullOrEmpty(Model.State))
{
<span>, @Model.State</span>
}
@if (!string.IsNullOrEmpty(Model.ZipCode))
{
<span> @Model.ZipCode</span>
}
</p>
@if (!string.IsNullOrEmpty(Model.Country))
{
<p class="mb-0 text-muted">@Model.Country</p>
}
}
else
{
<p class="text-muted mb-0">No address provided</p>
}
</div>
</div>
<!-- Business Information -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-0 py-3 d-flex align-items-center gap-2">
<h5 class="mb-0 fw-semibold">
<i class="bi bi-briefcase me-2 text-primary"></i>Business Information
</h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Business Information"
data-bs-content="Account Number is your customer account at this vendor — printed on purchase orders. Payment Terms drives the due date on bills (e.g. Net 30 = 30 days after receipt date). Credit Limit is informational only and does not block orders.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="text-muted small mb-1">Account Number</label>
<p class="mb-0">@(Model.AccountNumber ?? "Not provided")</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Tax ID / EIN</label>
<p class="mb-0">@(Model.TaxId ?? "Not provided")</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Payment Terms</label>
<p class="mb-0">@(Model.PaymentTerms ?? "Not set")</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Credit Limit</label>
<p class="mb-0 fw-semibold">
@if (Model.CreditLimit.HasValue)
{
@Model.CreditLimit.Value.ToString("C")
}
else
{
<span class="text-muted">Not set</span>
}
</p>
</div>
</div>
</div>
</div>
<!-- Notes -->
@if (!string.IsNullOrEmpty(Model.Notes))
{
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-0 py-3">
<h5 class="mb-0 fw-semibold">
<i class="bi bi-journal-text me-2 text-primary"></i>Notes
</h5>
</div>
<div class="card-body">
<p class="mb-0" style="white-space: pre-wrap;">@Model.Notes</p>
</div>
</div>
}
</div>
<!-- Right Column - Information & Actions -->
<div class="col-lg-4">
<!-- Record Information -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-0 py-3">
<h5 class="mb-0 fw-semibold">
<i class="bi bi-clock-history me-2 text-primary"></i>Record Information
</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="text-muted small mb-1">Vendor Since</label>
<p class="mb-0">@Model.CreatedAt.ToString("MMMM dd, yyyy")</p>
</div>
<div class="mb-3">
<label class="text-muted small mb-1">Status</label>
<p class="mb-0">
@if (Model.IsActive)
{
<span class="badge bg-success">Active</span>
}
else
{
<span class="badge bg-secondary">Inactive</span>
}
</p>
</div>
<div>
<label class="text-muted small mb-1">Preferred Vendor</label>
<p class="mb-0">
@if (Model.IsPreferred)
{
<span class="badge bg-warning text-dark">
<i class="bi bi-star-fill"></i> Yes
</span>
}
else
{
<span class="text-muted">No</span>
}
</p>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0 py-3">
<h5 class="mb-0 fw-semibold">
<i class="bi bi-lightning me-2 text-primary"></i>Quick Actions
</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-outline-primary">
<i class="bi bi-pencil me-2"></i>Edit Vendor
</a>
<a asp-controller="Inventory" asp-action="Index" asp-route-vendorId="@Model.Id" class="btn btn-outline-success">
<i class="bi bi-box-seam me-2"></i>View Inventory Items
</a>
<a asp-controller="Inventory" asp-action="Create" asp-route-vendorId="@Model.Id" class="btn btn-outline-success">
<i class="bi bi-plus-circle me-2"></i>Add Inventory Item
</a>
<a asp-action="Delete" asp-route-id="@Model.Id" class="btn btn-outline-danger">
<i class="bi bi-trash me-2"></i>Delete Vendor
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,230 @@
@model PowderCoating.Application.DTOs.Vendor.UpdateVendorDto
@{
ViewData["Title"] = "Edit Vendor";
ViewData["PageIcon"] = "bi-truck";
ViewData["PageHelpTitle"] = "Edit Vendor";
ViewData["PageHelpContent"] = "Update vendor details here. Changing Default Expense Account or Payment Terms will apply to new bills going forward — existing bills are not affected. Marking a vendor Inactive hides them from purchase order pickers but preserves all history.";
}
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="d-flex justify-content-end align-items-center mb-4">
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>Back to List
</a>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<form asp-action="Edit" method="post">
<input type="hidden" asp-for="Id" />
<partial name="_ValidationSummary" />
<!-- Vendor Information Section -->
<div class="mb-4">
<div class="d-flex align-items-center gap-2 border-bottom pb-2 mb-3">
<h5 class="mb-0">
<i class="bi bi-truck me-2 text-primary"></i>Vendor Information
</h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Vendor Information"
data-bs-content="Vendor Name should match what appears on invoices. Preferred Vendor prioritizes this supplier in purchase order dropdowns. Inactive vendors are hidden from pickers but preserved in historical records.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="CompanyName" class="form-label">Vendor Name</label>
<input asp-for="CompanyName" class="form-control" placeholder="Enter vendor name" />
<span asp-validation-for="CompanyName" class="text-danger"></span>
</div>
<div class="col-md-3">
<label asp-for="IsActive" class="form-label">Status</label>
<select asp-for="IsActive" class="form-select">
<option value="true">Active</option>
<option value="false">Inactive</option>
</select>
</div>
<div class="col-md-3">
<div class="form-check form-switch mt-4">
<input asp-for="IsPreferred" class="form-check-input" type="checkbox" />
<label asp-for="IsPreferred" class="form-check-label">Preferred Vendor</label>
</div>
</div>
</div>
</div>
<!-- Contact Information Section -->
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">
<i class="bi bi-person me-2 text-primary"></i>Contact Information
</h5>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="ContactName" class="form-label">Contact Name</label>
<input asp-for="ContactName" class="form-control" placeholder="Enter contact name" />
<span asp-validation-for="ContactName" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Email" class="form-label">Email</label>
<input asp-for="Email" type="email" class="form-control" placeholder="name@example.com" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Phone" class="form-label">Phone</label>
<input asp-for="Phone" type="tel" class="form-control" placeholder="(555) 123-4567" />
<span asp-validation-for="Phone" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Website" class="form-label">Website</label>
<input asp-for="Website" type="url" class="form-control" placeholder="https://www.example.com" />
<span asp-validation-for="Website" class="text-danger"></span>
</div>
</div>
</div>
<!-- Address Section -->
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">
<i class="bi bi-geo-alt me-2 text-primary"></i>Address
</h5>
<div class="row g-3">
<div class="col-12">
<label asp-for="Address" class="form-label">Street Address</label>
<input asp-for="Address" class="form-control" placeholder="Enter street address" />
<span asp-validation-for="Address" class="text-danger"></span>
</div>
<div class="col-md-5">
<label asp-for="City" class="form-label">City</label>
<input asp-for="City" class="form-control" placeholder="Enter city" />
<span asp-validation-for="City" class="text-danger"></span>
</div>
<div class="col-md-3">
<label asp-for="State" class="form-label">State</label>
<input asp-for="State" class="form-control" placeholder="Enter state" maxlength="2" />
<span asp-validation-for="State" class="text-danger"></span>
</div>
<div class="col-md-2">
<label asp-for="ZipCode" class="form-label">Zip Code</label>
<input asp-for="ZipCode" class="form-control" placeholder="12345" />
<span asp-validation-for="ZipCode" class="text-danger"></span>
</div>
<div class="col-md-2">
<label asp-for="Country" class="form-label">Country</label>
<select asp-for="Country" class="form-select">
<option value="">-- Select --</option>
<option value="USA">USA</option>
<option value="Canada">Canada</option>
<option value="Mexico">Mexico</option>
<option value="United Kingdom">United Kingdom</option>
<option value="China">China</option>
<option value="Germany">Germany</option>
<option value="Japan">Japan</option>
<option value="India">India</option>
<option value="Australia">Australia</option>
<option value="France">France</option>
<option value="Italy">Italy</option>
<option value="Spain">Spain</option>
<option value="Brazil">Brazil</option>
<option value="South Korea">South Korea</option>
<option value="Netherlands">Netherlands</option>
<option value="Switzerland">Switzerland</option>
</select>
<span asp-validation-for="Country" class="text-danger"></span>
</div>
</div>
</div>
<!-- Business Information Section -->
<div class="mb-4">
<div class="d-flex align-items-center gap-2 border-bottom pb-2 mb-3">
<h5 class="mb-0">
<i class="bi bi-briefcase me-2 text-primary"></i>Business Information
</h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Business Information"
data-bs-content="Account Number is your customer account number at the vendor — appears on POs and bills. Default Expense Account pre-fills the expense category on bills created from this vendor's POs. Payment Terms sets the bill due date (Net 30 = 30 days after receipt). Credit Limit is informational only.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="AccountNumber" class="form-label">Account Number</label>
<input asp-for="AccountNumber" class="form-control" placeholder="Enter account number" />
<span asp-validation-for="AccountNumber" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="TaxId" class="form-label">Tax ID / EIN</label>
<input asp-for="TaxId" class="form-control" placeholder="Enter tax ID" />
<span asp-validation-for="TaxId" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="DefaultExpenseAccountId" class="form-label">Default Expense Account</label>
<select asp-for="DefaultExpenseAccountId" asp-items="@ViewBag.ExpenseAccounts" class="form-select"></select>
<div class="form-text text-muted">Used to pre-fill bills created from this vendor's POs.</div>
</div>
<div class="col-md-6">
<label asp-for="PaymentTerms" class="form-label">Payment Terms</label>
<select asp-for="PaymentTerms" class="form-select">
<option value="">Select payment terms</option>
<option value="Net 15">Net 15</option>
<option value="Net 30">Net 30</option>
<option value="Net 45">Net 45</option>
<option value="Net 60">Net 60</option>
<option value="Due on Receipt">Due on Receipt</option>
<option value="Cash on Delivery">Cash on Delivery</option>
<option value="Prepaid">Prepaid</option>
</select>
<span asp-validation-for="PaymentTerms" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="CreditLimit" class="form-label">Credit Limit</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="CreditLimit" type="number" step="0.01" min="0" class="form-control" placeholder="0.00" />
</div>
<span asp-validation-for="CreditLimit" class="text-danger"></span>
</div>
<div class="col-md-6">
<div class="form-check form-switch mt-4">
<input asp-for="Is1099Vendor" class="form-check-input" type="checkbox" />
<label asp-for="Is1099Vendor" class="form-check-label">1099 Vendor</label>
</div>
<div class="form-text">Check if this vendor requires a 1099-NEC at year end (typically non-incorporated service providers paid ≥ $600).</div>
</div>
</div>
</div>
<!-- Notes Section -->
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">
<i class="bi bi-journal-text me-2 text-primary"></i>Notes
</h5>
<div class="row g-3">
<div class="col-12">
<label asp-for="Notes" class="form-label">Notes</label>
<textarea asp-for="Notes" class="form-control" rows="4" placeholder="Enter any additional notes about this vendor"></textarea>
<span asp-validation-for="Notes" class="text-danger"></span>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="d-flex gap-2 justify-content-end pt-3 border-top">
<a asp-action="Index" class="btn btn-outline-secondary px-4">Cancel</a>
<button type="submit" class="btn btn-primary px-4">
<i class="bi bi-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
@@ -0,0 +1,249 @@
@model PowderCoating.Application.DTOs.Common.PagedResult<PowderCoating.Application.DTOs.Vendor.VendorListDto>
@{
ViewData["Title"] = "Vendors";
ViewData["PageIcon"] = "bi-truck";
ViewData["PageHelpTitle"] = "Vendors";
ViewData["PageHelpContent"] = "Your supplier directory — companies you buy powder, consumables, and materials from. Preferred Vendors (★) are highlighted for quick selection in purchase orders. Inventory Items shows how many items are assigned to each vendor. Inactive vendors are hidden from pickers but preserved in history. Click any row to view full details.";
}
<div class="d-flex justify-content-end align-items-center mb-4">
<a asp-action="Create" class="btn btn-primary">
<i class="bi bi-plus-circle me-2"></i>Add Vendor
</a>
</div>
<!-- Search Form -->
<form method="get" class="mb-4">
<div class="row g-3">
<div class="col-md-8">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" name="searchTerm" class="form-control" placeholder="Search by name, contact, email, phone, or city..." value="@ViewBag.SearchTerm">
<button type="submit" class="btn btn-primary"><i class="bi bi-search"></i></button>
@if (!string.IsNullOrWhiteSpace(ViewBag.SearchTerm))
{
<a href="@Url.Action("Index")" class="btn btn-outline-secondary">Clear</a>
}
</div>
</div>
<div class="col-md-4 text-end">
<div class="btn-group">
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
<i class="bi bi-funnel me-1"></i>Filters
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="@Url.Action("Index")">All Vendors</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="@Url.Action("Index", new { searchTerm = ViewBag.SearchTerm, sortColumn = "IsPreferred", sortDirection = "desc" })">Preferred Vendors</a></li>
<li><a class="dropdown-item" href="@Url.Action("Index", new { searchTerm = ViewBag.SearchTerm, sortColumn = "IsActive", sortDirection = "desc" })">Active Only</a></li>
</ul>
</div>
</div>
</div>
</form>
<!-- Vendors Table -->
<div class="card">
<div class="card-body p-0">
<!-- Desktop Table View -->
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead class="table-light">
<tr>
<th sortable-column column-name="CompanyName" current-column="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Vendor Name</th>
<th sortable-column column-name="ContactName" current-column="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Contact</th>
<th sortable-column column-name="Phone" current-column="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Phone</th>
<th sortable-column column-name="Email" current-column="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Email</th>
<th>Inventory Items</th>
<th sortable-column column-name="IsPreferred" current-column="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection" class="text-center">Preferred</th>
<th sortable-column column-name="IsActive" current-column="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection" class="text-center">Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@if (Model.Items.Any())
{
@foreach (var vendor in Model.Items)
{
<tr style="cursor:pointer" onclick="window.location='@Url.Action("Details", new { id = vendor.Id })'">
<td>
<strong>@vendor.CompanyName</strong>
</td>
<td>@(vendor.ContactName ?? "—")</td>
<td>@(vendor.Phone ?? "—")</td>
<td>@(vendor.Email ?? "—")</td>
<td>
@if (vendor.InventoryItemCount > 0)
{
<span class="badge bg-info">@vendor.InventoryItemCount</span>
}
else
{
<span class="text-muted">0</span>
}
</td>
<td class="text-center">
@if (vendor.IsPreferred)
{
<i class="bi bi-star-fill text-warning" title="Preferred Vendor"></i>
}
else
{
<span class="text-muted">—</span>
}
</td>
<td class="text-center">
@if (vendor.IsActive)
{
<span class="badge bg-success">Active</span>
}
else
{
<span class="badge bg-secondary">Inactive</span>
}
</td>
<td onclick="event.stopPropagation()">
<div class="btn-group btn-group-sm">
<a asp-action="Details" asp-route-id="@vendor.Id" class="btn btn-outline-primary" title="View Details">
<i class="bi bi-eye"></i>
</a>
<a asp-action="Edit" asp-route-id="@vendor.Id" class="btn btn-outline-secondary" title="Edit">
<i class="bi bi-pencil"></i>
</a>
<a asp-action="Delete" asp-route-id="@vendor.Id" class="btn btn-outline-danger" title="Delete">
<i class="bi bi-trash"></i>
</a>
</div>
</td>
</tr>
}
}
else
{
<tr>
<td colspan="8" class="text-center py-5 text-muted">
<i class="bi bi-inbox" style="font-size: 3rem; opacity: 0.3;"></i>
<p class="mt-2">No vendors found.</p>
@if (!string.IsNullOrWhiteSpace(ViewBag.SearchTerm))
{
<a href="@Url.Action("Index")" class="btn btn-sm btn-outline-secondary">Clear Search</a>
}
else
{
<a asp-action="Create" class="btn btn-sm btn-primary">Add First Vendor</a>
}
</td>
</tr>
}
</tbody>
</table>
</div>
<!-- Mobile Card View -->
<div class="mobile-card-view">
@if (!Model.Items.Any())
{
<div class="text-center py-5 text-muted">
<i class="bi bi-inbox" style="font-size: 3rem; opacity: 0.3;"></i>
<p class="mt-2">No vendors found.</p>
@if (!string.IsNullOrWhiteSpace(ViewBag.SearchTerm))
{
<a href="@Url.Action("Index")" class="btn btn-sm btn-outline-secondary">Clear Search</a>
}
else
{
<a asp-action="Create" class="btn btn-sm btn-primary">Add First Vendor</a>
}
</div>
}
else
{
<div class="mobile-card-list">
@foreach (var vendor in Model.Items)
{
<a asp-action="Details" asp-route-id="@vendor.Id" class="mobile-data-card text-decoration-none">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);">
<i class="bi bi-truck"></i>
</div>
<div class="mobile-card-title">
<h6>@vendor.CompanyName</h6>
@if (!string.IsNullOrEmpty(vendor.ContactName))
{
<small>@vendor.ContactName</small>
}
</div>
</div>
<div class="mobile-card-body">
@if (!string.IsNullOrEmpty(vendor.Phone))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Phone</span>
<span class="mobile-card-value">@vendor.Phone</span>
</div>
}
@if (!string.IsNullOrEmpty(vendor.Email))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Email</span>
<span class="mobile-card-value">@vendor.Email</span>
</div>
}
<div class="mobile-card-row">
<span class="mobile-card-label">Inventory Items</span>
<span class="mobile-card-value">
@if (vendor.InventoryItemCount > 0)
{
<span class="badge bg-info">@vendor.InventoryItemCount</span>
}
else
{
<span class="text-muted">0</span>
}
</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value">
@if (vendor.IsPreferred)
{
<i class="bi bi-star-fill text-warning me-1"></i>
}
@if (vendor.IsActive)
{
<span class="badge bg-success">Active</span>
}
else
{
<span class="badge bg-secondary">Inactive</span>
}
</span>
</div>
</div>
<div class="mobile-card-footer">
<a href="@Url.Action("Details", new { id = vendor.Id })" class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation();">
<i class="bi bi-eye me-1"></i>View
</a>
<a href="@Url.Action("Edit", new { id = vendor.Id })" class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation();">
<i class="bi bi-pencil me-1"></i>Edit
</a>
</div>
</a>
}
</div>
}
</div>
</div>
@if (Model.TotalPages > 1)
{
<div class="card-footer bg-white">
<partial name="_Pagination" model="Model" />
</div>
}
</div>
@section Scripts {
<script src="~/js/grid-utils.js"></script>
}
@@ -0,0 +1,93 @@
@model PowderCoating.Application.DTOs.Accounting.VendorStatementDto
@{
ViewData["Title"] = $"Statement {Model.VendorName}";
}
<div class="d-flex justify-content-between align-items-start mb-4 flex-wrap gap-2">
<div>
<h4 class="mb-0">Vendor Statement</h4>
<p class="text-muted mb-0">@Model.VendorName &nbsp;·&nbsp; @Model.From.ToString("MMM d, yyyy") @Model.To.ToString("MMM d, yyyy")</p>
</div>
<div class="d-flex gap-2 flex-wrap">
<form method="get" class="d-flex gap-2 align-items-center">
<input type="hidden" name="id" value="@(ViewContext.RouteData.Values["id"])" />
<input type="date" name="from" class="form-control form-control-sm" value="@Model.From.ToString("yyyy-MM-dd")" />
<input type="date" name="to" class="form-control form-control-sm" value="@Model.To.ToString("yyyy-MM-dd")" />
<button type="submit" class="btn btn-sm btn-outline-secondary">Refresh</button>
</form>
<a asp-action="Statement" asp-route-id="@(ViewContext.RouteData.Values["id"])"
asp-route-from="@Model.From.ToString("yyyy-MM-dd")"
asp-route-to="@Model.To.ToString("yyyy-MM-dd")"
asp-route-pdf="true"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-file-earmark-pdf me-1"></i>Download PDF
</a>
<a asp-action="Details" asp-route-id="@(ViewContext.RouteData.Values["id"])" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back
</a>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header border-0 py-3 bg-white d-flex justify-content-between align-items-center">
<span class="fw-semibold">@Model.VendorName</span>
<div class="text-muted small">@Model.CompanyName</div>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0">
<thead class="table-dark">
<tr>
<th style="width:100px">Date</th>
<th style="width:120px">Type</th>
<th style="width:130px">Reference</th>
<th>Description</th>
<th class="text-end" style="width:110px">Debit</th>
<th class="text-end" style="width:110px">Credit</th>
<th class="text-end" style="width:120px">Balance</th>
</tr>
</thead>
<tbody>
<tr class="table-light fw-semibold">
<td class="text-muted">@Model.From.AddDays(-1).ToString("MM/dd/yy")</td>
<td colspan="5">Opening Balance</td>
<td class="text-end">@Model.OpeningBalance.ToString("C")</td>
</tr>
@if (!Model.Lines.Any())
{
<tr>
<td colspan="7" class="text-center text-muted py-4">No activity in this period.</td>
</tr>
}
else
{
@foreach (var line in Model.Lines)
{
<tr>
<td class="text-muted small">@line.Date.ToString("MM/dd/yy")</td>
<td>
<span class="badge @(line.Type == "Bill" ? "bg-danger" : line.Type == "Payment" ? "bg-success" : "bg-secondary") text-white">
@line.Type
</span>
</td>
<td class="small">@line.Reference</td>
<td class="small text-muted">@line.Description</td>
<td class="text-end small">@(line.Debit.HasValue ? line.Debit.Value.ToString("C") : "")</td>
<td class="text-end small">@(line.Credit.HasValue ? line.Credit.Value.ToString("C") : "")</td>
<td class="text-end small @(line.RunningBalance > 0 ? "text-danger" : "text-success") fw-semibold">
@line.RunningBalance.ToString("C")
</td>
</tr>
}
}
<tr class="table-secondary fw-bold">
<td colspan="6">Closing Balance</td>
<td class="text-end @(Model.ClosingBalance > 0 ? "text-danger" : "text-success")">
@Model.ClosingBalance.ToString("C")
</td>
</tr>
</tbody>
</table>
</div>
</div>