Rich worker cards: skills, certs, reliability bars — not just names
Each worker in a contract card now shows: - Initials avatar (color-coded) - Name + location on same line - Skill tags (blue pills, top 3 relevant) - Cert badges (green pills — OSHA, Forklift, Hazmat) - Archetype tag (purple — reliable, leader, etc) - Reliability bar with color (green >80%, yellow >50%, red <50%) - Availability bar with color - Individual Call/SMS buttons per worker Contract headers show: - Urgency dot (red/yellow/blue/green) - Client name, role × headcount, location, start time - Progress bar with fill count GitHub-style dark theme. Every piece of info visible at a glance without clicking anything. The staffer sees skills, certs, and reliability for every matched worker the moment the page loads. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
05785b4628
commit
845acfdcda
@ -4,366 +4,331 @@
|
||||
<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}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;background:#0b0f19;color:#c9d1d9;font-size:13px;-webkit-font-smoothing:antialiased}
|
||||
|
||||
/* 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}
|
||||
/* Top bar */
|
||||
.bar{background:#161b22;padding:10px 20px;border-bottom:1px solid #21262d;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px}
|
||||
.bar h1{font-size:15px;font-weight:600;color:#f0f6fc}
|
||||
.bar h1 span{color:#7c3aed}
|
||||
.bar .right{display:flex;gap:12px;align-items:center;font-size:11px;color:#8b949e}
|
||||
|
||||
/* 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}
|
||||
/* Pipeline counters */
|
||||
.pipeline{display:flex;gap:1px;margin:16px 16px 0;background:#161b22;border-radius:10px;overflow:hidden;border:1px solid #21262d}
|
||||
.pip{flex:1;padding:14px 8px;text-align:center}
|
||||
.pip .n{font-size:28px;font-weight:800;font-variant-numeric:tabular-nums}
|
||||
.pip .l{font-size:9px;text-transform:uppercase;letter-spacing:1px;color:#8b949e;margin-top:2px}
|
||||
.pip.red .n{color:#f85149}.pip.yel .n{color:#d29922}.pip.blu .n{color:#58a6ff}.pip.grn .n{color:#3fb950}
|
||||
|
||||
/* 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}
|
||||
/* Content */
|
||||
.content{padding:16px;max-width:1200px;margin:0 auto}
|
||||
.section-label{font-size:11px;color:#8b949e;text-transform:uppercase;letter-spacing:1px;font-weight:600;margin:20px 0 10px;display:flex;justify-content:space-between;align-items:center}
|
||||
|
||||
/* 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}
|
||||
/* Contract card */
|
||||
.contract{background:#161b22;border:1px solid #21262d;border-radius:10px;margin-bottom:14px;overflow:hidden}
|
||||
.contract .hdr{padding:14px 16px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #21262d;flex-wrap:wrap;gap:8px}
|
||||
.contract .hdr .left{display:flex;align-items:center;gap:10px}
|
||||
.contract .urgency{width:10px;height:10px;border-radius:50%;flex-shrink:0}
|
||||
.urgency.urgent{background:#f85149}.urgency.high{background:#d29922}.urgency.medium{background:#58a6ff}.urgency.low{background:#3fb950}.urgency.filled{background:#3fb950}
|
||||
.contract .client{font-size:15px;font-weight:600;color:#f0f6fc}
|
||||
.contract .need{font-size:12px;color:#8b949e}
|
||||
.contract .hdr .right{display:flex;align-items:center;gap:8px}
|
||||
.progress{background:#21262d;border-radius:10px;height:6px;width:80px;overflow:hidden}
|
||||
.progress .fill{height:100%;border-radius:10px;transition:width .3s}
|
||||
.fill.full{background:#3fb950}.fill.partial{background:#d29922}.fill.empty{background:#484f58}
|
||||
.contract .hdr .count{font-size:12px;font-weight:600;color:#f0f6fc}
|
||||
|
||||
/* 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}
|
||||
/* Worker card inside contract */
|
||||
.workers{padding:6px}
|
||||
.worker{display:flex;align-items:flex-start;gap:12px;padding:10px 12px;border-radius:8px;transition:background .15s}
|
||||
.worker:hover{background:#1c2333}
|
||||
.worker .avatar{width:36px;height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#f0f6fc;flex-shrink:0}
|
||||
.avatar.a1{background:#1a3a2a}.avatar.a2{background:#1a2a3a}.avatar.a3{background:#2a1a3a}.avatar.a4{background:#3a2a1a}.avatar.a5{background:#1a3a3a}
|
||||
.worker .info{flex:1;min-width:0}
|
||||
.worker .name-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
||||
.worker .wname{font-weight:600;color:#f0f6fc;font-size:13px}
|
||||
.worker .wloc{color:#8b949e;font-size:11px}
|
||||
.worker .tags{display:flex;gap:4px;flex-wrap:wrap;margin-top:4px}
|
||||
.tag{padding:1px 7px;border-radius:10px;font-size:10px;font-weight:500}
|
||||
.tag.skill{background:#1a2744;color:#58a6ff;border:1px solid #1f3d68}
|
||||
.tag.cert{background:#1a3a2a;color:#3fb950;border:1px solid #238636}
|
||||
.tag.type{background:#2a1a3a;color:#bc8cff;border:1px solid #553098}
|
||||
.worker .metrics{display:flex;gap:12px;margin-top:6px;align-items:center}
|
||||
.metric{display:flex;align-items:center;gap:4px}
|
||||
.metric .label{color:#8b949e;font-size:10px}
|
||||
.metric .bar{width:48px;height:4px;background:#21262d;border-radius:2px;overflow:hidden}
|
||||
.metric .bar .val{height:100%;border-radius:2px}
|
||||
.bar .high{background:#3fb950}.bar .med{background:#d29922}.bar .low{background:#f85149}
|
||||
.metric .pct{font-size:10px;font-weight:600;font-variant-numeric:tabular-nums}
|
||||
.worker .actions{display:flex;gap:4px;flex-shrink:0;align-self:center}
|
||||
.wbtn{padding:4px 10px;border-radius:4px;font-size:10px;cursor:pointer;border:1px solid #21262d;background:#161b22;color:#8b949e;font-weight:500}
|
||||
.wbtn:hover{border-color:#58a6ff;color:#58a6ff}
|
||||
.wbtn.primary{background:#1f3d68;color:#58a6ff;border-color:#1f3d68}
|
||||
|
||||
/* Alerts strip */
|
||||
.alerts{display:flex;gap:8px;margin:16px 16px 0;flex-wrap:wrap}
|
||||
.alrt{padding:6px 12px;border-radius:6px;font-size:11px;display:flex;align-items:center;gap:4px}
|
||||
.alrt.w{background:#2d1b00;color:#d29922;border:1px solid #533d13}
|
||||
.alrt.i{background:#0d1d33;color:#58a6ff;border:1px solid #1f3d68}
|
||||
.alrt.g{background:#0d261a;color:#3fb950;border:1px solid #238636}
|
||||
|
||||
/* Search */
|
||||
.search-area{margin-top:20px;background:#161b22;border:1px solid #21262d;border-radius:10px;padding:14px}
|
||||
.search-area summary{cursor:pointer;color:#8b949e;font-size:12px;list-style:none;display:flex;align-items:center;gap:6px}
|
||||
.search-area summary::-webkit-details-marker{display:none}
|
||||
.search-area[open] summary{margin-bottom:10px}
|
||||
.search-area input[type=text]{width:100%;padding:10px 14px;background:#0b0f19;border:1px solid #21262d;border-radius:6px;color:#f0f6fc;font-size:13px;outline:none;margin-bottom:8px}
|
||||
.search-area input:focus{border-color:#58a6ff}
|
||||
.srow{display:flex;gap:6px;margin-bottom:8px}
|
||||
.search-area select{flex:1;padding:7px;background:#0b0f19;border:1px solid #21262d;border-radius:4px;color:#c9d1d9;font-size:11px}
|
||||
.sbtn{width:100%;padding:8px;background:#7c3aed;border:none;border-radius:6px;color:#fff;font-size:12px;font-weight:600;cursor:pointer}
|
||||
.sbtn:hover{background:#6d28d9}
|
||||
#sresults{margin-top:10px}
|
||||
|
||||
.footer{text-align:center;padding:16px;color:#484f58;font-size:11px;margin-top:20px}
|
||||
.footer a{color:#58a6ff;text-decoration:none}
|
||||
.loading{color:#484f58;text-align:center;padding:16px}
|
||||
|
||||
@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%}
|
||||
.pipeline{flex-wrap:wrap}.pip{min-width:25%}
|
||||
.worker{flex-direction:column}.worker .actions{align-self:flex-start}
|
||||
.alerts{flex-direction:column}
|
||||
.contract .hdr{flex-direction:column;align-items:flex-start}
|
||||
}
|
||||
</style></head><body>
|
||||
<div class="bar">
|
||||
<h1>Staffing Co-Pilot</h1>
|
||||
<div class="info" id="status">Loading...</div>
|
||||
<h1><span>●</span> Staffing Co-Pilot</h1>
|
||||
<div class="right"><span id="status">Loading...</span></div>
|
||||
</div>
|
||||
<div class="wrap">
|
||||
<div class="main">
|
||||
<h2>Today's Pipeline</h2>
|
||||
<div class="pipeline" id="pipeline"></div>
|
||||
<div class="pipeline" id="pipeline"></div>
|
||||
<div class="alerts" id="alerts"></div>
|
||||
<div class="content">
|
||||
<div class="section-label"><span>Your Contracts</span><span id="day-label"></span></div>
|
||||
<div id="contracts"><div class="loading">Loading your day...</div></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()">
|
||||
<details class="search-area">
|
||||
<summary>🔍 Search all 500,000 workers</summary>
|
||||
<input type="text" id="sq" placeholder="Type what you need..." 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>
|
||||
<button class="sbtn" onclick="doSearch()">Search</button>
|
||||
<div id="sresults"></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<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 class="footer"><a href="proof">View Proof of Work</a> · 500K workers · 673K AI-indexed</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var P = location.pathname.indexOf('/lakehouse') >= 0 ? '/lakehouse' : '';
|
||||
var A = location.origin + P;
|
||||
var colors = ['a1','a2','a3','a4','a5'];
|
||||
|
||||
// Load the day's data on page open
|
||||
window.addEventListener('load', function() { loadDay(); });
|
||||
window.addEventListener('load', 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);
|
||||
var sum = d.summary || {};
|
||||
renderPipeline(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';
|
||||
document.getElementById('status').textContent = sum.total_filled + '/' + sum.total_needed + ' filled · ' + sum.emergencies + ' urgent';
|
||||
document.getElementById('day-label').textContent = today ? today.label + ' · ' + today.staffer : '';
|
||||
})
|
||||
.catch(function(e) {
|
||||
document.getElementById('contracts').textContent = 'Could not load contracts: ' + e.message;
|
||||
});
|
||||
.catch(function(e) { document.getElementById('contracts').textContent = 'Error: ' + 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) {
|
||||
// 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'));
|
||||
(d.rows||[]).forEach(function(r){
|
||||
var a=document.createElement('div');
|
||||
a.className='alrt '+(r.archetype==='erratic'?'w':'i');
|
||||
a.textContent=(r.archetype==='erratic'?'⚠ ':'📵 ')+r.cnt+' '+r.archetype+' workers';
|
||||
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);
|
||||
});
|
||||
var g=document.createElement('div');g.className='alrt g';g.textContent='✓ All systems online';
|
||||
el.appendChild(g);
|
||||
}).catch(function(){});
|
||||
}
|
||||
|
||||
function renderPipeline(summary, today) {
|
||||
function renderPipeline(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);
|
||||
var u=0,f=0,fl=0,t=0;
|
||||
if (today&&today.contracts) today.contracts.forEach(function(c){
|
||||
t++;if(c.priority==='urgent')u++;if(c.filled>=c.headcount)fl++;else f++;
|
||||
});
|
||||
[['red',u,'Urgent'],['yel',f,'Filling'],['blu',t,'Contracts'],['grn',fl,'Filled']].forEach(function(p){
|
||||
var d=document.createElement('div');d.className='pip '+p[0];
|
||||
var n=document.createElement('div');n.className='n';n.textContent=p[1];
|
||||
var l=document.createElement('div');l.className='l';l.textContent=p[2];
|
||||
d.appendChild(n);d.appendChild(l);el.appendChild(d);
|
||||
});
|
||||
}
|
||||
|
||||
function parseWorker(text) {
|
||||
var parts = (text||'').split(/—|–/);
|
||||
var name = parts[0] ? parts[0].trim() : '?';
|
||||
var rest = parts[1] ? parts[1].trim() : '';
|
||||
var rm = rest.match(/^(.+?) in (.+?)\./);
|
||||
var sm = rest.match(/Skills: ([^.]+)/);
|
||||
var cm = rest.match(/Certs?: ([^.]+)/);
|
||||
var rr = rest.match(/Reliability: ([\d.]+)/);
|
||||
var av = rest.match(/Availability: ([\d.]+)/);
|
||||
var ar = rest.match(/Archetype: (\w+)/);
|
||||
return {
|
||||
name: name, role: rm?rm[1]:'', location: rm?rm[2]:'',
|
||||
skills: sm?sm[1].split('|').slice(0,4):[],
|
||||
certs: cm?cm[1].split('|').filter(function(c){return c!=='none'}):[],
|
||||
reliability: rr?parseFloat(rr[1]):0,
|
||||
availability: av?parseFloat(av[1]):0,
|
||||
archetype: ar?ar[1]:''
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
if (!today||!today.contracts||!today.contracts.length) { el.textContent='No contracts'; return; }
|
||||
var sorted = today.contracts.slice().sort(function(a,b){
|
||||
var p={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 (p[a.priority]||2)-(p[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);
|
||||
var card = document.createElement('div'); card.className = 'contract';
|
||||
|
||||
// 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);
|
||||
var hdr = document.createElement('div'); hdr.className = 'hdr';
|
||||
var left = document.createElement('div'); left.className = 'left';
|
||||
var dot = document.createElement('div'); dot.className = 'urgency ' + (isFilled?'filled':c.priority);
|
||||
var cl = document.createElement('span'); cl.className = 'client'; cl.textContent = c.client;
|
||||
var need = document.createElement('span'); need.className = 'need';
|
||||
need.textContent = c.role + ' × ' + c.headcount + ' · ' + (c.city||c.state) + ' · ' + c.start;
|
||||
left.appendChild(dot); left.appendChild(cl); left.appendChild(need);
|
||||
|
||||
// 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);
|
||||
var right = document.createElement('div'); right.className = 'right';
|
||||
var prog = document.createElement('div'); prog.className = 'progress';
|
||||
var fill = document.createElement('div'); fill.className = 'fill ' + (isFilled?'full':c.filled>0?'partial':'empty');
|
||||
fill.style.width = Math.round(c.filled/c.headcount*100) + '%';
|
||||
prog.appendChild(fill);
|
||||
var cnt = document.createElement('span'); cnt.className = 'count'; cnt.textContent = c.filled + '/' + c.headcount;
|
||||
right.appendChild(prog); right.appendChild(cnt);
|
||||
hdr.appendChild(left); hdr.appendChild(right);
|
||||
card.appendChild(hdr);
|
||||
|
||||
// Matched workers
|
||||
// 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);
|
||||
var wdiv = document.createElement('div'); wdiv.className = 'workers';
|
||||
c.matches.slice(0, c.headcount).forEach(function(m, idx) {
|
||||
var w = parseWorker(m.chunk_text || '');
|
||||
var wcard = document.createElement('div'); wcard.className = 'worker';
|
||||
|
||||
// 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);
|
||||
// Avatar
|
||||
var av = document.createElement('div');
|
||||
av.className = 'avatar ' + colors[idx % colors.length];
|
||||
av.textContent = (w.name||'?').split(' ').map(function(n){return n[0]||''}).join('').substring(0,2);
|
||||
wcard.appendChild(av);
|
||||
|
||||
// Info
|
||||
var info = document.createElement('div'); info.className = 'info';
|
||||
var nameRow = document.createElement('div'); nameRow.className = 'name-row';
|
||||
var nm = document.createElement('span'); nm.className = 'wname'; nm.textContent = w.name || m.name || m.doc_id;
|
||||
var loc = document.createElement('span'); loc.className = 'wloc'; loc.textContent = w.location;
|
||||
nameRow.appendChild(nm); nameRow.appendChild(loc);
|
||||
info.appendChild(nameRow);
|
||||
|
||||
// Tags
|
||||
var tags = document.createElement('div'); tags.className = 'tags';
|
||||
w.skills.slice(0,3).forEach(function(s) {
|
||||
var t = document.createElement('span'); t.className = 'tag skill'; t.textContent = s.trim(); tags.appendChild(t);
|
||||
});
|
||||
w.certs.slice(0,2).forEach(function(c) {
|
||||
var t = document.createElement('span'); t.className = 'tag cert'; t.textContent = c.trim(); tags.appendChild(t);
|
||||
});
|
||||
if (w.archetype) {
|
||||
var t = document.createElement('span'); t.className = 'tag type'; t.textContent = w.archetype; tags.appendChild(t);
|
||||
}
|
||||
info.appendChild(tags);
|
||||
|
||||
// Metrics
|
||||
var metrics = document.createElement('div'); metrics.className = 'metrics';
|
||||
addMetric(metrics, 'Reliability', w.reliability);
|
||||
addMetric(metrics, 'Availability', w.availability);
|
||||
info.appendChild(metrics);
|
||||
wcard.appendChild(info);
|
||||
|
||||
// Actions
|
||||
var actions = document.createElement('div'); actions.className = 'actions';
|
||||
var call = document.createElement('button'); call.className = 'wbtn primary'; call.textContent = 'Call';
|
||||
var sms = document.createElement('button'); sms.className = 'wbtn'; sms.textContent = 'SMS';
|
||||
actions.appendChild(call); actions.appendChild(sms);
|
||||
wcard.appendChild(actions);
|
||||
|
||||
wdiv.appendChild(wcard);
|
||||
});
|
||||
card.appendChild(wdiv);
|
||||
}
|
||||
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 addMetric(parent, label, val) {
|
||||
var m = document.createElement('div'); m.className = 'metric';
|
||||
var l = document.createElement('span'); l.className = 'label'; l.textContent = label;
|
||||
var bar = document.createElement('div'); bar.className = 'bar';
|
||||
var v = document.createElement('div');
|
||||
v.className = 'val ' + (val >= 0.8 ? 'high' : val >= 0.5 ? 'med' : 'low');
|
||||
v.style.width = Math.round(val * 100) + '%';
|
||||
bar.appendChild(v);
|
||||
var pct = document.createElement('span'); pct.className = 'pct';
|
||||
pct.style.color = val >= 0.8 ? '#3fb950' : val >= 0.5 ? '#d29922' : '#f85149';
|
||||
pct.textContent = Math.round(val * 100) + '%';
|
||||
m.appendChild(l); m.appendChild(bar); m.appendChild(pct);
|
||||
parent.appendChild(m);
|
||||
}
|
||||
|
||||
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);
|
||||
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.';return}
|
||||
var h=document.createElement('div');h.style.cssText='color:#8b949e;font-size:11px;margin-bottom:8px';
|
||||
h.textContent=(d.sql_matches||0)+' matches → '+sources.length+' best ('+(d.duration_ms||0)+'ms)';out.appendChild(h);
|
||||
sources.forEach(function(s,idx){
|
||||
var w=parseWorker(s.chunk_text);
|
||||
var wcard=document.createElement('div');wcard.className='worker';wcard.style.cssText='background:#161b22;border:1px solid #21262d;border-radius:8px;padding:10px;margin-bottom:6px';
|
||||
var av=document.createElement('div');av.className='avatar '+colors[idx%colors.length];
|
||||
av.textContent=(w.name||'?').split(' ').map(function(n){return n[0]||''}).join('').substring(0,2);wcard.appendChild(av);
|
||||
var info=document.createElement('div');info.className='info';
|
||||
var nr=document.createElement('div');nr.className='name-row';
|
||||
var nm=document.createElement('span');nm.className='wname';nm.textContent=w.name;
|
||||
var loc=document.createElement('span');loc.className='wloc';loc.textContent=w.location;
|
||||
nr.appendChild(nm);nr.appendChild(loc);info.appendChild(nr);
|
||||
var tags=document.createElement('div');tags.className='tags';
|
||||
w.skills.slice(0,3).forEach(function(sk){var t=document.createElement('span');t.className='tag skill';t.textContent=sk.trim();tags.appendChild(t)});
|
||||
w.certs.slice(0,2).forEach(function(c){var t=document.createElement('span');t.className='tag cert';t.textContent=c.trim();tags.appendChild(t)});
|
||||
info.appendChild(tags);
|
||||
var metrics=document.createElement('div');metrics.className='metrics';
|
||||
addMetric(metrics,'Rel',w.reliability);addMetric(metrics,'Avail',w.availability);
|
||||
info.appendChild(metrics);wcard.appendChild(info);out.appendChild(wcard);
|
||||
});
|
||||
})
|
||||
.catch(function(e) { out.textContent = 'Error: ' + e.message; });
|
||||
}).catch(function(e){out.textContent='Error: '+e.message});
|
||||
}
|
||||
</script>
|
||||
</body></html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user