Files
terra-view/templates/fleet_calendar.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 %}