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:
parent
e87155306b
commit
be7436b6f0
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user