@@ -650,7 +650,7 @@
<!-- Fleet Summary (shown on jobs list) -->
< div id = "right-fleet-summary" class = "bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6 flex flex-col gap-4" >
< h2 class = "text-xl font-semibold text-gray-900 dark:text-white" > Fleet Summary< / h2 >
< div id = "fleet-summary-stats" class = "grid grid-cols-2 sm:grid -cols-4 gap-3 text-center " >
< div id = "fleet-summary-stats" class = "flex flex -col gap-0 " >
<!-- Populated by JS -->
< / div >
< input type = "text" id = "summary-search" placeholder = "Search by unit ID..."
@@ -713,6 +713,70 @@
< / div >
< / div >
<!-- Unit Quick - Info Modal -->
< div id = "unit-quick-modal" class = "fixed inset-0 z-50 hidden" >
< div class = "fixed inset-0 bg-black/50" onclick = "closeUnitQuickModal()" > < / div >
< div class = "fixed inset-0 flex items-center justify-center p-4 pointer-events-none" >
< div id = "unit-quick-modal-inner" class = "bg-white dark:bg-slate-800 rounded-xl shadow-2xl w-full max-w-md pointer-events-auto" onclick = "event.stopPropagation()" >
<!-- Header -->
< div class = "flex items-center justify-between px-5 py-4 border-b border-gray-200 dark:border-gray-700" >
< div class = "flex items-center gap-3" >
< h3 id = "uqm-title" class = "text-lg font-bold text-gray-900 dark:text-white" > < / h3 >
< span id = "uqm-deployed-badge" > < / span >
< span id = "uqm-outforcal-badge" > < / span >
< / div >
< button onclick = "closeUnitQuickModal()" class = "text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" >
< 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 = "M6 18L18 6M6 6l12 12" / > < / svg >
< / button >
< / div >
<!-- Body -->
< div class = "px-5 py-4 flex flex-col gap-4" >
<!-- Cal row -->
< div class = "flex gap-6" >
< div >
< p class = "text-xs text-gray-500 dark:text-gray-400 mb-0.5" > Last Calibration< / p >
< p id = "uqm-cal-date" class = "text-sm font-medium text-gray-900 dark:text-white" > < / p >
< / div >
< div >
< p class = "text-xs text-gray-500 dark:text-gray-400 mb-0.5" > Cal Due< / p >
< p id = "uqm-cal-due" class = "text-sm font-medium" > < / p >
< / div >
< / div >
<!-- Location / address -->
< div id = "uqm-address-row" class = "hidden" >
< p class = "text-xs text-gray-500 dark:text-gray-400 mb-0.5" > Address / Location< / p >
< p id = "uqm-address" class = "text-sm text-gray-800 dark:text-gray-200" > < / p >
< / div >
<!-- Project -->
< div id = "uqm-project-row" class = "hidden" >
< p class = "text-xs text-gray-500 dark:text-gray-400 mb-0.5" > Project< / p >
< p id = "uqm-project" class = "text-sm text-gray-800 dark:text-gray-200" > < / p >
< / div >
<!-- Modem -->
< div id = "uqm-modem-row" class = "hidden" >
< p class = "text-xs text-gray-500 dark:text-gray-400 mb-0.5" > Deployed With Modem< / p >
< p id = "uqm-modem" class = "text-sm text-gray-800 dark:text-gray-200" > < / p >
< / div >
<!-- Last seen -->
< div id = "uqm-lastseen-row" class = "hidden" >
< p class = "text-xs text-gray-500 dark:text-gray-400 mb-0.5" > Last Seen< / p >
< p id = "uqm-lastseen" class = "text-sm text-gray-800 dark:text-gray-200" > < / p >
< / div >
<!-- Note -->
< div id = "uqm-note-row" class = "hidden" >
< p class = "text-xs text-gray-500 dark:text-gray-400 mb-0.5" > Note< / p >
< p id = "uqm-note" class = "text-sm text-gray-800 dark:text-gray-200 italic" > < / p >
< / div >
<!-- Reservations -->
< div id = "uqm-reservations-row" class = "hidden" >
< p class = "text-xs text-gray-500 dark:text-gray-400 mb-1" > Upcoming Jobs< / p >
< div id = "uqm-reservations" class = "flex flex-col gap-1" > < / div >
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- Day Detail Slide Panel -->
< div id = "panel-backdrop" class = "panel-backdrop" onclick = "closeDayPanel()" > < / div >
< div id = "day-panel" class = "slide-panel" >
@@ -1678,7 +1742,7 @@ function plannerRenderUnits() {
row . innerHTML = `
<div class="flex flex-col gap-0.5 min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<button onclick="event.stopPropagation(); openUnitDetail Modal(' ${ unit . id } ')"
<button onclick="event.stopPropagation(); openUnitQuick Modal(' ${ unit . id } ')"
class="font-medium text-blue-600 dark:text-blue-400 hover:underline text-sm"> ${ unit . id } </button>
${ deployedBadge }
${ expiryWarning }
@@ -1707,6 +1771,108 @@ function closeUnitDetailModal() {
document . getElementById ( 'unit-detail-iframe' ) . src = '' ;
}
async function openUnitQuickModal ( unitId ) {
document . getElementById ( 'unit-quick-modal' ) . classList . remove ( 'hidden' ) ;
// Reset while loading
document . getElementById ( 'uqm-title' ) . textContent = unitId ;
document . getElementById ( 'uqm-deployed-badge' ) . innerHTML = '' ;
document . getElementById ( 'uqm-outforcal-badge' ) . innerHTML = '' ;
document . getElementById ( 'uqm-cal-date' ) . textContent = '…' ;
document . getElementById ( 'uqm-cal-due' ) . textContent = '…' ;
[ 'uqm-address-row' , 'uqm-project-row' , 'uqm-modem-row' , 'uqm-lastseen-row' , 'uqm-note-row' , 'uqm-reservations-row' ]
. forEach ( id => document . getElementById ( id ) . classList . add ( 'hidden' ) ) ;
try {
const resp = await fetch ( ` /api/fleet-calendar/unit-quick-info/ ${ unitId } ` ) ;
if ( ! resp . ok ) throw new Error ( 'Not found' ) ;
const u = await resp . json ( ) ;
const today = new Date ( ) ; today . setHours ( 0 , 0 , 0 , 0 ) ;
// Deployed badge
document . getElementById ( 'uqm-deployed-badge' ) . innerHTML = u . 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>'
: '<span class="text-xs px-2 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 rounded-full">Benched</span>' ;
// Out for cal badge
if ( u . out _for _calibration ) {
document . getElementById ( 'uqm-outforcal-badge' ) . innerHTML =
'<span class="text-xs px-2 py-0.5 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 rounded-full">Out for Cal</span>' ;
}
// Cal date
const calDateEl = document . getElementById ( 'uqm-cal-date' ) ;
calDateEl . textContent = u . last _calibrated
? new Date ( u . last _calibrated + 'T00:00:00' ) . toLocaleDateString ( 'en-US' , { month : 'short' , day : 'numeric' , year : 'numeric' } )
: 'No record' ;
calDateEl . className = ` text-sm font-medium ${ ! u . last _calibrated ? 'text-red-500 dark:text-red-400' : 'text-gray-900 dark:text-white' } ` ;
// Cal due
const calDueEl = document . getElementById ( 'uqm-cal-due' ) ;
if ( u . next _calibration _due ) {
const due = new Date ( u . next _calibration _due + 'T00:00:00' ) ;
const expired = due < today ;
calDueEl . textContent = due . toLocaleDateString ( 'en-US' , { month : 'short' , day : 'numeric' , year : 'numeric' } ) + ( expired ? ' (expired)' : '' ) ;
calDueEl . className = ` text-sm font-medium ${ expired ? 'text-red-500 dark:text-red-400' : 'text-green-600 dark:text-green-400' } ` ;
} else {
calDueEl . textContent = '—' ;
calDueEl . className = 'text-sm font-medium text-red-500 dark:text-red-400' ;
}
// Address
if ( u . address ) {
document . getElementById ( 'uqm-address-row' ) . classList . remove ( 'hidden' ) ;
document . getElementById ( 'uqm-address' ) . textContent = u . address ;
}
// Project
if ( u . project _id ) {
document . getElementById ( 'uqm-project-row' ) . classList . remove ( 'hidden' ) ;
document . getElementById ( 'uqm-project' ) . textContent = u . project _id ;
}
// Modem
if ( u . deployed _with _modem _id ) {
document . getElementById ( 'uqm-modem-row' ) . classList . remove ( 'hidden' ) ;
document . getElementById ( 'uqm-modem' ) . textContent = u . deployed _with _modem _id ;
}
// Last seen
if ( u . last _seen ) {
document . getElementById ( 'uqm-lastseen-row' ) . classList . remove ( 'hidden' ) ;
document . getElementById ( 'uqm-lastseen' ) . textContent =
new Date ( u . last _seen ) . toLocaleString ( 'en-US' , { month : 'short' , day : 'numeric' , year : 'numeric' , hour : 'numeric' , minute : '2-digit' } ) ;
}
// Note
if ( u . note ) {
document . getElementById ( 'uqm-note-row' ) . classList . remove ( 'hidden' ) ;
document . getElementById ( 'uqm-note' ) . textContent = u . note ;
}
// Reservations
if ( u . reservations && u . reservations . length > 0 ) {
document . getElementById ( 'uqm-reservations-row' ) . classList . remove ( 'hidden' ) ;
document . getElementById ( 'uqm-reservations' ) . innerHTML = 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 _tbd ? 'TBD' : ( r . end _date ? new Date ( r . end _date + 'T00:00:00' ) . toLocaleDateString ( 'en-US' , { month : 'short' , day : 'numeric' } ) : 'TBD' ) ;
const loc = r . location _name ? ` · ${ r . location _name } ` : '' ;
return ` <div class="flex items-center gap-2 text-sm">
<span class="w-2.5 h-2.5 rounded-full flex-shrink-0" style="background: ${ r . color } "></span>
<span class="font-medium text-gray-800 dark:text-gray-200"> ${ r . name } </span>
<span class="text-gray-400 dark:text-gray-500"> ${ s } – ${ e } ${ loc } </span>
</div> ` ;
} ) . join ( '' ) ;
}
} catch ( e ) {
document . getElementById ( 'uqm-cal-date' ) . textContent = 'Error loading' ;
}
}
function closeUnitQuickModal ( ) {
document . getElementById ( 'unit-quick-modal' ) . classList . add ( 'hidden' ) ;
}
function plannerAddSlot ( ) {
plannerState . slots . push ( { unit _id : null , power _type : null , notes : null , location _name : null } ) ;
plannerRenderSlots ( ) ;
@@ -1716,7 +1882,7 @@ function plannerAddSlot() {
// Fleet Summary (right panel on jobs list)
// ============================================================
let summaryAllUnits = [ ] ;
let summaryActiveFilter = null ; // n ull | 'deployed' | 'benched' | 'cal_expired'
let summaryActiveFilters = new Set ( ) ; // m ulti-select: 'deployed' | 'benched' | 'cal_expired' | 'cal_good' | 'out_for_cal' | 'reserved'
async function loadFleetSummary ( ) {
const plannerDeviceType = document . querySelector ( 'input[name="planner_device_type"]:checked' ) ? . value || 'seismograph' ;
@@ -1724,7 +1890,7 @@ async function loadFleetSummary() {
const resp = await fetch ( ` /api/fleet-calendar/planner-availability?device_type= ${ plannerDeviceType } ` ) ;
const data = await resp . json ( ) ;
summaryAllUnits = data . units || [ ] ;
summaryActiveFilter = null ;
summaryActiveFilters = new Set ( ) ;
renderFleetSummary ( ) ;
} catch ( e ) { console . error ( 'Fleet summary load error' , e ) ; }
}
@@ -1733,88 +1899,158 @@ function summaryFilterUnits() {
renderFleetSummary ( ) ;
}
// Stat cards: set exactly this one filter (or clear all if already the only active one)
function summarySetFilter ( f ) {
summaryActiveFilter = summaryActiveFilter === f ? null : f ;
if ( f === null ) {
summaryActiveFilters = new Set ( ) ;
} else if ( summaryActiveFilters . size === 1 && summaryActiveFilters . has ( f ) ) {
summaryActiveFilters = new Set ( ) ;
} else {
summaryActiveFilters = new Set ( [ f ] ) ;
}
renderFleetSummary ( ) ;
}
// Pills: toggle independently (multi-select)
function summaryToggleFilter ( f ) {
if ( summaryActiveFilters . has ( f ) ) summaryActiveFilters . delete ( f ) ;
else summaryActiveFilters . add ( f ) ;
renderFleetSummary ( ) ;
}
function renderFleetSummary ( ) {
const search = document . getElementById ( 'summary-search' ) ? . value . toLowerCase ( ) || '' ;
const today = new Date ( ) ; today . setHours ( 0 , 0 , 0 , 0 ) ;
// 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 ;
// Computed flags for each unit
const withFlags = summaryAllUnits . map ( u => {
const expiry = u . expiry _date ? new Date ( u . expiry _date + 'T00:00:00' ) : null ;
return {
... u ,
_calExpired : ! u . last _calibrated || ( expiry && expiry < today ) ,
_calGood : u . last _calibrated && expiry && expiry >= today ,
_outForCal : ! ! u . out _for _calibration ,
_allocated : ! ! u . allocated ,
_reserved : ( u . reservations || [ ] ) . length > 0 ,
} ;
} ) ;
// Counts always against full list
const counts = {
total : withFlags . length ,
deployed : withFlags . filter ( u => u . deployed ) . length ,
benched : withFlags . filter ( u => ! u . deployed ) . length ,
cal _expired : withFlags . filter ( u => u . _calExpired ) . length ,
cal _good : withFlags . filter ( u => u . _calGood ) . length ,
out _for _cal : withFlags . filter ( u => u . _outForCal ) . length ,
allocated : withFlags . filter ( u => u . _allocated ) . length ,
reserved : withFlags . filter ( u => u . _reserved ) . length ,
} ;
const af = summaryActiveFilters ;
// Stat cards — single-shortcut behavior, highlighted when they're the sole active filter
const cardActive = ( f ) => af . size === 1 && af . has ( f ) ;
const card = ( f , label , count , colorClass , ringColor ) => {
const isActive = f === null ? af . size === 0 : cardActive ( f ) ;
return ` <button onclick="summarySetFilter( ${ f === null ? 'null' : ` ' ${ f } ' ` } )"
class="rounded-lg p-3 text-left w-full cursor-pointer transition-all ring-2 ${ isActive ? ringColor : 'ring-transparent' } ${ colorClass } ">
<p class="text-2xl font-bold"> ${ count } </p>
<p class="text-xs opacity-80"> ${ label } </p>
</button> ` ;
} ;
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 = `
<button onclick="summarySetFilter(null)"
class=" ${ cardBase } ${ ! active ? 'rin g-gray-40 0 dark:ring-gray-300' : 'ring-transparent' } bg -gray-5 0 dark:bg-slate-700 hover:bg-gray-100 dark:hover:bg-slate-600">
<p class="text-2xl font-bold text-gray-9 00 dark:text-white"> ${ total } </p>
<p class="text-xs text-gray-500 dark:text-gray-400">Total</p>
</button>
<button onclick="summarySetFilter('deployed')"
class="${ cardBase } ${ active === 'deployed' ? 'ring-green-500' : 'ring-transparent' } bg-green-50 dark:bg-green-900/20 hover:bg-green-100 dark:hover:bg-green-900/40 ">
<p class="text-2xl font-bold text-green-700 dark:text-green-400"> ${ deployed } </p>
<p class="text-xs text-green-600 dark:text-green-500">Deployed</p>
</button>
<button onclick="summarySetFilter('benched')"
class=" ${ cardBase } ${ active === 'benched' ? 'ring-blue-500' : 'ring-transparent' } bg-blue-50 dark:bg-blue-900/20 hover:bg-blue-100 dark:hover:bg-blue-900/40">
<p class="text-2xl font-bold text-blue-700 dark:text-blue-400"> ${ benched } </p>
<p class="text-xs text-blue-600 dark:text-blue-500">Benched</p>
</button>
<button onclick="summarySetFilter('cal_expired')"
class=" ${ cardBase } ${ active === 'cal_expired' ? 'ring-red-500' : 'ring-transparent' } bg-red-50 dark:bg-red-900/20 hover:bg-red-100 dark:hover:bg-red-900/40">
<p class="text-2xl font-bold text-red-700 dark:text-red-400"> ${ calExpired } </p>
<p class="text-xs text-red-600 dark:text-red-500">Cal Expired</p>
</button>
` ;
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
${ card ( null , 'Total' , counts . total , 'b g-gray-5 0 dark:bg-slate-700 text -gray-90 0 dark:text-white hover:bg-gray-100 dark:hover:bg-slate-600' , 'ring-gray-400 dark:ring-gray-300' ) }
${ card ( 'deployed' , 'Deployed' , counts . deployed , 'bg-green-50 dark:bg-green-900/20 text-green-7 00 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-900/40' , 'ring-green-500' ) }
${ card ( 'benched' , 'Benched' , counts . benched , 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/40' , 'ring-blue-500' ) }
${ card ( 'cal_good' , 'Cal Good' , counts . cal _good , 'bg-teal-50 dark:bg-teal-900/20 text-teal-700 dark:text-teal-400 hover:bg-teal-100 dark:hover:bg-teal-900/40' , 'ring-teal-500' ) }
</div>
<div class="flex flex-wrap gap-1.5 mt-2 ">
${ summaryPill ( 'cal_expired' , 'Cal Expired' , counts . cal _expired , af ) }
${ summaryPill ( 'out_for_cal' , 'Out for Cal' , counts . out _for _cal , af ) }
${ summaryPill ( 'allocated' , 'Allocated' , counts . allocated , af ) }
${ summaryPill ( 'reserved' , 'Reserved' , counts . reserved , af ) }
</div> ` ;
// 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 ( ) ) ;
// Apply all active filters (AND logic) + search
const filterFns = {
deployed : u => u . deployed ,
benched : u => ! u . deployed ,
cal _expired: u => u . _calExpired ,
cal _good : u => u . _calGood ,
out _for _cal : u => u . _outForCal ,
allocated : u => u . _allocated ,
reserved : u => u . _reserved ,
} ;
let units = af . size === 0 ? withFlags : withFlags . filter ( u => [ ... af ] . some ( f => filterFns [ f ] ( u ) ) ) ;
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 = '<p class="text-sm text-gray-400 dark:text-gray-500 text-center py-8">No units found </p>' ;
list . innerHTML = '<p class="text-sm text-gray-400 dark:text-gray-500 text-center py-8">No units match </p>' ;
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 ( ) ;
: null ;
const expiryDate = u . expiry _date
? new Date ( u . expiry _date + 'T00:00:00' ) . toLocaleDateString ( 'en-US' , { month : 'short' , day : 'numeric' , year : 'numeric' } )
: null ;
const deployedBadge = u . deployed
? '<span class="text-xs 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="text-xs px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 rounded">Benched</span>' ;
const c alBadge = expired
? ` <span class="text-xs px-1.5 py-0.5 bg-red -100 dark:bg-red -900/30 text-red-6 00 dark:text-red -400 rounded">Cal expired </span>`
: ` <span class="text-xs text-gray-400 dark:text-gray-500">Cal: ${ calDate } </span> ` ;
const outForC alBadge = u . _outForCal
? ' <span class="text-xs px-1.5 py-0.5 bg-purple -100 dark:bg-purple -900/30 text-purple-7 00 dark:text-purple -400 rounded">Out for Cal </span>'
: '' ;
const allocatedBadge = u . _allocated
? ` <span class="text-xs px-1.5 py-0.5 bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-400 rounded" title=" ${ u . allocated _to _project _id ? 'For: ' + u . allocated _to _project _id : '' } ">Allocated ${ u . allocated _to _project _id ? ': ' + u . allocated _to _project _id : '' } </span> `
: '' ;
let calBadge ;
if ( ! calDate ) {
calBadge = '<span class="text-xs px-1.5 py-0.5 bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 rounded">Cal expired</span>' ;
} else if ( u . _calExpired ) {
calBadge = ` <span class="text-xs px-1.5 py-0.5 bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 rounded">Cal expired ${ expiryDate } </span> ` ;
} else {
calBadge = ` <span class="text-xs text-gray-400 dark:text-gray-500">Cal: ${ calDate } · exp. ${ expiryDate } </span> ` ;
}
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 ` <span class="text-xs px-1.5 py-0.5 rounded font-medium" style="background-color: ${ r . color } 22; color: ${ r . color } ; border:1px solid ${ r . color } 66;"><span class="opacity-60">Reserved:</span> ${ r . reservation _name } ${ s } – ${ e } </span> ` ;
return ` <span class="text-xs px-1.5 py-0.5 rounded font-medium" style="background-color: ${ r . color } 22; color: ${ r . color } ; border:1px solid ${ r . color } 66;"> ${ r . reservation _name } ${ s } – ${ e } </span> ` ;
} ) . join ( '' ) ;
return `
<div class="flex flex-col gap-1 px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50">
<div class="flex items-center gap-2 flex-wrap">
<button onclick="openUnitDetail Modal(' ${ u . id } ')"
<button onclick="openUnitQuick Modal(' ${ u . id } ')"
class="font-medium text-blue-600 dark:text-blue-400 hover:underline text-sm"> ${ u . id } </button>
${ deployedBadge }
${ calBadge }
${ deployedBadge } ${ outForCalBadge } ${ allocatedBadge } ${ calBadge }
</div>
${ resBadges ? ` <div class="flex flex-wrap gap-1"> ${ resBadges } </div> ` : '' }
</div> ` ;
} ) . join ( '' ) ;
}
function summaryPill ( f , label , count , activeSet ) {
const isActive = activeSet . has ( f ) ;
const pillColors = {
cal _expired : isActive ? 'bg-red-600 text-white border-red-600' : 'bg-transparent border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:border-red-500 hover:text-red-600 dark:hover:text-red-400' ,
out _for _cal : isActive ? 'bg-purple-600 text-white border-purple-600' : 'bg-transparent border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:border-purple-500 hover:text-purple-600 dark:hover:text-purple-400' ,
allocated : isActive ? 'bg-orange-500 text-white border-orange-500' : 'bg-transparent border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:border-orange-500 hover:text-orange-600 dark:hover:text-orange-400' ,
reserved : isActive ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-transparent border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:border-indigo-500 hover:text-indigo-600 dark:hover:text-indigo-400' ,
} ;
return ` <button onclick="summaryToggleFilter(' ${ f } ')"
class="text-xs px-2.5 py-1 rounded-full font-medium border transition-colors ${ pillColors [ f ] } ">
${ label } <span class=" ${ isActive ? 'opacity-80' : 'opacity-60' } "> ${ count } </span>
</button> ` ;
}
function showRightPanel ( panel ) {
document . getElementById ( 'right-fleet-summary' ) . classList . toggle ( 'hidden' , panel !== 'summary' ) ;
document . getElementById ( 'right-available-units' ) . classList . toggle ( 'hidden' , panel !== 'available' ) ;
@@ -1959,37 +2195,32 @@ 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 unitInfoL ine = '' ;
// Build inline cal text for filled slots
let calInl ine = '' ;
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 today = new Date ( ) ; today . setHours ( 0 , 0 , 0 , 0 ) ;
const expiry = uData . expiry _date ? new Date ( uData . expiry _date + 'T00:00:00' ) : null ;
const calExpired = ! uData . last _calibrated || ( expiry && expiry < today ) ;
const start = document . getElementById ( 'planner-start' ) . value ;
const end = document . getElementById ( 'planner-end' ) . value ;
let expiryBadge = '' ;
if ( u Data . 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 jobStart = start ? new Date ( start + 'T00:00:00' ) : null ;
const jobEnd = end ? new Date ( end + 'T00:00:00' ) : null ;
const expiresInJob = expiry && jobStart && jobEnd && expiry >= jobStart && expiry <= jobEnd ;
if ( ! u Data . last _calibrated ) {
calInline = ` <span class="text-xs text-red-500 dark:text-red-400 font-medium">No cal</span> ` ;
} else if ( calExpired ) {
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 ) {
expiryBadg e = ` <span class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray -500 dark:text-gray-400 rounded ">c al exp. ${ expiryStr } </span> ` ;
}
calInline = ` <span class="text-xs text-red-500 dark:text-red-400 font-medium">Cal exp. ${ expiryStr } </span> ` ;
} else if ( expiresInJob ) {
const expiryStr = expiry . toLocaleDateString ( 'en-US' , { month : 'short' , day : 'numeric' , year : 'numeric' } ) ;
calInlin e = ` <span class="text-xs text-amber -500 dark:text-amber-400 font-medium ">C al 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>' ;
const expiryStr = expiry . toLocaleDateString ( 'en-US' , { month : 'short' , day : 'numeric' , year : 'numeric' } ) ;
calInline = ` <span class="text-xs text-gray-400 dark:text-gray-500">Cal exp. ${ expiryStr } </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> ` ;
}
}
@@ -1998,7 +2229,8 @@ function plannerRenderSlots() {
${ dragHandle }
<span class="text-sm text-gray-500 dark:text-gray-400 w-16 flex-shrink-0">Loc. ${ idx + 1 } </span>
${ slot . unit _id
? ` <span class="flex-1 font-medium text-gray-9 00 dark:text-whit e"> ${ slot . unit _id } </spa n>
? ` <button onclick="openUnitQuickModal(' ${ slot . unit _id } ')" class="font-medium text-blue-6 00 dark:text-blue-400 hover:underlin e"> ${ slot . unit _id } </butto n>
${ calInline ? ` <span class="flex-1"> ${ calInline } </span> ` : '<span class="flex-1"></span>' }
${ powerSelect }
<button onclick="plannerClearSlot( ${ idx } )" class="text-xs text-gray-400 hover:text-red-500 flex-shrink-0" title="Remove unit">✕</button> `
: ` <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>
@@ -2006,7 +2238,6 @@ function plannerRenderSlots() {
<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 , '"' ) : '' } "
oninput="plannerSetLocationName( ${ idx } , this.value)"