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