Predictive staffing forecast + per-contract timeline
J's ask: move the system from retrospective ranking to predictive anticipation. Show it tracks the clock, not just the roster. New endpoint /intelligence/staffing_forecast: - Pulls 30-day Chicago permit window (200 permits) - Maps work_type → role via industry heuristic - Aggregates predicted worker demand per role - Joins IL bench supply (workers_500k state='IL' group by role) - Computes coverage_pct, reliable_coverage_pct - Classifies risk: critical/tight/watch/ok - Computes earliest staffing deadline per role (permit issue_date + 31d = 45d construction start - 14d window) - Surfaces recent Chicago playbook ops for the role-specific memory New UI 'Staffing Forecast' section ABOVE Live Contracts: - Top card: total construction value, permit count, workers needed, critical/tight role count - Per-role rows: demand vs available supply, coverage %, deadline with red/amber/green urgency coloring Per-contract timeline on Live Contracts: - estimated_construction_start, staffing_window_opens, days_to_deadline - urgency classification: overdue/urgent/soon/scheduled - card border colored by urgency - timeline line explicitly shows recruiter: OVERDUE/URGENT + days count This is the 'system already thinks about when, not just who' surface J was asking for. CRMs store; this anticipates.
This commit is contained in:
+71
-4
@@ -112,6 +112,14 @@ body{font-family:'Inter',-apple-system,system-ui,'Segoe UI',sans-serif;backgroun
|
||||
|
||||
<div id="main"><div class="ld">Analyzing contracts and workers...</div></div>
|
||||
|
||||
<div class="section" id="staffing-forecast-section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">Staffing Forecast — Next 30 Days</span>
|
||||
<span class="section-meta">Permits → predicted demand · Bench supply · Days to staffing deadline</span>
|
||||
</div>
|
||||
<div id="staffing-forecast"><div class="ld">Loading forecast...</div></div>
|
||||
</div>
|
||||
|
||||
<div class="section" id="live-contracts-section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">Live Contracts — Chicago Permits → Proposed Fills</span>
|
||||
@@ -155,7 +163,56 @@ var P=location.pathname.indexOf('/lakehouse')>=0?'/lakehouse':'';
|
||||
var A=location.origin+P;
|
||||
var AC=['#1a2744','#1a3a2a','#2a1a3a','#3a2a1a','#1a3a3a','#2a2a1a'];
|
||||
var lastQuery='';
|
||||
window.addEventListener('load',function(){loadDay();loadLiveContracts();loadMarket();loadLearning()});
|
||||
window.addEventListener('load',function(){loadDay();loadStaffingForecast();loadLiveContracts();loadMarket();loadLearning()});
|
||||
|
||||
function loadStaffingForecast(){
|
||||
api('/intelligence/staffing_forecast',{}).then(function(r){
|
||||
var el=document.getElementById('staffing-forecast');el.textContent='';
|
||||
if(!r||!r.forecast){el.textContent='Forecast unavailable.';return}
|
||||
// Header summary
|
||||
var hdr=document.createElement('div');hdr.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:14px;margin-bottom:10px;display:flex;justify-content:space-between;gap:16px';
|
||||
var left=document.createElement('div');
|
||||
var big=document.createElement('div');big.style.cssText='font-size:22px;font-weight:700;color:#e6edf3;letter-spacing:-0.5px';
|
||||
big.textContent='$'+(r.total_cost||0).toLocaleString('en-US',{maximumFractionDigits:0})+' in construction coming';
|
||||
var sub=document.createElement('div');sub.style.cssText='color:#8b949e;font-size:12px;margin-top:4px';
|
||||
sub.textContent=r.permit_count+' permits filed last 30 days · ~'+r.total_estimated_workers+' workers needed across roles';
|
||||
left.appendChild(big);left.appendChild(sub);hdr.appendChild(left);
|
||||
var right=document.createElement('div');right.style.cssText='text-align:right;font-size:11px';
|
||||
if(r.critical_roles>0){
|
||||
var c=document.createElement('div');c.style.cssText='color:#f85149;font-weight:700';
|
||||
c.textContent=r.critical_roles+' CRITICAL role'+(r.critical_roles!==1?'s':'')+' — supply gap';right.appendChild(c);
|
||||
} else if(r.tight_roles>0){
|
||||
var t=document.createElement('div');t.style.cssText='color:#d29922;font-weight:700';
|
||||
t.textContent=r.tight_roles+' tight role'+(r.tight_roles!==1?'s':'');right.appendChild(t);
|
||||
} else {
|
||||
var g=document.createElement('div');g.style.cssText='color:#3fb950;font-weight:600';g.textContent='bench covers predicted demand';right.appendChild(g);
|
||||
}
|
||||
var when=document.createElement('div');when.style.cssText='color:#545d68;margin-top:2px';
|
||||
when.textContent='updated '+((r.duration_ms||0)/1000).toFixed(1)+'s ago';right.appendChild(when);
|
||||
hdr.appendChild(right);el.appendChild(hdr);
|
||||
// Per-role rows
|
||||
r.forecast.forEach(function(f){
|
||||
var row=document.createElement('div');
|
||||
var riskColor={critical:'#f85149',tight:'#d29922',watch:'#d29922',ok:'#3fb950'}[f.risk]||'#8b949e';
|
||||
row.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:8px;padding:10px 14px;margin-bottom:6px;border-left:3px solid '+riskColor+';display:flex;justify-content:space-between;gap:12px;font-size:12px;align-items:center';
|
||||
var l=document.createElement('div');
|
||||
var n=document.createElement('div');n.style.cssText='color:#e6edf3;font-weight:600;font-size:13px';
|
||||
n.textContent=f.role;
|
||||
var d=document.createElement('div');d.style.cssText='color:#8b949e;font-size:11px;margin-top:2px';
|
||||
d.textContent=f.demand_permits+' permit'+(f.demand_permits!==1?'s':'')+' · est '+f.demand_workers+' workers · earliest staffing deadline '+f.earliest_staffing_deadline;
|
||||
l.appendChild(n);l.appendChild(d);row.appendChild(l);
|
||||
var r2=document.createElement('div');r2.style.cssText='text-align:right;white-space:nowrap';
|
||||
var cov=document.createElement('div');cov.style.cssText='color:'+riskColor+';font-weight:700;font-size:13px';
|
||||
cov.textContent=f.bench_available.toLocaleString()+' / '+f.demand_workers+' available ('+f.coverage_pct+'%)';
|
||||
var days=document.createElement('div');days.style.cssText='color:'+(f.days_to_deadline<=0?'#f85149':f.days_to_deadline<=7?'#d29922':'#8b949e')+';font-size:11px;margin-top:2px';
|
||||
days.textContent=f.days_to_deadline<=0?(Math.abs(f.days_to_deadline)+'d overdue'):(f.days_to_deadline+'d to deadline');
|
||||
r2.appendChild(cov);r2.appendChild(days);row.appendChild(r2);
|
||||
el.appendChild(row);
|
||||
});
|
||||
}).catch(function(e){
|
||||
document.getElementById('staffing-forecast').textContent='Forecast error: '+(e.message||e);
|
||||
});
|
||||
}
|
||||
|
||||
function api(path,body){
|
||||
return fetch(A+path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}).then(function(r){return r.json()})
|
||||
@@ -171,16 +228,26 @@ function loadLiveContracts(){
|
||||
el.textContent='No permits returned.';return;
|
||||
}
|
||||
r.contracts.forEach(function(c){
|
||||
var p=c.permit||{}, prop=c.proposed||{};
|
||||
var p=c.permit||{}, prop=c.proposed||{}, tl=c.timeline||{};
|
||||
var urg=tl.urgency||'scheduled';
|
||||
var borderColor={overdue:'#f85149',urgent:'#d29922',soon:'#388bfd',scheduled:'#2ea043'}[urg]||'#388bfd';
|
||||
var card=document.createElement('div');card.className='insight info';
|
||||
card.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:16px;margin-bottom:10px;border-left:3px solid #388bfd';
|
||||
card.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:16px;margin-bottom:10px;border-left:3px solid '+borderColor;
|
||||
// Header — permit
|
||||
var hdr=document.createElement('div');hdr.style.cssText='display:flex;justify-content:space-between;margin-bottom:8px;gap:12px';
|
||||
var left=document.createElement('div');
|
||||
var title=document.createElement('div');title.style.cssText='font-weight:600;color:#e6edf3;font-size:14px';
|
||||
title.textContent='$'+(p.cost||0).toLocaleString()+' · '+(p.work_type||'');
|
||||
var addr=document.createElement('div');addr.style.cssText='color:#8b949e;font-size:12px;margin-top:2px';
|
||||
addr.textContent=(p.address||'')+' · Chicago, IL · '+(p.issue_date||'');
|
||||
addr.textContent=(p.address||'')+' · Chicago, IL · filed '+(p.issue_date||'');
|
||||
// Timeline chip
|
||||
if(tl.days_to_deadline!==undefined){
|
||||
var tmline=document.createElement('div');tmline.style.cssText='color:'+borderColor+';font-size:11px;font-weight:600;margin-top:4px';
|
||||
var urgLabel={overdue:'OVERDUE',urgent:'URGENT',soon:'SOON',scheduled:'SCHEDULED'}[urg]||'SCHEDULED';
|
||||
var dd=tl.days_to_deadline;
|
||||
var txt=urgLabel+' · staffing window opens '+(tl.staffing_window_opens||'')+' ('+(dd<=0?Math.abs(dd)+'d overdue':dd+'d to deadline')+') · construction est '+(tl.estimated_construction_start||'');
|
||||
tmline.textContent=txt;addr.appendChild(document.createElement('br'));left.appendChild(tmline);
|
||||
}
|
||||
left.appendChild(title);left.appendChild(addr);
|
||||
var right=document.createElement('div');right.style.cssText='color:#58a6ff;font-size:12px;font-weight:600;text-align:right;white-space:nowrap';
|
||||
right.textContent=prop.count+'× '+prop.role;
|
||||
|
||||
Reference in New Issue
Block a user