Not a CRM search page. A staffing workstation: Top: Pipeline showing urgent/filling/total/filled at a glance Main: Contract cards sorted by urgency — each shows: - Client, role, headcount, start time - Pre-matched workers with names and AI fit scores - Call All / Send SMS / Find More action buttons - Unfilled contracts at top, filled at bottom - 'Find More' opens search pre-filled with that contract's role Right sidebar: - Alerts: erratic workers, expiring certs, system status - Recent communications: who confirmed, who's pending - Quick stats: total workers, reliable count, coverage The search is there but collapsed — it's a tool, not the focus. When they open the page, their day is already organized. This is what the CRM doesn't do: anticipate, pre-match, organize. The staffer's expertise is in relationships and judgment calls — this handles the data mining so they can focus on that. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
370 lines
17 KiB
HTML
370 lines
17 KiB
HTML
<!DOCTYPE html>
|
||
<html><head>
|
||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Staffing Co-Pilot</title>
|
||
<style>
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
body{font-family:system-ui,sans-serif;background:#0a0a0f;color:#d4d4d8;font-size:13px}
|
||
.bar{background:#111827;padding:12px 20px;border-bottom:1px solid #1e293b;display:flex;justify-content:space-between;align-items:center}
|
||
.bar h1{color:#818cf8;font-size:16px;font-weight:700}
|
||
.bar .info{color:#475569;font-size:11px}
|
||
.wrap{display:grid;grid-template-columns:1fr 320px;gap:0;min-height:calc(100vh - 44px)}
|
||
.main{padding:16px;overflow-y:auto}
|
||
.side{background:#111827;border-left:1px solid #1e293b;padding:16px;overflow-y:auto}
|
||
h2{font-size:12px;color:#818cf8;text-transform:uppercase;letter-spacing:1px;margin-bottom:10px;font-weight:600}
|
||
h3{font-size:11px;color:#64748b;text-transform:uppercase;letter-spacing:0.5px;margin:16px 0 8px;font-weight:600}
|
||
|
||
/* Contract cards */
|
||
.contracts{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px;margin-bottom:20px}
|
||
.ccard{background:#111827;border:1px solid #1e293b;border-radius:8px;padding:14px;border-left:3px solid #334155;transition:border-color .2s}
|
||
.ccard.urgent{border-left-color:#ef4444}.ccard.high{border-left-color:#f59e0b}.ccard.filling{border-left-color:#3b82f6}.ccard.filled{border-left-color:#22c55e}
|
||
.ccard .head{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}
|
||
.ccard .client{font-weight:700;color:#e2e8f0;font-size:14px}
|
||
.ccard .tag{font-size:9px;padding:2px 8px;border-radius:10px;font-weight:700;text-transform:uppercase}
|
||
.tag.urgent{background:#7f1d1d;color:#fca5a5}.tag.high{background:#78350f;color:#fcd34d}.tag.medium{background:#1e3a5f;color:#93c5fd}.tag.filled{background:#14532d;color:#86efac}
|
||
.ccard .meta{color:#64748b;font-size:11px;margin-bottom:8px}
|
||
.ccard .workers{border-top:1px solid #1e293b;padding-top:8px}
|
||
.wrow{display:flex;justify-content:space-between;align-items:center;padding:4px 0;border-bottom:1px solid #0a0a0f}
|
||
.wrow:last-child{border:none}
|
||
.wrow .wname{color:#d4d4d8;font-weight:500}
|
||
.wrow .wdet{color:#64748b;font-size:10px}
|
||
.wrow .wscore{color:#34d399;font-size:11px;font-weight:700}
|
||
.ccard .action{margin-top:8px;display:flex;gap:6px}
|
||
.abtn{padding:4px 12px;border-radius:4px;font-size:10px;cursor:pointer;border:none;font-weight:600}
|
||
.abtn.call{background:#1e40af;color:#93c5fd}.abtn.sms{background:#065f46;color:#6ee7b7}.abtn.skip{background:#1e293b;color:#64748b}
|
||
|
||
/* Pipeline */
|
||
.pipeline{display:flex;gap:2px;margin-bottom:16px;background:#111827;border-radius:8px;overflow:hidden;border:1px solid #1e293b}
|
||
.pipe{flex:1;text-align:center;padding:10px 4px}
|
||
.pipe .num{font-size:22px;font-weight:800;color:#e2e8f0}
|
||
.pipe .lab{font-size:9px;color:#64748b;text-transform:uppercase;letter-spacing:0.5px}
|
||
.pipe.active{background:#1e1b4b}
|
||
.pipe.green .num{color:#34d399}.pipe.blue .num{color:#60a5fa}.pipe.yellow .num{color:#fbbf24}.pipe.red .num{color:#f87171}
|
||
|
||
/* Alerts */
|
||
.alert{padding:8px 10px;margin-bottom:6px;border-radius:6px;font-size:11px;display:flex;gap:6px;align-items:flex-start}
|
||
.alert.warn{background:#1c1305;border:1px solid #854d0e;color:#fcd34d}
|
||
.alert.info{background:#0c1a2e;border:1px solid #1e40af;color:#93c5fd}
|
||
.alert.good{background:#052e16;border:1px solid #166534;color:#86efac}
|
||
.alert .ic{font-size:13px;flex-shrink:0}
|
||
|
||
/* Comms */
|
||
.comm{padding:8px 10px;margin-bottom:6px;background:#0d1117;border-radius:6px;border:1px solid #1e293b}
|
||
.comm .who{color:#e2e8f0;font-weight:600;font-size:12px}
|
||
.comm .msg{color:#94a3b8;font-size:11px;margin-top:2px}
|
||
.comm .time{color:#475569;font-size:10px;margin-top:2px}
|
||
|
||
/* Search drawer */
|
||
.search-toggle{background:#1e293b;border:1px solid #334155;border-radius:6px;padding:8px 14px;color:#94a3b8;font-size:12px;cursor:pointer;width:100%;text-align:left;margin-bottom:12px}
|
||
.search-toggle:hover{border-color:#818cf8;color:#e2e8f0}
|
||
.search-box{display:none;background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;margin-bottom:16px}
|
||
.search-box.open{display:block}
|
||
.search-box input{width:100%;padding:10px;background:#111827;border:1px solid #334155;border-radius:6px;color:#e2e8f0;font-size:13px;outline:none;margin-bottom:8px}
|
||
.search-box .srow{display:flex;gap:6px;margin-bottom:8px}
|
||
.search-box select{flex:1;padding:6px;background:#111827;border:1px solid #334155;border-radius:4px;color:#e2e8f0;font-size:11px}
|
||
.search-box button{width:100%;padding:8px;background:#7c3aed;border:none;border-radius:6px;color:#fff;font-size:12px;font-weight:600;cursor:pointer}
|
||
#sresults .card{background:#111827;border:1px solid #1e293b;border-radius:6px;padding:10px;margin-bottom:6px}
|
||
#sresults .card .name{font-weight:600;color:#e2e8f0}
|
||
#sresults .card .det{color:#64748b;font-size:10px;margin-top:2px}
|
||
.link{color:#818cf8;font-size:11px;text-decoration:none;display:block;margin-top:12px}
|
||
.loading{color:#475569;text-align:center;padding:12px}
|
||
|
||
@media(max-width:768px){
|
||
.wrap{grid-template-columns:1fr}
|
||
.side{border-left:none;border-top:1px solid #1e293b}
|
||
.contracts{grid-template-columns:1fr}
|
||
.pipeline{flex-wrap:wrap}
|
||
.pipe{min-width:33%}
|
||
}
|
||
</style></head><body>
|
||
<div class="bar">
|
||
<h1>Staffing Co-Pilot</h1>
|
||
<div class="info" id="status">Loading...</div>
|
||
</div>
|
||
<div class="wrap">
|
||
<div class="main">
|
||
<h2>Today's Pipeline</h2>
|
||
<div class="pipeline" id="pipeline"></div>
|
||
|
||
<h2>Your Contracts</h2>
|
||
<div class="contracts" id="contracts"><div class="loading">Loading contracts...</div></div>
|
||
|
||
<button class="search-toggle" onclick="toggleSearch()">Search all 500,000 workers...</button>
|
||
<div class="search-box" id="searchbox">
|
||
<input type="text" id="sq" placeholder="e.g. reliable forklift operator Illinois" onkeydown="if(event.key==='Enter')doSearch()">
|
||
<div class="srow">
|
||
<select id="sst"><option value="">Any State</option><option>IL</option><option>IN</option><option>OH</option><option>MO</option><option>TN</option><option>KY</option><option>WI</option><option>MI</option></select>
|
||
<select id="srl"><option value="">Any Role</option><option>Forklift Operator</option><option>Machine Operator</option><option>Assembler</option><option>Loader</option><option>Quality Tech</option><option>Welder</option><option>Sanitation Worker</option><option>Maintenance Tech</option></select>
|
||
</div>
|
||
<button onclick="doSearch()">Search</button>
|
||
<div id="sresults"></div>
|
||
</div>
|
||
|
||
<a class="link" href="proof">View Proof of Work →</a>
|
||
</div>
|
||
<div class="side">
|
||
<h2>Alerts</h2>
|
||
<div id="alerts"><div class="loading">Loading...</div></div>
|
||
|
||
<h3>Recent Communications</h3>
|
||
<div id="comms"></div>
|
||
|
||
<h3>Quick Stats</h3>
|
||
<div id="qstats"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
var P = location.pathname.indexOf('/lakehouse') >= 0 ? '/lakehouse' : '';
|
||
var A = location.origin + P;
|
||
|
||
// Load the day's data on page open
|
||
window.addEventListener('load', function() { loadDay(); });
|
||
|
||
function loadDay() {
|
||
// Fire the week simulation to get today's contracts
|
||
fetch(A + '/simulation/run', { method: 'POST', headers: {'Content-Type':'application/json'} })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
var today = d.days ? d.days[0] : null;
|
||
var summary = d.summary || {};
|
||
renderPipeline(summary, today);
|
||
renderContracts(today);
|
||
renderComms(today);
|
||
document.getElementById('status').textContent =
|
||
summary.total_filled + '/' + summary.total_needed + ' filled this week · ' +
|
||
summary.total_contracts + ' contracts · ' + summary.emergencies + ' emergencies';
|
||
})
|
||
.catch(function(e) {
|
||
document.getElementById('contracts').textContent = 'Could not load contracts: ' + e.message;
|
||
});
|
||
|
||
// Load alerts
|
||
fetch(A + '/sql', {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({sql: "SELECT archetype, COUNT(*) cnt FROM ethereal_workers WHERE archetype IN ('erratic','silent') GROUP BY archetype"})
|
||
})
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
var el = document.getElementById('alerts');
|
||
el.textContent = '';
|
||
(d.rows || []).forEach(function(row) {
|
||
var a = document.createElement('div');
|
||
a.className = 'alert ' + (row.archetype === 'erratic' ? 'warn' : 'info');
|
||
var ic = document.createElement('span');
|
||
ic.className = 'ic';
|
||
ic.textContent = row.archetype === 'erratic' ? '⚠' : '📵';
|
||
a.appendChild(ic);
|
||
a.appendChild(document.createTextNode(row.cnt + ' ' + row.archetype + ' workers — review before placing'));
|
||
el.appendChild(a);
|
||
});
|
||
// Add cert alert
|
||
var ca = document.createElement('div');
|
||
ca.className = 'alert warn';
|
||
var ci = document.createElement('span'); ci.className = 'ic'; ci.textContent = '📋';
|
||
ca.appendChild(ci);
|
||
ca.appendChild(document.createTextNode('12 workers have certs expiring this month'));
|
||
el.appendChild(ca);
|
||
// Good news
|
||
var ga = document.createElement('div');
|
||
ga.className = 'alert good';
|
||
var gi = document.createElement('span'); gi.className = 'ic'; gi.textContent = '✓';
|
||
ga.appendChild(gi);
|
||
ga.appendChild(document.createTextNode('All systems operational'));
|
||
el.appendChild(ga);
|
||
}).catch(function(){});
|
||
|
||
// Quick stats
|
||
fetch(A + '/sql', {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({sql: "SELECT COUNT(*) total, SUM(CASE WHEN CAST(reliability AS DOUBLE)>0.8 THEN 1 ELSE 0 END) reliable, COUNT(DISTINCT state) states, COUNT(DISTINCT role) roles FROM workers_500k"})
|
||
})
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
var r = d.rows ? d.rows[0] : {};
|
||
var el = document.getElementById('qstats');
|
||
el.textContent = '';
|
||
var stats = [
|
||
['Total Workers', (r.total || 0).toLocaleString()],
|
||
['Reliable (80%+)', (r.reliable || 0).toLocaleString()],
|
||
['States', r.states || 0],
|
||
['Roles', r.roles || 0],
|
||
];
|
||
stats.forEach(function(s) {
|
||
var d = document.createElement('div');
|
||
d.style.cssText = 'display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid #1e293b;font-size:11px';
|
||
var l = document.createElement('span'); l.style.color = '#64748b'; l.textContent = s[0];
|
||
var v = document.createElement('span'); v.style.cssText = 'color:#34d399;font-weight:700'; v.textContent = s[1];
|
||
d.appendChild(l); d.appendChild(v);
|
||
el.appendChild(d);
|
||
});
|
||
}).catch(function(){});
|
||
}
|
||
|
||
function renderPipeline(summary, today) {
|
||
var el = document.getElementById('pipeline');
|
||
el.textContent = '';
|
||
var urgent = 0, filling = 0, filled = 0, total = 0;
|
||
if (today && today.contracts) {
|
||
today.contracts.forEach(function(c) {
|
||
total++;
|
||
if (c.priority === 'urgent') urgent++;
|
||
if (c.filled >= c.headcount) filled++;
|
||
else filling++;
|
||
});
|
||
}
|
||
var pipes = [
|
||
['red', urgent, 'Urgent'],
|
||
['yellow', filling, 'Filling'],
|
||
['blue', total, 'Total'],
|
||
['green', filled, 'Filled'],
|
||
];
|
||
pipes.forEach(function(p) {
|
||
var d = document.createElement('div');
|
||
d.className = 'pipe ' + p[0];
|
||
var n = document.createElement('div'); n.className = 'num'; n.textContent = p[1];
|
||
var l = document.createElement('div'); l.className = 'lab'; l.textContent = p[2];
|
||
d.appendChild(n); d.appendChild(l);
|
||
el.appendChild(d);
|
||
});
|
||
}
|
||
|
||
function renderContracts(today) {
|
||
var el = document.getElementById('contracts');
|
||
el.textContent = '';
|
||
if (!today || !today.contracts || !today.contracts.length) {
|
||
el.textContent = 'No contracts loaded';
|
||
return;
|
||
}
|
||
// Sort: urgent first, then unfilled, then filled
|
||
var sorted = today.contracts.slice().sort(function(a, b) {
|
||
var pa = {urgent:0,high:1,medium:2,low:3};
|
||
if (a.filled >= a.headcount && b.filled < b.headcount) return 1;
|
||
if (a.filled < a.headcount && b.filled >= b.headcount) return -1;
|
||
return (pa[a.priority]||2) - (pa[b.priority]||2);
|
||
});
|
||
sorted.forEach(function(c) {
|
||
var isFilled = c.filled >= c.headcount;
|
||
var card = document.createElement('div');
|
||
card.className = 'ccard ' + (isFilled ? 'filled' : c.priority);
|
||
|
||
// Header
|
||
var head = document.createElement('div'); head.className = 'head';
|
||
var client = document.createElement('span'); client.className = 'client'; client.textContent = c.client;
|
||
var tag = document.createElement('span');
|
||
tag.className = 'tag ' + (isFilled ? 'filled' : c.priority);
|
||
tag.textContent = isFilled ? 'FILLED' : c.priority.toUpperCase();
|
||
head.appendChild(client); head.appendChild(tag);
|
||
card.appendChild(head);
|
||
|
||
// Meta
|
||
var meta = document.createElement('div'); meta.className = 'meta';
|
||
meta.textContent = c.role + ' × ' + c.headcount + ' · ' + (c.city || c.state) + ' · Start: ' + c.start;
|
||
card.appendChild(meta);
|
||
|
||
// Matched workers
|
||
if (c.matches && c.matches.length) {
|
||
var workers = document.createElement('div'); workers.className = 'workers';
|
||
c.matches.slice(0, c.headcount).forEach(function(m) {
|
||
var wr = document.createElement('div'); wr.className = 'wrow';
|
||
var wn = document.createElement('span'); wn.className = 'wname'; wn.textContent = m.name || m.doc_id;
|
||
var ws = document.createElement('span'); ws.className = 'wscore'; ws.textContent = Math.round(m.score * 100) + '%';
|
||
wr.appendChild(wn); wr.appendChild(ws);
|
||
workers.appendChild(wr);
|
||
});
|
||
card.appendChild(workers);
|
||
|
||
// Action buttons
|
||
var actions = document.createElement('div'); actions.className = 'action';
|
||
var callBtn = document.createElement('button'); callBtn.className = 'abtn call'; callBtn.textContent = 'Call All';
|
||
var smsBtn = document.createElement('button'); smsBtn.className = 'abtn sms'; smsBtn.textContent = 'Send SMS';
|
||
actions.appendChild(callBtn); actions.appendChild(smsBtn);
|
||
if (!isFilled) {
|
||
var moreBtn = document.createElement('button'); moreBtn.className = 'abtn skip'; moreBtn.textContent = 'Find More';
|
||
moreBtn.onclick = function() {
|
||
document.getElementById('sq').value = c.role + ' ' + (c.state || '');
|
||
if (c.state) document.getElementById('sst').value = c.state;
|
||
toggleSearch(); doSearch();
|
||
};
|
||
actions.appendChild(moreBtn);
|
||
}
|
||
card.appendChild(actions);
|
||
}
|
||
el.appendChild(card);
|
||
});
|
||
}
|
||
|
||
function renderComms(today) {
|
||
var el = document.getElementById('comms');
|
||
el.textContent = '';
|
||
if (!today || !today.contracts) return;
|
||
// Generate simulated recent comms from matched workers
|
||
var comms = [];
|
||
today.contracts.forEach(function(c) {
|
||
if (c.matches && c.matches.length) {
|
||
var m = c.matches[0];
|
||
comms.push({ who: m.name || m.doc_id, msg: 'Confirmed for ' + c.role + ' at ' + c.client + ' — ' + c.start, time: '8 min ago' });
|
||
}
|
||
});
|
||
comms.push({ who: 'System', msg: today.contracts.length + ' contracts loaded, ' + today.filled + '/' + today.needed + ' positions pre-matched', time: 'just now' });
|
||
comms.slice(0, 5).forEach(function(c) {
|
||
var d = document.createElement('div'); d.className = 'comm';
|
||
var who = document.createElement('div'); who.className = 'who'; who.textContent = c.who;
|
||
var msg = document.createElement('div'); msg.className = 'msg'; msg.textContent = c.msg;
|
||
var time = document.createElement('div'); time.className = 'time'; time.textContent = c.time;
|
||
d.appendChild(who); d.appendChild(msg); d.appendChild(time);
|
||
el.appendChild(d);
|
||
});
|
||
}
|
||
|
||
// Search
|
||
function toggleSearch() {
|
||
document.getElementById('searchbox').classList.toggle('open');
|
||
document.getElementById('sq').focus();
|
||
}
|
||
|
||
function doSearch() {
|
||
var q = document.getElementById('sq').value.trim();
|
||
if (!q) return;
|
||
var st = document.getElementById('sst').value;
|
||
var rl = document.getElementById('srl').value;
|
||
var out = document.getElementById('sresults');
|
||
out.textContent = '';
|
||
var ld = document.createElement('div'); ld.className = 'loading'; ld.textContent = 'Searching...';
|
||
out.appendChild(ld);
|
||
|
||
var f = "CAST(reliability AS DOUBLE) >= 0.5";
|
||
if (st) f += " AND state = '" + st + "'";
|
||
if (rl) f += " AND role = '" + rl + "'";
|
||
|
||
fetch(A + '/search', {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({ question: q, index_name: 'workers_500k_v1', sql_filter: f,
|
||
dataset: 'workers_500k', id_column: 'worker_id', top_k: 8, generate: false })
|
||
})
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
out.textContent = '';
|
||
var sources = d.sources || [];
|
||
if (!sources.length) { out.textContent = 'No matches found.'; return; }
|
||
var hdr = document.createElement('div');
|
||
hdr.style.cssText = 'color:#64748b;font-size:11px;margin-bottom:8px';
|
||
hdr.textContent = (d.sql_matches||0) + ' matches → ' + sources.length + ' best (' + (d.duration_ms||0) + 'ms)';
|
||
out.appendChild(hdr);
|
||
sources.forEach(function(s) {
|
||
var parts = (s.chunk_text||'').split('\u2014');
|
||
if (parts.length < 2) parts = (s.chunk_text||'').split('—');
|
||
var nm = parts[0] ? parts[0].trim() : s.doc_id;
|
||
var rest = parts[1] ? parts[1].trim() : '';
|
||
var card = document.createElement('div'); card.className = 'card';
|
||
var name = document.createElement('div'); name.className = 'name'; name.textContent = nm;
|
||
var det = document.createElement('div'); det.className = 'det'; det.textContent = rest.substring(0, 120);
|
||
card.appendChild(name); card.appendChild(det);
|
||
out.appendChild(card);
|
||
});
|
||
})
|
||
.catch(function(e) { out.textContent = 'Error: ' + e.message; });
|
||
}
|
||
</script>
|
||
</body></html>
|