merge v0.9.2. #40

Merged
serversdown merged 5 commits from dev into main 2026-03-27 14:58:34 -04:00
Showing only changes of commit 4f71d528ce - Show all commits

View File

@@ -1434,6 +1434,8 @@ function toggleJobLayer(layer) {
// ============================================================
// Reservation Planner
// ============================================================
let plannerSelectedSlotIdx = null;
let plannerState = {
reservation_id: null, // null = creating new
slots: [], // array of {unit_id: string|null, power_type: string|null, notes: string|null, location_name: string|null}
@@ -1607,6 +1609,22 @@ function plannerRenderUnits() {
const placeholder = document.getElementById('planner-units-placeholder');
const list = document.getElementById('planner-units-list');
// Show/hide slot-selection hint banner
let slotHint = document.getElementById('planner-slot-hint');
if (!slotHint) {
slotHint = document.createElement('div');
slotHint.id = 'planner-slot-hint';
list.parentNode.insertBefore(slotHint, list);
}
if (plannerSelectedSlotIdx !== null) {
const slotNum = plannerSelectedSlotIdx + 1;
slotHint.className = 'mb-2 px-3 py-2 rounded-lg bg-blue-50 dark:bg-blue-900/30 border border-blue-300 dark:border-blue-700 text-sm text-blue-700 dark:text-blue-300';
slotHint.textContent = `Assigning to Loc. ${slotNum} — click a unit below`;
} else {
slotHint.className = 'hidden';
slotHint.textContent = '';
}
if (plannerState.allUnits.length === 0) {
placeholder.classList.remove('hidden');
const start = document.getElementById('planner-start').value;
@@ -1838,13 +1856,24 @@ function plannerSyncSlotsToEstimate() {
plannerRenderSlots();
}
function plannerSelectSlot(idx) {
plannerSelectedSlotIdx = (plannerSelectedSlotIdx === idx) ? null : idx;
plannerRenderSlots();
plannerRenderUnits();
}
function plannerAssignUnit(unitId) {
if (plannerSelectedSlotIdx !== null && plannerSelectedSlotIdx < plannerState.slots.length && !plannerState.slots[plannerSelectedSlotIdx].unit_id) {
plannerState.slots[plannerSelectedSlotIdx].unit_id = unitId;
plannerSelectedSlotIdx = null;
} else {
const emptyIdx = plannerState.slots.findIndex(s => !s.unit_id);
if (emptyIdx >= 0) {
plannerState.slots[emptyIdx].unit_id = unitId;
} else {
plannerState.slots.push({ unit_id: unitId, power_type: null, notes: null, location_name: null });
}
}
plannerRenderSlots();
plannerRenderUnits();
}
@@ -1879,8 +1908,13 @@ function plannerRenderSlots() {
emptyMsg.classList.add('hidden');
plannerState.slots.forEach((slot, idx) => {
const isSelected = !slot.unit_id && plannerSelectedSlotIdx === idx;
const row = document.createElement('div');
row.className = 'planner-slot-row flex flex-col gap-1.5 px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-slate-700/50';
row.className = `planner-slot-row flex flex-col gap-1.5 px-3 py-2 rounded-lg border ${
isSelected
? 'border-blue-500 dark:border-blue-400 bg-blue-50 dark:bg-blue-900/30 ring-2 ring-blue-400 dark:ring-blue-500'
: 'border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-slate-700/50'
}`;
row.dataset.idx = idx;
row.draggable = !!slot.unit_id;
@@ -1904,11 +1938,10 @@ function plannerRenderSlots() {
e.preventDefault();
row.classList.remove('ring-2', 'ring-blue-400');
if (dragSrcIdx === null || dragSrcIdx === idx) return;
// Swap unit_id and power_type only (keep location notes in place)
// Swap unit_id only — power_type stays with the location slot
const srcSlot = plannerState.slots[dragSrcIdx];
const dstSlot = plannerState.slots[idx];
[srcSlot.unit_id, dstSlot.unit_id] = [dstSlot.unit_id, srcSlot.unit_id];
[srcSlot.power_type, dstSlot.power_type] = [dstSlot.power_type, srcSlot.power_type];
dragSrcIdx = null;
plannerRenderSlots();
plannerRenderUnits();
@@ -1926,6 +1959,40 @@ function plannerRenderSlots() {
? `<span class="text-gray-300 dark:text-gray-600 cursor-grab active:cursor-grabbing select-none" title="Drag to reorder">⠿</span>`
: `<span class="w-4"></span>`;
// Build unit info badges for filled slots
let unitInfoLine = '';
if (slot.unit_id) {
const uData = plannerState.allUnits.find(u => u.id === slot.unit_id);
if (uData) {
const deployedBadge = uData.deployed
? '<span class="px-1.5 py-0.5 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 rounded">Deployed</span>'
: '<span class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 rounded">Benched</span>';
const outForCalBadge = uData.out_for_calibration
? '<span class="px-1.5 py-0.5 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 rounded">Out for Cal</span>'
: '';
const calStr = uData.last_calibrated
? new Date(uData.last_calibrated + 'T00:00:00').toLocaleDateString('en-US', {month:'short', day:'numeric', year:'numeric'})
: 'No cal date';
const start = document.getElementById('planner-start').value;
const end = document.getElementById('planner-end').value;
let expiryBadge = '';
if (uData.expiry_date) {
const expiry = new Date(uData.expiry_date + 'T00:00:00');
const jobStart = start ? new Date(start + 'T00:00:00') : null;
const jobEnd = end ? new Date(end + 'T00:00:00') : null;
const expiryStr = expiry.toLocaleDateString('en-US', {month:'short', day:'numeric', year:'numeric'});
if (jobStart && jobEnd && expiry >= jobStart && expiry <= jobEnd) {
expiryBadge = `<span class="px-1.5 py-0.5 bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 rounded border border-amber-200 dark:border-amber-800">cal expires ${expiryStr}</span>`;
} else if (!jobStart || !jobEnd) {
expiryBadge = `<span class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 rounded">cal exp. ${expiryStr}</span>`;
}
} else {
expiryBadge = '<span class="px-1.5 py-0.5 bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 rounded">No cal</span>';
}
unitInfoLine = `<div class="pl-6 flex items-center gap-1.5 flex-wrap text-xs mt-0.5">${deployedBadge}${outForCalBadge}${expiryBadge}<span class="text-gray-400 dark:text-gray-500">Cal: ${calStr}</span></div>`;
}
}
row.innerHTML = `
<div class="flex items-center gap-2">
${dragHandle}
@@ -1934,11 +2001,12 @@ function plannerRenderSlots() {
? `<span class="flex-1 font-medium text-gray-900 dark:text-white">${slot.unit_id}</span>
${powerSelect}
<button onclick="plannerClearSlot(${idx})" class="text-xs text-gray-400 hover:text-red-500 flex-shrink-0" title="Remove unit">✕</button>`
: `<span class="flex-1 text-sm text-gray-400 dark:text-gray-500 italic">Empty — click a unit</span>
: `<button onclick="plannerSelectSlot(${idx})" class="flex-1 text-left text-sm italic ${plannerSelectedSlotIdx === idx ? 'text-blue-600 dark:text-blue-400 font-medium' : 'text-gray-400 dark:text-gray-500'}">${plannerSelectedSlotIdx === idx ? '← click a unit to assign here' : 'Empty — click to select'}</button>
${powerSelect}
<button onclick="plannerRemoveSlot(${idx})" class="text-xs text-gray-400 hover:text-red-500 flex-shrink-0" title="Remove location">✕</button>`
}
</div>
${unitInfoLine}
<div class="pl-8 flex flex-col gap-1 mt-1">
<input type="text" value="${slot.location_name ? slot.location_name.replace(/"/g, '&quot;') : ''}"
oninput="plannerSetLocationName(${idx}, this.value)"
@@ -1956,12 +2024,13 @@ function plannerRenderSlots() {
function plannerClearSlot(idx) {
plannerState.slots[idx].unit_id = null;
plannerState.slots[idx].power_type = null;
plannerSelectedSlotIdx = null;
plannerRenderSlots();
plannerRenderUnits();
}
function plannerReset() {
plannerSelectedSlotIdx = null;
plannerState = { reservation_id: null, slots: [], allUnits: [] };
document.getElementById('planner-name').value = '';
document.getElementById('planner-project').value = '';