Diverse scenario engine: 15 weighted staffing situations replace crisis-every-refresh

Simulation now uses weighted random selection across 4 priority tiers:
- Urgent (walkoff, quarantine, no-show), High (new client, cert expiry, expansion),
  Medium (recurring, seasonal, medical leave, cross-train), Low (future, exploratory)
- Color-coded scenario banners on ALL contracts, not just urgent
- Each scenario carries context (note) + recommended action

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root 2026-04-17 16:41:00 -05:00
parent e87155306b
commit be7436b6f0
2 changed files with 79 additions and 33 deletions

View File

@ -1061,19 +1061,47 @@ const CITIES: Record<string, string[]> = {
WI: ["Milwaukee","Madison"], MI: ["Detroit","Grand Rapids"],
};
const CLIENTS = ["Midwest Logistics","Precision Mfg","Amazon DSP","CleanSpace","AutoParts Direct","Great Lakes Steel","Heartland Foods","Summit Packaging","Cardinal Health","TechFlow Assembly","River City Plastics","Prairie Wind Energy"];
const PRIORITIES = ["urgent","high","medium","medium","medium","low"];
const STARTS = ["5:00 AM","6:00 AM","6:30 AM","7:00 AM","7:30 AM","8:00 AM"];
const NOTES = [
"Warehouse expansion — need experienced workers",
"Peak season surge — client called last night",
"2nd shift, CNC preferred",
"Chemical plant — hazmat cert MANDATORY",
"ISO audit next week — need detail-oriented workers",
"Structural welding — experienced only",
"Regular fill — ongoing contract",
"Client doubled their order",
"Night shift coverage needed",
"Replacing 2 no-shows from yesterday",
// Diverse scenarios — each tells a different story about WHY this contract exists
const SCENARIOS = [
// URGENT — real emergencies that need immediate action
{ priority: "urgent", weight: 8, note: "Worker walked off the job at 3 PM yesterday — client needs replacement by morning",
situation: "walkoff", action: "Replacement needed ASAP — previous worker quit mid-shift" },
{ priority: "urgent", weight: 5, note: "Client emailed at 11 PM — their regular crew has COVID exposure, entire team quarantined",
situation: "quarantine", action: "Full crew replacement — health emergency at job site" },
{ priority: "urgent", weight: 5, note: "2 no-shows this morning — client is short-staffed on the floor right now",
situation: "noshow", action: "Immediate backfill — client waiting on the phone" },
// HIGH — important but not crisis
{ priority: "high", weight: 10, note: "New contract starting Monday — client wants to meet workers this week",
situation: "new_client", action: "New client onboarding — first impression matters" },
{ priority: "high", weight: 8, note: "Client expanding to 2nd shift — need additional crew by next week",
situation: "expansion", action: "Growth opportunity — client adding a shift" },
{ priority: "high", weight: 6, note: "Worker's OSHA certification expires Friday — need certified replacement lined up",
situation: "cert_expiry", action: "Cert compliance — current worker can't continue without renewal" },
{ priority: "high", weight: 5, note: "Client requested specific workers back from last month's project",
situation: "client_request", action: "Client relationship — they asked for specific people" },
// MEDIUM — standard day-to-day operations
{ priority: "medium", weight: 15, note: "Ongoing weekly fill — same client, same role, reliable pipeline",
situation: "recurring", action: "Recurring contract — steady work" },
{ priority: "medium", weight: 12, note: "Seasonal uptick — warehouse volume increasing ahead of holidays",
situation: "seasonal", action: "Seasonal planning — volume ramping up" },
{ priority: "medium", weight: 10, note: "Backfill for worker on approved medical leave — returns in 3 weeks",
situation: "medical_leave", action: "Temporary coverage — worker returning soon" },
{ priority: "medium", weight: 8, note: "Client testing new role — wants to try 2 workers for a week before committing",
situation: "trial", action: "Trial placement — client evaluating the role" },
{ priority: "medium", weight: 6, note: "Cross-training opportunity — client wants workers who can learn a new skill",
situation: "cross_train", action: "Development opportunity — workers can learn new skills" },
// LOW — planning ahead
{ priority: "low", weight: 10, note: "Future fill — project starts in 2 weeks, gathering candidates now",
situation: "future", action: "Pipeline building — no rush, quality over speed" },
{ priority: "low", weight: 8, note: "Client exploring staffing options — not committed yet, just want to see who's available",
situation: "exploratory", action: "Exploratory — client shopping, impress them with quality" },
{ priority: "low", weight: 5, note: "Internal transfer — moving a worker from one site to another, need replacement at original",
situation: "transfer", action: "Planned transition — smooth handoff between sites" },
];
function pick<T>(arr: T[]): T { return arr[Math.floor(Math.random() * arr.length)]; }
@ -1095,10 +1123,16 @@ async function runWeekSimulation() {
const state = pick(STATES);
const city = pick(CITIES[state] || [state]);
const role = pick(ROLES);
const priority = pick(PRIORITIES) as string;
const headcount = priority === "urgent" ? 4 + Math.floor(Math.random() * 5) :
priority === "high" ? 3 + Math.floor(Math.random() * 3) :
2 + Math.floor(Math.random() * 3);
// Weighted scenario selection
const totalWeight = SCENARIOS.reduce((s, sc) => s + sc.weight, 0);
let r = Math.random() * totalWeight;
let scenario = SCENARIOS[0];
for (const sc of SCENARIOS) { r -= sc.weight; if (r <= 0) { scenario = sc; break; } }
const priority = scenario.priority;
const headcount = priority === "urgent" ? 3 + Math.floor(Math.random() * 4) :
priority === "high" ? 2 + Math.floor(Math.random() * 3) :
priority === "medium" ? 2 + Math.floor(Math.random() * 3) :
1 + Math.floor(Math.random() * 2);
const minRel = priority === "urgent" ? 0.6 : priority === "high" ? 0.75 : 0.8;
const cid = `W${d+1}-${String(c+1).padStart(3,"0")}`;
@ -1132,7 +1166,8 @@ async function runWeekSimulation() {
contracts.push({
id: cid, client: pick(CLIENTS), role, state, city,
headcount, filled: Math.min(filled, headcount), priority,
start: pick(STARTS), notes: pick(NOTES), matches,
start: pick(STARTS), notes: scenario.note, situation: scenario.situation,
action: scenario.action, matches,
staffer, handoff_to: d < 4 ? handoffTo : null,
});
}

View File

@ -209,25 +209,36 @@ function addContractInsight(parent,c,isUrgent){
var cd=document.createElement('div');cd.style.cssText='background:#0d1117;border-radius:8px;padding:12px;margin-bottom:8px';
// Urgent reason banner — explain WHY this is urgent
if(isUrgent){
var reasons=['Client called last night — needs workers by morning',
'Short notice fill — original crew cancelled',
'Production surge — client doubled their headcount',
'Emergency coverage — 2 no-shows reported',
'Rush order — client needs bodies on site ASAP',
'Replacement needed — previous worker reassigned'];
var reason=reasons[Math.floor(Math.random()*reasons.length)];
// Scenario banner — shows for ALL contracts, not just urgent
if(c.notes||c.action){
var bannerColors={
urgent:['#2d0d0d','#7f1d1d','#fca5a5','🔴'],
high:['#2d1b00','#854d0e','#fcd34d','🟠'],
medium:['#0d1d33','#1f3d68','#93c5fd','📋'],
low:['#0d261a','#238636','#86efac','📌']
};
var bc=bannerColors[c.priority]||bannerColors.medium;
var banner=document.createElement('div');
banner.style.cssText='background:#2d0d0d;border:1px solid #7f1d1d;border-radius:6px;padding:10px 12px;margin-bottom:10px;display:flex;align-items:flex-start;gap:8px';
var icon=document.createElement('span');icon.style.cssText='font-size:16px;flex-shrink:0';icon.textContent='🔴';
banner.style.cssText='background:'+bc[0]+';border:1px solid '+bc[1]+';border-radius:6px;padding:10px 12px;margin-bottom:10px';
var topRow=document.createElement('div');topRow.style.cssText='display:flex;align-items:flex-start;gap:8px';
var icon=document.createElement('span');icon.style.cssText='font-size:14px;flex-shrink:0';icon.textContent=bc[3];
var bannerText=document.createElement('div');
var reasonLine=document.createElement('div');reasonLine.style.cssText='color:#fca5a5;font-size:12px;font-weight:600';reasonLine.textContent=reason;
var actionLine=document.createElement('div');actionLine.style.cssText='color:#8b949e;font-size:11px;margin-top:2px';
var reasonLine=document.createElement('div');reasonLine.style.cssText='color:'+bc[2]+';font-size:12px;font-weight:600';
reasonLine.textContent=c.notes||'';
bannerText.appendChild(reasonLine);
if(c.action){
var actionLine=document.createElement('div');actionLine.style.cssText='color:#8b949e;font-size:11px;margin-top:2px';
actionLine.textContent=c.action;
bannerText.appendChild(actionLine);
}
var unfilled=c.headcount-c.filled;
if(unfilled>0)actionLine.textContent='Need '+unfilled+' more worker'+(unfilled>1?'s':'')+' — see suggested replacements below';
else actionLine.textContent='All '+c.headcount+' positions matched — confirm workers and send shift details now';
bannerText.appendChild(reasonLine);bannerText.appendChild(actionLine);
banner.appendChild(icon);banner.appendChild(bannerText);cd.appendChild(banner);
if(unfilled>0){
var gapLine=document.createElement('div');gapLine.style.cssText='color:'+bc[2]+';font-size:11px;margin-top:4px;font-weight:500';
gapLine.textContent='→ Need '+unfilled+' more worker'+(unfilled>1?'s':'')+' — see matches below';
bannerText.appendChild(gapLine);
}
topRow.appendChild(icon);topRow.appendChild(bannerText);banner.appendChild(topRow);
cd.appendChild(banner);
}
var hdr=document.createElement('div');hdr.style.cssText='display:flex;justify-content:space-between;align-items:center;margin-bottom:8px';