f467862877
Pages were blank on phones because mobile-cards.css hides .table-responsive below 992px. Added .mobile-card-view sections to: GiftCertificates, PurchaseOrders, CreditMemos, VendorCredits, JournalEntries, Appointments, InAppNotifications, BankReconciliations, FixedAssets, RecurringTemplates, SmsAgreements, SmsConsentAudit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
361 lines
19 KiB
Plaintext
361 lines
19 KiB
Plaintext
@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="mobile-card-view">
|
|
<div class="mobile-card-list">
|
|
@foreach (var appointment in Model.Items)
|
|
{
|
|
<div class="mobile-data-card">
|
|
<div class="mobile-card-header">
|
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);">
|
|
<i class="bi bi-calendar-event"></i>
|
|
</div>
|
|
<div class="mobile-card-title">
|
|
<h6>@appointment.Title</h6>
|
|
<small>@appointment.ScheduledStartTime.ToString("MMM dd, yyyy")<br />@(!appointment.IsAllDay ? $"{appointment.ScheduledStartTime:h:mm tt} – {appointment.ScheduledEndTime:h:mm tt}" : "All Day")</small>
|
|
</div>
|
|
</div>
|
|
<div class="mobile-card-body">
|
|
<div class="mobile-card-row">
|
|
<span class="mobile-card-label">Status</span>
|
|
<span class="mobile-card-value">
|
|
<span class="badge bg-@appointment.StatusColorClass">@appointment.StatusDisplayName</span>
|
|
</span>
|
|
</div>
|
|
<div class="mobile-card-row">
|
|
<span class="mobile-card-label">Type</span>
|
|
<span class="mobile-card-value">
|
|
<span class="badge bg-@appointment.TypeColorClass">@appointment.TypeDisplayName</span>
|
|
</span>
|
|
</div>
|
|
@if (!string.IsNullOrEmpty(appointment.CustomerName))
|
|
{
|
|
<div class="mobile-card-row">
|
|
<span class="mobile-card-label">Customer</span>
|
|
<span class="mobile-card-value">@appointment.CustomerName</span>
|
|
</div>
|
|
}
|
|
@if (!string.IsNullOrEmpty(appointment.AssignedWorkerName))
|
|
{
|
|
<div class="mobile-card-row">
|
|
<span class="mobile-card-label">Worker</span>
|
|
<span class="mobile-card-value">@appointment.AssignedWorkerName</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
<div class="mobile-card-footer">
|
|
<a asp-action="Details" asp-route-id="@appointment.Id" class="btn btn-sm btn-outline-primary">
|
|
<i class="bi bi-eye me-1"></i>View
|
|
</a>
|
|
<a asp-action="Edit" asp-route-id="@appointment.Id" class="btn btn-sm btn-outline-secondary">
|
|
<i class="bi bi-pencil me-1"></i>Edit
|
|
</a>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
<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>
|
|
}
|