Initial commit
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
@{
|
||||
ViewData["Title"] = "Schedule";
|
||||
ViewData["PageIcon"] = "bi-calendar3";
|
||||
ViewData["PageHelpTitle"] = "Schedule";
|
||||
ViewData["PageHelpContent"] = "Master schedule view showing jobs, appointments, and maintenance together. Jobs appear as all-day banners; appointments as timed blocks. Drag unscheduled jobs from the left panel onto any day to schedule them. Drag jobs between days to reschedule. Click any event to view details.";
|
||||
var currentView = ViewBag.CurrentView ?? "month";
|
||||
var currentDate = ViewBag.CurrentDate ?? DateTime.Today;
|
||||
}
|
||||
|
||||
<link rel="stylesheet" href="~/css/appointment-calendar.css" asp-append-version="true" />
|
||||
|
||||
@Html.AntiForgeryToken()
|
||||
|
||||
<div class="d-flex justify-content-end align-items-center mb-4">
|
||||
<div class="btn-group" role="group">
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-list-ul"></i> List View
|
||||
</a>
|
||||
<a asp-action="Create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> New Appointment
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calendar Controls (full width) -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-4">
|
||||
<div class="btn-group w-100" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary" id="btnPrevious">
|
||||
<i class="bi bi-chevron-left"></i> Previous
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary" id="btnToday">
|
||||
Today
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="btnNext">
|
||||
Next <i class="bi bi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<h5 class="mb-0 mt-3 mt-md-0" id="currentDateDisplay">Loading...</h5>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="btn-group w-100 mt-3 mt-md-0" role="group">
|
||||
<button type="button" class="btn btn-outline-primary" id="btnDayView" data-view="day">
|
||||
<i class="bi bi-calendar-day"></i> Day
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary" id="btnWeekView" data-view="week">
|
||||
<i class="bi bi-calendar-week"></i> Week
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary" id="btnMonthView" data-view="month">
|
||||
<i class="bi bi-calendar-month"></i> Month
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar + Calendar flex layout -->
|
||||
<div class="d-flex gap-3 align-items-start mb-3">
|
||||
|
||||
<!-- Unscheduled Jobs Sidebar -->
|
||||
<div id="unscheduledSidebar" class="schedule-sidebar">
|
||||
<div class="card h-100">
|
||||
<div class="card-header schedule-sidebar-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold small" id="sidebarTitleText"><i class="bi bi-clock-history me-1"></i>Unscheduled</span>
|
||||
<button type="button" class="btn btn-link btn-sm p-0 text-secondary" id="btnCollapseSidebar" title="Collapse panel">
|
||||
<i class="bi bi-chevron-left" id="sidebarChevron"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-muted" style="font-size:0.7rem;" id="unscheduledCount"></div>
|
||||
</div>
|
||||
<div class="card-body p-2 schedule-sidebar-body" id="unscheduledJobsPanel">
|
||||
<div class="text-center py-3">
|
||||
<div class="spinner-border spinner-border-sm text-secondary" role="status"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calendar Container -->
|
||||
<div class="flex-grow-1 min-width-0">
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div id="calendarContainer">
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading calendar...</span>
|
||||
</div>
|
||||
<p class="text-muted mt-3">Loading schedule...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="card mt-0">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<!-- Appointment Types -->
|
||||
<div class="col-md-5">
|
||||
<h6 class="mb-2"><i class="bi bi-calendar-event me-2"></i>Appointment Types</h6>
|
||||
<div class="row g-1">
|
||||
@{
|
||||
var appointmentTypes = ViewBag.AppointmentTypesForLegend as List<PowderCoating.Core.Entities.AppointmentTypeLookup> ?? new List<PowderCoating.Core.Entities.AppointmentTypeLookup>();
|
||||
foreach (var type in appointmentTypes)
|
||||
{
|
||||
<div class="col-6">
|
||||
<span class="badge calendar-event-@type.ColorClass w-100 text-truncate">
|
||||
@if (!string.IsNullOrEmpty(type.IconClass))
|
||||
{
|
||||
<i class="@type.IconClass me-1"></i>
|
||||
}
|
||||
@type.DisplayName
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job Status Colors -->
|
||||
<div class="col-md-4">
|
||||
<h6 class="mb-2"><i class="bi bi-briefcase me-2"></i>Job Status</h6>
|
||||
<div class="row g-1">
|
||||
<div class="col-6"><span class="badge w-100" style="background:#6c757d">Pending/Quoted</span></div>
|
||||
<div class="col-6"><span class="badge w-100" style="background:#0dcaf0">Approved</span></div>
|
||||
<div class="col-6"><span class="badge w-100" style="background:#0d6efd">In Prep</span></div>
|
||||
<div class="col-6"><span class="badge w-100" style="background:#fd7e14">In Oven</span></div>
|
||||
<div class="col-6"><span class="badge w-100" style="background:#198754">Ready/Done</span></div>
|
||||
<div class="col-6"><span class="badge w-100" style="background:#dc3545">Overdue</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Maintenance Priority -->
|
||||
<div class="col-md-3">
|
||||
<h6 class="mb-2"><i class="bi bi-tools me-2"></i>Maintenance</h6>
|
||||
<div class="row g-1">
|
||||
<div class="col-12"><span class="badge calendar-event-red w-100">Critical</span></div>
|
||||
<div class="col-12"><span class="badge calendar-event-orange w-100">High</span></div>
|
||||
<div class="col-12"><span class="badge calendar-event-yellow w-100">Normal</span></div>
|
||||
<div class="col-12"><span class="badge calendar-event-gray w-100">Low</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job hover preview card -->
|
||||
<div id="sjPreviewCard" style="display:none;position:fixed;z-index:1200;pointer-events:none;"></div>
|
||||
|
||||
<!-- Toast container -->
|
||||
<div id="scheduleToastContainer" class="position-fixed bottom-0 end-0 p-3" style="z-index:1100"></div>
|
||||
|
||||
<!-- Include Quick Create Modal -->
|
||||
<partial name="_QuickCreateModal" />
|
||||
|
||||
@section Scripts {
|
||||
<script src="~/js/appointment-calendar.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const initialView = '@currentView';
|
||||
const initialDate = new Date('@currentDate.ToString("yyyy-MM-dd")');
|
||||
appointmentCalendar.init(initialView, initialDate);
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
@model PowderCoating.Application.DTOs.Appointment.CreateAppointmentDto
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "New Appointment";
|
||||
ViewData["PageIcon"] = "bi-calendar-plus";
|
||||
ViewData["PageHelpTitle"] = "New Appointment";
|
||||
ViewData["PageHelpContent"] = "Create an appointment to schedule a customer visit, drop-off, pick-up, or consultation. Select the Type first — the Linked Job field appears once a type is chosen. Reminder notifications fire before the scheduled start time.";
|
||||
}
|
||||
|
||||
<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"></i> Back to List
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form asp-action="Create" method="post">
|
||||
@if (!ViewData.ModelState.IsValid)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<h6 class="alert-heading"><i class="bi bi-exclamation-triangle me-2"></i>Please correct the following errors:</h6>
|
||||
<partial name="_ValidationSummary" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Title -->
|
||||
<div class="mb-3">
|
||||
<label asp-for="Title" class="form-label">Title <span class="text-danger">*</span></label>
|
||||
<input asp-for="Title" class="form-control" placeholder="e.g., John's Rims - Drop Off" />
|
||||
<span asp-validation-for="Title" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="mb-3">
|
||||
<label asp-for="Description" class="form-label">Description</label>
|
||||
<textarea asp-for="Description" class="form-control" rows="3" placeholder="Additional details about the appointment..."></textarea>
|
||||
<span asp-validation-for="Description" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Customer -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label asp-for="CustomerId" class="form-label">Customer</label>
|
||||
<select asp-for="CustomerId" class="form-select" asp-items="ViewBag.Customers">
|
||||
<option value="">-- Select Customer (Optional) --</option>
|
||||
</select>
|
||||
<span asp-validation-for="CustomerId" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<!-- Appointment Type -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="d-flex align-items-center gap-1 mb-1">
|
||||
<label asp-for="AppointmentTypeId" class="form-label mb-0">Type <span class="text-danger">*</span></label>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Appointment Type"
|
||||
data-bs-content="Drop-Off: customer brings items in. Pick-Up: customer collects completed work. Consultation/Quote: meeting to discuss pricing. Job Work: block time for a specific job. The Linked Job field appears after you select a type.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
<select asp-for="AppointmentTypeId" class="form-select" asp-items="ViewBag.AppointmentTypes" id="appointmentType">
|
||||
<option value="">-- Select Type --</option>
|
||||
</select>
|
||||
<span asp-validation-for="AppointmentTypeId" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job (conditional) -->
|
||||
<div class="mb-3" id="jobField" style="display: none;">
|
||||
<div class="d-flex align-items-center gap-1 mb-1">
|
||||
<label asp-for="JobId" class="form-label mb-0">Linked Job <span class="text-danger" id="jobRequired">*</span></label>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Linked Job"
|
||||
data-bs-content="Connect this appointment to an active job so it appears on the job timeline. Useful for Drop-Offs, Pick-Ups, and scheduled job work. Optional for consultations and internal meetings.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
<select asp-for="JobId" class="form-select" asp-items="ViewBag.Jobs">
|
||||
<option value="">-- Select Job (Optional) --</option>
|
||||
</select>
|
||||
<span asp-validation-for="JobId" class="text-danger"></span>
|
||||
<small class="text-muted">Link this appointment to an existing job.</small>
|
||||
</div>
|
||||
|
||||
<!-- All Day Checkbox -->
|
||||
<div class="mb-3 form-check">
|
||||
<input asp-for="IsAllDay" class="form-check-input" id="isAllDay" />
|
||||
<label asp-for="IsAllDay" class="form-check-label">
|
||||
All Day Event
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Start Date/Time -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label asp-for="ScheduledStartTime" class="form-label">Start <span class="text-danger">*</span></label>
|
||||
<input asp-for="ScheduledStartTime" type="datetime-local" class="form-control" id="startTime" />
|
||||
<span asp-validation-for="ScheduledStartTime" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<!-- End Date/Time -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label asp-for="ScheduledEndTime" class="form-label">End <span class="text-danger">*</span></label>
|
||||
<input asp-for="ScheduledEndTime" type="datetime-local" class="form-control" id="endTime" />
|
||||
<span asp-validation-for="ScheduledEndTime" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Assigned Worker -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label asp-for="AssignedUserId" class="form-label">Assign To Worker</label>
|
||||
<select asp-for="AssignedUserId" class="form-select" asp-items="ViewBag.Workers">
|
||||
<option value="">-- No Assignment --</option>
|
||||
</select>
|
||||
<span asp-validation-for="AssignedUserId" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<!-- Location -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label asp-for="Location" class="form-label">Location</label>
|
||||
<input asp-for="Location" class="form-control" placeholder="e.g., Main Office, Loading Dock" />
|
||||
<span asp-validation-for="Location" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Internal Notes -->
|
||||
<div class="mb-3">
|
||||
<label asp-for="Notes" class="form-label">Internal Notes</label>
|
||||
<textarea asp-for="Notes" class="form-control" rows="2" placeholder="Notes for staff (not visible to customer)..."></textarea>
|
||||
<span asp-validation-for="Notes" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<!-- Reminder Settings -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<h6 class="mb-0"><i class="bi bi-bell me-2"></i>Reminder Settings</h6>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Reminder Settings"
|
||||
data-bs-content="Enable a reminder to receive an in-app notification before the appointment. Set how many minutes in advance — e.g., 30 for a brief heads-up, 1440 for a full day before. Reminders are per-appointment and do not send external emails or SMS.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3 form-check">
|
||||
<input asp-for="IsReminderEnabled" class="form-check-input" id="reminderEnabled" checked />
|
||||
<label asp-for="IsReminderEnabled" class="form-check-label">
|
||||
Send reminder notification
|
||||
</label>
|
||||
</div>
|
||||
<div class="mb-0" id="reminderTime">
|
||||
<label asp-for="ReminderMinutesBefore" class="form-label">Remind me</label>
|
||||
<div class="input-group">
|
||||
<input asp-for="ReminderMinutesBefore" type="number" class="form-control" min="5" max="1440" value="30" />
|
||||
<span class="input-group-text">minutes before</span>
|
||||
</div>
|
||||
<span asp-validation-for="ReminderMinutesBefore" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i> Create Appointment
|
||||
</button>
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar - Help -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h6 class="mb-0"><i class="bi bi-info-circle me-2"></i>Tips</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6>Appointment Types:</h6>
|
||||
<ul class="small mb-3">
|
||||
<li><strong>Customer Drop-Off:</strong> Customer bringing items to the shop</li>
|
||||
<li><strong>Customer Pick-Up:</strong> Customer collecting completed items</li>
|
||||
<li><strong>Consultation/Quote:</strong> Meeting to discuss pricing and requirements</li>
|
||||
<li><strong>Scheduled Job Work:</strong> Blocking time for working on a specific job</li>
|
||||
</ul>
|
||||
<h6>Reminders:</h6>
|
||||
<p class="small mb-0">Set a reminder to receive a notification before the appointment starts. Useful for preparing materials or coordinating with staff.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
<script>
|
||||
// Show/hide job field based on appointment type
|
||||
document.getElementById('appointmentType').addEventListener('change', function() {
|
||||
const jobField = document.getElementById('jobField');
|
||||
const jobRequired = document.getElementById('jobRequired');
|
||||
// You can add logic here to check if selected type requires job link
|
||||
// For now, we'll show it for all types but mark required only for JOB_WORK
|
||||
if (this.value) {
|
||||
jobField.style.display = 'block';
|
||||
} else {
|
||||
jobField.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle reminder time visibility
|
||||
document.getElementById('reminderEnabled').addEventListener('change', function() {
|
||||
const reminderTime = document.getElementById('reminderTime');
|
||||
reminderTime.style.display = this.checked ? 'block' : 'none';
|
||||
});
|
||||
|
||||
// Set default start time to tomorrow at 9 AM if empty
|
||||
const startTimeInput = document.getElementById('startTime');
|
||||
const endTimeInput = document.getElementById('endTime');
|
||||
|
||||
if (!startTimeInput.value) {
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(9, 0, 0, 0);
|
||||
startTimeInput.value = tomorrow.toISOString().slice(0, 16);
|
||||
|
||||
const endTime = new Date(tomorrow);
|
||||
endTime.setHours(10, 0, 0, 0);
|
||||
endTimeInput.value = endTime.toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
// Auto-update end time when start time changes
|
||||
startTimeInput.addEventListener('change', function() {
|
||||
const isAllDay = document.getElementById('isAllDay').checked;
|
||||
const newEndTime = new Date(this.value);
|
||||
|
||||
if (isAllDay) {
|
||||
// For all-day events, set end date to same as start date
|
||||
endTimeInput.value = this.value;
|
||||
} else {
|
||||
// For timed events, set end time to 1 hour after start time
|
||||
newEndTime.setHours(newEndTime.getHours() + 1);
|
||||
endTimeInput.value = newEndTime.toISOString().slice(0, 16);
|
||||
}
|
||||
});
|
||||
|
||||
// Hide time inputs when "All Day" is checked
|
||||
document.getElementById('isAllDay').addEventListener('change', function() {
|
||||
const timeInputs = document.querySelectorAll('#startTime, #endTime');
|
||||
timeInputs.forEach(input => {
|
||||
if (this.checked) {
|
||||
input.type = 'date';
|
||||
} else {
|
||||
input.type = 'datetime-local';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
@model PowderCoating.Application.DTOs.Appointment.AppointmentDto
|
||||
|
||||
@{
|
||||
ViewData["Title"] = $"Appointment {Model.AppointmentNumber}";
|
||||
ViewData["PageIcon"] = "bi-calendar-event";
|
||||
ViewData["PageHelpTitle"] = "Appointment Details";
|
||||
ViewData["PageHelpContent"] = "View all details for this appointment. Edit to update status or record actual times. Deleting permanently removes the record — consider setting status to Cancelled instead to preserve history.";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-end gap-2 mb-4">
|
||||
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</a>
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Back to List
|
||||
</a>
|
||||
</div><div class="row g-4">
|
||||
<!-- Left Column - Customer & Schedule Info -->
|
||||
<div class="col-lg-6">
|
||||
<!-- Customer Information -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0"><i class="bi bi-person-circle me-2"></i>Customer Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (Model.CustomerId.HasValue && !string.IsNullOrEmpty(Model.CustomerName))
|
||||
{
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4 text-muted">Customer:</div>
|
||||
<div class="col-sm-8">
|
||||
<strong>@Model.CustomerName</strong>
|
||||
</div>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(Model.CustomerPhone))
|
||||
{
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4 text-muted">Phone:</div>
|
||||
<div class="col-sm-8">
|
||||
<a href="tel:@Model.CustomerPhone">@Model.CustomerPhone</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.CustomerEmail))
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm-4 text-muted">Email:</div>
|
||||
<div class="col-sm-8">
|
||||
<a href="mailto:@Model.CustomerEmail">@Model.CustomerEmail</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-info mb-0">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
<strong>Internal Appointment</strong><br />
|
||||
<small>This appointment is not associated with a customer.</small>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schedule Information -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0"><i class="bi bi-calendar-event me-2"></i>Schedule</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4 text-muted">Date:</div>
|
||||
<div class="col-sm-8">
|
||||
<strong>@Model.ScheduledStartTime.ToString("dddd, MMMM dd, yyyy")</strong>
|
||||
</div>
|
||||
</div>
|
||||
@if (Model.IsAllDay)
|
||||
{
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4 text-muted">Time:</div>
|
||||
<div class="col-sm-8">
|
||||
<span class="badge bg-secondary">All Day</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4 text-muted">Start Time:</div>
|
||||
<div class="col-sm-8">@Model.ScheduledStartTime.ToString("h:mm tt")</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4 text-muted">End Time:</div>
|
||||
<div class="col-sm-8">@Model.ScheduledEndTime.ToString("h:mm tt")</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-4 text-muted">Duration:</div>
|
||||
<div class="col-sm-8">
|
||||
@{
|
||||
var duration = Model.ScheduledEndTime - Model.ScheduledStartTime;
|
||||
var hours = (int)duration.TotalHours;
|
||||
var minutes = duration.Minutes;
|
||||
}
|
||||
@if (hours > 0)
|
||||
{
|
||||
<span>@hours hour@(hours != 1 ? "s" : "")</span>
|
||||
}
|
||||
@if (minutes > 0)
|
||||
{
|
||||
<span>@minutes minute@(minutes != 1 ? "s" : "")</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.Location))
|
||||
{
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-sm-4 text-muted">Location:</div>
|
||||
<div class="col-sm-8">
|
||||
<i class="bi bi-geo-alt"></i> @Model.Location
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actual Times (if recorded) -->
|
||||
@if (Model.ActualStartTime.HasValue || Model.ActualEndTime.HasValue)
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h5 class="mb-0"><i class="bi bi-clock-history me-2"></i>Actual Times</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (Model.ActualStartTime.HasValue)
|
||||
{
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4 text-muted">Arrived:</div>
|
||||
<div class="col-sm-8">@Model.ActualStartTime.Value.ToString("h:mm tt")</div>
|
||||
</div>
|
||||
}
|
||||
@if (Model.ActualEndTime.HasValue)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm-4 text-muted">Completed:</div>
|
||||
<div class="col-sm-8">@Model.ActualEndTime.Value.ToString("h:mm tt")</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Right Column - Appointment Details -->
|
||||
<div class="col-lg-6">
|
||||
<!-- Appointment Information -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-secondary text-white">
|
||||
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>Appointment Details</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4 text-muted">Type:</div>
|
||||
<div class="col-sm-8">
|
||||
<span class="badge bg-@Model.TypeColorClass">
|
||||
@if (!string.IsNullOrEmpty(Model.TypeIconClass))
|
||||
{
|
||||
<i class="@Model.TypeIconClass me-1"></i>
|
||||
}
|
||||
@Model.TypeDisplayName
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4 text-muted">Status:</div>
|
||||
<div class="col-sm-8">
|
||||
<span class="badge bg-@Model.StatusColorClass">
|
||||
@if (!string.IsNullOrEmpty(Model.StatusIconClass))
|
||||
{
|
||||
<i class="@Model.StatusIconClass me-1"></i>
|
||||
}
|
||||
@Model.StatusDisplayName
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (Model.JobId.HasValue)
|
||||
{
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4 text-muted">Linked Job:</div>
|
||||
<div class="col-sm-8">
|
||||
<a asp-controller="Jobs" asp-action="Details" asp-route-id="@Model.JobId">
|
||||
@Model.JobNumber
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.AssignedWorkerName))
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm-4 text-muted">Assigned To:</div>
|
||||
<div class="col-sm-8">
|
||||
<span class="badge bg-info">
|
||||
<i class="bi bi-person"></i> @Model.AssignedWorkerName
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description & Notes -->
|
||||
@if (!string.IsNullOrEmpty(Model.Description) || !string.IsNullOrEmpty(Model.Notes))
|
||||
{
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-file-text me-2"></i>Description & Notes</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (!string.IsNullOrEmpty(Model.Description))
|
||||
{
|
||||
<div class="mb-3">
|
||||
<strong>Description:</strong>
|
||||
<p class="mb-0 mt-1">@Model.Description</p>
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.Notes))
|
||||
{
|
||||
<div>
|
||||
<strong>Internal Notes:</strong>
|
||||
<p class="mb-0 mt-1 text-muted">@Model.Notes</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Reminder Settings -->
|
||||
@if (Model.IsReminderEnabled)
|
||||
{
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-bell me-2"></i>Reminder</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-4 text-muted">Reminder:</div>
|
||||
<div class="col-sm-8">
|
||||
<i class="bi bi-check-circle text-success"></i>
|
||||
@Model.ReminderMinutesBefore minutes before
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Audit Information -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-clock-history me-2"></i>History</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-4 text-muted">Created:</div>
|
||||
<div class="col-sm-8">
|
||||
@Model.CreatedAt.ToString("MMM dd, yyyy h:mm tt")
|
||||
@if (!string.IsNullOrEmpty(Model.CreatedBy))
|
||||
{
|
||||
<br /><small class="text-muted">by @Model.CreatedBy</small>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-primary">
|
||||
<i class="bi bi-pencil"></i> Edit Appointment
|
||||
</a>
|
||||
<a asp-action="Calendar" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-calendar3"></i> View Calendar
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-danger" onclick="deleteAppointment(@Model.Id, '@Model.AppointmentNumber')">
|
||||
<i class="bi bi-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function deleteAppointment(id, appointmentNumber) {
|
||||
if (confirm(`Are you sure you want to delete appointment ${appointmentNumber}?\n\nThis action cannot be undone.`)) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '@Url.Action("Delete")';
|
||||
|
||||
const idInput = document.createElement('input');
|
||||
idInput.type = 'hidden';
|
||||
idInput.name = 'id';
|
||||
idInput.value = id;
|
||||
form.appendChild(idInput);
|
||||
|
||||
const tokenInput = document.createElement('input');
|
||||
tokenInput.type = 'hidden';
|
||||
tokenInput.name = '__RequestVerificationToken';
|
||||
tokenInput.value = '@Html.AntiForgeryToken()'.match(/value="([^"]+)"/)[1];
|
||||
form.appendChild(tokenInput);
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
@model PowderCoating.Application.DTOs.Appointment.UpdateAppointmentDto
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Edit Appointment";
|
||||
ViewData["PageIcon"] = "bi-pencil";
|
||||
ViewData["PageHelpTitle"] = "Edit Appointment";
|
||||
ViewData["PageHelpContent"] = "Update appointment details, change status, record actual arrival/completion times, or adjust the reminder. Use Actual Times to track punctuality vs scheduled time.";
|
||||
}
|
||||
|
||||
<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"></i> Back to Details
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form asp-action="Edit" method="post">
|
||||
<input type="hidden" asp-for="Id" />
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<!-- Title -->
|
||||
<div class="mb-3">
|
||||
<label asp-for="Title" class="form-label">Title <span class="text-danger">*</span></label>
|
||||
<input asp-for="Title" class="form-control" />
|
||||
<span asp-validation-for="Title" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="mb-3">
|
||||
<label asp-for="Description" class="form-label">Description</label>
|
||||
<textarea asp-for="Description" class="form-control" rows="3"></textarea>
|
||||
<span asp-validation-for="Description" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Customer -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label asp-for="CustomerId" class="form-label">Customer</label>
|
||||
<select asp-for="CustomerId" class="form-select" asp-items="ViewBag.Customers">
|
||||
<option value="">-- Select Customer (Optional) --</option>
|
||||
</select>
|
||||
<span asp-validation-for="CustomerId" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<!-- Appointment Type -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label asp-for="AppointmentTypeId" class="form-label">Type <span class="text-danger">*</span></label>
|
||||
<select asp-for="AppointmentTypeId" class="form-select" asp-items="ViewBag.AppointmentTypes" id="appointmentType">
|
||||
<option value="">-- Select Type --</option>
|
||||
</select>
|
||||
<span asp-validation-for="AppointmentTypeId" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Status -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="d-flex align-items-center gap-1 mb-1">
|
||||
<label asp-for="AppointmentStatusId" class="form-label mb-0">Status <span class="text-danger">*</span></label>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Appointment Status"
|
||||
data-bs-content="Scheduled → Confirmed (customer acknowledged) → In Progress (currently happening) → Completed. Use Cancelled for cancellations, No Show if the customer didn't arrive, and Rescheduled when moved to a new time.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
<select asp-for="AppointmentStatusId" class="form-select" asp-items="ViewBag.AppointmentStatuses">
|
||||
<option value="">-- Select Status --</option>
|
||||
</select>
|
||||
<span asp-validation-for="AppointmentStatusId" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<!-- Job (conditional) -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label asp-for="JobId" class="form-label">Linked Job</label>
|
||||
<select asp-for="JobId" class="form-select" asp-items="ViewBag.Jobs">
|
||||
<option value="">-- Select Job (Optional) --</option>
|
||||
</select>
|
||||
<span asp-validation-for="JobId" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- All Day Checkbox -->
|
||||
<div class="mb-3 form-check">
|
||||
<input asp-for="IsAllDay" class="form-check-input" id="isAllDay" />
|
||||
<label asp-for="IsAllDay" class="form-check-label">
|
||||
All Day Event
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Start Date/Time -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label asp-for="ScheduledStartTime" class="form-label">Start <span class="text-danger">*</span></label>
|
||||
<input asp-for="ScheduledStartTime" type="datetime-local" class="form-control" id="startTime" />
|
||||
<span asp-validation-for="ScheduledStartTime" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<!-- End Date/Time -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label asp-for="ScheduledEndTime" class="form-label">End <span class="text-danger">*</span></label>
|
||||
<input asp-for="ScheduledEndTime" type="datetime-local" class="form-control" id="endTime" />
|
||||
<span asp-validation-for="ScheduledEndTime" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actual Times -->
|
||||
<div class="card mb-3 bg-light">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<h6 class="mb-0"><i class="bi bi-clock-history me-2"></i>Actual Times</h6>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Actual Times"
|
||||
data-bs-content="Record when the customer actually arrived and when the appointment finished. These are optional and separate from the scheduled times — useful for tracking punctuality and measuring how accurately appointments are estimated.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3 mb-md-0">
|
||||
<label asp-for="ActualStartTime" class="form-label">Actual Start</label>
|
||||
<input asp-for="ActualStartTime" type="datetime-local" class="form-control" />
|
||||
<span asp-validation-for="ActualStartTime" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="ActualEndTime" class="form-label">Actual End</label>
|
||||
<input asp-for="ActualEndTime" type="datetime-local" class="form-control" />
|
||||
<span asp-validation-for="ActualEndTime" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Assigned Worker -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label asp-for="AssignedUserId" class="form-label">Assign To Worker</label>
|
||||
<select asp-for="AssignedUserId" class="form-select" asp-items="ViewBag.Workers">
|
||||
<option value="">-- No Assignment --</option>
|
||||
</select>
|
||||
<span asp-validation-for="AssignedUserId" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<!-- Location -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label asp-for="Location" class="form-label">Location</label>
|
||||
<input asp-for="Location" class="form-control" />
|
||||
<span asp-validation-for="Location" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Internal Notes -->
|
||||
<div class="mb-3">
|
||||
<label asp-for="Notes" class="form-label">Internal Notes</label>
|
||||
<textarea asp-for="Notes" class="form-control" rows="2"></textarea>
|
||||
<span asp-validation-for="Notes" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<!-- Reminder Settings -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<h6 class="mb-0"><i class="bi bi-bell me-2"></i>Reminder Settings</h6>
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Reminder Settings"
|
||||
data-bs-content="Enable a reminder to receive an in-app notification before the appointment. Set how many minutes in advance — e.g., 30 for a brief heads-up, 1440 for a full day before. Reminders are per-appointment and do not send external emails or SMS.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3 form-check">
|
||||
<input asp-for="IsReminderEnabled" class="form-check-input" id="reminderEnabled" />
|
||||
<label asp-for="IsReminderEnabled" class="form-check-label">
|
||||
Send reminder notification
|
||||
</label>
|
||||
</div>
|
||||
<div class="mb-0" id="reminderTime" style="display: @(Model.IsReminderEnabled ? "block" : "none")">
|
||||
<label asp-for="ReminderMinutesBefore" class="form-label">Remind me</label>
|
||||
<div class="input-group">
|
||||
<input asp-for="ReminderMinutesBefore" type="number" class="form-control" min="5" max="1440" />
|
||||
<span class="input-group-text">minutes before</span>
|
||||
</div>
|
||||
<span asp-validation-for="ReminderMinutesBefore" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i> Save Changes
|
||||
</button>
|
||||
<a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar - Status Guide -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h6 class="mb-0"><i class="bi bi-info-circle me-2"></i>Status Guide</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6>Status Meanings:</h6>
|
||||
<ul class="small mb-3">
|
||||
<li><strong>Scheduled:</strong> Initial appointment booking</li>
|
||||
<li><strong>Confirmed:</strong> Customer has confirmed attendance</li>
|
||||
<li><strong>In Progress:</strong> Appointment is currently happening</li>
|
||||
<li><strong>Completed:</strong> Appointment finished successfully</li>
|
||||
<li><strong>Cancelled:</strong> Appointment was cancelled</li>
|
||||
<li><strong>No Show:</strong> Customer didn't arrive</li>
|
||||
<li><strong>Rescheduled:</strong> Moved to a different time</li>
|
||||
</ul>
|
||||
<h6>Actual Times:</h6>
|
||||
<p class="small mb-0">Record when the customer actually arrived and when the appointment was completed. Useful for tracking punctuality and duration accuracy.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
<script>
|
||||
// Toggle reminder time visibility
|
||||
document.getElementById('reminderEnabled').addEventListener('change', function() {
|
||||
const reminderTime = document.getElementById('reminderTime');
|
||||
reminderTime.style.display = this.checked ? 'block' : 'none';
|
||||
});
|
||||
|
||||
// Hide time inputs when "All Day" is checked
|
||||
const isAllDayCheckbox = document.getElementById('isAllDay');
|
||||
const startTimeInput = document.getElementById('startTime');
|
||||
const endTimeInput = document.getElementById('endTime');
|
||||
|
||||
function toggleTimeInputs() {
|
||||
if (isAllDayCheckbox.checked) {
|
||||
startTimeInput.type = 'date';
|
||||
endTimeInput.type = 'date';
|
||||
} else {
|
||||
startTimeInput.type = 'datetime-local';
|
||||
endTimeInput.type = 'datetime-local';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
toggleTimeInputs();
|
||||
|
||||
isAllDayCheckbox.addEventListener('change', toggleTimeInputs);
|
||||
|
||||
// Auto-update end time when start time changes
|
||||
startTimeInput.addEventListener('change', function() {
|
||||
if (isAllDayCheckbox.checked) {
|
||||
// For all-day events, set end date to same as start date
|
||||
endTimeInput.value = this.value;
|
||||
} else {
|
||||
// For timed events, set end time to 1 hour after start time
|
||||
const newEndTime = new Date(this.value);
|
||||
newEndTime.setHours(newEndTime.getHours() + 1);
|
||||
endTimeInput.value = newEndTime.toISOString().slice(0, 16);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
@model PagedResult<PowderCoating.Application.DTOs.Appointment.AppointmentListDto>
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Appointments";
|
||||
ViewData["PageIcon"] = "bi-calendar-event";
|
||||
ViewData["PageHelpTitle"] = "Appointments";
|
||||
ViewData["PageHelpContent"] = "Schedule and track customer visits, drop-offs, pick-ups, consultations, and internal meetings. Appointments can be linked to customers and jobs. Statuses: Scheduled → Confirmed → In Progress → Completed. Use the Calendar view for a visual day/week/month overview.";
|
||||
}
|
||||
|
||||
<!-- Stats Cards - Desktop -->
|
||||
<div class="stats-cards-desktop">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<p class="text-muted mb-1" style="font-size: 0.875rem;">Total Appointments</p>
|
||||
<h3 class="mb-0 fw-bold">@Model.TotalCount</h3>
|
||||
</div>
|
||||
<div class="rounded-circle p-3" style="background: #dbeafe;">
|
||||
<i class="bi bi-calendar-event text-primary" style="font-size: 1.5rem;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<p class="text-muted mb-1" style="font-size: 0.875rem;">Today</p>
|
||||
<h3 class="mb-0 fw-bold">@Model.Items.Count(a => a.ScheduledStartTime.Date == DateTime.Today)</h3>
|
||||
</div>
|
||||
<div class="rounded-circle p-3" style="background: #fef3c7;">
|
||||
<i class="bi bi-clock text-warning" style="font-size: 1.5rem;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<p class="text-muted mb-1" style="font-size: 0.875rem;">This Week</p>
|
||||
<h3 class="mb-0 fw-bold">@Model.Items.Count(a => a.ScheduledStartTime >= DateTime.Today && a.ScheduledStartTime < DateTime.Today.AddDays(7))</h3>
|
||||
</div>
|
||||
<div class="rounded-circle p-3" style="background: #d1fae5;">
|
||||
<i class="bi bi-calendar-week text-success" style="font-size: 1.5rem;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<p class="text-muted mb-1" style="font-size: 0.875rem;">Confirmed</p>
|
||||
<h3 class="mb-0 fw-bold">@Model.Items.Count(a => a.StatusDisplayName == "Confirmed")</h3>
|
||||
</div>
|
||||
<div class="rounded-circle p-3" style="background: #e0e7ff;">
|
||||
<i class="bi bi-check-circle text-info" style="font-size: 1.5rem;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Compact Stats - Mobile -->
|
||||
<div class="mobile-stats-compact">
|
||||
<div class="card">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon"><i class="bi bi-calendar-event text-primary"></i></div>
|
||||
<div class="stat-value">@Model.TotalCount</div>
|
||||
<div class="stat-label">Total</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon"><i class="bi bi-clock text-warning"></i></div>
|
||||
<div class="stat-value">@Model.Items.Count(a => a.ScheduledStartTime.Date == DateTime.Today)</div>
|
||||
<div class="stat-label">Today</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon"><i class="bi bi-calendar-week text-success"></i></div>
|
||||
<div class="stat-value">@Model.Items.Count(a => a.ScheduledStartTime >= DateTime.Today && a.ScheduledStartTime < DateTime.Today.AddDays(7))</div>
|
||||
<div class="stat-label">This Week</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon"><i class="bi bi-check-circle text-info"></i></div>
|
||||
<div class="stat-value">@Model.Items.Count(a => a.StatusDisplayName == "Confirmed")</div>
|
||||
<div class="stat-label">Confirmed</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search and Actions Bar -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<form method="get" asp-action="Index">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="searchTerm" value="@ViewBag.SearchTerm" placeholder="Search appointments...">
|
||||
<input type="hidden" name="sortColumn" value="@ViewBag.SortColumn" />
|
||||
<input type="hidden" name="sortDirection" value="@ViewBag.SortDirection" />
|
||||
<input type="hidden" name="pageSize" value="@Model.PageSize" />
|
||||
<button class="btn btn-outline-secondary" type="submit">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
@if (!string.IsNullOrEmpty(ViewBag.SearchTerm))
|
||||
{
|
||||
<a href="@Url.Action("Index")" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-x"></i>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<form method="get" asp-action="Index" id="typeFilterForm">
|
||||
<input type="hidden" name="searchTerm" value="@ViewBag.SearchTerm" />
|
||||
<input type="hidden" name="statusFilter" value="@ViewBag.StatusFilter" />
|
||||
<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 class="form-select" name="typeFilter" onchange="document.getElementById('typeFilterForm').submit()">
|
||||
<option value="">All Types</option>
|
||||
@foreach (var item in (SelectList)ViewBag.TypeFilterList)
|
||||
{
|
||||
<option value="@item.Value" selected="@(item.Value == ViewBag.TypeFilter?.ToString())">@item.Text</option>
|
||||
}
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<form method="get" asp-action="Index" id="statusFilterForm">
|
||||
<input type="hidden" name="searchTerm" value="@ViewBag.SearchTerm" />
|
||||
<input type="hidden" name="typeFilter" value="@ViewBag.TypeFilter" />
|
||||
<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 class="form-select" name="statusFilter" onchange="document.getElementById('statusFilterForm').submit()">
|
||||
<option value="">All Statuses</option>
|
||||
@foreach (var item in (SelectList)ViewBag.StatusFilterList)
|
||||
{
|
||||
<option value="@item.Value" selected="@(item.Value == ViewBag.StatusFilter?.ToString())">@item.Text</option>
|
||||
}
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-2 text-end">
|
||||
<a asp-action="Calendar" class="btn btn-outline-primary w-100 mb-2">
|
||||
<i class="bi bi-calendar3"></i> View Calendar
|
||||
</a>
|
||||
<a asp-action="Create" class="btn btn-primary w-100">
|
||||
<i class="bi bi-plus-circle"></i> New Appointment
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Appointments Table -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
@if (Model.Items.Any())
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th sortable-column="AppointmentNumber" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Number</th>
|
||||
<th sortable-column="Title" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Title</th>
|
||||
<th sortable-column="Customer" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Customer</th>
|
||||
<th sortable-column="Type" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Type</th>
|
||||
<th sortable-column="Status" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Status</th>
|
||||
<th sortable-column="ScheduledStartTime" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Scheduled</th>
|
||||
<th>Worker</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var appointment in Model.Items)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<a asp-action="Details" asp-route-id="@appointment.Id" class="text-decoration-none">
|
||||
@appointment.AppointmentNumber
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<strong>@appointment.Title</strong>
|
||||
@if (appointment.IsAllDay)
|
||||
{
|
||||
<span class="badge bg-secondary ms-1">All Day</span>
|
||||
}
|
||||
</td>
|
||||
<td>@(appointment.CustomerName ?? "<em class=\"text-muted\">Internal</em>")</td>
|
||||
<td>
|
||||
<span class="badge bg-@appointment.TypeColorClass">
|
||||
@appointment.TypeDisplayName
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-@appointment.StatusColorClass">
|
||||
@appointment.StatusDisplayName
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div>@appointment.ScheduledStartTime.ToString("MMM dd, yyyy")</div>
|
||||
@if (!appointment.IsAllDay)
|
||||
{
|
||||
<small class="text-muted">@appointment.ScheduledStartTime.ToString("h:mm tt") - @appointment.ScheduledEndTime.ToString("h:mm tt")</small>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(appointment.AssignedWorkerName))
|
||||
{
|
||||
<span class="badge bg-info">
|
||||
<i class="bi bi-person"></i> @appointment.AssignedWorkerName
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Unassigned</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a asp-action="Details" asp-route-id="@appointment.Id" class="btn btn-sm btn-outline-primary" title="View Details">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a asp-action="Edit" asp-route-id="@appointment.Id" class="btn btn-sm btn-outline-secondary" title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteAppointment(@appointment.Id, '@appointment.AppointmentNumber')" title="Delete">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<partial name="_Pagination" model="Model" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-calendar-x text-muted" style="font-size: 3rem;"></i>
|
||||
<p class="text-muted mt-3">No appointments found.</p>
|
||||
<a asp-action="Create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Create First Appointment
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function deleteAppointment(id, appointmentNumber) {
|
||||
if (confirm(`Are you sure you want to delete appointment ${appointmentNumber}?`)) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '@Url.Action("Delete")';
|
||||
|
||||
const idInput = document.createElement('input');
|
||||
idInput.type = 'hidden';
|
||||
idInput.name = 'id';
|
||||
idInput.value = id;
|
||||
form.appendChild(idInput);
|
||||
|
||||
const tokenInput = document.createElement('input');
|
||||
tokenInput.type = 'hidden';
|
||||
tokenInput.name = '__RequestVerificationToken';
|
||||
tokenInput.value = document.querySelector('input[name="__RequestVerificationToken"]')?.value || '';
|
||||
form.appendChild(tokenInput);
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
|
||||
function changePageSize(pageSize) {
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('pageSize', pageSize);
|
||||
url.searchParams.set('pageNumber', '1');
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
</script>
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<!-- Quick Create Appointment Modal -->
|
||||
<div class="modal fade" id="quickCreateModal" tabindex="-1" aria-labelledby="quickCreateModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title" id="quickCreateModalLabel">
|
||||
<i class="bi bi-calendar-plus me-2"></i>Quick Create Appointment
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="quickCreateForm">
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-danger d-none" id="quickCreateError"></div>
|
||||
|
||||
<!-- Title -->
|
||||
<div class="mb-3">
|
||||
<label for="quickTitle" class="form-label">Title <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="quickTitle" name="Title" required placeholder="e.g., Customer Drop-Off">
|
||||
</div>
|
||||
|
||||
<!-- Customer -->
|
||||
<div class="mb-3">
|
||||
<label for="quickCustomer" class="form-label">Customer</label>
|
||||
<select class="form-select" id="quickCustomer" name="CustomerId">
|
||||
<option value="">-- Select Customer (Optional) --</option>
|
||||
@foreach (var customer in (SelectList)ViewBag.Customers)
|
||||
{
|
||||
<option value="@customer.Value">@customer.Text</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Appointment Type -->
|
||||
<div class="mb-3">
|
||||
<label for="quickType" class="form-label">Type <span class="text-danger">*</span></label>
|
||||
<select class="form-select" id="quickType" name="AppointmentTypeId" required>
|
||||
<option value="">-- Select Type --</option>
|
||||
@foreach (var type in (SelectList)ViewBag.AppointmentTypes)
|
||||
{
|
||||
<option value="@type.Value">@type.Text</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- All Day -->
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="quickAllDay" name="IsAllDay">
|
||||
<label class="form-check-label" for="quickAllDay">
|
||||
All Day Event
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Start Date/Time -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="quickStart" class="form-label">Start <span class="text-danger">*</span></label>
|
||||
<input type="datetime-local" class="form-control" id="quickStart" name="ScheduledStartTime" required>
|
||||
</div>
|
||||
|
||||
<!-- End Date/Time -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="quickEnd" class="form-label">End <span class="text-danger">*</span></label>
|
||||
<input type="datetime-local" class="form-control" id="quickEnd" name="ScheduledEndTime" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-muted small mb-0">
|
||||
<i class="bi bi-info-circle"></i> For more options, use the <a asp-action="Create">full create form</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" id="quickCreateSubmit">
|
||||
<i class="bi bi-check-circle"></i> Create Appointment
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Quick Create Modal Logic
|
||||
document.getElementById('quickCreateForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = document.getElementById('quickCreateSubmit');
|
||||
const errorDiv = document.getElementById('quickCreateError');
|
||||
errorDiv.classList.add('d-none');
|
||||
|
||||
// Disable submit button
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Creating...';
|
||||
|
||||
try {
|
||||
const formData = new FormData(this);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
// Convert checkbox to boolean
|
||||
data.IsAllDay = formData.get('IsAllDay') === 'on';
|
||||
|
||||
const response = await fetch('@Url.Action("QuickCreate")', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Close modal
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('quickCreateModal'));
|
||||
modal.hide();
|
||||
|
||||
// Show success toast
|
||||
showToast('success', result.message || 'Appointment created successfully');
|
||||
|
||||
// Reload calendar events
|
||||
if (typeof loadCalendarEvents === 'function') {
|
||||
loadCalendarEvents();
|
||||
}
|
||||
|
||||
// Reset form
|
||||
this.reset();
|
||||
} else {
|
||||
errorDiv.textContent = result.message || 'An error occurred';
|
||||
errorDiv.classList.remove('d-none');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
errorDiv.textContent = 'An unexpected error occurred';
|
||||
errorDiv.classList.remove('d-none');
|
||||
} finally {
|
||||
// Re-enable submit button
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="bi bi-check-circle"></i> Create Appointment';
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle datetime/date input based on All Day checkbox
|
||||
document.getElementById('quickAllDay').addEventListener('change', function() {
|
||||
const startInput = document.getElementById('quickStart');
|
||||
const endInput = document.getElementById('quickEnd');
|
||||
|
||||
if (this.checked) {
|
||||
startInput.type = 'date';
|
||||
endInput.type = 'date';
|
||||
} else {
|
||||
startInput.type = 'datetime-local';
|
||||
endInput.type = 'datetime-local';
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-update end time when start time changes
|
||||
document.getElementById('quickStart').addEventListener('change', function() {
|
||||
const endInput = document.getElementById('quickEnd');
|
||||
const isAllDay = document.getElementById('quickAllDay').checked;
|
||||
|
||||
// Always update end time based on start time
|
||||
if (isAllDay) {
|
||||
endInput.value = this.value;
|
||||
} else {
|
||||
const newEndTime = new Date(this.value);
|
||||
newEndTime.setHours(newEndTime.getHours() + 1);
|
||||
endInput.value = newEndTime.toISOString().slice(0, 16);
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to show toast notifications
|
||||
function showToast(type, message) {
|
||||
// Simple toast implementation - can be enhanced with Bootstrap Toast component
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3`;
|
||||
alertDiv.style.zIndex = '9999';
|
||||
alertDiv.innerHTML = `
|
||||
<i class="bi bi-${type === 'success' ? 'check-circle' : 'exclamation-triangle'} me-2"></i>${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
setTimeout(() => {
|
||||
alertDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user