-
-
+
+
+
+
+
+
Fleet Summary
+
+
+
+
+
-
-
-
-
- Set start and end dates to see available units
-
+
+
+
+
+
+
+ Set start and end dates to see available units
+
+
+
@@ -805,9 +901,38 @@ const deviceType = '{{ device_type }}';
const startYear = {{ start_year }};
const startMonth = {{ start_month }};
+// Cal expire dot toggle — off by default
+let calExpireDotsVisible = false;
+function toggleCalExpireDots() {
+ calExpireDotsVisible = !calExpireDotsVisible;
+ document.getElementById('view-calendar').classList.toggle('cal-expire-dots-hidden', !calExpireDotsVisible);
+ const btn = document.getElementById('cal-expire-toggle');
+ if (calExpireDotsVisible) {
+ btn.classList.remove('text-gray-400', 'dark:text-gray-500');
+ btn.classList.add('text-gray-700', 'dark:text-gray-200', 'bg-gray-100', 'dark:bg-gray-700');
+ } else {
+ btn.classList.add('text-gray-400', 'dark:text-gray-500');
+ btn.classList.remove('text-gray-700', 'dark:text-gray-200', 'bg-gray-100', 'dark:bg-gray-700');
+ }
+}
+
// Load today's stats
document.addEventListener('DOMContentLoaded', function() {
loadTodayStats();
+ // Apply hidden class immediately (dots off by default)
+ document.getElementById('view-calendar').classList.add('cal-expire-dots-hidden');
+ // Load fleet summary for right panel
+ loadFleetSummary();
+ // Restore active tab from hash
+ if (window.location.hash === '#cal') {
+ switchTab('calendar');
+ // Scroll today's month into view
+ const todayCell = document.querySelector('.day-cell.today');
+ if (todayCell) {
+ const monthCard = todayCell.closest('.month-card');
+ if (monthCard) monthCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+ }
});
async function loadTodayStats() {
@@ -882,6 +1007,20 @@ function openReservationModal() {
updateCalendarAvailability();
}
+function toggleResMenu(id) {
+ const menu = document.getElementById('res-menu-' + id);
+ if (!menu) return;
+ const isHidden = menu.classList.contains('hidden');
+ // Close all other open menus first
+ document.querySelectorAll('[id^="res-menu-"]').forEach(m => m.classList.add('hidden'));
+ if (isHidden) menu.classList.remove('hidden');
+}
+
+// Close any open res menu when clicking elsewhere
+document.addEventListener('click', () => {
+ document.querySelectorAll('[id^="res-menu-"]').forEach(m => m.classList.add('hidden'));
+});
+
function toggleResCard(id) {
const detail = document.getElementById('res-detail-' + id);
const chevron = document.getElementById('chevron-' + id);
@@ -906,18 +1045,18 @@ async function deleteReservation(id, name) {
htmx.trigger('#planner-reservations-list', 'load');
} else {
const data = await response.json();
- alert('Error: ' + (data.detail || 'Failed to delete'));
+ showPlannerToast('Error: ' + (data.detail || 'Failed to delete'), true);
}
} catch (error) {
console.error('Error:', error);
- alert('Error deleting reservation');
+ showPlannerToast('Error deleting reservation', true);
}
}
async function editReservation(id) {
try {
const response = await fetch(`/api/fleet-calendar/reservations/${id}`);
- if (!response.ok) { alert('Failed to load reservation'); return; }
+ if (!response.ok) { showPlannerToast('Failed to load reservation', true); return; }
const res = await response.json();
// Switch modal to "edit" mode
@@ -963,7 +1102,7 @@ async function editReservation(id) {
updateCalendarAvailability();
} catch (error) {
console.error('Error loading reservation:', error);
- alert('Error loading reservation');
+ showPlannerToast('Error loading reservation', true);
}
}
@@ -1003,8 +1142,10 @@ async function confirmPromote() {
const result = await resp.json();
if (!resp.ok) throw new Error(result.detail || 'Failed to promote');
closePromoteModal();
- // Navigate to the new project page
- window.location.href = `/projects/${result.project_id}`;
+ showPlannerToast('Project created! Redirecting…');
+ const listUrl = document.getElementById('planner-reservations-list')?.getAttribute('hx-get');
+ if (listUrl) htmx.ajax('GET', listUrl, {target: '#planner-reservations-list', swap: 'innerHTML'});
+ setTimeout(() => { window.location.href = `/projects/${result.project_id}`; }, 1200);
} catch (e) {
errEl.textContent = e.message;
errEl.classList.remove('hidden');
@@ -1169,7 +1310,7 @@ async function submitReservation(event) {
};
if (!data.end_date && !data.end_date_tbd) {
- alert('Please enter an end date or check "TBD / Ongoing"');
+ showPlannerToast('Please enter an end date or check "TBD / Ongoing"');
return;
}
@@ -1198,11 +1339,11 @@ async function submitReservation(event) {
closeReservationModal();
window.location.reload();
} else {
- alert(`Error ${isEdit ? 'saving' : 'creating'} reservation: ` + (result.detail || 'Unknown error'));
+ showPlannerToast(`Error ${isEdit ? 'saving' : 'creating'} reservation: ` + (result.detail || 'Unknown error'));
}
} catch (error) {
console.error('Error:', error);
- alert(`Error ${isEdit ? 'saving' : 'creating'} reservation`);
+ showPlannerToast(`Error ${isEdit ? 'saving' : 'creating'} reservation`);
}
}
@@ -1215,40 +1356,79 @@ document.addEventListener('keydown', function(e) {
});
// ============================================================
-// Tab + sub-tab switching
+// Panel switching
// ============================================================
function switchPlannerTab(tab) {
const isAssign = tab === 'assign';
document.getElementById('ptab-list').classList.toggle('hidden', isAssign);
document.getElementById('ptab-assign').classList.toggle('hidden', !isAssign);
+ showRightPanel(isAssign ? 'available' : 'summary');
+}
- ['list', 'assign'].forEach(t => {
- const btn = document.getElementById(`ptab-btn-${t}`);
- if (t === tab) {
- btn.classList.add('bg-white', 'dark:bg-slate-600', 'text-gray-900', 'dark:text-white', 'shadow');
- btn.classList.remove('text-gray-600', 'dark:text-gray-300', 'hover:text-gray-900', 'dark:hover:text-white');
- } else {
- btn.classList.remove('bg-white', 'dark:bg-slate-600', 'text-gray-900', 'dark:text-white', 'shadow');
- btn.classList.add('text-gray-600', 'dark:text-gray-300', 'hover:text-gray-900', 'dark:hover:text-white');
- }
- });
+function switchDeviceType(dt) {
+ const hash = document.getElementById('view-calendar').classList.contains('hidden') ? '' : '#cal';
+ window.location.href = `/fleet-calendar?year={{ start_year }}&month={{ start_month }}&device_type=${dt}${hash}`;
}
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');
+ const isCalendar = tab === 'calendar';
+ document.getElementById('view-calendar').classList.toggle('hidden', !isCalendar);
+ document.getElementById('view-planner').classList.toggle('hidden', isCalendar);
- ['calendar', 'planner'].forEach(t => {
+ ['planner', 'calendar'].forEach(t => {
const btn = document.getElementById(`tab-btn-${t}`);
- if (t === tab) {
- btn.classList.add('bg-white', 'dark:bg-slate-600', 'text-gray-900', 'dark:text-white', 'shadow');
- btn.classList.remove('text-gray-600', 'dark:text-gray-300', 'hover:text-gray-900', 'dark:hover:text-white');
- } else {
- btn.classList.remove('bg-white', 'dark:bg-slate-600', 'text-gray-900', 'dark:text-white', 'shadow');
- btn.classList.add('text-gray-600', 'dark:text-gray-300', 'hover:text-gray-900', 'dark:hover:text-white');
- }
+ const active = t === tab;
+ btn.classList.toggle('bg-white', active);
+ btn.classList.toggle('dark:bg-slate-600', active);
+ btn.classList.toggle('text-gray-900', active);
+ btn.classList.toggle('dark:text-white', active);
+ btn.classList.toggle('shadow', active);
+ btn.classList.toggle('text-gray-600', !active);
+ btn.classList.toggle('dark:text-gray-300', !active);
+ btn.classList.toggle('hover:text-gray-900', !active);
+ btn.classList.toggle('dark:hover:text-white', !active);
});
+
+ if (isCalendar) {
+ htmx.trigger(document.body, 'calendar-reservations-refresh');
+ // Scroll today into view
+ requestAnimationFrame(() => {
+ const todayCell = document.querySelector('.day-cell.today');
+ if (todayCell) {
+ const monthCard = todayCell.closest('.month-card');
+ if (monthCard) monthCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+ });
+ }
+ history.replaceState(null, '', isCalendar ? '#cal' : window.location.pathname + window.location.search);
+}
+
+// Job layer toggles
+let confirmedVisible = true;
+let plannedVisible = true;
+
+function toggleJobLayer(layer) {
+ if (layer === 'confirmed') {
+ confirmedVisible = !confirmedVisible;
+ document.getElementById('view-calendar').classList.toggle('hide-confirmed', !confirmedVisible);
+ const btn = document.getElementById('toggle-confirmed');
+ btn.classList.toggle('text-gray-700', confirmedVisible);
+ btn.classList.toggle('dark:text-gray-200', confirmedVisible);
+ btn.classList.toggle('bg-gray-100', confirmedVisible);
+ btn.classList.toggle('dark:bg-gray-700', confirmedVisible);
+ btn.classList.toggle('text-gray-400', !confirmedVisible);
+ btn.classList.toggle('dark:text-gray-500', !confirmedVisible);
+ } else {
+ plannedVisible = !plannedVisible;
+ document.getElementById('view-calendar').classList.toggle('hide-planned', !plannedVisible);
+ const btn = document.getElementById('toggle-planned');
+ btn.classList.toggle('text-gray-700', plannedVisible);
+ btn.classList.toggle('dark:text-gray-200', plannedVisible);
+ btn.classList.toggle('bg-gray-100', plannedVisible);
+ btn.classList.toggle('dark:bg-gray-700', plannedVisible);
+ btn.classList.toggle('text-gray-400', !plannedVisible);
+ btn.classList.toggle('dark:text-gray-500', !plannedVisible);
+ }
}
// ============================================================
@@ -1261,10 +1441,115 @@ let plannerState = {
};
let dragSrcIdx = null;
+// ---- Color picker ----
+const PLANNER_PALETTE = [
+ '#3B82F6','#2563EB','#06B6D4','#0D9488',
+ '#10B981','#84CC16','#EAB308','#F97316',
+ '#EF4444','#DC2626','#EC4899','#A855F7',
+ '#8B5CF6','#6366F1','#78716C','#94A3B8',
+ '#F59E0B','#14B8A6'
+];
+
+// Colors in use by existing reservations (populated from Jinja at page load)
+const EXISTING_JOB_COLORS = {{ calendar_projects | map(attribute='color') | list | tojson }};
+
+function plannerPickSmartColor() {
+ // Parse hex to HSL, pick the palette color that maximizes minimum hue distance from existing colors
+ function hexToHsl(hex) {
+ const r = parseInt(hex.slice(1,3),16)/255, g = parseInt(hex.slice(3,5),16)/255, b = parseInt(hex.slice(5,7),16)/255;
+ const max = Math.max(r,g,b), min = Math.min(r,g,b), l = (max+min)/2;
+ if (max === min) return [0, 0, l];
+ const d = max - min, s = l > 0.5 ? d/(2-max-min) : d/(max+min);
+ let h;
+ if (max === r) h = ((g-b)/d + (g
c && c.startsWith('#'))
+ .map(c => hexToHsl(c)[0]);
+
+ // Also exclude the current planner color if editing
+ const cur = document.getElementById('planner-color-value')?.value;
+ if (cur && plannerState.reservation_id) usedHues.push(hexToHsl(cur)[0]);
+
+ let best = PLANNER_PALETTE[0], bestDist = -1;
+ for (const c of PLANNER_PALETTE) {
+ const h = hexToHsl(c)[0];
+ const minDist = usedHues.length ? Math.min(...usedHues.map(uh => hueDist(h, uh))) : 999;
+ if (minDist > bestDist) { bestDist = minDist; best = c; }
+ }
+ return best;
+}
+
+function plannerRenderColorSwatches(selected) {
+ const container = document.getElementById('planner-color-swatches');
+ if (!container) return;
+ container.innerHTML = PLANNER_PALETTE.map(c => {
+ const active = c.toLowerCase() === (selected||'').toLowerCase();
+ return ``;
+ }).join('');
+}
+
+function plannerSetColor(hex, fromSwatch) {
+ document.getElementById('planner-color-value').value = hex;
+ document.getElementById('planner-color-wheel').value = hex.length === 7 ? hex : '#3B82F6';
+ plannerRenderColorSwatches(hex);
+ const dot = document.getElementById('planner-meta-color-dot');
+ if (dot) dot.style.background = hex;
+}
+
+function togglePlannerMeta() {
+ const body = document.getElementById('planner-meta-body');
+ const chevron = document.getElementById('planner-meta-chevron');
+ const open = !body.classList.contains('hidden');
+ body.classList.toggle('hidden', open);
+ chevron.style.transform = open ? '' : 'rotate(180deg)';
+}
+
+function plannerSetMetaOpen(open) {
+ const body = document.getElementById('planner-meta-body');
+ const chevron = document.getElementById('planner-meta-chevron');
+ body.classList.toggle('hidden', !open);
+ chevron.style.transform = open ? 'rotate(180deg)' : '';
+}
+
+function plannerUpdateMetaSummary() {
+ const name = document.getElementById('planner-name')?.value.trim();
+ const start = document.getElementById('planner-start')?.value;
+ const end = document.getElementById('planner-end')?.value;
+ const summary = document.getElementById('planner-meta-summary');
+ if (!summary) return;
+ let text = name || 'New Job';
+ if (start) {
+ const s = new Date(start + 'T00:00:00').toLocaleDateString('en-US', {month:'short', day:'numeric'});
+ const e = end ? new Date(end + 'T00:00:00').toLocaleDateString('en-US', {month:'short', day:'numeric'}) : '…';
+ text += ` · ${s} – ${e}`;
+ }
+ summary.textContent = text;
+}
+
+// Initialize swatches and meta summary on page load
+document.addEventListener('DOMContentLoaded', () => {
+ plannerRenderColorSwatches('#3B82F6');
+ plannerUpdateMetaSummary();
+});
+
function plannerDatesChanged() {
plannerLoadUnits();
}
+function plannerDeviceTypeChanged() {
+ plannerDatesChanged();
+ loadFleetSummary();
+}
+
async function plannerLoadUnits() {
const start = document.getElementById('planner-start').value;
const end = document.getElementById('planner-end').value;
@@ -1277,6 +1562,11 @@ async function plannerLoadUnits() {
}
if (excludeId) url += `&exclude_reservation_id=${excludeId}`;
+ // Show loading state
+ const placeholder = document.getElementById('planner-units-placeholder');
+ if (placeholder) { placeholder.classList.remove('hidden'); placeholder.textContent = 'Loading units...'; }
+ document.getElementById('planner-units-list')?.querySelectorAll('.planner-unit-row').forEach(el => el.remove());
+
try {
const resp = await fetch(url);
const data = await resp.json();
@@ -1319,7 +1609,9 @@ function plannerRenderUnits() {
if (plannerState.allUnits.length === 0) {
placeholder.classList.remove('hidden');
- placeholder.textContent = 'Loading units...';
+ const start = document.getElementById('planner-start').value;
+ const end = document.getElementById('planner-end').value;
+ placeholder.textContent = (start && end) ? 'No units available for this period' : 'Set start and end dates to see available units';
list.querySelectorAll('.planner-unit-row').forEach(el => el.remove());
return;
}
@@ -1402,6 +1694,150 @@ function plannerAddSlot() {
plannerRenderSlots();
}
+// ============================================================
+// Fleet Summary (right panel on jobs list)
+// ============================================================
+let summaryAllUnits = [];
+let summaryActiveFilter = null; // null | 'deployed' | 'benched' | 'cal_expired'
+
+async function loadFleetSummary() {
+ const plannerDeviceType = document.querySelector('input[name="planner_device_type"]:checked')?.value || 'seismograph';
+ try {
+ const resp = await fetch(`/api/fleet-calendar/planner-availability?device_type=${plannerDeviceType}`);
+ const data = await resp.json();
+ summaryAllUnits = data.units || [];
+ summaryActiveFilter = null;
+ renderFleetSummary();
+ } catch(e) { console.error('Fleet summary load error', e); }
+}
+
+function summaryFilterUnits() {
+ renderFleetSummary();
+}
+
+function summarySetFilter(f) {
+ summaryActiveFilter = summaryActiveFilter === f ? null : f;
+ renderFleetSummary();
+}
+
+function renderFleetSummary() {
+ const search = document.getElementById('summary-search')?.value.toLowerCase() || '';
+
+ // Stats (always against full list)
+ const total = summaryAllUnits.length;
+ const deployed = summaryAllUnits.filter(u => u.deployed).length;
+ const benched = summaryAllUnits.filter(u => !u.deployed).length;
+ const calExpired = summaryAllUnits.filter(u => u.expiry_date && new Date(u.expiry_date + 'T00:00:00') < new Date()).length;
+
+ const cardBase = 'rounded-lg p-3 text-left w-full cursor-pointer transition-all ring-2';
+ const active = summaryActiveFilter;
+ document.getElementById('fleet-summary-stats').innerHTML = `
+
+
+
+
+ `;
+
+ // Apply filter + search to the list
+ let units = summaryAllUnits;
+ if (active === 'deployed') units = units.filter(u => u.deployed);
+ else if (active === 'benched') units = units.filter(u => !u.deployed);
+ else if (active === 'cal_expired') units = units.filter(u => u.expiry_date && new Date(u.expiry_date + 'T00:00:00') < new Date());
+ if (search) units = units.filter(u => u.id.toLowerCase().includes(search));
+
+ // Unit list
+ const list = document.getElementById('fleet-summary-list');
+ if (units.length === 0) {
+ list.innerHTML = 'No units found
';
+ return;
+ }
+
+ list.innerHTML = units.map(u => {
+ const calDate = u.last_calibrated
+ ? new Date(u.last_calibrated + 'T00:00:00').toLocaleDateString('en-US', {month:'short', day:'numeric', year:'numeric'})
+ : 'No cal date';
+ const expired = u.expiry_date && new Date(u.expiry_date + 'T00:00:00') < new Date();
+ const deployedBadge = u.deployed
+ ? '
Deployed'
+ : '
Benched';
+ const calBadge = expired
+ ? `
Cal expired`
+ : `
Cal: ${calDate}`;
+ const resBadges = (u.reservations || []).map(r => {
+ const s = r.start_date ? new Date(r.start_date + 'T00:00:00').toLocaleDateString('en-US', {month:'short', day:'numeric'}) : '';
+ const e = r.end_date ? new Date(r.end_date + 'T00:00:00').toLocaleDateString('en-US', {month:'short', day:'numeric'}) : 'TBD';
+ return `
Reserved: ${r.reservation_name} ${s}–${e}`;
+ }).join('');
+ return `
+
+
+
+ ${deployedBadge}
+ ${calBadge}
+
+ ${resBadges ? `
${resBadges}
` : ''}
+
`;
+ }).join('');
+}
+
+function showRightPanel(panel) {
+ document.getElementById('right-fleet-summary').classList.toggle('hidden', panel !== 'summary');
+ document.getElementById('right-available-units').classList.toggle('hidden', panel !== 'available');
+}
+
+function showPlannerToast(msg, isError = false) {
+ let toast = document.getElementById('planner-toast');
+ if (!toast) {
+ toast = document.createElement('div');
+ toast.id = 'planner-toast';
+ toast.className = 'fixed bottom-6 left-1/2 -translate-x-1/2 z-50 px-5 py-2.5 rounded-lg shadow-lg text-white text-sm font-medium transition-opacity duration-300 pointer-events-none';
+ document.body.appendChild(toast);
+ }
+ toast.style.background = isError ? '#ef4444' : '#22c55e';
+ toast.textContent = (isError ? '✕ ' : '✓ ') + msg;
+ toast.style.opacity = '1';
+ clearTimeout(toast._hideTimer);
+ toast._hideTimer = setTimeout(() => { toast.style.opacity = '0'; }, isError ? 4000 : 2500);
+}
+
+function plannerSyncSlotsToEstimate() {
+ const target = parseInt(document.getElementById('planner-est-units').value);
+ if (!target || target < 1) return;
+ const current = plannerState.slots.length;
+ if (target > current) {
+ // Add empty slots up to target
+ for (let i = current; i < target; i++) {
+ plannerState.slots.push({ unit_id: null, power_type: null, notes: null, location_name: null });
+ }
+ } else if (target < current) {
+ // Remove empty slots from the bottom, never remove filled ones
+ for (let i = plannerState.slots.length - 1; i >= target; i--) {
+ if (!plannerState.slots[i].unit_id) {
+ plannerState.slots.splice(i, 1);
+ }
+ if (plannerState.slots.length <= target) break;
+ }
+ }
+ plannerRenderSlots();
+}
+
function plannerAssignUnit(unitId) {
const emptyIdx = plannerState.slots.findIndex(s => !s.unit_id);
if (emptyIdx >= 0) {
@@ -1538,11 +1974,16 @@ function plannerReset() {
if (defaultDt) defaultDt.checked = true;
document.getElementById('planner-deployed-only').checked = false;
document.getElementById('planner-avail-count').textContent = '';
- document.querySelector('input[name="planner_color"][value="#3B82F6"]').checked = true;
+ const smartColor = plannerPickSmartColor();
+ plannerSetColor(smartColor, true);
const titleEl = document.getElementById('planner-form-title');
- if (titleEl) titleEl.textContent = 'New Reservation';
- document.getElementById('planner-save-btn').textContent = 'Save Reservation';
+ if (titleEl) titleEl.textContent = 'New Job';
+ document.getElementById('planner-save-btn').textContent = 'Save Job';
document.getElementById('planner-meta-fields').style.display = '';
+ document.getElementById('planner-edit-actions').classList.add('hidden');
+ plannerSetMetaOpen(true);
+ const summaryEl = document.getElementById('planner-meta-summary');
+ if (summaryEl) summaryEl.textContent = 'New Job';
plannerRenderSlots();
plannerRenderUnits();
}
@@ -1553,13 +1994,14 @@ async function plannerSave() {
const end = document.getElementById('planner-end').value;
const projectId = document.getElementById('planner-project').value;
const notes = document.getElementById('planner-notes').value.trim();
- const color = document.querySelector('input[name="planner_color"]:checked')?.value || '#3B82F6';
+ const color = document.getElementById('planner-color-value')?.value || '#3B82F6';
const estUnits = parseInt(document.getElementById('planner-est-units').value) || null;
+ const totalSlots = plannerState.slots.length || null;
const filledSlots = plannerState.slots.filter(s => s.unit_id);
- if (!name) { alert('Please enter a reservation name.'); return; }
- if (!start || !end) { alert('Please set start and end dates.'); return; }
- if (end < start) { alert('End date must be after start date.'); return; }
+ if (!name) { showPlannerToast('Please enter a job name.', true); return; }
+ if (!start || !end) { showPlannerToast('Please set start and end dates.', true); return; }
+ if (end < start) { showPlannerToast('End date must be after start date.', true); return; }
const btn = document.getElementById('planner-save-btn');
btn.disabled = true;
@@ -1579,7 +2021,8 @@ async function plannerSave() {
assignment_type: 'specific',
device_type: plannerDeviceType,
color, notes: notes || null,
- quantity_needed: estUnits
+ estimated_units: estUnits,
+ quantity_needed: totalSlots
};
const resp = await fetch(url, {
@@ -1615,19 +2058,47 @@ async function plannerSave() {
const assignResult = await assignResp.json();
if (assignResult.conflicts && assignResult.conflicts.length > 0) {
const conflictIds = assignResult.conflicts.map(c => c.unit_id).join(', ');
- alert(`Saved! Note: ${assignResult.conflicts.length} unit(s) had conflicts and were not assigned: ${conflictIds}`);
+ showPlannerToast(`Saved! ${assignResult.conflicts.length} unit(s) had conflicts and were skipped: ${conflictIds}`);
}
plannerReset();
switchPlannerTab('list');
// Reload the reservations list partial
- htmx.trigger('#planner-reservations-list', 'load');
+ const listUrl = document.getElementById('planner-reservations-list').getAttribute('hx-get');
+ htmx.ajax('GET', listUrl, {target: '#planner-reservations-list', swap: 'innerHTML'});
+ showPlannerToast(isEdit ? 'Job updated!' : 'Job created!');
} catch (e) {
console.error('Planner save error', e);
- alert('Error saving reservation: ' + e.message);
+ showPlannerToast('Error saving job: ' + e.message, true);
} finally {
btn.disabled = false;
- btn.textContent = plannerState.reservation_id ? 'Save Changes' : 'Save Reservation';
+ btn.textContent = plannerState.reservation_id ? 'Save Changes' : 'Save Job';
+ }
+}
+
+function plannerPromote() {
+ if (!plannerState.reservation_id) return;
+ const name = document.getElementById('planner-form-title').textContent;
+ openPromoteModal(plannerState.reservation_id, name);
+}
+
+async function plannerDeleteCurrent() {
+ const name = document.getElementById('planner-form-title').textContent;
+ if (!confirm(`Delete job "${name}"?\n\nThis will remove all unit assignments.`)) return;
+ try {
+ const resp = await fetch(`/api/fleet-calendar/reservations/${plannerState.reservation_id}`, { method: 'DELETE' });
+ if (resp.ok) {
+ switchPlannerTab('list');
+ plannerReset();
+ const listUrl = document.getElementById('planner-reservations-list').getAttribute('hx-get');
+ htmx.ajax('GET', listUrl, {target: '#planner-reservations-list', swap: 'innerHTML'});
+ showPlannerToast('Job deleted');
+ } else {
+ const data = await resp.json();
+ showPlannerToast('Error: ' + (data.detail || 'Failed to delete'), true);
+ }
+ } catch (e) {
+ showPlannerToast('Error deleting job', true);
}
}
@@ -1643,9 +2114,8 @@ async function openPlanner(reservationId) {
document.getElementById('planner-start').value = res.start_date;
document.getElementById('planner-end').value = res.end_date || '';
document.getElementById('planner-notes').value = res.notes || '';
- document.getElementById('planner-est-units').value = res.quantity_needed || '';
- const colorRadio = document.querySelector(`input[name="planner_color"][value="${res.color}"]`);
- if (colorRadio) colorRadio.checked = true;
+ document.getElementById('planner-est-units').value = res.estimated_units || '';
+ plannerSetColor(res.color || '#3B82F6', true);
const dtRadio = document.querySelector(`input[name="planner_device_type"][value="${res.device_type || 'seismograph'}"]`);
if (dtRadio) dtRadio.checked = true;
// Pre-fill slots from existing assigned units
@@ -1656,9 +2126,12 @@ async function openPlanner(reservationId) {
const titleEl = document.getElementById('planner-form-title');
if (titleEl) titleEl.textContent = res.name;
document.getElementById('planner-save-btn').textContent = 'Save Changes';
- document.getElementById('planner-meta-fields').style.display = 'none';
+ document.getElementById('planner-meta-fields').style.display = '';
+ document.getElementById('planner-edit-actions').classList.remove('hidden');
+ plannerSetMetaOpen(false);
+ plannerUpdateMetaSummary();
plannerRenderSlots();
- if (res.start_date && res.end_date) plannerLoadUnits();
+ plannerLoadUnits();
} catch (e) {
console.error('Error loading reservation for planner', e);
}
diff --git a/templates/partials/fleet_calendar/reservations_list.html b/templates/partials/fleet_calendar/reservations_list.html
index 4df4751..06a89f0 100644
--- a/templates/partials/fleet_calendar/reservations_list.html
+++ b/templates/partials/fleet_calendar/reservations_list.html
@@ -44,52 +44,79 @@
-
- {% if res.quantity_needed %}
- {{ item.assigned_count }}/{{ res.quantity_needed }}
- {% else %}
- {{ item.assigned_count }}
+
+
+ {% set full = item.assigned_count == item.location_count and item.location_count > 0 %}
+ {% set remaining = item.location_count - item.assigned_count %}
+
+
+
est. {% if res.estimated_units %}{{ res.estimated_units }}{% else %}—{% endif %}
+
·
+
+ {{ item.assigned_count }}/{{ item.location_count }}
+
+ {% if remaining > 0 %}
+
({{ remaining }} more)
{% endif %}
-
-
- {{ 'assigned' if item.assigned_count != 1 else 'assigned' }}
- {% if res.quantity_needed %} needed{% endif %}
-
+
+
+ {% if item.location_count > 0 %}
+
+ {% for i in range(item.location_count) %}
+
+ {% endfor %}
+
+ {% endif %}
-
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -104,15 +131,15 @@
{% endif %}
- {% if res.quantity_needed %}
-
Est. units needed
-
{{ res.quantity_needed }}
- {% endif %}
-
Assigned
-
{{ item.assigned_count }} unit{{ 's' if item.assigned_count != 1 else '' }}
- {% if res.quantity_needed and item.assigned_count < res.quantity_needed %}
+
Estimated
+
+ {% if res.estimated_units %}{{ res.estimated_units }} unit{{ 's' if res.estimated_units != 1 else '' }}{% else %}not specified{% endif %}
+
+
Locations
+
{{ item.assigned_count }} of {{ item.location_count }} filled
+ {% if item.assigned_count < item.location_count %}
Still needed
-
{{ res.quantity_needed - item.assigned_count }} more
+
{{ item.location_count - item.assigned_count }} location{{ 's' if (item.location_count - item.assigned_count) != 1 else '' }} remaining
{% endif %}
{% if item.has_conflicts %}
Cal swaps
@@ -170,7 +197,7 @@
-
No reservations found
-
Click "New Reservation" to plan unit assignments
+
No jobs yet
+
Click "New Job" to start planning a deployment
{% endif %}
diff --git a/templates/partials/projects/project_dashboard.html b/templates/partials/projects/project_dashboard.html
index 1cf04f6..6f04129 100644
--- a/templates/partials/projects/project_dashboard.html
+++ b/templates/partials/projects/project_dashboard.html
@@ -11,7 +11,9 @@
{% endif %}
- {% if project.status == 'active' %}
+ {% if project.status == 'upcoming' %}
+
Upcoming
+ {% elif project.status == 'active' %}
Active
{% elif project.status == 'on_hold' %}
On Hold
diff --git a/templates/partials/projects/project_header.html b/templates/partials/projects/project_header.html
index 65e6f5e..ed21c4a 100644
--- a/templates/partials/projects/project_header.html
+++ b/templates/partials/projects/project_header.html
@@ -3,12 +3,26 @@
{{ project.name }}
-
- {{ project.status|title }}
-
+
+
+
+
+
+
{% if project_type %}
{{ project_type.name }}
{% endif %}
diff --git a/templates/partials/projects/project_list_compact.html b/templates/partials/projects/project_list_compact.html
index a2acf79..6ef1e01 100644
--- a/templates/partials/projects/project_list_compact.html
+++ b/templates/partials/projects/project_list_compact.html
@@ -14,7 +14,9 @@
{% endif %}
- {% if item.project.status == 'active' %}
+ {% if item.project.status == 'upcoming' %}
+
Upcoming
+ {% elif item.project.status == 'active' %}
Active
{% elif item.project.status == 'on_hold' %}
On Hold
diff --git a/templates/projects/detail.html b/templates/projects/detail.html
index 784cd55..5a4a8cf 100644
--- a/templates/projects/detail.html
+++ b/templates/projects/detail.html
@@ -328,6 +328,7 @@