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,176 @@
@model PowderCoating.Application.DTOs.Equipment.CreateEquipmentDto
@{
ViewData["Title"] = "Add New Equipment";
ViewData["PageIcon"] = "bi-tools";
ViewData["PageHelpTitle"] = "Add New Equipment";
ViewData["PageHelpContent"] = "Equipment records track physical assets in your shop — ovens, spray booths, compressors, and other machinery. Enter a name, type, and location at minimum. Serial number and warranty details help with service claims. The maintenance interval drives the Next Scheduled date shown on the equipment list.";
var statusList = ViewBag.StatusList as Array ?? Array.Empty<object>();
}
<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" />
<!-- Basic 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-info-circle me-2 text-primary"></i>Basic 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="Basic Information"
data-bs-content="Equipment Name is what appears throughout the system. Type describes the category (e.g., Oven, Spray Booth, Compressor). Equipment Number is an optional internal reference code like EQ-001. Location helps staff quickly find the equipment on the shop floor.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="EquipmentName" class="form-label">Equipment Name <span class="text-danger">*</span></label>
<input asp-for="EquipmentName" class="form-control" placeholder="Enter equipment name" />
<span asp-validation-for="EquipmentName" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="EquipmentNumber" class="form-label">Equipment Number</label>
<input asp-for="EquipmentNumber" class="form-control" placeholder="e.g., EQ-001" />
<span asp-validation-for="EquipmentNumber" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="EquipmentType" class="form-label">Type <span class="text-danger">*</span></label>
<input asp-for="EquipmentType" class="form-control" placeholder="e.g., Oven, Spray Booth, Compressor" />
<span asp-validation-for="EquipmentType" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Location" class="form-label">Location</label>
<input asp-for="Location" class="form-control" placeholder="e.g., Shop Floor A, Building 2" />
<span asp-validation-for="Location" class="text-danger"></span>
</div>
</div>
</div>
<!-- Manufacturer 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-building me-2 text-primary"></i>Manufacturer 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="Manufacturer Information"
data-bs-content="Manufacturer, model, and serial number are useful for warranty claims, ordering replacement parts, and scheduling manufacturer-recommended service. The serial number is especially important if the equipment is registered for warranty support.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="row g-3">
<div class="col-md-4">
<label asp-for="Manufacturer" class="form-label">Manufacturer</label>
<input asp-for="Manufacturer" class="form-control" placeholder="Enter manufacturer name" />
<span asp-validation-for="Manufacturer" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="Model" class="form-label">Model</label>
<input asp-for="Model" class="form-control" placeholder="Enter model number" />
<span asp-validation-for="Model" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="SerialNumber" class="form-label">Serial Number</label>
<input asp-for="SerialNumber" class="form-control" placeholder="Enter serial number" />
<span asp-validation-for="SerialNumber" class="text-danger"></span>
</div>
</div>
</div>
<!-- Purchase & Warranty 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-receipt me-2 text-primary"></i>Purchase &amp; Warranty</h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Purchase &amp; Warranty"
data-bs-content="Purchase Date and Price help track the asset value of your equipment. Warranty Expiration is shown in green (active) or red (expired) on the Details page, so you know whether a repair might be covered before calling a technician.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="row g-3">
<div class="col-md-4">
<label asp-for="PurchaseDate" class="form-label">Purchase Date</label>
<input asp-for="PurchaseDate" type="date" class="form-control" />
<span asp-validation-for="PurchaseDate" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="PurchasePrice" class="form-label">Purchase Price</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="PurchasePrice" type="number" step="0.01" min="0" value="0" class="form-control" placeholder="0.00" />
</div>
<span asp-validation-for="PurchasePrice" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="WarrantyExpiration" class="form-label">Warranty Expiration</label>
<input asp-for="WarrantyExpiration" type="date" class="form-control" />
<span asp-validation-for="WarrantyExpiration" class="text-danger"></span>
</div>
</div>
</div>
<!-- Status 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-toggle-on me-2 text-primary"></i>Status</h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Status"
data-bs-content="Operational = working normally. Needs Maintenance = flagged for upcoming service. Under Maintenance = currently being serviced. Out of Service = not working, requires repair before use. Retired = permanently decommissioned. Status is shown on the equipment list and can be updated at any time from the Edit page.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="row g-3">
<div class="col-md-4">
<label asp-for="Status" class="form-label">Initial Status</label>
<select asp-for="Status" class="form-select">
@foreach (var status in statusList)
{
<option value="@status">@status.ToString()</option>
}
</select>
<span asp-validation-for="Status" class="text-danger"></span>
</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">Additional Notes</label>
<textarea asp-for="Notes" class="form-control" rows="4" placeholder="Enter any additional notes about this equipment"></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 Equipment
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
@@ -0,0 +1,108 @@
@model PowderCoating.Application.DTOs.Equipment.EquipmentDto
@{
ViewData["Title"] = "Delete Equipment";
ViewData["PageIcon"] = "bi-tools";
}
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="d-flex justify-content-end align-items-center mb-4">
<a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>Back to Details
</a>
</div>
<!-- Warning Banner -->
<div class="alert alert-danger alert-permanent d-flex align-items-start mb-4">
<i class="bi bi-exclamation-triangle me-3" style="font-size: 1.5rem;"></i>
<div>
<strong>Warning: This action cannot be undone!</strong>
<p class="mb-0 mt-1">This equipment record will be marked as deleted. All associated maintenance records will be preserved but the equipment will no longer appear in the active list.</p>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<!-- Equipment Summary -->
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">Equipment Information</h5>
<div class="row g-3">
<div class="col-md-6">
<label class="text-muted small mb-1">Equipment Name</label>
<p class="fw-semibold mb-0">@Model.EquipmentName</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Equipment Number</label>
<p class="mb-0">@(Model.EquipmentNumber ?? "Not assigned")</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Type</label>
<p class="mb-0">@Model.EquipmentType</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Location</label>
<p class="mb-0">@(Model.Location ?? "—")</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Status</label>
<p class="mb-0">
@{
var statusClass = Model.Status switch
{
"Operational" => "success",
"NeedsMaintenance" => "warning",
"UnderMaintenance" => "info",
"OutOfService" => "danger",
_ => "secondary"
};
}
<span class="badge bg-@statusClass bg-opacity-10 text-@statusClass">
@Model.StatusDisplay
</span>
</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Purchase Price</label>
<p class="mb-0">@Model.PurchasePrice.ToString("C")</p>
</div>
</div>
</div>
@if (!string.IsNullOrEmpty(Model.Manufacturer) || !string.IsNullOrEmpty(Model.Model))
{
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">Manufacturer Information</h5>
<div class="row g-3">
<div class="col-md-4">
<label class="text-muted small mb-1">Manufacturer</label>
<p class="mb-0">@(Model.Manufacturer ?? "—")</p>
</div>
<div class="col-md-4">
<label class="text-muted small mb-1">Model</label>
<p class="mb-0">@(Model.Model ?? "—")</p>
</div>
<div class="col-md-4">
<label class="text-muted small mb-1">Serial Number</label>
<p class="mb-0">@(Model.SerialNumber ?? "—")</p>
</div>
</div>
</div>
}
<!-- Delete Confirmation Form -->
<form asp-action="Delete" method="post" class="mt-4">
<input type="hidden" asp-for="Id" />
<div class="d-flex gap-2 justify-content-end pt-3 border-top">
<a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-outline-secondary px-4">
<i class="bi bi-x-circle me-2"></i>Cancel
</a>
<button type="submit" class="btn btn-danger px-4">
<i class="bi bi-trash me-2"></i>Delete Equipment
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@@ -0,0 +1,450 @@
@model PowderCoating.Application.DTOs.Equipment.EquipmentDto
@{
ViewData["Title"] = Model.EquipmentName;
ViewData["PageIcon"] = "bi-tools";
ViewData["PageHelpTitle"] = "Equipment Details";
ViewData["PageHelpContent"] = "The status banner shows whether this equipment is currently available. Maintenance Schedule shows the interval and when service is next due. Use Add Maintenance or Schedule Maintenance to log upcoming or completed service. Upload a PDF user manual here so staff can access it without leaving the system.";
var maintenanceHistory = ViewBag.MaintenanceHistory as List<PowderCoating.Application.DTOs.Maintenance.MaintenanceListDto> ?? new List<PowderCoating.Application.DTOs.Maintenance.MaintenanceListDto>();
var hasManual = !string.IsNullOrWhiteSpace(Model.ManualFilePath) || (!string.IsNullOrWhiteSpace(Model.Notes) && Model.Notes.StartsWith("uploads/"));
}
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<p class="text-muted mb-0">@Model.EquipmentType @(!string.IsNullOrEmpty(Model.EquipmentNumber) ? $"• {Model.EquipmentNumber}" : "")</p>
</div>
<div class="d-flex gap-2">
<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>
</div>
<!-- Status Banner -->
@{
var statusClass = Model.Status switch
{
"Operational" => "alert-success",
"NeedsMaintenance" => "alert-warning",
"UnderMaintenance" => "alert-info",
"OutOfService" => "alert-danger",
_ => "alert-secondary"
};
var statusIcon = Model.Status switch
{
"Operational" => "bi-check-circle",
"NeedsMaintenance" => "bi-exclamation-triangle",
"UnderMaintenance" => "bi-wrench",
"OutOfService" => "bi-x-circle",
_ => "bi-info-circle"
};
}
<div class="alert @statusClass alert-permanent d-flex align-items-center mb-4">
<i class="bi @statusIcon me-2" style="font-size: 1.5rem;"></i>
<div>
<strong>Status:</strong> @Model.StatusDisplay
@if (Model.DaysUntilMaintenance.HasValue && Model.DaysUntilMaintenance < 7)
{
<span class="ms-2">• Maintenance due in @Model.DaysUntilMaintenance days</span>
}
</div>
</div>
<div class="row g-4">
<!-- Left Column -->
<div class="col-lg-8">
<!-- Equipment 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-info-circle me-2 text-primary"></i>Equipment Information
</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="text-muted small mb-1">Equipment Name</label>
<p class="fw-semibold mb-0">@Model.EquipmentName</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Equipment Number</label>
<p class="mb-0">@(Model.EquipmentNumber ?? "Not assigned")</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Type</label>
<p class="mb-0">@Model.EquipmentType</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Location</label>
<p class="mb-0">
@if (!string.IsNullOrEmpty(Model.Location))
{
<span><i class="bi bi-geo-alt me-1"></i>@Model.Location</span>
}
else
{
<span class="text-muted">Not specified</span>
}
</p>
</div>
<div class="col-md-4">
<label class="text-muted small mb-1">Manufacturer</label>
<p class="mb-0">@(Model.Manufacturer ?? "—")</p>
</div>
<div class="col-md-4">
<label class="text-muted small mb-1">Model</label>
<p class="mb-0">@(Model.Model ?? "—")</p>
</div>
<div class="col-md-4">
<label class="text-muted small mb-1">Serial Number</label>
<p class="mb-0">@(Model.SerialNumber ?? "—")</p>
</div>
</div>
</div>
</div>
<!-- Purchase & Warranty -->
<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-receipt me-2 text-primary"></i>Purchase & Warranty
</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="text-muted small mb-1">Purchase Date</label>
<p class="mb-0">
@(Model.PurchaseDate.HasValue ? Model.PurchaseDate.Value.ToString("MMM dd, yyyy") : "Not recorded")
</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Purchase Price</label>
<p class="mb-0 fw-semibold">@Model.PurchasePrice.ToString("C")</p>
</div>
<div class="col-md-12">
<label class="text-muted small mb-1">Warranty Expiration</label>
<p class="mb-0">
@if (Model.WarrantyExpiration.HasValue)
{
var isExpired = Model.WarrantyExpiration.Value < DateTime.Now;
<span class="@(isExpired ? "text-danger" : "text-success")">
@Model.WarrantyExpiration.Value.ToString("MMM dd, yyyy")
@(isExpired ? "(Expired)" : "(Active)")
</span>
}
else
{
<span class="text-muted">No warranty information</span>
}
</p>
</div>
</div>
</div>
</div>
<!-- Maintenance Schedule -->
<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-calendar-check me-2 text-primary"></i>Maintenance Schedule
</h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Maintenance Schedule"
data-bs-content="Maintenance Interval is how many days between scheduled services — set this on the Edit page. Next Scheduled is calculated automatically as Last Maintenance date plus the interval. If no maintenance has been completed yet, Next Scheduled will be blank until the first service is recorded as Completed.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="text-muted small mb-1">Maintenance Interval</label>
<p class="mb-0">@Model.RecommendedMaintenanceIntervalDays days</p>
</div>
<div class="col-md-4">
<label class="text-muted small mb-1">Last Maintenance</label>
<p class="mb-0">
@(Model.LastMaintenanceDate.HasValue ? Model.LastMaintenanceDate.Value.ToString("MMM dd, yyyy") : "Never")
</p>
</div>
<div class="col-md-4">
<label class="text-muted small mb-1">Next Scheduled</label>
<p class="mb-0">
@if (Model.NextScheduledMaintenance.HasValue)
{
<span>@Model.NextScheduledMaintenance.Value.ToString("MMM dd, yyyy")</span>
@if (Model.DaysUntilMaintenance.HasValue)
{
<br />
<small class="text-muted">(@Model.DaysUntilMaintenance days away)</small>
}
}
else
{
<span class="text-muted">Not scheduled</span>
}
</p>
</div>
</div>
</div>
</div>
<!-- Maintenance History -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-0 py-3 d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-2">
<h5 class="mb-0 fw-semibold">
<i class="bi bi-clock-history me-2 text-primary"></i>Maintenance History
</h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Maintenance History"
data-bs-content="Shows the 10 most recent maintenance records for this equipment. Click View All Maintenance to see the full history. Completed records update the Last Maintenance date and recalculate the Next Scheduled date. Use Add Maintenance to log an upcoming or completed service task.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<a asp-controller="Maintenance" asp-action="Create" asp-route-equipmentId="@Model.Id" class="btn btn-sm btn-primary">
<i class="bi bi-plus-circle me-1"></i>Add Maintenance
</a>
</div>
<div class="card-body p-0">
@if (!maintenanceHistory.Any())
{
<div class="text-center py-4">
<i class="bi bi-inbox" style="font-size: 3rem; color: #d1d5db;"></i>
<p class="text-muted mt-2 mb-0">No maintenance records yet</p>
</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Status</th>
<th>Priority</th>
<th>Cost</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var maintenance in maintenanceHistory.Take(10))
{
<tr>
<td>@maintenance.ScheduledDate.ToString("MMM dd, yyyy")</td>
<td>@maintenance.MaintenanceType</td>
<td>
@{
var badgeClass = maintenance.Status switch
{
"Scheduled" => "bg-primary",
"InProgress" => "bg-warning",
"Completed" => "bg-success",
"Overdue" => "bg-danger",
"Cancelled" => "bg-secondary",
_ => "bg-secondary"
};
}
<span class="badge @badgeClass bg-opacity-10 text-@badgeClass.Replace("bg-", "")">
@maintenance.StatusDisplay
</span>
</td>
<td>
@{
var priorityClass = maintenance.Priority switch
{
"Critical" => "danger",
"High" => "warning",
"Normal" => "primary",
"Low" => "secondary",
_ => "secondary"
};
}
<span class="badge bg-@priorityClass bg-opacity-10 text-@priorityClass">
@maintenance.PriorityDisplay
</span>
</td>
<td>@maintenance.TotalCost.ToString("C")</td>
<td>
<a asp-controller="Maintenance" asp-action="Details" asp-route-id="@maintenance.Id" class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
</div>
<!-- User Manuals -->
<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-file-earmark-pdf me-2 text-primary"></i>User Manual
</h5>
</div>
<div class="card-body">
@if (hasManual)
{
// Get the filename from new storage or legacy storage
var fileName = !string.IsNullOrWhiteSpace(Model.ManualFileName)
? Model.ManualFileName
: (!string.IsNullOrWhiteSpace(Model.Notes) ? System.IO.Path.GetFileName(Model.Notes) : "User Manual");
// Remove GUID prefix from legacy filenames
if (fileName.Length > 37 && fileName[36] == '-')
{
fileName = fileName.Substring(37);
}
<div class="d-flex justify-content-between align-items-center p-3 bg-light rounded">
<div class="d-flex align-items-center" style="max-width: 70%;">
<i class="bi bi-file-earmark-pdf text-danger me-2" style="font-size: 1.5rem;"></i>
<span class="fw-semibold text-truncate" title="@fileName">@fileName</span>
</div>
<div class="d-flex gap-2">
<a asp-action="DownloadManual" asp-route-id="@Model.Id" class="btn btn-sm btn-primary">
<i class="bi bi-download me-1"></i>Download
</a>
<form asp-action="DeleteManual" method="post" class="d-inline delete-manual-form" data-equipment-id="@Model.Id">
@Html.AntiForgeryToken()
<input type="hidden" name="equipmentId" value="@Model.Id" />
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</div>
}
else
{
<div class="text-center py-3">
<i class="bi bi-file-earmark-x" style="font-size: 3rem; color: #d1d5db;"></i>
<p class="text-muted mt-2 mb-3">No manual uploaded</p>
</div>
}
<form asp-action="UploadManual" method="post" enctype="multipart/form-data" class="mt-3" id="uploadManualForm">
@Html.AntiForgeryToken()
<input type="hidden" name="equipmentId" value="@Model.Id" />
<div class="input-group">
<input type="file" class="form-control" name="manualFile" id="manualFile" accept=".pdf,.doc,.docx,.xls,.xlsx">
<button type="submit" class="btn btn-primary">
<i class="bi bi-upload me-1"></i>Upload
</button>
</div>
<small class="text-muted d-block mt-1">Allowed: PDF, DOC, DOCX, XLS, XLSX (Max 10 MB)</small>
</form>
</div>
</div>
</div>
<!-- Right Column -->
<div class="col-lg-4">
<!-- Quick Actions -->
<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-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-warning">
<i class="bi bi-pencil me-2"></i>Edit Equipment
</a>
<a asp-controller="Maintenance" asp-action="Create" asp-route-equipmentId="@Model.Id" class="btn btn-primary">
<i class="bi bi-wrench me-2"></i>Schedule Maintenance
</a>
<a asp-controller="Maintenance" asp-action="Index" asp-route-equipmentId="@Model.Id" class="btn btn-outline-primary">
<i class="bi bi-list me-2"></i>View All Maintenance
</a>
<hr />
<a asp-action="Delete" asp-route-id="@Model.Id" class="btn btn-outline-danger">
<i class="bi bi-trash me-2"></i>Delete Equipment
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
// Handle manual upload via AJAX
document.getElementById('uploadManualForm')?.addEventListener('submit', function(e) {
e.preventDefault();
const fileInput = document.getElementById('manualFile');
if (!fileInput.files.length) {
showWarning('Please select a file to upload.', 'No File Selected');
return;
}
const formData = new FormData(this);
const button = this.querySelector('button[type="submit"]');
const originalButtonText = button.innerHTML;
button.disabled = true;
button.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Uploading...';
fetch(this.action, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
showError(data.message || 'Upload failed', 'Upload Failed');
button.disabled = false;
button.innerHTML = originalButtonText;
}
})
.catch(error => {
showError('An error occurred during upload', 'Upload Error');
button.disabled = false;
button.innerHTML = originalButtonText;
});
});
// Handle manual deletion
document.querySelector('.delete-manual-form')?.addEventListener('submit', function(e) {
e.preventDefault();
if (!confirm('Are you sure you want to delete this manual?')) {
return;
}
const formData = new FormData(this);
fetch(this.action, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
showError(data.message || 'Delete failed', 'Delete Failed');
}
})
.catch(error => {
showError('An error occurred during deletion', 'Delete Error');
});
});
</script>
}
@@ -0,0 +1,187 @@
@model PowderCoating.Application.DTOs.Equipment.UpdateEquipmentDto
@{
ViewData["Title"] = "Edit Equipment";
ViewData["PageIcon"] = "bi-tools";
ViewData["PageHelpTitle"] = "Edit Equipment";
ViewData["PageHelpContent"] = "Update the equipment record details. Changes take effect immediately on save. Marking equipment Inactive removes it from pickers and the default list view but preserves its maintenance history.";
var statusList = ViewBag.StatusList as Array ?? Array.Empty<object>();
}
<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="Details" asp-route-id="@Model.Id" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>Back to Details
</a>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<form asp-action="Edit" method="post">
<partial name="_ValidationSummary" />
<input type="hidden" asp-for="Id" />
<!-- Basic 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-info-circle me-2 text-primary"></i>Basic 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="Basic Information"
data-bs-content="Equipment Name is what appears throughout the system. Type describes the category (e.g., Oven, Spray Booth, Compressor). Equipment Number is an optional internal reference code like EQ-001. Location helps staff quickly find the equipment on the shop floor.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="EquipmentName" class="form-label">Equipment Name <span class="text-danger">*</span></label>
<input asp-for="EquipmentName" class="form-control" placeholder="Enter equipment name" />
<span asp-validation-for="EquipmentName" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="EquipmentNumber" class="form-label">Equipment Number</label>
<input asp-for="EquipmentNumber" class="form-control" placeholder="e.g., EQ-001" />
<span asp-validation-for="EquipmentNumber" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="EquipmentType" class="form-label">Type <span class="text-danger">*</span></label>
<input asp-for="EquipmentType" class="form-control" placeholder="e.g., Oven, Spray Booth, Compressor" />
<span asp-validation-for="EquipmentType" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Location" class="form-label">Location</label>
<input asp-for="Location" class="form-control" placeholder="e.g., Shop Floor A, Building 2" />
<span asp-validation-for="Location" class="text-danger"></span>
</div>
</div>
</div>
<!-- Manufacturer 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-building me-2 text-primary"></i>Manufacturer 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="Manufacturer Information"
data-bs-content="Manufacturer, model, and serial number are useful for warranty claims, ordering replacement parts, and scheduling manufacturer-recommended service. The serial number is especially important if the equipment is registered for warranty support.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="row g-3">
<div class="col-md-4">
<label asp-for="Manufacturer" class="form-label">Manufacturer</label>
<input asp-for="Manufacturer" class="form-control" placeholder="Enter manufacturer name" />
<span asp-validation-for="Manufacturer" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="Model" class="form-label">Model</label>
<input asp-for="Model" class="form-control" placeholder="Enter model number" />
<span asp-validation-for="Model" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="SerialNumber" class="form-label">Serial Number</label>
<input asp-for="SerialNumber" class="form-control" placeholder="Enter serial number" />
<span asp-validation-for="SerialNumber" class="text-danger"></span>
</div>
</div>
</div>
<!-- Purchase & Warranty 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-receipt me-2 text-primary"></i>Purchase &amp; Warranty</h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Purchase &amp; Warranty"
data-bs-content="Purchase Date and Price help track the asset value of your equipment. Warranty Expiration is shown in green (active) or red (expired) on the Details page, so you know whether a repair might be covered before calling a technician.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="row g-3">
<div class="col-md-4">
<label asp-for="PurchaseDate" class="form-label">Purchase Date</label>
<input asp-for="PurchaseDate" type="date" class="form-control" />
<span asp-validation-for="PurchaseDate" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="PurchasePrice" class="form-label">Purchase Price</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="PurchasePrice" type="number" step="0.01" min="0" class="form-control" placeholder="0.00" />
</div>
<span asp-validation-for="PurchasePrice" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="WarrantyExpiration" class="form-label">Warranty Expiration</label>
<input asp-for="WarrantyExpiration" type="date" class="form-control" />
<span asp-validation-for="WarrantyExpiration" class="text-danger"></span>
</div>
</div>
</div>
<!-- Status 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-toggle-on me-2 text-primary"></i>Status</h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Status"
data-bs-content="Operational = working normally. Needs Maintenance = flagged for upcoming service. Under Maintenance = currently being serviced. Out of Service = not working, requires repair. Retired = permanently decommissioned. Unchecking Active hides the equipment from the default list and from dropdowns in other parts of the system.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="row g-3">
<div class="col-md-4">
<label asp-for="Status" class="form-label">Status</label>
<select asp-for="Status" class="form-select">
@foreach (var status in statusList)
{
<option value="@status">@status.ToString()</option>
}
</select>
<span asp-validation-for="Status" class="text-danger"></span>
</div>
<div class="col-md-8 d-flex align-items-end pb-1">
<div class="form-check">
<input asp-for="IsActive" type="checkbox" class="form-check-input" />
<label asp-for="IsActive" class="form-check-label">
Active Equipment
<small class="text-muted d-block">Inactive equipment will not appear in the main list</small>
</label>
</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">Additional Notes</label>
<textarea asp-for="Notes" class="form-control" rows="4" placeholder="Enter any additional notes about this equipment"></textarea>
<span asp-validation-for="Notes" class="text-danger"></span>
<small class="text-muted">Note: User manual file information is stored separately</small>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="d-flex gap-2 justify-content-end pt-3 border-top">
<a asp-action="Details" asp-route-id="@Model.Id" 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>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
@@ -0,0 +1,292 @@
@model PagedResult<PowderCoating.Application.DTOs.Equipment.EquipmentListDto>
@{
ViewData["Title"] = "Equipment";
ViewData["PageIcon"] = "bi-tools";
ViewData["PageHelpTitle"] = "Equipment";
ViewData["PageHelpContent"] = "Track all shop equipment — ovens, spray booths, compressors, and other machinery. Status shows whether each piece is Operational, Needs Maintenance, Under Maintenance, or Out of Service. Next Maintenance date is calculated from the last completed maintenance plus the equipment's maintenance interval. Click any row to view full details and maintenance history.";
}
<div class="pcl-metric-strip">
<div class="pcl-metric-strip-cell">
@await Html.PartialAsync("_Metric", (Label: "TOTAL", Value: Model.TotalCount.ToString(), Delta: (string?)null, DeltaDir: (string?)null))
</div>
<div class="pcl-metric-strip-cell">
@await Html.PartialAsync("_Metric", (Label: "OPERATIONAL", Value: Model.Items.Count(e => e.Status == "Operational").ToString(), Delta: (string?)null, DeltaDir: (string?)null))
</div>
<div class="pcl-metric-strip-cell">
@await Html.PartialAsync("_Metric", (Label: "NEEDS SERVICE", Value: Model.Items.Count(e => e.Status == "NeedsMaintenance").ToString(), Delta: (string?)null, DeltaDir: (string?)null))
</div>
<div class="pcl-metric-strip-cell">
@await Html.PartialAsync("_Metric", (Label: "IN MAINTENANCE", Value: Model.Items.Count(e => e.Status == "UnderMaintenance").ToString(), Delta: (string?)null, DeltaDir: (string?)null))
</div>
</div>
<!-- Equipment Table Card -->
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0 py-3">
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-start align-items-lg-center gap-3">
<div class="d-flex flex-column flex-sm-row gap-2 w-100 w-lg-auto">
<form method="get" class="d-flex flex-column flex-sm-row gap-2 flex-grow-1 flex-lg-grow-0">
<input type="hidden" name="sortColumn" value="@ViewBag.SortColumn" />
<input type="hidden" name="sortDirection" value="@ViewBag.SortDirection" />
<input type="hidden" name="pageSize" value="@Model.PageSize" />
<select name="statusFilter" class="form-select" style="max-width: 250px; min-width: 150px;" onchange="this.form.submit()">
<option value="">All Statuses</option>
<option value="@((int)PowderCoating.Core.Enums.EquipmentStatus.Operational)" selected="@(ViewBag.StatusFilter == PowderCoating.Core.Enums.EquipmentStatus.Operational)">Operational</option>
<option value="@((int)PowderCoating.Core.Enums.EquipmentStatus.NeedsMaintenance)" selected="@(ViewBag.StatusFilter == PowderCoating.Core.Enums.EquipmentStatus.NeedsMaintenance)">Needs Maintenance</option>
<option value="@((int)PowderCoating.Core.Enums.EquipmentStatus.UnderMaintenance)" selected="@(ViewBag.StatusFilter == PowderCoating.Core.Enums.EquipmentStatus.UnderMaintenance)">Under Maintenance</option>
<option value="@((int)PowderCoating.Core.Enums.EquipmentStatus.OutOfService)" selected="@(ViewBag.StatusFilter == PowderCoating.Core.Enums.EquipmentStatus.OutOfService)">Out of Service</option>
<option value="@((int)PowderCoating.Core.Enums.EquipmentStatus.Retired)" selected="@(ViewBag.StatusFilter == PowderCoating.Core.Enums.EquipmentStatus.Retired)">Retired</option>
</select>
<div class="input-group" style="max-width: 350px; min-width: 200px;">
<span class="input-group-text bg-white border-end-0">
<i class="bi bi-search text-muted"></i>
</span>
<input type="text" name="searchTerm" class="form-control border-start-0"
placeholder="Search equipment..." value="@ViewBag.SearchTerm">
<button type="submit" class="btn btn-primary">
<i class="bi bi-search"></i>
</button>
</div>
</form>
<a asp-action="Create" class="btn btn-primary text-nowrap">
<i class="bi bi-plus-circle me-2"></i>
<span class="d-none d-sm-inline">Add Equipment</span>
<span class="d-inline d-sm-none">Add</span>
</a>
</div>
</div>
</div>
<div class="card-body p-0">
@if (!Model.Items.Any())
{
<div class="text-center py-5">
<i class="bi bi-inbox" style="font-size: 4rem; color: #d1d5db;"></i>
<h5 class="mt-3 text-muted">No equipment found</h5>
<p class="text-muted mb-4">Get started by adding your first equipment</p>
<a asp-action="Create" class="btn btn-primary">
<i class="bi bi-plus-circle me-2"></i>Add Your First Equipment
</a>
</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th sortable="Name" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection" class="ps-4">Equipment</th>
<th sortable="EquipmentCode" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Code</th>
<th>Type</th>
<th sortable="Status" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Status</th>
<th>Location</th>
<th sortable="NextMaintenanceDate" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Next Maintenance</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody id="equipmentTable">
@foreach (var equipment in Model.Items.Where(e => e.IsActive))
{
<tr class="equipment-row" data-equipment-id="@equipment.Id" style="cursor: pointer;">
<td class="ps-4">
<div class="d-flex align-items-center gap-2">
<div class="rounded-circle d-flex align-items-center justify-content-center"
style="width: 40px; height: 40px; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; font-weight: 600;">
@if (!string.IsNullOrEmpty(equipment.EquipmentType))
{
@equipment.EquipmentType.Substring(0, 1).ToUpper()
}
else
{
@equipment.EquipmentName.Substring(0, 1).ToUpper()
}
</div>
<div>
<div class="fw-semibold">@equipment.EquipmentName</div>
@if (!string.IsNullOrEmpty(equipment.EquipmentNumber))
{
<small class="text-muted">@equipment.EquipmentNumber</small>
}
</div>
</div>
</td>
<td>@equipment.EquipmentType</td>
<td>
@await Html.PartialAsync("_StatusChip", (Kind: StatusChipHelper.EquipmentStatus(equipment.Status), Text: equipment.StatusDisplay))
</td>
<td>
@if (!string.IsNullOrEmpty(equipment.Location))
{
<span><i class="bi bi-geo-alt me-1"></i>@equipment.Location</span>
}
else
{
<span class="text-muted">—</span>
}
</td>
<td>
@if (equipment.NextScheduledMaintenance.HasValue)
{
<span>@equipment.NextScheduledMaintenance.Value.ToString("MMM dd, yyyy")</span>
}
else
{
<span class="text-muted">Not scheduled</span>
}
</td>
<td class="text-end pe-4">
<div class="btn-group btn-group-sm">
<a asp-action="Details" asp-route-id="@equipment.Id" class="btn btn-outline-primary" title="View Details">
<i class="bi bi-eye"></i>
</a>
<a asp-action="Edit" asp-route-id="@equipment.Id" class="btn btn-outline-warning" title="Edit">
<i class="bi bi-pencil"></i>
</a>
<a asp-action="Delete" asp-route-id="@equipment.Id" class="btn btn-outline-danger" title="Delete">
<i class="bi bi-trash"></i>
</a>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
<!-- Mobile Card View -->
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var equipment in Model.Items.Where(e => e.IsActive))
{
<div class="mobile-data-card"
data-id="@equipment.Id"
onclick="window.location.href='@Url.Action("Details", new { id = equipment.Id })'">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
<i class="bi bi-tools"></i>
</div>
<div class="mobile-card-title">
<h6>@equipment.EquipmentName</h6>
<small>@equipment.EquipmentType</small>
</div>
</div>
<div class="mobile-card-body">
@if (!string.IsNullOrEmpty(equipment.EquipmentNumber))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Code</span>
<span class="mobile-card-value">@equipment.EquipmentNumber</span>
</div>
}
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value">
@switch (equipment.Status)
{
case "Operational":
<span class="badge bg-success bg-opacity-10 text-success">
<i class="bi bi-check-circle me-1"></i>@equipment.StatusDisplay
</span>
break;
case "NeedsMaintenance":
<span class="badge bg-warning bg-opacity-10 text-warning">
<i class="bi bi-exclamation-triangle me-1"></i>@equipment.StatusDisplay
</span>
break;
case "UnderMaintenance":
<span class="badge bg-info bg-opacity-10 text-info">
<i class="bi bi-wrench me-1"></i>@equipment.StatusDisplay
</span>
break;
case "OutOfService":
<span class="badge bg-danger bg-opacity-10 text-danger">
<i class="bi bi-x-circle me-1"></i>@equipment.StatusDisplay
</span>
break;
default:
<span class="badge bg-secondary bg-opacity-10 text-secondary">
@equipment.StatusDisplay
</span>
break;
}
</span>
</div>
@if (!string.IsNullOrEmpty(equipment.Location))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Location</span>
<span class="mobile-card-value"><i class="bi bi-geo-alt me-1"></i>@equipment.Location</span>
</div>
}
@if (equipment.NextScheduledMaintenance.HasValue)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Next Maintenance</span>
<span class="mobile-card-value">@equipment.NextScheduledMaintenance.Value.ToString("MMM dd, yyyy")</span>
</div>
}
</div>
<div class="mobile-card-footer">
<a href="@Url.Action("Details", new { id = equipment.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 = equipment.Id })"
class="btn btn-sm btn-outline-secondary"
onclick="event.stopPropagation();">
<i class="bi bi-pencil me-1"></i>Edit
</a>
</div>
</div>
}
</div>
</div>
}
</div>
@if (Model.TotalCount > 0)
{
@await Html.PartialAsync("_Pagination", Model)
}
</div>
@section Scripts {
<script>
// Simple search functionality
document.getElementById('searchInput')?.addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
const rows = document.querySelectorAll('#equipmentTable tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(searchTerm) ? '' : 'none';
});
});
// Make table rows clickable
document.querySelectorAll('.equipment-row').forEach(row => {
row.addEventListener('click', function(e) {
// Don't navigate if clicking on action buttons or links
if (e.target.closest('.btn-group') || e.target.closest('a') || e.target.closest('button')) {
return;
}
const equipmentId = this.getAttribute('data-equipment-id');
window.location.href = '@Url.Action("Details", "Equipment")/' + equipmentId;
});
// Add hover effect
row.addEventListener('mouseenter', function() {
this.style.backgroundColor = '#f8f9fa';
});
row.addEventListener('mouseleave', function() {
this.style.backgroundColor = '';
});
});
</script>
}