Feat: expands project reservation system.
-Reservation list view -expandable project cards
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
{% set card_id = "res-card-" ~ res.id %}
|
||||
{% set detail_id = "res-detail-" ~ res.id %}
|
||||
|
||||
<div class="rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"
|
||||
<div class="rounded-lg border border-gray-200 dark:border-gray-700"
|
||||
style="border-left: 4px solid {{ res.color }};">
|
||||
|
||||
<!-- Header row (always visible, clickable) -->
|
||||
<div class="res-card-header flex items-center justify-between p-4 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors select-none"
|
||||
data-res-id="{{ res.id }}">
|
||||
data-res-id="{{ res.id }}"
|
||||
onclick="toggleResCard('{{ res.id }}')">
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
@@ -59,30 +60,30 @@
|
||||
</div>
|
||||
|
||||
<!-- Action buttons (stop propagation so clicks don't toggle card) -->
|
||||
<div class="flex items-center gap-1 flex-shrink-0" onclick="event.stopPropagation(); event.preventDefault();">
|
||||
<button onclick="openPlanner('{{ res.id }}')"
|
||||
<div class="flex items-center gap-1 flex-shrink-0">
|
||||
<button onclick="event.stopPropagation(); openPlanner('{{ res.id }}')"
|
||||
class="p-2 text-gray-400 hover:text-green-600 dark:hover:text-green-400 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
title="Plan units">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick="editReservation('{{ res.id }}')"
|
||||
<button onclick="event.stopPropagation(); 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">
|
||||
<svg class="w-4 h-4" 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 }}')"
|
||||
<button onclick="event.stopPropagation(); 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">
|
||||
<svg class="w-4 h-4" 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>
|
||||
<!-- Chevron -->
|
||||
<svg id="chevron-{{ res.id }}" class="w-4 h-4 text-gray-400 transition-transform duration-200 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<!-- Chevron (not in stopPropagation zone so clicking it still toggles the card) -->
|
||||
<svg id="chevron-{{ res.id }}" class="w-4 h-4 text-gray-400 transition-transform duration-200 ml-1 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -116,23 +117,28 @@
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 mb-2">Monitoring Locations</p>
|
||||
<div class="flex flex-col gap-1">
|
||||
{% for u in item.assigned_units %}
|
||||
<div class="flex items-center gap-3 px-3 py-1.5 rounded bg-white dark:bg-slate-700 border border-gray-100 dark:border-gray-600 text-sm">
|
||||
<span class="text-gray-400 dark:text-gray-500 text-xs w-12 flex-shrink-0">Loc. {{ loop.index }}</span>
|
||||
<button onclick="openUnitDetailModal('{{ u.id }}')"
|
||||
class="font-medium text-blue-600 dark:text-blue-400 hover:underline">{{ u.id }}</button>
|
||||
<span class="flex-1"></span>
|
||||
{% if u.power_type == 'ac' %}
|
||||
<span class="text-xs px-1.5 py-0.5 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 rounded">A/C</span>
|
||||
{% elif u.power_type == 'solar' %}
|
||||
<span class="text-xs px-1.5 py-0.5 bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400 rounded">Solar</span>
|
||||
{% endif %}
|
||||
{% if u.deployed %}
|
||||
<span class="text-xs px-1.5 py-0.5 bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 rounded">Deployed</span>
|
||||
{% else %}
|
||||
<span class="text-xs px-1.5 py-0.5 bg-gray-100 dark:bg-gray-600 text-gray-500 dark:text-gray-400 rounded">Benched</span>
|
||||
{% endif %}
|
||||
{% if u.last_calibrated %}
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">Cal: {{ u.last_calibrated.strftime('%b %d, %Y') }}</span>
|
||||
<div class="rounded bg-white dark:bg-slate-700 border border-gray-100 dark:border-gray-600 text-sm">
|
||||
<div class="flex items-center gap-3 px-3 py-1.5">
|
||||
<span class="text-gray-400 dark:text-gray-500 text-xs w-12 flex-shrink-0">Loc. {{ loop.index }}</span>
|
||||
<button onclick="openUnitDetailModal('{{ u.id }}')"
|
||||
class="font-medium text-blue-600 dark:text-blue-400 hover:underline">{{ u.id }}</button>
|
||||
<span class="flex-1"></span>
|
||||
{% if u.power_type == 'ac' %}
|
||||
<span class="text-xs px-1.5 py-0.5 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 rounded">A/C</span>
|
||||
{% elif u.power_type == 'solar' %}
|
||||
<span class="text-xs px-1.5 py-0.5 bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400 rounded">Solar</span>
|
||||
{% endif %}
|
||||
{% if u.deployed %}
|
||||
<span class="text-xs px-1.5 py-0.5 bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 rounded">Deployed</span>
|
||||
{% else %}
|
||||
<span class="text-xs px-1.5 py-0.5 bg-gray-100 dark:bg-gray-600 text-gray-500 dark:text-gray-400 rounded">Benched</span>
|
||||
{% endif %}
|
||||
{% if u.last_calibrated %}
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">Cal: {{ u.last_calibrated.strftime('%b %d, %Y') }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if u.notes %}
|
||||
<p class="px-3 pb-1.5 text-xs text-gray-400 dark:text-gray-500 italic">{{ u.notes }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
Reference in New Issue
Block a user