Feat: expands project reservation system.

-Reservation list view
-expandable project cards
This commit is contained in:
2026-03-15 05:25:23 +00:00
parent e4d1f0d684
commit 0e3f512203
3 changed files with 71 additions and 48 deletions

View File

@@ -389,7 +389,7 @@
<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-trigger="calendar-reservations-refresh from:body"
hx-swap="innerHTML">
<p class="text-gray-500">Loading reservations...</p>
</div>
@@ -417,7 +417,7 @@
</div>
<!-- Sub-tab: Reservations list -->
<div id="ptab-list" class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6 flex flex-col gap-4 flex-1">
<div id="ptab-list" class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6 flex flex-col gap-4">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Project Reservations</h2>
<button onclick="plannerReset(); switchPlannerTab('assign')"
@@ -428,7 +428,7 @@
New
</button>
</div>
<div id="planner-reservations-list" class="overflow-y-auto" style="max-height: 60vh;"
<div id="planner-reservations-list" class="overflow-y-visible"
hx-get="/api/fleet-calendar/reservations-list?year={{ start_year }}&month={{ start_month }}&device_type={{ device_type }}"
hx-trigger="load"
hx-swap="innerHTML">
@@ -447,15 +447,18 @@
<h2 class="text-xl font-semibold text-gray-900 dark:text-white" id="planner-form-title">New Reservation</h2>
</div>
<!-- Metadata fields: only shown when creating a new reservation -->
<div id="planner-meta-fields">
<!-- Name -->
<div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Job / Reservation Name *</label>
<input type="text" id="planner-name" placeholder="e.g., Pine Street May 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>
<!-- Device Type -->
<div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Device Type *</label>
<div class="flex rounded-lg border border-gray-300 dark:border-gray-600 overflow-hidden">
<label class="flex-1 cursor-pointer">
@@ -474,7 +477,7 @@
</div>
<!-- Project -->
<div>
<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 id="planner-project"
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">
@@ -486,7 +489,7 @@
</div>
<!-- Dates -->
<div class="grid grid-cols-2 gap-3">
<div class="grid grid-cols-2 gap-3 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Start Date *</label>
<input type="date" id="planner-start"
@@ -502,7 +505,7 @@
</div>
<!-- Color -->
<div>
<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" id="planner-colors">
{% for color in ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899'] %}
@@ -516,12 +519,14 @@
</div>
<!-- Estimated Units Needed -->
<div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Estimated Units Needed</label>
<input type="number" id="planner-est-units" min="1" placeholder="e.g. 5"
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><!-- end #planner-meta-fields -->
<!-- Monitoring Locations -->
<div class="flex items-center justify-between mt-2">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Monitoring Locations</h3>
@@ -851,17 +856,16 @@ function toggleResCard(id) {
const chevron = document.getElementById('chevron-' + id);
if (!detail) return;
const isHidden = detail.classList.contains('hidden');
detail.classList.toggle('hidden', !isHidden);
if (isHidden) {
detail.classList.remove('hidden');
detail.style.display = 'block';
} else {
detail.classList.add('hidden');
detail.style.display = 'none';
}
if (chevron) chevron.style.transform = isHidden ? 'rotate(180deg)' : '';
}
// Event delegation for reservation cards (handles HTMX-loaded content)
document.addEventListener('click', function(e) {
const header = e.target.closest('.res-card-header');
if (!header) return;
const id = header.dataset.resId;
if (id) toggleResCard(id);
});
async function deleteReservation(id, name) {
if (!confirm(`Delete reservation "${name}"?\n\nThis will remove all unit assignments.`)) return;
@@ -1156,6 +1160,7 @@ function switchPlannerTab(tab) {
function switchTab(tab) {
document.getElementById('view-calendar').classList.toggle('hidden', tab !== 'calendar');
document.getElementById('view-planner').classList.toggle('hidden', tab !== 'planner');
if (tab === 'calendar') htmx.trigger(document.body, 'calendar-reservations-refresh');
['calendar', 'planner'].forEach(t => {
const btn = document.getElementById(`tab-btn-${t}`);
@@ -1452,6 +1457,7 @@ function plannerReset() {
const titleEl = document.getElementById('planner-form-title');
if (titleEl) titleEl.textContent = 'New Reservation';
document.getElementById('planner-save-btn').textContent = 'Save Reservation';
document.getElementById('planner-meta-fields').style.display = '';
plannerRenderSlots();
plannerRenderUnits();
}
@@ -1504,13 +1510,17 @@ async function plannerSave() {
// Always call assign-units (even with empty list) — endpoint does a full replace
const unitIds = filledSlots.map(s => s.unit_id);
const powerTypes = {};
filledSlots.forEach(s => { if (s.power_type) powerTypes[s.unit_id] = s.power_type; });
const locationNotes = {};
filledSlots.forEach(s => {
if (s.power_type) powerTypes[s.unit_id] = s.power_type;
if (s.notes) locationNotes[s.unit_id] = s.notes;
});
const assignResp = await fetch(
`/api/fleet-calendar/reservations/${reservationId}/assign-units`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unit_ids: unitIds, power_types: powerTypes })
body: JSON.stringify({ unit_ids: unitIds, power_types: powerTypes, location_notes: locationNotes })
}
);
const assignResult = await assignResp.json();
@@ -1553,9 +1563,11 @@ async function openPlanner(reservationId) {
for (const u of (res.assigned_units || [])) {
plannerState.slots.push({ unit_id: u.id, power_type: u.power_type || null, notes: u.notes || null });
}
const titleEl = document.getElementById('planner-form-title');
if (titleEl) titleEl.textContent = 'Edit: ' + res.name;
if (titleEl) titleEl.textContent = res.name;
document.getElementById('planner-save-btn').textContent = 'Save Changes';
document.getElementById('planner-meta-fields').style.display = 'none';
plannerRenderSlots();
if (res.start_date && res.end_date) plannerLoadUnits();
} catch (e) {