Phase 8.5 was fully built on the Rust side (WorkspaceManager with
create/handoff/search/shortlist/activity/get/list, persisted to
object storage, zero-copy handoff between agents). Nothing surfaced
it in the recruiter UI. This page closes that gap.
/workspaces — split-pane UI:
Left: scrollable list of all workspaces, sorted by updated_at.
Each card shows name, tier pill (daily/weekly/monthly/pinned),
current owner, count of shortlisted candidates + activity events.
Right: selected workspace detail with five sections:
1. Header — name, tier, owner, created/updated dates, description,
previous-owners audit trail (each handoff is preserved)
2. Actions row — Hand off, Shortlist candidate, Save search, Log activity
3. Shortlist — candidates flagged with dataset + record_id + notes
4. Saved searches — named SQL queries the staffer wants to rerun
5. Activity — chronological (newest first) log of what happened
Four modals for the add/edit actions (create, handoff, shortlist,
save-search, log-activity). All forms POST through the existing
/api/* passthrough to the gateway's /workspaces/* routes.
End-to-end verified live:
1. Sarah creates 'Demo: Toledo Week 17' workspace
2. Shortlists Helen Sanchez (W500K-4661) with notes about prior endorsements
3. Logs activity: 'called — Helen confirmed Tuesday 7am shift'
4. Hands off to Kim with reason 'end of shift'
5. Kim opens the workspace: owner=kim, previous_owners=[{sarah→kim}],
sees all 3 prior events + the shortlisted Helen
— no data copy, pointer swap only (Phase 8.5 design)
Security: all dynamic content built via el(tag,cls,text) DOM helper.
Zero innerHTML on API-derived strings. Modal close-on-backdrop-click
is guarded to the backdrop element.
Nav updated across all 7 pages. Workspaces is the 7th tab.
Dashboard · Walkthrough · Architecture · Spec · Onboard · Alerts · Workspaces.
488 lines
21 KiB
HTML
488 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en"><head>
|
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Lakehouse — Workspaces</title>
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:'Inter',-apple-system,system-ui,sans-serif;background:#090c10;color:#b0b8c4;font-size:14px;line-height:1.55;-webkit-font-smoothing:antialiased}
|
|
a{color:#58a6ff;text-decoration:none}
|
|
a:hover{color:#79c0ff}
|
|
|
|
.bar{background:#0d1117;padding:0 24px;height:56px;border-bottom:1px solid #171d27;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:10}
|
|
.bar h1{font-size:14px;font-weight:600;color:#e6edf3;letter-spacing:-0.2px}
|
|
.bar nav{display:flex;gap:2px;flex-wrap:wrap}
|
|
.bar nav a{font-size:12px;color:#545d68;padding:6px 14px;border-radius:6px;transition:all 0.15s}
|
|
.bar nav a:hover{color:#e6edf3;background:#161b22}
|
|
.bar nav a.active{color:#e6edf3;background:#1c2333}
|
|
|
|
.wrap{max-width:1200px;margin:0 auto;padding:24px 20px 60px}
|
|
|
|
h2{color:#e6edf3;font-size:22px;font-weight:700;letter-spacing:-0.3px;margin-bottom:4px}
|
|
.lede{color:#8b949e;font-size:13px;margin-bottom:18px;line-height:1.6;max-width:720px}
|
|
|
|
.layout{display:grid;grid-template-columns:320px 1fr;gap:16px}
|
|
|
|
.list{display:flex;flex-direction:column;gap:8px;max-height:calc(100vh - 200px);overflow-y:auto;padding-right:4px}
|
|
.ws-card{background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:12px 14px;cursor:pointer;transition:all 0.15s}
|
|
.ws-card:hover{border-color:#30363d;background:#161b22}
|
|
.ws-card.active{border-color:#1f6feb;background:#0d1d38}
|
|
.ws-card .name{color:#e6edf3;font-weight:600;font-size:13px;margin-bottom:4px;line-height:1.4}
|
|
.ws-card .meta{color:#8b949e;font-size:11px}
|
|
.ws-card .tier{display:inline-block;padding:1px 7px;border-radius:8px;font-size:9px;font-weight:600;margin-right:6px;letter-spacing:0.3px}
|
|
.tier.daily{background:#2d1b00;color:#d29922;border:1px solid #854d0e}
|
|
.tier.weekly{background:#0d2340;color:#58a6ff;border:1px solid #1f6feb80}
|
|
.tier.monthly{background:#1e1047;color:#a78bfa;border:1px solid #5b21b680}
|
|
.tier.pinned{background:#0d2818;color:#3fb950;border:1px solid #2ea04380}
|
|
|
|
.detail{background:#0d1117;border:1px solid #171d27;border-radius:12px;padding:18px;min-height:400px}
|
|
|
|
.section{margin-bottom:20px}
|
|
.section-hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}
|
|
.section-hdr .title{color:#545d68;font-size:11px;text-transform:uppercase;letter-spacing:1.2px;font-weight:600}
|
|
.section-hdr .count{color:#8b949e;font-size:11px}
|
|
|
|
.entry{background:#161b22;border-radius:6px;padding:10px 12px;margin-bottom:6px;font-size:12px}
|
|
.entry .title{color:#e6edf3;font-weight:500;font-size:13px}
|
|
.entry .meta{color:#8b949e;font-size:11px;margin-top:2px}
|
|
.entry .sub{color:#545d68;font-size:11px;margin-top:2px;font-family:ui-monospace,Menlo,monospace}
|
|
|
|
.empty{color:#484f58;font-size:12px;font-style:italic;padding:10px 12px}
|
|
|
|
.btn{padding:8px 14px;background:#1f6feb;border:none;border-radius:6px;color:#fff;font-size:12px;font-weight:600;cursor:pointer}
|
|
.btn:hover{background:#388bfd}
|
|
.btn:disabled{opacity:0.4;cursor:not-allowed}
|
|
.btn.ghost{background:transparent;border:1px solid #21262d;color:#c9d1d9}
|
|
.btn.ghost:hover{background:#161b22}
|
|
.btn.green{background:#2ea043}
|
|
.btn.green:hover{background:#3fb950}
|
|
.btn.sm{padding:4px 10px;font-size:11px}
|
|
|
|
.form{display:flex;flex-direction:column;gap:10px}
|
|
.form label{color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;font-weight:600;margin-bottom:-6px}
|
|
.form input,.form textarea,.form select{padding:8px 12px;background:#161b22;border:1px solid #21262d;border-radius:6px;color:#e6edf3;font-size:13px;outline:none;font-family:inherit}
|
|
.form input:focus,.form textarea:focus,.form select:focus{border-color:#388bfd}
|
|
.form textarea{resize:vertical;min-height:60px;font-family:inherit}
|
|
.form-row{display:flex;gap:10px}
|
|
.form-row>*{flex:1}
|
|
|
|
.modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:100;display:none;align-items:center;justify-content:center;padding:20px}
|
|
.modal-backdrop.show{display:flex}
|
|
.modal{background:#0d1117;border:1px solid #30363d;border-radius:12px;padding:22px;max-width:520px;width:100%;max-height:90vh;overflow-y:auto}
|
|
.modal h3{color:#e6edf3;font-size:17px;font-weight:700;margin-bottom:12px}
|
|
|
|
.hdr-block{padding-bottom:14px;border-bottom:1px solid #171d27;margin-bottom:16px}
|
|
.hdr-block .wname{color:#e6edf3;font-size:18px;font-weight:700;margin-bottom:4px}
|
|
.hdr-block .wmeta{color:#8b949e;font-size:12px}
|
|
.hdr-block .desc{color:#c9d1d9;font-size:13px;margin-top:8px;line-height:1.6}
|
|
|
|
.prev-owners{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}
|
|
.prev-owners .chip{padding:2px 8px;border-radius:10px;font-size:10px;background:#161b22;color:#8b949e;border:1px solid #21262d}
|
|
|
|
.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
|
|
|
|
.flash{padding:10px 14px;border-radius:6px;margin-top:10px;font-size:12px}
|
|
.flash.ok{background:#0d2818;color:#86efac;border:1px solid #2ea04360}
|
|
.flash.err{background:#3a1a1a;color:#fca5a5;border:1px solid #f8514960}
|
|
|
|
.activity{max-height:240px;overflow-y:auto}
|
|
.activity .entry{border-left:2px solid #2ea043}
|
|
|
|
.footer{border-top:1px solid #171d27;padding:20px;text-align:center;color:#3d444d;font-size:11px}
|
|
|
|
@media(max-width:900px){
|
|
.layout{grid-template-columns:1fr}
|
|
.list{max-height:none}
|
|
.bar nav{font-size:10px}
|
|
}
|
|
</style></head>
|
|
<body>
|
|
|
|
<div class="bar">
|
|
<h1>Lakehouse — Workspaces</h1>
|
|
<nav>
|
|
<a href=".">Dashboard</a>
|
|
<a href="console">Walkthrough</a>
|
|
<a href="proof">Architecture</a>
|
|
<a href="spec">Spec</a>
|
|
<a href="onboard">Onboard</a>
|
|
<a href="alerts">Alerts</a>
|
|
<a href="workspaces" class="active">Workspaces</a>
|
|
</nav>
|
|
</div>
|
|
|
|
<div class="wrap">
|
|
|
|
<div style="margin-bottom:18px">
|
|
<h2>Per-contract workspaces</h2>
|
|
<div class="lede">
|
|
Each contract gets its own workspace: saved searches, shortlisted candidates, activity log, tier (daily / weekly / monthly / pinned). Persisted across sessions. One-click zero-copy handoff between staffers — Sarah hands off to Kim, Kim opens the workspace and sees everything Sarah saw. This is the layer that keeps institutional memory per-contract, not per-person.
|
|
</div>
|
|
<button class="btn" id="new-ws-btn">New workspace</button>
|
|
</div>
|
|
|
|
<div class="layout">
|
|
<div class="list" id="ws-list"><div class="empty">Loading workspaces…</div></div>
|
|
<div class="detail" id="ws-detail">
|
|
<div class="empty">Select a workspace from the list, or create one.</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Modals -->
|
|
<div class="modal-backdrop" id="modal-new">
|
|
<div class="modal">
|
|
<h3>New workspace</h3>
|
|
<form class="form" id="form-new" onsubmit="return false">
|
|
<label>Contract / fill name</label>
|
|
<input type="text" id="nw-name" placeholder="e.g. Riverfront Steel — Week 17 fills" required>
|
|
<label>Description</label>
|
|
<textarea id="nw-desc" placeholder="What's this contract about?"></textarea>
|
|
<div class="form-row">
|
|
<div>
|
|
<label>Tier</label>
|
|
<select id="nw-tier">
|
|
<option value="daily">Daily</option>
|
|
<option value="weekly" selected>Weekly</option>
|
|
<option value="monthly">Monthly</option>
|
|
<option value="pinned">Pinned</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label>Owner (staffer)</label>
|
|
<input type="text" id="nw-owner" placeholder="sarah" value="sarah">
|
|
</div>
|
|
</div>
|
|
<div class="actions">
|
|
<button type="button" class="btn green" id="nw-create">Create</button>
|
|
<button type="button" class="btn ghost" data-close="modal-new">Cancel</button>
|
|
</div>
|
|
<div id="nw-result"></div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-backdrop" id="modal-handoff">
|
|
<div class="modal">
|
|
<h3>Hand off to another staffer</h3>
|
|
<div style="color:#8b949e;font-size:12px;margin-bottom:14px;line-height:1.6">
|
|
Transfers ownership of this workspace. The prior owner is preserved in <code>previous_owners</code> for the audit trail.
|
|
The recipient sees all saved searches, shortlist entries, and activity from the prior owner — no copy, no migration.
|
|
</div>
|
|
<form class="form" onsubmit="return false">
|
|
<label>New owner</label>
|
|
<input type="text" id="ho-owner" placeholder="kim">
|
|
<label>Reason</label>
|
|
<input type="text" id="ho-reason" placeholder="e.g. end of shift, taking over for Sarah">
|
|
<div class="actions">
|
|
<button type="button" class="btn green" id="ho-submit">Hand off</button>
|
|
<button type="button" class="btn ghost" data-close="modal-handoff">Cancel</button>
|
|
</div>
|
|
<div id="ho-result"></div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-backdrop" id="modal-shortlist">
|
|
<div class="modal">
|
|
<h3>Shortlist a candidate</h3>
|
|
<form class="form" onsubmit="return false">
|
|
<label>Dataset</label>
|
|
<input type="text" id="sl-dataset" placeholder="workers_500k" value="workers_500k">
|
|
<label>Worker ID</label>
|
|
<input type="text" id="sl-record" placeholder="e.g. 4661 or W500K-4661">
|
|
<label>Notes</label>
|
|
<textarea id="sl-notes" placeholder="Why this candidate — skills, past fills, availability window"></textarea>
|
|
<div class="actions">
|
|
<button type="button" class="btn green" id="sl-submit">Add to shortlist</button>
|
|
<button type="button" class="btn ghost" data-close="modal-shortlist">Cancel</button>
|
|
</div>
|
|
<div id="sl-result"></div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-backdrop" id="modal-search">
|
|
<div class="modal">
|
|
<h3>Save a search</h3>
|
|
<form class="form" onsubmit="return false">
|
|
<label>Search name</label>
|
|
<input type="text" id="sv-name" placeholder="e.g. Available Toledo welders with OSHA">
|
|
<label>SQL (will run against the catalog via /query/sql)</label>
|
|
<textarea id="sv-sql" placeholder="SELECT * FROM workers_500k WHERE role='Welder' AND city='Toledo' AND state='OH' AND CAST(availability AS DOUBLE) > 0.5 LIMIT 20"></textarea>
|
|
<div class="actions">
|
|
<button type="button" class="btn green" id="sv-submit">Save search</button>
|
|
<button type="button" class="btn ghost" data-close="modal-search">Cancel</button>
|
|
</div>
|
|
<div id="sv-result"></div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-backdrop" id="modal-activity">
|
|
<div class="modal">
|
|
<h3>Log activity</h3>
|
|
<form class="form" onsubmit="return false">
|
|
<label>Action</label>
|
|
<input type="text" id="act-action" placeholder="e.g. called, emailed, shortlisted, rejected">
|
|
<label>Detail</label>
|
|
<textarea id="act-detail" placeholder="What happened and what's the next step"></textarea>
|
|
<div class="actions">
|
|
<button type="button" class="btn green" id="act-submit">Log activity</button>
|
|
<button type="button" class="btn ghost" data-close="modal-activity">Cancel</button>
|
|
</div>
|
|
<div id="act-result"></div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="footer">Lakehouse · workspaces · per-contract state, zero-copy handoff (Phase 8.5)</div>
|
|
|
|
<script>
|
|
var P=location.pathname.indexOf('/lakehouse')>=0?'/lakehouse':'';
|
|
var A=location.origin+P;
|
|
|
|
var state={list:[], current:null, currentAgent:'sarah'};
|
|
|
|
function el(t,c,x){var e=document.createElement(t);if(c)e.className=c;if(x!==undefined&&x!==null)e.textContent=String(x);return e}
|
|
|
|
function apiGet(p){
|
|
return fetch(A+'/api'+p).then(function(r){return r.ok?r.json():r.text().then(function(b){throw new Error('HTTP '+r.status+': '+b)})});
|
|
}
|
|
function apiPost(p,body){
|
|
return fetch(A+'/api'+p,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body||{})}).then(function(r){
|
|
return r.text().then(function(t){
|
|
if(!r.ok) throw new Error('HTTP '+r.status+': '+t.slice(0,200));
|
|
try{return JSON.parse(t)}catch(e){return {raw:t}}
|
|
});
|
|
});
|
|
}
|
|
|
|
function showModal(id){ document.getElementById(id).classList.add('show') }
|
|
function hideModal(id){ document.getElementById(id).classList.remove('show') }
|
|
|
|
function loadList(preselectId){
|
|
apiGet('/workspaces').then(function(arr){
|
|
if(!Array.isArray(arr)) arr=[];
|
|
state.list=arr;
|
|
renderList();
|
|
if(preselectId){ openWorkspace(preselectId); }
|
|
else if(arr.length>0 && !state.current){ openWorkspace(arr[0].id); }
|
|
}).catch(function(e){
|
|
var host=document.getElementById('ws-list');host.textContent='';
|
|
host.appendChild(el('div','empty','Failed to load: '+e.message));
|
|
});
|
|
}
|
|
|
|
function renderList(){
|
|
var host=document.getElementById('ws-list');host.textContent='';
|
|
if(state.list.length===0){
|
|
host.appendChild(el('div','empty','No workspaces yet. Create one to get started.'));
|
|
return;
|
|
}
|
|
state.list.slice().sort(function(a,b){
|
|
return (b.updated_at||'').localeCompare(a.updated_at||'');
|
|
}).forEach(function(w){
|
|
var card=el('div','ws-card'+(state.current&&state.current.id===w.id?' active':''));
|
|
card.onclick=function(){ openWorkspace(w.id); };
|
|
card.appendChild(el('div','name',w.name||'(unnamed)'));
|
|
var meta=el('div','meta');
|
|
meta.appendChild(el('span','tier '+(w.tier||'weekly'),(w.tier||'weekly').toUpperCase()));
|
|
meta.appendChild(document.createTextNode((w.owner||'?')+' · '+((w.shortlist||[]).length)+' shortlisted · '+((w.activity||[]).length)+' events'));
|
|
card.appendChild(meta);
|
|
host.appendChild(card);
|
|
});
|
|
}
|
|
|
|
function openWorkspace(id){
|
|
apiGet('/workspaces/'+encodeURIComponent(id)).then(function(w){
|
|
if(!w||w.error){ throw new Error(w&&w.error?w.error:'not found'); }
|
|
state.current=w;
|
|
renderList();
|
|
renderDetail();
|
|
}).catch(function(e){
|
|
var host=document.getElementById('ws-detail');host.textContent='';
|
|
host.appendChild(el('div','empty','Failed to load: '+e.message));
|
|
});
|
|
}
|
|
|
|
function renderDetail(){
|
|
var host=document.getElementById('ws-detail');host.textContent='';
|
|
var w=state.current;
|
|
if(!w){ host.appendChild(el('div','empty','Select a workspace from the list, or create one.')); return; }
|
|
|
|
// Header
|
|
var hdr=el('div','hdr-block');
|
|
hdr.appendChild(el('div','wname',w.name||'(unnamed)'));
|
|
var meta=el('div','wmeta');
|
|
meta.appendChild(el('span','tier '+(w.tier||'weekly'),(w.tier||'weekly').toUpperCase()));
|
|
meta.appendChild(document.createTextNode(' owner: '+(w.owner||'?')+' · created '+(w.created_at||'').slice(0,10)+' · updated '+(w.updated_at||'').slice(0,10)));
|
|
hdr.appendChild(meta);
|
|
if(w.description){ hdr.appendChild(el('div','desc',w.description)); }
|
|
if((w.previous_owners||[]).length>0){
|
|
var pos=el('div','prev-owners');
|
|
pos.appendChild(el('span','chip','Previous owners:'));
|
|
w.previous_owners.forEach(function(p){ pos.appendChild(el('span','chip',p)); });
|
|
hdr.appendChild(pos);
|
|
}
|
|
// Action buttons
|
|
var actions=el('div','actions');
|
|
var handoffBtn=el('button','btn','Hand off');
|
|
handoffBtn.onclick=function(){ showModal('modal-handoff'); };
|
|
actions.appendChild(handoffBtn);
|
|
var slBtn=el('button','btn ghost','Shortlist candidate');
|
|
slBtn.onclick=function(){ showModal('modal-shortlist'); };
|
|
actions.appendChild(slBtn);
|
|
var svBtn=el('button','btn ghost','Save search');
|
|
svBtn.onclick=function(){ showModal('modal-search'); };
|
|
actions.appendChild(svBtn);
|
|
var actBtn=el('button','btn ghost','Log activity');
|
|
actBtn.onclick=function(){ showModal('modal-activity'); };
|
|
actions.appendChild(actBtn);
|
|
hdr.appendChild(actions);
|
|
host.appendChild(hdr);
|
|
|
|
// Shortlist
|
|
host.appendChild(sectionHeader('Shortlist',(w.shortlist||[]).length+' candidates'));
|
|
if((w.shortlist||[]).length===0){
|
|
host.appendChild(el('div','empty','No candidates shortlisted yet.'));
|
|
} else {
|
|
w.shortlist.forEach(function(sl){
|
|
var e=el('div','entry');
|
|
e.appendChild(el('div','title',(sl.dataset||'?')+' · '+(sl.record_id||'?')));
|
|
if(sl.notes) e.appendChild(el('div','meta',sl.notes));
|
|
e.appendChild(el('div','sub','by '+(sl.by_agent||sl.agent||'?')+' · '+(sl.at||sl.added_at||'').slice(0,16).replace('T',' ')));
|
|
host.appendChild(e);
|
|
});
|
|
}
|
|
|
|
// Saved searches
|
|
host.appendChild(sectionHeader('Saved searches',(w.saved_searches||[]).length+' queries'));
|
|
if((w.saved_searches||[]).length===0){
|
|
host.appendChild(el('div','empty','No saved searches yet.'));
|
|
} else {
|
|
w.saved_searches.forEach(function(sv){
|
|
var e=el('div','entry');
|
|
e.appendChild(el('div','title',sv.name||'(unnamed)'));
|
|
if(sv.sql) e.appendChild(el('div','sub',sv.sql));
|
|
e.appendChild(el('div','sub','by '+(sv.by_agent||sv.agent||'?')+' · '+(sv.at||sv.added_at||'').slice(0,16).replace('T',' ')));
|
|
host.appendChild(e);
|
|
});
|
|
}
|
|
|
|
// Activity
|
|
host.appendChild(sectionHeader('Activity',(w.activity||[]).length+' events'));
|
|
if((w.activity||[]).length===0){
|
|
host.appendChild(el('div','empty','No activity logged yet.'));
|
|
} else {
|
|
var act=el('div','activity');
|
|
w.activity.slice().reverse().forEach(function(a){
|
|
var e=el('div','entry');
|
|
e.appendChild(el('div','title',a.action||'(no action)'));
|
|
if(a.detail) e.appendChild(el('div','meta',a.detail));
|
|
e.appendChild(el('div','sub','by '+(a.by_agent||a.agent||'?')+' · '+(a.at||'').slice(0,16).replace('T',' ')));
|
|
act.appendChild(e);
|
|
});
|
|
host.appendChild(act);
|
|
}
|
|
}
|
|
|
|
function sectionHeader(title,count){
|
|
var s=el('div','section-hdr');
|
|
s.appendChild(el('span','title',title));
|
|
s.appendChild(el('span','count',count));
|
|
return s;
|
|
}
|
|
|
|
function flash(id,text,kind){
|
|
var box=document.getElementById(id);
|
|
box.textContent='';
|
|
box.appendChild(el('div','flash '+(kind||'ok'),text));
|
|
}
|
|
|
|
function createWorkspace(){
|
|
var name=document.getElementById('nw-name').value.trim();
|
|
var desc=document.getElementById('nw-desc').value.trim();
|
|
var tier=document.getElementById('nw-tier').value;
|
|
var owner=document.getElementById('nw-owner').value.trim()||'sarah';
|
|
if(!name){ flash('nw-result','Name is required','err');return; }
|
|
apiPost('/workspaces/create',{name:name,description:desc,owner:owner,tier:tier}).then(function(r){
|
|
state.currentAgent=owner;
|
|
hideModal('modal-new');
|
|
document.getElementById('nw-name').value='';
|
|
document.getElementById('nw-desc').value='';
|
|
loadList(r.id);
|
|
}).catch(function(e){ flash('nw-result',e.message,'err'); });
|
|
}
|
|
|
|
function submitHandoff(){
|
|
var w=state.current;if(!w)return;
|
|
var to=document.getElementById('ho-owner').value.trim();
|
|
var reason=document.getElementById('ho-reason').value.trim();
|
|
if(!to){ flash('ho-result','New owner required','err');return; }
|
|
apiPost('/workspaces/'+encodeURIComponent(w.id)+'/handoff',{to_agent:to,reason:reason}).then(function(){
|
|
state.currentAgent=to;
|
|
hideModal('modal-handoff');
|
|
loadList(w.id);
|
|
}).catch(function(e){ flash('ho-result',e.message,'err'); });
|
|
}
|
|
|
|
function submitShortlist(){
|
|
var w=state.current;if(!w)return;
|
|
var dataset=document.getElementById('sl-dataset').value.trim()||'workers_500k';
|
|
var record=document.getElementById('sl-record').value.trim();
|
|
var notes=document.getElementById('sl-notes').value.trim();
|
|
if(!record){ flash('sl-result','Worker ID required','err');return; }
|
|
apiPost('/workspaces/'+encodeURIComponent(w.id)+'/shortlist',{dataset:dataset,record_id:record,notes:notes,agent:state.currentAgent}).then(function(){
|
|
hideModal('modal-shortlist');
|
|
document.getElementById('sl-record').value='';
|
|
document.getElementById('sl-notes').value='';
|
|
loadList(w.id);
|
|
}).catch(function(e){ flash('sl-result',e.message,'err'); });
|
|
}
|
|
|
|
function submitSaveSearch(){
|
|
var w=state.current;if(!w)return;
|
|
var name=document.getElementById('sv-name').value.trim();
|
|
var sql=document.getElementById('sv-sql').value.trim();
|
|
if(!name||!sql){ flash('sv-result','Name and SQL are required','err');return; }
|
|
apiPost('/workspaces/'+encodeURIComponent(w.id)+'/search',{name:name,sql:sql,agent:state.currentAgent}).then(function(){
|
|
hideModal('modal-search');
|
|
document.getElementById('sv-name').value='';
|
|
document.getElementById('sv-sql').value='';
|
|
loadList(w.id);
|
|
}).catch(function(e){ flash('sv-result',e.message,'err'); });
|
|
}
|
|
|
|
function submitActivity(){
|
|
var w=state.current;if(!w)return;
|
|
var action=document.getElementById('act-action').value.trim();
|
|
var detail=document.getElementById('act-detail').value.trim();
|
|
if(!action){ flash('act-result','Action required','err');return; }
|
|
apiPost('/workspaces/'+encodeURIComponent(w.id)+'/activity',{action:action,detail:detail,agent:state.currentAgent}).then(function(){
|
|
hideModal('modal-activity');
|
|
document.getElementById('act-action').value='';
|
|
document.getElementById('act-detail').value='';
|
|
loadList(w.id);
|
|
}).catch(function(e){ flash('act-result',e.message,'err'); });
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded',function(){
|
|
document.getElementById('new-ws-btn').addEventListener('click',function(){ showModal('modal-new'); });
|
|
document.getElementById('nw-create').addEventListener('click',createWorkspace);
|
|
document.getElementById('ho-submit').addEventListener('click',submitHandoff);
|
|
document.getElementById('sl-submit').addEventListener('click',submitShortlist);
|
|
document.getElementById('sv-submit').addEventListener('click',submitSaveSearch);
|
|
document.getElementById('act-submit').addEventListener('click',submitActivity);
|
|
Array.prototype.forEach.call(document.querySelectorAll('[data-close]'),function(b){
|
|
b.addEventListener('click',function(){ hideModal(b.getAttribute('data-close')); });
|
|
});
|
|
// Click-outside-to-close
|
|
Array.prototype.forEach.call(document.querySelectorAll('.modal-backdrop'),function(m){
|
|
m.addEventListener('click',function(e){ if(e.target===m) m.classList.remove('show'); });
|
|
});
|
|
loadList();
|
|
});
|
|
</script>
|
|
</body></html>
|