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,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>
}