add: Calander and reservation mode implemented.
This commit is contained in:
@@ -151,6 +151,13 @@
|
||||
Projects
|
||||
</a>
|
||||
|
||||
<a href="/fleet-calendar" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path.startswith('/fleet-calendar') %}bg-gray-100 dark:bg-gray-700{% endif %}">
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
Fleet Calendar
|
||||
</a>
|
||||
|
||||
<a href="/settings" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path == '/settings' %}bg-gray-100 dark:bg-gray-700{% endif %}">
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
|
||||
811
templates/fleet_calendar.html
Normal file
811
templates/fleet_calendar.html
Normal file
@@ -0,0 +1,811 @@
|
||||
{% 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">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>
|
||||
|
||||
<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"
|
||||
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() {
|
||||
document.getElementById('reservation-modal').classList.remove('hidden');
|
||||
reservationModeActive = true;
|
||||
// Show reservation legend, hide main legend
|
||||
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');
|
||||
// Trigger availability update
|
||||
updateCalendarAvailability();
|
||||
}
|
||||
|
||||
function closeReservationModal() {
|
||||
document.getElementById('reservation-modal').classList.add('hidden');
|
||||
document.getElementById('reservation-form').reset();
|
||||
reservationModeActive = false;
|
||||
// Restore main legend
|
||||
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');
|
||||
// Reset calendar colors
|
||||
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 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
|
||||
};
|
||||
|
||||
// Validate: need either end_date or TBD checked
|
||||
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');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/fleet-calendar/reservations', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
closeReservationModal();
|
||||
// Reload the page to refresh calendar
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Error creating reservation: ' + (result.detail || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Error creating reservation');
|
||||
}
|
||||
}
|
||||
|
||||
// Close panels on escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeDayPanel();
|
||||
closeReservationModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
40
templates/partials/fleet_calendar/available_units.html
Normal file
40
templates/partials/fleet_calendar/available_units.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!-- Available Units for Assignment -->
|
||||
{% if units %}
|
||||
<div class="space-y-1">
|
||||
{% for unit in units %}
|
||||
<label class="flex items-center gap-3 p-2 hover:bg-gray-50 dark:hover:bg-gray-700 rounded cursor-pointer">
|
||||
<input type="checkbox" name="unit_ids" value="{{ unit.id }}"
|
||||
class="w-4 h-4 text-blue-600 focus:ring-blue-500 rounded border-gray-300 dark:border-gray-600">
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ unit.id }}</span>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400 flex-1">
|
||||
{% if unit.last_calibrated %}
|
||||
Cal: {{ unit.last_calibrated }}
|
||||
{% else %}
|
||||
No cal date
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if unit.calibration_status == 'expiring_soon' %}
|
||||
<span class="text-xs px-2 py-0.5 bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 rounded-full">
|
||||
Expiring soon
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if unit.deployed %}
|
||||
<span class="text-xs px-2 py-0.5 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 rounded-full">
|
||||
Deployed
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-xs px-2 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-full">
|
||||
Benched
|
||||
</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm py-4 text-center">
|
||||
No units available for this date range.
|
||||
{% if start_date and end_date %}
|
||||
<br><span class="text-xs">All units are either reserved, have expired calibrations, or are retired.</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
186
templates/partials/fleet_calendar/day_detail.html
Normal file
186
templates/partials/fleet_calendar/day_detail.html
Normal file
@@ -0,0 +1,186 @@
|
||||
<!-- Day Detail Panel Content -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">{{ date_display }}</h2>
|
||||
<button onclick="closeDayPanel()" 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>
|
||||
|
||||
<!-- Summary Stats -->
|
||||
<div class="grid grid-cols-2 gap-3 mb-6">
|
||||
<div class="bg-green-50 dark:bg-green-900/20 rounded-lg p-3 text-center">
|
||||
<p class="text-2xl font-bold text-green-700 dark:text-green-300">{{ day_data.counts.available }}</p>
|
||||
<p class="text-xs text-green-600 dark:text-green-400">Available</p>
|
||||
</div>
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3 text-center">
|
||||
<p class="text-2xl font-bold text-blue-700 dark:text-blue-300">{{ day_data.counts.reserved }}</p>
|
||||
<p class="text-xs text-blue-600 dark:text-blue-400">Reserved</p>
|
||||
</div>
|
||||
<div class="bg-yellow-50 dark:bg-yellow-900/20 rounded-lg p-3 text-center">
|
||||
<p class="text-2xl font-bold text-yellow-700 dark:text-yellow-300">{{ day_data.counts.expiring_soon }}</p>
|
||||
<p class="text-xs text-yellow-600 dark:text-yellow-400">Expiring Soon</p>
|
||||
</div>
|
||||
<div class="bg-red-50 dark:bg-red-900/20 rounded-lg p-3 text-center">
|
||||
<p class="text-2xl font-bold text-red-700 dark:text-red-300">{{ day_data.counts.expired }}</p>
|
||||
<p class="text-xs text-red-600 dark:text-red-400">Cal. Expired</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calibration Expiring TODAY - Important alert -->
|
||||
{% if day_data.cal_expiring_today %}
|
||||
<div class="mb-6 p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<h3 class="text-sm font-semibold text-red-700 dark:text-red-400 mb-2 flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Calibration Expires Today ({{ day_data.cal_expiring_today|length }})
|
||||
</h3>
|
||||
<div class="space-y-1">
|
||||
{% for unit in day_data.cal_expiring_today %}
|
||||
<div class="flex items-center justify-between p-2 bg-white dark:bg-gray-800 rounded text-sm">
|
||||
<a href="/unit/{{ unit.id }}" class="font-medium text-red-600 dark:text-red-400 hover:underline">
|
||||
{{ unit.id }}
|
||||
</a>
|
||||
<span class="text-red-500 text-xs">
|
||||
Last cal: {{ unit.last_calibrated }}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Reservations on this date -->
|
||||
{% if day_data.reservations %}
|
||||
<div class="mb-6">
|
||||
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Reservations</h3>
|
||||
{% for res in day_data.reservations %}
|
||||
<div class="reservation-bar mb-2" style="background-color: {{ res.color }}20; border-left: 4px solid {{ res.color }};">
|
||||
<div class="flex-1">
|
||||
<p class="font-medium text-gray-900 dark:text-white">{{ res.name }}</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ res.start_date }} - {{ res.end_date }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="font-semibold text-gray-900 dark:text-white">
|
||||
{% if res.assignment_type == 'quantity' %}
|
||||
{{ res.assigned_count }}/{{ res.quantity_needed or '?' }}
|
||||
{% else %}
|
||||
{{ res.assigned_count }} units
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Available Units -->
|
||||
{% if day_data.available_units %}
|
||||
<div class="mb-6">
|
||||
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||||
Available Units ({{ day_data.available_units|length }})
|
||||
</h3>
|
||||
<div class="max-h-48 overflow-y-auto space-y-1">
|
||||
{% for unit in day_data.available_units %}
|
||||
<div class="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-700/50 rounded text-sm">
|
||||
<a href="/unit/{{ unit.id }}" class="font-medium text-blue-600 dark:text-blue-400 hover:underline">
|
||||
{{ unit.id }}
|
||||
</a>
|
||||
<span class="text-gray-500 dark:text-gray-400 text-xs">
|
||||
{% if unit.last_calibrated %}
|
||||
Cal: {{ unit.last_calibrated }}
|
||||
{% else %}
|
||||
No cal date
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Reserved Units -->
|
||||
{% if day_data.reserved_units %}
|
||||
<div class="mb-6">
|
||||
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||||
Reserved Units ({{ day_data.reserved_units|length }})
|
||||
</h3>
|
||||
<div class="max-h-48 overflow-y-auto space-y-1">
|
||||
{% for unit in day_data.reserved_units %}
|
||||
<div class="flex items-center justify-between p-2 bg-blue-50 dark:bg-blue-900/20 rounded text-sm">
|
||||
<a href="/unit/{{ unit.id }}" class="font-medium text-blue-600 dark:text-blue-400 hover:underline">
|
||||
{{ unit.id }}
|
||||
</a>
|
||||
<span class="text-blue-600 dark:text-blue-400 text-xs">
|
||||
{{ unit.reservation_name }}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Calibration Expired -->
|
||||
{% if day_data.expired_units %}
|
||||
<div class="mb-6">
|
||||
<h3 class="text-sm font-semibold text-red-600 dark:text-red-400 mb-3">
|
||||
Calibration Expired ({{ day_data.expired_units|length }})
|
||||
</h3>
|
||||
<div class="max-h-48 overflow-y-auto space-y-1">
|
||||
{% for unit in day_data.expired_units %}
|
||||
<div class="flex items-center justify-between p-2 bg-red-50 dark:bg-red-900/20 rounded text-sm">
|
||||
<a href="/unit/{{ unit.id }}" class="font-medium text-red-600 dark:text-red-400 hover:underline">
|
||||
{{ unit.id }}
|
||||
</a>
|
||||
<span class="text-red-500 text-xs">
|
||||
Expired: {{ unit.expiry_date }}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Needs Calibration -->
|
||||
{% if day_data.needs_calibration_units %}
|
||||
<div class="mb-6">
|
||||
<h3 class="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-3">
|
||||
Needs Calibration Date ({{ day_data.needs_calibration_units|length }})
|
||||
</h3>
|
||||
<div class="max-h-32 overflow-y-auto space-y-1">
|
||||
{% for unit in day_data.needs_calibration_units %}
|
||||
<div class="flex items-center justify-between p-2 bg-gray-100 dark:bg-gray-700/50 rounded text-sm">
|
||||
<a href="/unit/{{ unit.id }}" class="font-medium text-gray-600 dark:text-gray-400 hover:underline">
|
||||
{{ unit.id }}
|
||||
</a>
|
||||
<span class="text-gray-400 text-xs">No cal date set</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Expiring Soon (informational) -->
|
||||
{% if day_data.expiring_soon_units %}
|
||||
<div class="mb-6">
|
||||
<h3 class="text-sm font-semibold text-yellow-600 dark:text-yellow-400 mb-3">
|
||||
Calibration Expiring Soon ({{ day_data.expiring_soon_units|length }})
|
||||
</h3>
|
||||
<div class="max-h-32 overflow-y-auto space-y-1">
|
||||
{% for unit in day_data.expiring_soon_units %}
|
||||
<div class="flex items-center justify-between p-2 bg-yellow-50 dark:bg-yellow-900/20 rounded text-sm">
|
||||
<a href="/unit/{{ unit.id }}" class="font-medium text-yellow-700 dark:text-yellow-400 hover:underline">
|
||||
{{ unit.id }}
|
||||
</a>
|
||||
<span class="text-yellow-600 text-xs">
|
||||
Expires: {{ unit.expiry_date }}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
103
templates/partials/fleet_calendar/reservations_list.html
Normal file
103
templates/partials/fleet_calendar/reservations_list.html
Normal file
@@ -0,0 +1,103 @@
|
||||
<!-- Reservations List -->
|
||||
{% if reservations %}
|
||||
<div class="space-y-3">
|
||||
{% for item in reservations %}
|
||||
{% set res = item.reservation %}
|
||||
<div class="flex items-center justify-between p-4 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
||||
style="border-left: 4px solid {{ res.color }};">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">{{ res.name }}</h3>
|
||||
{% if item.has_conflicts %}
|
||||
<span class="px-2 py-0.5 text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 rounded-full"
|
||||
title="{{ item.conflict_count }} unit(s) have calibration expiring during this job">
|
||||
{{ item.conflict_count }} conflict{{ 's' if item.conflict_count != 1 else '' }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{{ res.start_date.strftime('%b %d, %Y') }} -
|
||||
{% if res.end_date %}
|
||||
{{ res.end_date.strftime('%b %d, %Y') }}
|
||||
{% elif res.end_date_tbd %}
|
||||
<span class="text-yellow-600 dark:text-yellow-400 font-medium">TBD</span>
|
||||
{% if res.estimated_end_date %}
|
||||
<span class="text-gray-400">(est. {{ res.estimated_end_date.strftime('%b %d, %Y') }})</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-yellow-600 dark:text-yellow-400">Ongoing</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if res.notes %}
|
||||
<p class="text-sm text-gray-400 dark:text-gray-500 mt-1">{{ res.notes }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-right ml-4">
|
||||
<p class="text-lg font-bold text-gray-900 dark:text-white">
|
||||
{% if res.assignment_type == 'quantity' %}
|
||||
{{ item.assigned_count }}/{{ res.quantity_needed or '?' }}
|
||||
{% else %}
|
||||
{{ item.assigned_count }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ 'units needed' if res.assignment_type == 'quantity' else 'units assigned' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="ml-4 flex items-center gap-2">
|
||||
<button onclick="editReservation('{{ res.id }}')"
|
||||
class="p-2 text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
title="Edit reservation">
|
||||
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick="deleteReservation('{{ res.id }}', '{{ res.name }}')"
|
||||
class="p-2 text-gray-400 hover:text-red-600 dark:hover:text-red-400 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
title="Delete reservation">
|
||||
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function deleteReservation(id, name) {
|
||||
if (!confirm(`Delete reservation "${name}"?\n\nThis will remove all unit assignments.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/fleet-calendar/reservations/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
alert('Error: ' + (data.detail || 'Failed to delete'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Error deleting reservation');
|
||||
}
|
||||
}
|
||||
|
||||
function editReservation(id) {
|
||||
// For now, just show alert - can implement edit modal later
|
||||
alert('Edit functionality coming soon. For now, delete and recreate the reservation.');
|
||||
}
|
||||
</script>
|
||||
{% else %}
|
||||
<div class="text-center py-8">
|
||||
<svg class="w-12 h-12 mx-auto text-gray-400 dark:text-gray-500 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<p class="text-gray-500 dark:text-gray-400">No reservations for {{ year }}</p>
|
||||
<p class="text-sm text-gray-400 dark:text-gray-500 mt-1">Click "New Reservation" to plan unit assignments</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user