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,253 @@
@model PowderCoating.Application.DTOs.Maintenance.CreateMaintenanceDto
@{
ViewData["Title"] = "Schedule Maintenance";
ViewData["PageIcon"] = "bi-wrench";
ViewData["PageHelpTitle"] = "Schedule Maintenance";
ViewData["PageHelpContent"] = "Create a maintenance record to track scheduled or completed service for any piece of equipment. You can fill in Completion Details now if the work is already done, or leave them blank and update the record after service is performed. Enabling Recurrence automatically generates future occurrences on the chosen schedule.";
var equipmentList = ViewBag.EquipmentList as Microsoft.AspNetCore.Mvc.Rendering.SelectList;
var statusList = ViewBag.StatusList as Array ?? Array.Empty<object>();
var priorityList = ViewBag.PriorityList 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="Select the equipment this service applies to. Maintenance Type is a free-text label like Preventive, Repair, or Inspection. Status: Scheduled = planned, In Progress = underway, Completed = finished. Priority: Low / Normal / High / Critical — Critical and High priorities are highlighted in the list view and on the equipment status banner.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="row g-3">
<div class="col-md-12">
<label asp-for="EquipmentId" class="form-label">Equipment <span class="text-danger">*</span></label>
<select asp-for="EquipmentId" asp-items="equipmentList" class="form-select">
<option value="">-- Select Equipment --</option>
</select>
<span asp-validation-for="EquipmentId" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="MaintenanceType" class="form-label">Maintenance Type <span class="text-danger">*</span></label>
<input asp-for="MaintenanceType" class="form-control" placeholder="e.g., Preventive, Repair, Inspection" />
<span asp-validation-for="MaintenanceType" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="ScheduledDate" class="form-label">Scheduled Date <span class="text-danger">*</span></label>
<input asp-for="ScheduledDate" type="date" class="form-control" />
<span asp-validation-for="ScheduledDate" class="text-danger"></span>
</div>
<div class="col-md-6">
<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-6">
<label asp-for="Priority" class="form-label">Priority</label>
<select asp-for="Priority" class="form-select">
@foreach (var priority in priorityList)
{
<option value="@priority">@priority.ToString()</option>
}
</select>
<span asp-validation-for="Priority" class="text-danger"></span>
</div>
</div>
</div>
<!-- Description Section -->
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">
<i class="bi bi-file-text me-2 text-primary"></i>Description
</h5>
<div class="row g-3">
<div class="col-12">
<label asp-for="Description" class="form-label">Description <span class="text-danger">*</span></label>
<textarea asp-for="Description" class="form-control" rows="4" placeholder="Describe the maintenance work to be performed"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
</div>
</div>
<!-- Completion Details (Optional) -->
<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-clipboard-check me-2 text-primary"></i>Completion Details <small class="text-muted">(Optional)</small></h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Completion Details"
data-bs-content="Fill these in when the maintenance work is finished. Completed Date updates the equipment's Last Maintenance date and triggers recalculation of its Next Scheduled date. Downtime Hours tracks how long the equipment was unavailable. Work Performed and Parts Replaced create a service history record for future reference.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="CompletedDate" class="form-label">Completed Date</label>
<input asp-for="CompletedDate" type="date" class="form-control" />
<span asp-validation-for="CompletedDate" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="DowntimeHours" class="form-label">Downtime (Hours)</label>
<input asp-for="DowntimeHours" type="number" step="0.1" min="0" class="form-control" placeholder="0.0" />
<span asp-validation-for="DowntimeHours" class="text-danger"></span>
</div>
<div class="col-12">
<label asp-for="WorkPerformed" class="form-label">Work Performed</label>
<textarea asp-for="WorkPerformed" class="form-control" rows="3" placeholder="Describe the work that was performed"></textarea>
<span asp-validation-for="WorkPerformed" class="text-danger"></span>
</div>
<div class="col-12">
<label asp-for="PartsReplaced" class="form-label">Parts Replaced</label>
<textarea asp-for="PartsReplaced" class="form-control" rows="2" placeholder="List any parts that were replaced"></textarea>
<span asp-validation-for="PartsReplaced" class="text-danger"></span>
</div>
</div>
</div>
<!-- Cost Estimates 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-currency-dollar me-2 text-primary"></i>Cost Estimates</h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Cost Estimates"
data-bs-content="Labor Cost covers technician time; Parts Cost covers materials and replacement components. Total Cost (Labor + Parts) is shown on the equipment's Maintenance History table and rolled up in reports. Enter estimates now and update with actuals after the work is complete.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="LaborCost" class="form-label">Labor Cost</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="LaborCost" type="number" step="0.01" min="0" class="form-control" placeholder="0.00" />
</div>
<span asp-validation-for="LaborCost" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="PartsCost" class="form-label">Parts Cost</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="PartsCost" type="number" step="0.01" min="0" class="form-control" placeholder="0.00" />
</div>
<span asp-validation-for="PartsCost" 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">General Notes</label>
<textarea asp-for="Notes" class="form-control" rows="3" placeholder="Enter any additional notes"></textarea>
<span asp-validation-for="Notes" class="text-danger"></span>
</div>
<div class="col-12">
<label asp-for="TechnicianNotes" class="form-label">Technician Notes</label>
<textarea asp-for="TechnicianNotes" class="form-control" rows="3" placeholder="Technical notes for the maintenance team"></textarea>
<span asp-validation-for="TechnicianNotes" class="text-danger"></span>
</div>
</div>
</div>
<!-- Recurrence 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-arrow-repeat me-2 text-primary"></i>Recurrence <small class="text-muted">(Optional)</small></h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Recurrence"
data-bs-content="Enable recurrence to auto-generate future maintenance tasks on a set schedule. Choose a frequency (Daily through Bi-Annually) and an optional end date. If no end date is set, occurrences are generated up to a default horizon: Daily = 90 days, Weekly/Bi-Weekly = 1 year, Monthly = 2 years, Quarterly/Annually = 3 years. Maximum 365 occurrences per series.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="form-check form-switch mb-3">
<input asp-for="IsRecurring" class="form-check-input" id="isRecurringToggle" role="switch" />
<label class="form-check-label" for="isRecurringToggle">Make this a recurring maintenance task</label>
</div>
<div id="recurrenceOptions" style="display:none;">
<div class="row g-3">
<div class="col-md-6">
<label asp-for="RecurrenceFrequency" class="form-label">Frequency <span class="text-danger">*</span></label>
<select asp-for="RecurrenceFrequency" class="form-select" id="recurrenceFrequency">
<option value="">-- Select Frequency --</option>
<option value="1">Daily</option>
<option value="2">Weekly</option>
<option value="3">Bi-Weekly (Every 2 Weeks)</option>
<option value="4">Monthly</option>
<option value="7">Quarterly (Every 3 Months)</option>
<option value="5">Annually</option>
<option value="6">Bi-Annually (Every 6 Months)</option>
</select>
<span asp-validation-for="RecurrenceFrequency" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="RecurrenceEndDate" class="form-label">End Date <small class="text-muted">(leave blank for default horizon)</small></label>
<input asp-for="RecurrenceEndDate" type="date" class="form-control" />
<span asp-validation-for="RecurrenceEndDate" class="text-danger"></span>
</div>
</div>
<div class="alert alert-info mt-3 small mb-0">
<i class="bi bi-info-circle me-1"></i>
If no end date is set, occurrences will be generated for:
Daily = 90 days &bull; Weekly / Bi-Weekly = 1 year &bull; Monthly = 2 years &bull; Quarterly / Annually = 3 years &bull; Bi-Annually = 18 months.
Maximum 365 occurrences per series.
</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>Schedule Maintenance
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
<script>
(function () {
var toggle = document.getElementById('isRecurringToggle');
var options = document.getElementById('recurrenceOptions');
var freqSelect = document.getElementById('recurrenceFrequency');
function syncVisibility() {
options.style.display = toggle.checked ? 'block' : 'none';
if (!toggle.checked) freqSelect.value = '';
}
toggle.addEventListener('change', syncVisibility);
syncVisibility(); // run on load (handles validation redisplay)
})();
</script>
}
@@ -0,0 +1,120 @@
@model PowderCoating.Application.DTOs.Maintenance.MaintenanceRecordDto
@{
ViewData["Title"] = "Delete Maintenance Record";
ViewData["PageIcon"] = "bi-wrench";
var seriesDeletableCount = ViewBag.SeriesDeletableCount as int?;
}
<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>
<div class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Warning:</strong> This action cannot be undone.
</div>
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-danger bg-opacity-10 border-0 py-3">
<h5 class="mb-0 text-danger">
<i class="bi bi-wrench me-2"></i>@Model.MaintenanceType — @Model.EquipmentName
</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<small class="text-muted d-block">Status</small>
<strong>@Model.StatusDisplay</strong>
</div>
<div class="col-md-6">
<small class="text-muted d-block">Priority</small>
<strong>@Model.PriorityDisplay</strong>
</div>
<div class="col-md-6">
<small class="text-muted d-block">Scheduled Date</small>
<strong>@Model.ScheduledDate.ToString("MMMM dd, yyyy")</strong>
</div>
@if (Model.IsRecurring)
{
<div class="col-md-6">
<small class="text-muted d-block">Recurrence</small>
<span class="badge bg-info text-dark">
<i class="bi bi-arrow-repeat me-1"></i>@Model.RecurrenceFrequency
</span>
</div>
}
@if (!string.IsNullOrEmpty(Model.Description))
{
<div class="col-12">
<small class="text-muted d-block">Description</small>
<span>@Model.Description</span>
</div>
}
</div>
</div>
</div>
@if (Model.IsRecurring && seriesDeletableCount.HasValue && seriesDeletableCount.Value > 0)
{
<!-- Recurring series options -->
<div class="card border-warning shadow-sm mb-4">
<div class="card-header bg-warning bg-opacity-10 border-0 py-3">
<h5 class="mb-0">
<i class="bi bi-arrow-repeat me-2 text-warning"></i>This is Part of a Recurring Series
</h5>
</div>
<div class="card-body">
<p class="mb-3">
There are <strong>@seriesDeletableCount</strong> other future scheduled/overdue occurrence@(seriesDeletableCount == 1 ? "" : "s") in this series.
Completed occurrences will not be affected.
</p>
<div class="d-flex flex-column gap-3">
<form asp-action="Delete" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="id" value="@Model.Id" />
<input type="hidden" name="deleteMode" value="single" />
<button type="submit" class="btn btn-outline-danger w-100 text-start">
<i class="bi bi-trash me-2"></i>
<strong>Delete this occurrence only</strong>
<br /><small class="ms-4">Removes only this record. The other @seriesDeletableCount occurrence@(seriesDeletableCount == 1 ? "" : "s") remain.</small>
</button>
</form>
<form asp-action="Delete" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="id" value="@Model.Id" />
<input type="hidden" name="deleteMode" value="series" />
<button type="submit" class="btn btn-danger w-100 text-start">
<i class="bi bi-collection me-2"></i>
<strong>Delete entire series</strong>
<br /><small class="ms-4">Removes this record and all @seriesDeletableCount future scheduled/overdue occurrences. Completed records are preserved.</small>
</button>
</form>
</div>
</div>
</div>
<div class="text-end">
<a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-secondary">Cancel</a>
</div>
}
else
{
<!-- Simple single delete -->
<div class="d-flex gap-2 justify-content-end">
<a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-secondary">Cancel</a>
<form asp-action="Delete" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="id" value="@Model.Id" />
<input type="hidden" name="deleteMode" value="single" />
<button type="submit" class="btn btn-danger">
<i class="bi bi-trash me-2"></i>Delete Maintenance Record
</button>
</form>
</div>
}
</div>
</div>
@@ -0,0 +1,316 @@
@model PowderCoating.Application.DTOs.Maintenance.MaintenanceRecordDto
@{
ViewData["Title"] = "Maintenance Details";
ViewData["PageIcon"] = "bi-wrench";
ViewData["PageHelpTitle"] = "Maintenance Details";
ViewData["PageHelpContent"] = "The status banner shows the current state and highlights Critical or High priority tasks. Recurring tasks display their frequency and series size. The Timeline on the right shows when this record was created, when service is scheduled, and when it was completed. Setting Status to Completed and saving updates the equipment&apos;s Next Scheduled date.";
}
<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.MaintenanceType for @Model.EquipmentName</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
{
"Scheduled" => "alert-primary",
"InProgress" => "alert-warning",
"Completed" => "alert-success",
"Overdue" => "alert-danger",
"Cancelled" => "alert-secondary",
_ => "alert-secondary"
};
var statusIcon = Model.Status switch
{
"Scheduled" => "bi-calendar-check",
"InProgress" => "bi-hourglass-split",
"Completed" => "bi-check-circle",
"Overdue" => "bi-exclamation-triangle",
"Cancelled" => "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 class="flex-grow-1">
<strong>Status:</strong> @Model.StatusDisplay
@if (Model.Priority == "Critical" || Model.Priority == "High")
{
<span class="ms-2">• Priority: <strong>@Model.PriorityDisplay</strong></span>
}
@if (Model.IsRecurring)
{
<span class="ms-3">
<span class="badge bg-info text-dark">
<i class="bi bi-arrow-repeat me-1"></i>Recurring — @Model.RecurrenceFrequency
</span>
@if (Model.RecurrenceEndDate.HasValue)
{
<small class="text-muted ms-1">until @Model.RecurrenceEndDate.Value.ToString("MMM dd, yyyy")</small>
}
@if (ViewBag.SeriesCount is int sc)
{
<small class="text-muted ms-1">(@sc occurrence@(sc == 1 ? "" : "s") in series)</small>
}
</span>
}
</div>
</div> <div class="row g-4">
<!-- Left Column -->
<div class="col-lg-8">
<!-- Maintenance 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>Maintenance Information
</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="text-muted small mb-1">Maintenance Type</label>
<p class="fw-semibold mb-0">@Model.MaintenanceType</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Priority</label>
<p class="mb-0">
@{
var priorityClass = Model.Priority switch
{
"Critical" => "danger",
"High" => "warning",
"Normal" => "primary",
"Low" => "secondary",
_ => "secondary"
};
}
<span class="badge bg-@priorityClass bg-opacity-10 text-@priorityClass">
@Model.PriorityDisplay
</span>
</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Scheduled Date</label>
<p class="mb-0">@Model.ScheduledDate.ToString("MMMM dd, yyyy")</p>
</div>
<div class="col-md-6">
<label class="text-muted small mb-1">Completed Date</label>
<p class="mb-0">
@(Model.CompletedDate.HasValue ? Model.CompletedDate.Value.ToString("MMMM dd, yyyy") : "Not completed")
</p>
</div>
<div class="col-md-12">
<label class="text-muted small mb-1">Performed By</label>
<p class="mb-0">@(Model.PerformedByName ?? "Not assigned")</p>
</div>
</div>
</div>
</div>
<!-- Equipment Details -->
<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-tools me-2 text-primary"></i>Equipment
</h5>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<p class="fw-semibold mb-1">@Model.EquipmentName</p>
<a asp-controller="Equipment" asp-action="Details" asp-route-id="@Model.EquipmentId" class="text-decoration-none">
<i class="bi bi-box-arrow-up-right me-1"></i>View Equipment Details
</a>
</div>
</div>
</div>
</div>
<!-- Work Description -->
<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-text me-2 text-primary"></i>Description
</h5>
</div>
<div class="card-body">
<p class="mb-0">@Model.Description</p>
</div>
</div>
@if (!string.IsNullOrWhiteSpace(Model.WorkPerformed))
{
<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-clipboard-check me-2 text-primary"></i>Work Performed
</h5>
</div>
<div class="card-body">
<p class="mb-0">@Model.WorkPerformed</p>
</div>
</div>
}
@if (!string.IsNullOrWhiteSpace(Model.PartsReplaced))
{
<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-gear me-2 text-primary"></i>Parts Replaced
</h5>
</div>
<div class="card-body">
<p class="mb-0">@Model.PartsReplaced</p>
</div>
</div>
}
<!-- Costs Breakdown -->
<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-currency-dollar me-2 text-primary"></i>Costs
</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="text-muted small mb-1">Labor Cost</label>
<p class="fw-semibold mb-0">@Model.LaborCost.ToString("C")</p>
</div>
<div class="col-md-4">
<label class="text-muted small mb-1">Parts Cost</label>
<p class="fw-semibold mb-0">@Model.PartsCost.ToString("C")</p>
</div>
<div class="col-md-4">
<label class="text-muted small mb-1">Total Cost</label>
<p class="fw-bold text-primary mb-0" style="font-size: 1.25rem;">@Model.TotalCost.ToString("C")</p>
</div>
<div class="col-md-12">
<label class="text-muted small mb-1">Downtime</label>
<p class="mb-0">@Model.DowntimeHours hours</p>
</div>
</div>
</div>
</div>
@if (!string.IsNullOrWhiteSpace(Model.Notes) || !string.IsNullOrWhiteSpace(Model.TechnicianNotes))
{
<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">
@if (!string.IsNullOrWhiteSpace(Model.Notes))
{
<div class="mb-3">
<label class="text-muted small mb-1">General Notes</label>
<p class="mb-0">@Model.Notes</p>
</div>
}
@if (!string.IsNullOrWhiteSpace(Model.TechnicianNotes))
{
<div>
<label class="text-muted small mb-1">Technician Notes</label>
<p class="mb-0">@Model.TechnicianNotes</p>
</div>
}
</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 Maintenance
</a>
<a asp-controller="Equipment" asp-action="Details" asp-route-id="@Model.EquipmentId" class="btn btn-outline-primary">
<i class="bi bi-tools me-2"></i>View Equipment
</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 Record
</a>
</div>
</div>
</div>
<!-- Timeline -->
<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-clock-history me-2 text-primary"></i>Timeline
</h5>
</div>
<div class="card-body">
<div class="timeline">
<div class="timeline-item mb-3">
<div class="d-flex align-items-start">
<div class="rounded-circle bg-primary bg-opacity-10 p-2 me-3">
<i class="bi bi-plus-circle text-primary"></i>
</div>
<div>
<small class="text-muted">Created</small>
<p class="mb-0 fw-semibold">@Model.CreatedAt.ToString("MMM dd, yyyy")</p>
</div>
</div>
</div>
<div class="timeline-item mb-3">
<div class="d-flex align-items-start">
<div class="rounded-circle bg-info bg-opacity-10 p-2 me-3">
<i class="bi bi-calendar-check text-info"></i>
</div>
<div>
<small class="text-muted">Scheduled For</small>
<p class="mb-0 fw-semibold">@Model.ScheduledDate.ToString("MMM dd, yyyy")</p>
</div>
</div>
</div>
@if (Model.CompletedDate.HasValue)
{
<div class="timeline-item">
<div class="d-flex align-items-start">
<div class="rounded-circle bg-success bg-opacity-10 p-2 me-3">
<i class="bi bi-check-circle text-success"></i>
</div>
<div>
<small class="text-muted">Completed</small>
<p class="mb-0 fw-semibold">@Model.CompletedDate.Value.ToString("MMM dd, yyyy")</p>
</div>
</div>
</div>
}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,261 @@
@model PowderCoating.Application.DTOs.Maintenance.UpdateMaintenanceDto
@{
ViewData["Title"] = "Edit Maintenance";
ViewData["PageIcon"] = "bi-wrench";
ViewData["PageHelpTitle"] = "Edit Maintenance";
ViewData["PageHelpContent"] = "Update the maintenance record details. Setting the Status to Completed and filling in the Completed Date will update the equipment's Last Maintenance date and recalculate its Next Scheduled date. For recurring series, changing recurrence settings will delete future pending occurrences and regenerate them from this record's date.";
var equipmentList = ViewBag.EquipmentList as Microsoft.AspNetCore.Mvc.Rendering.SelectList;
var statusList = ViewBag.StatusList as Array ?? Array.Empty<object>();
var priorityList = ViewBag.PriorityList 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="Select the equipment this service applies to. Maintenance Type is a free-text label like Preventive, Repair, or Inspection. Status: Scheduled = planned, In Progress = underway, Completed = finished. Priority: Low / Normal / High / Critical — Critical and High priorities are highlighted in the list view and on the equipment status banner.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="row g-3">
<div class="col-md-12">
<label asp-for="EquipmentId" class="form-label">Equipment <span class="text-danger">*</span></label>
<select asp-for="EquipmentId" asp-items="equipmentList" class="form-select">
<option value="">-- Select Equipment --</option>
</select>
<span asp-validation-for="EquipmentId" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="MaintenanceType" class="form-label">Maintenance Type <span class="text-danger">*</span></label>
<input asp-for="MaintenanceType" class="form-control" placeholder="e.g., Preventive, Repair, Inspection" />
<span asp-validation-for="MaintenanceType" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="ScheduledDate" class="form-label">Scheduled Date <span class="text-danger">*</span></label>
<input asp-for="ScheduledDate" type="date" class="form-control" />
<span asp-validation-for="ScheduledDate" class="text-danger"></span>
</div>
<div class="col-md-6">
<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-6">
<label asp-for="Priority" class="form-label">Priority</label>
<select asp-for="Priority" class="form-select">
@foreach (var priority in priorityList)
{
<option value="@priority">@priority.ToString()</option>
}
</select>
<span asp-validation-for="Priority" class="text-danger"></span>
</div>
</div>
</div>
<!-- Description Section -->
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">
<i class="bi bi-file-text me-2 text-primary"></i>Description
</h5>
<div class="row g-3">
<div class="col-12">
<label asp-for="Description" class="form-label">Description <span class="text-danger">*</span></label>
<textarea asp-for="Description" class="form-control" rows="4" placeholder="Describe the maintenance work to be performed"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
</div>
</div>
<!-- Completion Details -->
<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-clipboard-check me-2 text-primary"></i>Completion Details</h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Completion Details"
data-bs-content="Fill these in when the maintenance work is finished. Completed Date updates the equipment's Last Maintenance date and triggers recalculation of its Next Scheduled date. Downtime Hours tracks how long the equipment was unavailable. Work Performed and Parts Replaced create a service history record for future reference.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="CompletedDate" class="form-label">Completed Date</label>
<input asp-for="CompletedDate" type="date" class="form-control" />
<span asp-validation-for="CompletedDate" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="DowntimeHours" class="form-label">Downtime (Hours)</label>
<input asp-for="DowntimeHours" type="number" step="0.1" min="0" class="form-control" placeholder="0.0" />
<span asp-validation-for="DowntimeHours" class="text-danger"></span>
</div>
<div class="col-12">
<label asp-for="WorkPerformed" class="form-label">Work Performed</label>
<textarea asp-for="WorkPerformed" class="form-control" rows="3" placeholder="Describe the work that was performed"></textarea>
<span asp-validation-for="WorkPerformed" class="text-danger"></span>
</div>
<div class="col-12">
<label asp-for="PartsReplaced" class="form-label">Parts Replaced</label>
<textarea asp-for="PartsReplaced" class="form-control" rows="2" placeholder="List any parts that were replaced"></textarea>
<span asp-validation-for="PartsReplaced" class="text-danger"></span>
</div>
</div>
</div>
<!-- Cost Estimates 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-currency-dollar me-2 text-primary"></i>Costs</h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Costs"
data-bs-content="Labor Cost covers technician time; Parts Cost covers materials and replacement components. Total Cost (Labor + Parts) is shown on the equipment's Maintenance History table and rolled up in reports. Update with actuals after the work is complete.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="LaborCost" class="form-label">Labor Cost</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="LaborCost" type="number" step="0.01" min="0" class="form-control" placeholder="0.00" />
</div>
<span asp-validation-for="LaborCost" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="PartsCost" class="form-label">Parts Cost</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="PartsCost" type="number" step="0.01" min="0" class="form-control" placeholder="0.00" />
</div>
<span asp-validation-for="PartsCost" 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">General Notes</label>
<textarea asp-for="Notes" class="form-control" rows="3" placeholder="Enter any additional notes"></textarea>
<span asp-validation-for="Notes" class="text-danger"></span>
</div>
<div class="col-12">
<label asp-for="TechnicianNotes" class="form-label">Technician Notes</label>
<textarea asp-for="TechnicianNotes" class="form-control" rows="3" placeholder="Technical notes for the maintenance team"></textarea>
<span asp-validation-for="TechnicianNotes" class="text-danger"></span>
</div>
</div>
</div>
<!-- Recurrence 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-arrow-repeat me-2 text-primary"></i>Recurrence <small class="text-muted">(Optional)</small></h5>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Recurrence"
data-bs-content="Enable recurrence to auto-generate future maintenance tasks on a set schedule. Changing the frequency or end date on a recurring series will delete all future Scheduled/Overdue occurrences and regenerate them from this record's date. Completed occurrences are never affected.">
<i class="bi bi-question-circle"></i>
</a>
</div>
@if (ViewBag.IsRecurringSeries as bool? == true)
{
<div class="alert alert-warning mb-3 small">
<i class="bi bi-exclamation-triangle me-1"></i>
<strong>This is part of a recurring series.</strong>
Changing recurrence settings will delete all future scheduled/overdue occurrences and regenerate them from this record's date.
Completed occurrences will not be affected.
</div>
}
<div class="form-check form-switch mb-3">
<input asp-for="IsRecurring" class="form-check-input" id="isRecurringToggle" role="switch" />
<label class="form-check-label" for="isRecurringToggle">Make this a recurring maintenance task</label>
</div>
<div id="recurrenceOptions" style="display:none;">
<div class="row g-3">
<div class="col-md-6">
<label asp-for="RecurrenceFrequency" class="form-label">Frequency <span class="text-danger">*</span></label>
<select asp-for="RecurrenceFrequency" class="form-select" id="recurrenceFrequency">
<option value="">-- Select Frequency --</option>
<option value="1">Daily</option>
<option value="2">Weekly</option>
<option value="3">Bi-Weekly (Every 2 Weeks)</option>
<option value="4">Monthly</option>
<option value="7">Quarterly (Every 3 Months)</option>
<option value="5">Annually</option>
<option value="6">Bi-Annually (Every 6 Months)</option>
</select>
<span asp-validation-for="RecurrenceFrequency" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="RecurrenceEndDate" class="form-label">End Date <small class="text-muted">(leave blank for default horizon)</small></label>
<input asp-for="RecurrenceEndDate" type="date" class="form-control" />
<span asp-validation-for="RecurrenceEndDate" class="text-danger"></span>
</div>
</div>
<div class="alert alert-info mt-3 small mb-0">
<i class="bi bi-info-circle me-1"></i>
If no end date is set, occurrences will be generated for:
Daily = 90 days &bull; Weekly / Bi-Weekly = 1 year &bull; Monthly = 2 years &bull; Quarterly / Annually = 3 years &bull; Bi-Annually = 18 months.
Maximum 365 occurrences per series.
</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" />
<script>
(function () {
var toggle = document.getElementById('isRecurringToggle');
var options = document.getElementById('recurrenceOptions');
function syncVisibility() {
options.style.display = toggle.checked ? 'block' : 'none';
}
toggle.addEventListener('change', syncVisibility);
syncVisibility();
})();
</script>
}
@@ -0,0 +1,234 @@
@model PagedResult<PowderCoating.Application.DTOs.Maintenance.MaintenanceListDto>
@{
ViewData["Title"] = "Maintenance Records";
ViewData["PageIcon"] = "bi-wrench";
ViewData["PageHelpTitle"] = "Maintenance";
ViewData["PageHelpContent"] = "Track all scheduled and completed maintenance for your shop equipment. Overdue records are those past their scheduled date without a Completed status. Recurring tasks auto-generate future occurrences based on the frequency you set. Click any row to view full details, or use Add Maintenance to schedule new service for any piece of equipment.";
var equipmentId = ViewBag.EquipmentId as int?;
var equipmentName = ViewBag.EquipmentName as string;
}
<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: "SCHEDULED", Value: Model.Items.Count(m => m.Status == "Scheduled").ToString(), Delta: (string?)null, DeltaDir: (string?)null))
</div>
<div class="pcl-metric-strip-cell">
@await Html.PartialAsync("_Metric", (Label: "IN PROGRESS", Value: Model.Items.Count(m => m.Status == "InProgress").ToString(), Delta: (string?)null, DeltaDir: (string?)null))
</div>
<div class="pcl-metric-strip-cell">
@await Html.PartialAsync("_Metric", (Label: "OVERDUE", Value: Model.Items.Count(m => m.Status == "Overdue").ToString(), Delta: (string?)null, DeltaDir: (string?)null))
</div>
</div>
@if ((bool)(ViewBag.PendingOnly ?? false))
{
<div class="alert alert-warning alert-permanent d-flex justify-content-between align-items-center mb-3">
<div>
<i class="bi bi-funnel-fill me-2"></i>
Showing <strong>@Model.TotalCount</strong> pending maintenance record@(Model.TotalCount == 1 ? "" : "s") — Scheduled, In Progress, or Overdue
</div>
<a href="@Url.Action("Index")" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-x me-1"></i>Show All
</a>
</div>
}
<!-- Maintenance Records Table Card -->
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0 py-3">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-2">
<span class="fw-semibold">
@if (equipmentId.HasValue && !string.IsNullOrEmpty(equipmentName))
{
@equipmentName
}
else
{
<span>All Maintenance Records</span>
}
</span>
<a tabindex="0" class="help-icon" role="button"
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
data-bs-title="Maintenance Records"
data-bs-content="Scheduled = upcoming work not yet started. In Progress = work has begun. Completed = service finished (updates Last Maintenance date on the equipment). Overdue = past the scheduled date with no completion recorded. Cancelled = task voided. Recurring tasks show a blue badge and auto-generate future occurrences when saved.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="d-flex gap-2">
@if (equipmentId.HasValue)
{
<a asp-controller="Equipment" asp-action="Details" asp-route-id="@equipmentId" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>Back to Equipment
</a>
<a asp-action="Create" asp-route-equipmentId="@equipmentId" class="btn btn-primary">
<i class="bi bi-plus-circle me-2"></i>Add Maintenance
</a>
}
else
{
<a asp-action="Create" class="btn btn-primary">
<i class="bi bi-plus-circle me-2"></i>Add Maintenance
</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 maintenance records found</h5>
<p class="text-muted mb-4">Get started by scheduling maintenance</p>
<a asp-action="Create" asp-route-equipmentId="@equipmentId" class="btn btn-primary">
<i class="bi bi-plus-circle me-2"></i>Schedule Maintenance
</a>
</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th class="ps-4">Equipment</th>
<th sortable="MaintenanceType" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Type</th>
<th sortable="ScheduledDate" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Scheduled Date</th>
<th sortable="Status" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Status</th>
<th sortable="Priority" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Priority</th>
<th sortable="Cost" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Cost</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var maintenance in Model.Items)
{
<tr style="cursor: pointer;" onclick="window.location.href='@Url.Action("Details", "Maintenance", new { id = maintenance.Id })'">
<td class="ps-4">
<div class="fw-semibold">@maintenance.EquipmentName</div>
</td>
<td>
@maintenance.MaintenanceType
@if (maintenance.IsRecurring)
{
<br /><span class="badge bg-info text-dark" style="font-size:0.7em;"><i class="bi bi-arrow-repeat me-1"></i>Recurring</span>
}
</td>
<td>
<div>@maintenance.ScheduledDate.ToString("MMM dd, yyyy")</div>
@if (maintenance.CompletedDate.HasValue)
{
<small class="text-muted">Completed: @maintenance.CompletedDate.Value.ToString("MMM dd, yyyy")</small>
}
</td>
<td>
@await Html.PartialAsync("_StatusChip", (Kind: StatusChipHelper.MaintenanceStatus(maintenance.Status), Text: maintenance.StatusDisplay))
</td>
<td>
@await Html.PartialAsync("_StatusChip", (Kind: StatusChipHelper.MaintenancePriority(maintenance.Priority), Text: maintenance.PriorityDisplay))
</td>
<td>
<span class="fw-semibold">@maintenance.TotalCost.ToString("C")</span>
</td>
<td class="text-end pe-4" onclick="event.stopPropagation();">
<div class="btn-group btn-group-sm">
<a asp-action="Details" asp-route-id="@maintenance.Id" class="btn btn-outline-primary" title="View Details">
<i class="bi bi-eye"></i>
</a>
<a asp-action="Edit" asp-route-id="@maintenance.Id" class="btn btn-outline-warning" title="Edit">
<i class="bi bi-pencil"></i>
</a>
<a asp-action="Delete" asp-route-id="@maintenance.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 maintenance in Model.Items)
{
<div class="mobile-data-card"
data-id="@maintenance.Id"
onclick="window.location.href='@Url.Action("Details", new { id = maintenance.Id })'">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
<i class="bi bi-wrench"></i>
</div>
<div class="mobile-card-title">
<h6>@maintenance.EquipmentName</h6>
<small>
@maintenance.MaintenanceType
@if (maintenance.IsRecurring)
{
<span class="badge bg-info text-dark ms-1" style="font-size:0.65em;"><i class="bi bi-arrow-repeat"></i> Recurring</span>
}
</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Scheduled</span>
<span class="mobile-card-value">@maintenance.ScheduledDate.ToString("MMM dd, yyyy")</span>
</div>
@if (maintenance.CompletedDate.HasValue)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Completed</span>
<span class="mobile-card-value">@maintenance.CompletedDate.Value.ToString("MMM dd, yyyy")</span>
</div>
}
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value">
@await Html.PartialAsync("_StatusChip", (Kind: StatusChipHelper.MaintenanceStatus(maintenance.Status), Text: maintenance.StatusDisplay))
</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Priority</span>
<span class="mobile-card-value">
@await Html.PartialAsync("_StatusChip", (Kind: StatusChipHelper.MaintenancePriority(maintenance.Priority), Text: maintenance.PriorityDisplay))
</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Cost</span>
<span class="mobile-card-value fw-semibold text-primary">@maintenance.TotalCost.ToString("C")</span>
</div>
</div>
<div class="mobile-card-footer">
<a href="@Url.Action("Details", new { id = maintenance.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 = maintenance.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>