874 lines
35 KiB
HTML
874 lines
35 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Fleet Calendar - Terra-View{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
<style>
|
|
/* Calendar grid layout */
|
|
.calendar-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 1rem;
|
|
}
|
|
|
|
@media (max-width: 1280px) {
|
|
.calendar-grid {
|
|
grid-template-columns: repeat(3, 1fr);
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.calendar-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.calendar-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
/* Month card */
|
|
.month-card {
|
|
background: white;
|
|
border-radius: 0.5rem;
|
|
padding: 0.75rem;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.dark .month-card {
|
|
background: rgb(30 41 59);
|
|
}
|
|
|
|
/* Day grid */
|
|
.day-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(7, 1fr);
|
|
gap: 2px;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.day-header {
|
|
text-align: center;
|
|
font-weight: 600;
|
|
color: #6b7280;
|
|
padding: 0.25rem 0;
|
|
}
|
|
|
|
.dark .day-header {
|
|
color: #9ca3af;
|
|
}
|
|
|
|
/* Day cell */
|
|
.day-cell {
|
|
aspect-ratio: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
position: relative;
|
|
transition: all 0.15s ease;
|
|
font-size: 0.7rem;
|
|
}
|
|
|
|
.day-cell:hover {
|
|
transform: scale(1.1);
|
|
z-index: 10;
|
|
}
|
|
|
|
.day-cell.today {
|
|
ring: 2px;
|
|
ring-color: #f48b1c;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* Default neutral day style */
|
|
.day-cell.neutral {
|
|
background-color: #f3f4f6;
|
|
color: #374151;
|
|
}
|
|
|
|
.dark .day-cell.neutral {
|
|
background-color: rgba(55, 65, 81, 0.5);
|
|
color: #d1d5db;
|
|
}
|
|
|
|
/* Status indicator dot */
|
|
.day-cell .status-dot {
|
|
position: absolute;
|
|
top: 2px;
|
|
right: 2px;
|
|
width: 5px;
|
|
height: 5px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.day-cell .status-dot.expired {
|
|
background-color: #ef4444;
|
|
}
|
|
|
|
/* Reservation mode colors - applied dynamically */
|
|
.day-cell.reservation-available {
|
|
background-color: #dcfce7;
|
|
color: #166534;
|
|
}
|
|
|
|
.dark .day-cell.reservation-available {
|
|
background-color: rgba(34, 197, 94, 0.2);
|
|
color: #86efac;
|
|
}
|
|
|
|
.day-cell.reservation-partial {
|
|
background-color: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.dark .day-cell.reservation-partial {
|
|
background-color: rgba(245, 158, 11, 0.2);
|
|
color: #fcd34d;
|
|
}
|
|
|
|
.day-cell.reservation-unavailable {
|
|
background-color: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
.dark .day-cell.reservation-unavailable {
|
|
background-color: rgba(239, 68, 68, 0.2);
|
|
color: #fca5a5;
|
|
}
|
|
|
|
/* Legacy status colors (kept for day detail) */
|
|
.day-cell.some-reserved {
|
|
background-color: #dbeafe;
|
|
color: #1e40af;
|
|
}
|
|
|
|
.dark .day-cell.some-reserved {
|
|
background-color: rgba(59, 130, 246, 0.2);
|
|
color: #93c5fd;
|
|
}
|
|
|
|
.day-cell.empty {
|
|
background: transparent;
|
|
cursor: default;
|
|
}
|
|
|
|
.day-cell.empty:hover {
|
|
transform: none;
|
|
}
|
|
|
|
/* Reservation bar */
|
|
.reservation-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0.5rem 0.75rem;
|
|
border-radius: 0.375rem;
|
|
margin-bottom: 0.5rem;
|
|
cursor: pointer;
|
|
transition: opacity 0.15s ease;
|
|
}
|
|
|
|
.reservation-bar:hover {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
/* Slide-over panel */
|
|
.slide-panel {
|
|
position: fixed;
|
|
top: 0;
|
|
right: 0;
|
|
width: 100%;
|
|
max-width: 28rem;
|
|
height: 100vh;
|
|
background: white;
|
|
box-shadow: -4px 0 15px rgba(0,0,0,0.1);
|
|
transform: translateX(100%);
|
|
transition: transform 0.3s ease;
|
|
z-index: 50;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.dark .slide-panel {
|
|
background: rgb(30 41 59);
|
|
}
|
|
|
|
.slide-panel.open {
|
|
transform: translateX(0);
|
|
}
|
|
|
|
.panel-backdrop {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0,0,0,0.3);
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition: all 0.3s ease;
|
|
z-index: 40;
|
|
}
|
|
|
|
.panel-backdrop.open {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="mb-6">
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Fleet Calendar</h1>
|
|
<p class="text-gray-600 dark:text-gray-400 mt-1">Plan unit assignments and track calibrations</p>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary Stats -->
|
|
<div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
|
|
<div class="bg-white dark:bg-slate-800 rounded-lg p-4 shadow">
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">Total Units</p>
|
|
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ calendar_data.total_units }}</p>
|
|
</div>
|
|
<div class="bg-green-50 dark:bg-green-900/20 rounded-lg p-4 shadow">
|
|
<p class="text-sm text-green-600 dark:text-green-400">Available Today</p>
|
|
<p class="text-2xl font-bold text-green-700 dark:text-green-300" id="available-today">--</p>
|
|
</div>
|
|
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 shadow">
|
|
<p class="text-sm text-blue-600 dark:text-blue-400">Reserved Today</p>
|
|
<p class="text-2xl font-bold text-blue-700 dark:text-blue-300" id="reserved-today">--</p>
|
|
</div>
|
|
<div class="bg-yellow-50 dark:bg-yellow-900/20 rounded-lg p-4 shadow">
|
|
<p class="text-sm text-yellow-600 dark:text-yellow-400">Expiring Soon</p>
|
|
<p class="text-2xl font-bold text-yellow-700 dark:text-yellow-300" id="expiring-today">--</p>
|
|
</div>
|
|
<div class="bg-red-50 dark:bg-red-900/20 rounded-lg p-4 shadow">
|
|
<p class="text-sm text-red-600 dark:text-red-400">Cal. Expired</p>
|
|
<p class="text-2xl font-bold text-red-700 dark:text-red-300" id="expired-today">--</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Bar -->
|
|
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
|
|
<div class="flex items-center gap-4">
|
|
<!-- Device Type Toggle -->
|
|
<div class="flex rounded-lg bg-gray-100 dark:bg-gray-700 p-1">
|
|
<a href="/fleet-calendar?year={{ start_year }}&month={{ start_month }}&device_type=seismograph"
|
|
class="px-4 py-2 rounded-md text-sm font-medium transition-colors {% if device_type == 'seismograph' %}bg-white dark:bg-slate-600 text-gray-900 dark:text-white shadow{% else %}text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white{% endif %}">
|
|
Seismographs
|
|
</a>
|
|
<a href="/fleet-calendar?year={{ start_year }}&month={{ start_month }}&device_type=slm"
|
|
class="px-4 py-2 rounded-md text-sm font-medium transition-colors {% if device_type == 'slm' %}bg-white dark:bg-slate-600 text-gray-900 dark:text-white shadow{% else %}text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white{% endif %}">
|
|
SLMs
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Legend -->
|
|
<div class="hidden md:flex items-center gap-4 text-sm" id="main-legend">
|
|
<span class="flex items-center gap-1">
|
|
<span class="w-3 h-3 rounded bg-gray-200 dark:bg-gray-600 relative">
|
|
<span class="absolute top-0 right-0 w-1.5 h-1.5 rounded-full bg-red-500"></span>
|
|
</span>
|
|
Cal expires
|
|
</span>
|
|
</div>
|
|
<!-- Reservation mode legend (hidden by default) -->
|
|
<div class="hidden md:hidden items-center gap-4 text-sm" id="reservation-legend">
|
|
<span class="flex items-center gap-1">
|
|
<span class="w-3 h-3 rounded bg-green-200 dark:bg-green-700"></span>
|
|
Available
|
|
</span>
|
|
<span class="flex items-center gap-1">
|
|
<span class="w-3 h-3 rounded bg-yellow-200 dark:bg-yellow-700"></span>
|
|
Partial
|
|
</span>
|
|
<span class="flex items-center gap-1">
|
|
<span class="w-3 h-3 rounded bg-red-200 dark:bg-red-700"></span>
|
|
Unavailable
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button onclick="openReservationModal()"
|
|
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center gap-2">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
|
</svg>
|
|
New Reservation
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Calendar Grid -->
|
|
<div class="calendar-grid mb-8">
|
|
{% for month_data in calendar_data.months %}
|
|
<div class="month-card">
|
|
<h3 class="font-semibold text-gray-900 dark:text-white mb-2 text-center">
|
|
{{ month_data.short_name }} '{{ month_data.year_short }}
|
|
</h3>
|
|
<div class="day-grid">
|
|
<!-- Day headers -->
|
|
<div class="day-header">S</div>
|
|
<div class="day-header">M</div>
|
|
<div class="day-header">T</div>
|
|
<div class="day-header">W</div>
|
|
<div class="day-header">T</div>
|
|
<div class="day-header">F</div>
|
|
<div class="day-header">S</div>
|
|
|
|
<!-- Empty cells for alignment (first_weekday is 0=Mon, we need 0=Sun) -->
|
|
{% set first_day_offset = (month_data.first_weekday + 1) % 7 %}
|
|
{% for i in range(first_day_offset) %}
|
|
<div class="day-cell empty"></div>
|
|
{% endfor %}
|
|
|
|
<!-- Day cells -->
|
|
{% for day_num in range(1, month_data.num_days + 1) %}
|
|
{% set day_data = month_data.days[day_num] %}
|
|
{% set date_str = '%04d-%02d-%02d'|format(month_data.year, month_data.month, day_num) %}
|
|
{% set is_today = date_str == today %}
|
|
{% set has_cal_expiring = day_data.cal_expiring_on_day is defined and day_data.cal_expiring_on_day > 0 %}
|
|
<div class="day-cell neutral {% if is_today %}today ring-2 ring-seismo-orange{% endif %}"
|
|
data-date="{{ date_str }}"
|
|
data-available="{{ day_data.available }}"
|
|
onclick="openDayPanel('{{ date_str }}')"
|
|
title="Available: {{ day_data.available }}, Reserved: {{ day_data.reserved }}{% if has_cal_expiring %}, Cal expires: {{ day_data.cal_expiring_on_day }}{% endif %}">
|
|
{{ day_num }}
|
|
{% if has_cal_expiring %}
|
|
<span class="status-dot expired"></span>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Month Navigation (centered below calendar) -->
|
|
<div class="flex items-center justify-center gap-3 mb-8">
|
|
<a href="/fleet-calendar?year={{ prev_year }}&month={{ prev_month }}&device_type={{ device_type }}"
|
|
class="px-3 py-2 rounded-lg bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300"
|
|
title="Previous month">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
</svg>
|
|
</a>
|
|
<span class="text-lg font-bold text-gray-900 dark:text-white px-3">
|
|
{{ calendar_data.months[0].name }} {{ calendar_data.months[0].year }} - {{ calendar_data.months[11].name }} {{ calendar_data.months[11].year }}
|
|
</span>
|
|
<a href="/fleet-calendar?year={{ next_year }}&month={{ next_month }}&device_type={{ device_type }}"
|
|
class="px-3 py-2 rounded-lg bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300"
|
|
title="Next month">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
</svg>
|
|
</a>
|
|
<a href="/fleet-calendar?device_type={{ device_type }}"
|
|
class="ml-2 px-4 py-2 rounded-lg bg-seismo-orange text-white hover:bg-orange-600">
|
|
Today
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Active Reservations -->
|
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6 mb-8">
|
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Active Reservations</h2>
|
|
<div id="reservations-list"
|
|
hx-get="/api/fleet-calendar/reservations-list?year={{ start_year }}&month={{ start_month }}&device_type={{ device_type }}"
|
|
hx-trigger="load"
|
|
hx-swap="innerHTML">
|
|
<p class="text-gray-500">Loading reservations...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Day Detail Slide Panel -->
|
|
<div id="panel-backdrop" class="panel-backdrop" onclick="closeDayPanel()"></div>
|
|
<div id="day-panel" class="slide-panel">
|
|
<div class="p-6" id="day-panel-content">
|
|
<!-- Content loaded via HTMX -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Reservation Modal -->
|
|
<div id="reservation-modal" class="fixed inset-0 z-50 hidden">
|
|
<div class="fixed inset-0 bg-black/50" onclick="closeReservationModal()"></div>
|
|
<div class="fixed inset-0 flex items-center justify-center p-4">
|
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl max-w-lg w-full max-h-[90vh] overflow-y-auto" onclick="event.stopPropagation()">
|
|
<div class="p-6">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white" id="modal-title">New Reservation</h2>
|
|
<button onclick="closeReservationModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<input type="hidden" id="editing-reservation-id" value="">
|
|
|
|
<form id="reservation-form" onsubmit="submitReservation(event)">
|
|
<!-- Name -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Reservation Name *</label>
|
|
<input type="text" name="name" required
|
|
placeholder="e.g., Job A - March Deployment"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
|
|
<!-- Project (optional) -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Link to Project (optional)</label>
|
|
<select name="project_id"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500">
|
|
<option value="">-- No project --</option>
|
|
{% for project in projects %}
|
|
<option value="{{ project.id }}">{{ project.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Date Range -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Start Date *</label>
|
|
<input type="date" name="start_date" required
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<div class="flex items-center justify-between mb-1">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">End Date</label>
|
|
<label class="flex items-center gap-2 cursor-pointer text-sm">
|
|
<input type="checkbox" name="end_date_tbd" id="end_date_tbd"
|
|
onchange="toggleEndDateTBD()"
|
|
class="w-4 h-4 text-blue-600 focus:ring-blue-500 rounded border-gray-300 dark:border-gray-600">
|
|
<span class="text-gray-600 dark:text-gray-400">TBD / Ongoing</span>
|
|
</label>
|
|
</div>
|
|
<input type="date" name="end_date" id="end_date_input"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
|
|
<!-- Estimated End Date (shown when TBD is checked) -->
|
|
<div id="estimated-end-section" class="mb-4 hidden">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Estimated End Date <span class="text-gray-400 font-normal">(for planning)</span>
|
|
</label>
|
|
<input type="date" name="estimated_end_date"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500">
|
|
<p class="text-xs text-gray-500 mt-1">Used for calendar visualization only. Can be updated later.</p>
|
|
</div>
|
|
|
|
<!-- Assignment Type -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Assignment Type *</label>
|
|
<div class="flex gap-4">
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input type="radio" name="assignment_type" value="quantity" checked
|
|
onchange="toggleAssignmentType(this.value)"
|
|
class="text-blue-600 focus:ring-blue-500">
|
|
<span class="text-gray-700 dark:text-gray-300">Reserve quantity</span>
|
|
</label>
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input type="radio" name="assignment_type" value="specific"
|
|
onchange="toggleAssignmentType(this.value)"
|
|
class="text-blue-600 focus:ring-blue-500">
|
|
<span class="text-gray-700 dark:text-gray-300">Pick specific units</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quantity (shown for quantity type) -->
|
|
<div id="quantity-section" class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Units Needed</label>
|
|
<input type="number" name="quantity_needed" min="1" value="1"
|
|
onchange="updateCalendarAvailability()"
|
|
oninput="updateCalendarAvailability()"
|
|
class="w-32 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500">
|
|
<p class="text-xs text-gray-500 mt-1">Calendar will highlight availability based on quantity</p>
|
|
</div>
|
|
|
|
<!-- Specific Units (shown for specific type) -->
|
|
<div id="specific-section" class="mb-4 hidden">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Select Units</label>
|
|
<div id="available-units-list" class="max-h-48 overflow-y-auto border border-gray-300 dark:border-gray-600 rounded-lg p-2">
|
|
<p class="text-gray-500 text-sm">Select dates first to see available units</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Color -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Color</label>
|
|
<div class="flex gap-2">
|
|
{% for color in ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899'] %}
|
|
<label class="cursor-pointer">
|
|
<input type="radio" name="color" value="{{ color }}" {% if loop.first %}checked{% endif %} class="sr-only peer">
|
|
<span class="block w-8 h-8 rounded-full peer-checked:ring-2 peer-checked:ring-offset-2 peer-checked:ring-gray-900 dark:peer-checked:ring-white"
|
|
style="background-color: {{ color }}"></span>
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notes -->
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Notes</label>
|
|
<textarea name="notes" rows="2"
|
|
placeholder="Optional notes about this reservation"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"></textarea>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="flex justify-end gap-3">
|
|
<button type="button" onclick="closeReservationModal()"
|
|
class="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" id="modal-submit-btn"
|
|
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
|
Create Reservation
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const deviceType = '{{ device_type }}';
|
|
const startYear = {{ start_year }};
|
|
const startMonth = {{ start_month }};
|
|
|
|
// Load today's stats
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadTodayStats();
|
|
});
|
|
|
|
async function loadTodayStats() {
|
|
try {
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const response = await fetch(`/api/fleet-calendar/day/${today}?device_type=${deviceType}`);
|
|
// Just update the stats from the day data response headers or a separate call
|
|
// For now, calculate from calendar data
|
|
const todayData = findTodayInCalendar();
|
|
if (todayData) {
|
|
document.getElementById('available-today').textContent = todayData.available;
|
|
document.getElementById('reserved-today').textContent = todayData.reserved;
|
|
document.getElementById('expiring-today').textContent = todayData.expiring_soon;
|
|
document.getElementById('expired-today').textContent = todayData.expired;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading today stats:', error);
|
|
}
|
|
}
|
|
|
|
function findTodayInCalendar() {
|
|
const today = new Date();
|
|
const year = today.getFullYear();
|
|
const month = today.getMonth() + 1;
|
|
const day = today.getDate();
|
|
const calendarData = {{ calendar_data | tojson }};
|
|
// months is now an array, find the matching month
|
|
const monthData = calendarData.months.find(m => m.year === year && m.month === month);
|
|
if (monthData && monthData.days[day]) {
|
|
return monthData.days[day];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function openDayPanel(dateStr) {
|
|
const panel = document.getElementById('day-panel');
|
|
const backdrop = document.getElementById('panel-backdrop');
|
|
const content = document.getElementById('day-panel-content');
|
|
|
|
// Load content via HTMX
|
|
htmx.ajax('GET', `/api/fleet-calendar/day/${dateStr}?device_type=${deviceType}`, {
|
|
target: '#day-panel-content',
|
|
swap: 'innerHTML'
|
|
});
|
|
|
|
panel.classList.add('open');
|
|
backdrop.classList.add('open');
|
|
}
|
|
|
|
function closeDayPanel() {
|
|
const panel = document.getElementById('day-panel');
|
|
const backdrop = document.getElementById('panel-backdrop');
|
|
panel.classList.remove('open');
|
|
backdrop.classList.remove('open');
|
|
}
|
|
|
|
let reservationModeActive = false;
|
|
|
|
function openReservationModal() {
|
|
// Reset to "create" mode
|
|
document.getElementById('modal-title').textContent = 'New Reservation';
|
|
document.getElementById('modal-submit-btn').textContent = 'Create Reservation';
|
|
document.getElementById('editing-reservation-id').value = '';
|
|
document.getElementById('reservation-form').reset();
|
|
|
|
document.getElementById('reservation-modal').classList.remove('hidden');
|
|
reservationModeActive = true;
|
|
document.getElementById('main-legend').classList.add('md:hidden');
|
|
document.getElementById('main-legend').classList.remove('md:flex');
|
|
document.getElementById('reservation-legend').classList.remove('md:hidden');
|
|
document.getElementById('reservation-legend').classList.add('md:flex');
|
|
updateCalendarAvailability();
|
|
}
|
|
|
|
async function editReservation(id) {
|
|
try {
|
|
const response = await fetch(`/api/fleet-calendar/reservations/${id}`);
|
|
if (!response.ok) { alert('Failed to load reservation'); return; }
|
|
const res = await response.json();
|
|
|
|
// Switch modal to "edit" mode
|
|
document.getElementById('modal-title').textContent = 'Edit Reservation';
|
|
document.getElementById('modal-submit-btn').textContent = 'Save Changes';
|
|
document.getElementById('editing-reservation-id').value = id;
|
|
|
|
// Populate fields
|
|
const form = document.getElementById('reservation-form');
|
|
form.querySelector('input[name="name"]').value = res.name;
|
|
form.querySelector('select[name="project_id"]').value = res.project_id || '';
|
|
form.querySelector('input[name="start_date"]').value = res.start_date;
|
|
form.querySelector('textarea[name="notes"]').value = res.notes || '';
|
|
|
|
// Color radio
|
|
const colorRadio = form.querySelector(`input[name="color"][value="${res.color}"]`);
|
|
if (colorRadio) colorRadio.checked = true;
|
|
|
|
// Assignment type
|
|
const atRadio = form.querySelector(`input[name="assignment_type"][value="${res.assignment_type}"]`);
|
|
if (atRadio) { atRadio.checked = true; toggleAssignmentType(res.assignment_type); }
|
|
if (res.assignment_type === 'quantity') {
|
|
form.querySelector('input[name="quantity_needed"]').value = res.quantity_needed || 1;
|
|
}
|
|
|
|
// End date / TBD
|
|
const tbdCheckbox = document.getElementById('end_date_tbd');
|
|
if (res.end_date_tbd) {
|
|
tbdCheckbox.checked = true;
|
|
form.querySelector('input[name="estimated_end_date"]').value = res.estimated_end_date || '';
|
|
} else {
|
|
tbdCheckbox.checked = false;
|
|
document.getElementById('end_date_input').value = res.end_date || '';
|
|
}
|
|
toggleEndDateTBD();
|
|
|
|
document.getElementById('reservation-modal').classList.remove('hidden');
|
|
reservationModeActive = true;
|
|
document.getElementById('main-legend').classList.add('md:hidden');
|
|
document.getElementById('main-legend').classList.remove('md:flex');
|
|
document.getElementById('reservation-legend').classList.remove('md:hidden');
|
|
document.getElementById('reservation-legend').classList.add('md:flex');
|
|
updateCalendarAvailability();
|
|
} catch (error) {
|
|
console.error('Error loading reservation:', error);
|
|
alert('Error loading reservation');
|
|
}
|
|
}
|
|
|
|
function closeReservationModal() {
|
|
document.getElementById('reservation-modal').classList.add('hidden');
|
|
document.getElementById('reservation-form').reset();
|
|
document.getElementById('editing-reservation-id').value = '';
|
|
reservationModeActive = false;
|
|
document.getElementById('main-legend').classList.remove('md:hidden');
|
|
document.getElementById('main-legend').classList.add('md:flex');
|
|
document.getElementById('reservation-legend').classList.add('md:hidden');
|
|
document.getElementById('reservation-legend').classList.remove('md:flex');
|
|
resetCalendarColors();
|
|
}
|
|
|
|
function resetCalendarColors() {
|
|
document.querySelectorAll('.day-cell:not(.empty)').forEach(cell => {
|
|
cell.classList.remove('reservation-available', 'reservation-partial', 'reservation-unavailable');
|
|
cell.classList.add('neutral');
|
|
});
|
|
}
|
|
|
|
function updateCalendarAvailability() {
|
|
if (!reservationModeActive) return;
|
|
|
|
const quantityInput = document.querySelector('input[name="quantity_needed"]');
|
|
const quantity = parseInt(quantityInput?.value) || 1;
|
|
|
|
// Update each day cell based on available units vs quantity needed
|
|
document.querySelectorAll('.day-cell:not(.empty)').forEach(cell => {
|
|
const available = parseInt(cell.dataset.available) || 0;
|
|
|
|
// Remove all status classes
|
|
cell.classList.remove('neutral', 'reservation-available', 'reservation-partial', 'reservation-unavailable');
|
|
|
|
if (available >= quantity) {
|
|
cell.classList.add('reservation-available');
|
|
} else if (available > 0) {
|
|
cell.classList.add('reservation-partial');
|
|
} else {
|
|
cell.classList.add('reservation-unavailable');
|
|
}
|
|
});
|
|
}
|
|
|
|
function toggleEndDateTBD() {
|
|
const checkbox = document.getElementById('end_date_tbd');
|
|
const endDateInput = document.getElementById('end_date_input');
|
|
const estimatedSection = document.getElementById('estimated-end-section');
|
|
|
|
if (checkbox.checked) {
|
|
endDateInput.disabled = true;
|
|
endDateInput.value = '';
|
|
endDateInput.classList.add('opacity-50', 'cursor-not-allowed');
|
|
estimatedSection.classList.remove('hidden');
|
|
} else {
|
|
endDateInput.disabled = false;
|
|
endDateInput.classList.remove('opacity-50', 'cursor-not-allowed');
|
|
estimatedSection.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
function toggleAssignmentType(type) {
|
|
const quantitySection = document.getElementById('quantity-section');
|
|
const specificSection = document.getElementById('specific-section');
|
|
|
|
if (type === 'quantity') {
|
|
quantitySection.classList.remove('hidden');
|
|
specificSection.classList.add('hidden');
|
|
} else {
|
|
quantitySection.classList.add('hidden');
|
|
specificSection.classList.remove('hidden');
|
|
// Load available units based on selected dates
|
|
loadAvailableUnits();
|
|
}
|
|
}
|
|
|
|
async function loadAvailableUnits() {
|
|
const startDate = document.querySelector('input[name="start_date"]').value;
|
|
const endDate = document.querySelector('input[name="end_date"]').value;
|
|
|
|
if (!startDate || !endDate) {
|
|
document.getElementById('available-units-list').innerHTML =
|
|
'<p class="text-gray-500 text-sm">Select dates first to see available units</p>';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(
|
|
`/api/fleet-calendar/availability?start_date=${startDate}&end_date=${endDate}&device_type=${deviceType}`
|
|
);
|
|
const data = await response.json();
|
|
|
|
if (data.available_units.length === 0) {
|
|
document.getElementById('available-units-list').innerHTML =
|
|
'<p class="text-gray-500 text-sm">No units available for this period</p>';
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
for (const unit of data.available_units) {
|
|
html += `
|
|
<label class="flex items-center gap-2 p-2 hover:bg-gray-50 dark:hover:bg-gray-700 rounded cursor-pointer">
|
|
<input type="checkbox" name="unit_ids" value="${unit.id}"
|
|
class="text-blue-600 focus:ring-blue-500 rounded">
|
|
<span class="text-gray-900 dark:text-white font-medium">${unit.id}</span>
|
|
<span class="text-gray-500 text-sm">Cal: ${unit.last_calibrated || 'N/A'}</span>
|
|
${unit.calibration_status === 'expiring_soon' ?
|
|
'<span class="text-yellow-600 text-xs">Expiring soon</span>' : ''}
|
|
</label>
|
|
`;
|
|
}
|
|
document.getElementById('available-units-list').innerHTML = html;
|
|
} catch (error) {
|
|
console.error('Error loading available units:', error);
|
|
}
|
|
}
|
|
|
|
// Watch for date changes to reload available units
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const startInput = document.querySelector('input[name="start_date"]');
|
|
const endInput = document.querySelector('input[name="end_date"]');
|
|
|
|
if (startInput && endInput) {
|
|
startInput.addEventListener('change', function() {
|
|
if (document.querySelector('input[name="assignment_type"]:checked').value === 'specific') {
|
|
loadAvailableUnits();
|
|
}
|
|
});
|
|
endInput.addEventListener('change', function() {
|
|
if (document.querySelector('input[name="assignment_type"]:checked').value === 'specific') {
|
|
loadAvailableUnits();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
async function submitReservation(event) {
|
|
event.preventDefault();
|
|
|
|
const form = event.target;
|
|
const formData = new FormData(form);
|
|
const endDateTbd = formData.get('end_date_tbd') === 'on';
|
|
const editingId = document.getElementById('editing-reservation-id').value;
|
|
|
|
const data = {
|
|
name: formData.get('name'),
|
|
project_id: formData.get('project_id') || null,
|
|
start_date: formData.get('start_date'),
|
|
end_date: endDateTbd ? null : formData.get('end_date'),
|
|
end_date_tbd: endDateTbd,
|
|
estimated_end_date: endDateTbd ? (formData.get('estimated_end_date') || null) : null,
|
|
assignment_type: formData.get('assignment_type'),
|
|
device_type: deviceType,
|
|
color: formData.get('color'),
|
|
notes: formData.get('notes') || null
|
|
};
|
|
|
|
if (!data.end_date && !data.end_date_tbd) {
|
|
alert('Please enter an end date or check "TBD / Ongoing"');
|
|
return;
|
|
}
|
|
|
|
if (data.assignment_type === 'quantity') {
|
|
data.quantity_needed = parseInt(formData.get('quantity_needed'));
|
|
} else {
|
|
data.unit_ids = formData.getAll('unit_ids');
|
|
}
|
|
|
|
const isEdit = editingId !== '';
|
|
const url = isEdit
|
|
? `/api/fleet-calendar/reservations/${editingId}`
|
|
: '/api/fleet-calendar/reservations';
|
|
const method = isEdit ? 'PUT' : 'POST';
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
closeReservationModal();
|
|
window.location.reload();
|
|
} else {
|
|
alert(`Error ${isEdit ? 'saving' : 'creating'} reservation: ` + (result.detail || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
alert(`Error ${isEdit ? 'saving' : 'creating'} reservation`);
|
|
}
|
|
}
|
|
|
|
// Close panels on escape key
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
closeDayPanel();
|
|
closeReservationModal();
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|