232 lines
20 KiB
HTML
232 lines
20 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Job Tracker · Anthony</title>
|
|
<script>
|
|
const API_BASE = (window.location.pathname.replace(/\/index\.html$/,'').replace(/\/+$/,'')) || '';
|
|
const API = { jobs:API_BASE+'/api/jobs', stats:API_BASE+'/api/stats', refresh:API_BASE+'/api/refresh', update:(id)=>API_BASE+'/api/jobs/'+id+'/update', bulk:API_BASE+'/api/jobs/bulk-update', clRegen:(id)=>API_BASE+'/api/jobs/'+id+'/cover-letter/regenerate' };
|
|
const STATUSES = ['Backlog','Applied','Phone Screen','Interview','Offer','Accepted','Rejected','Ghosted','Declined'];
|
|
const PRIOS = ['','Low','Medium','High','Urgent'];
|
|
const SCOLORS = { 'Backlog':'#64748b','Applied':'#3b82f6','Phone Screen':'#8b5cf6','Interview':'#f59e0b','Offer':'#22c55e','Accepted':'#16a34a','Rejected':'#ef4444','Ghosted':'#78716c','Declined':'#6b7280' };
|
|
const SORDER = ['Backlog','Applied','Phone Screen','Interview','Offer','Accepted','Rejected','Ghosted','Declined'];
|
|
let state = { jobs:[], stats:{}, filtered:[], selected:new Set(), filter:'all', search:'', sort:'date-desc' };
|
|
|
|
function notify(m,t) {
|
|
const e=document.getElementById('n');
|
|
e.textContent=m; e.className='n '+(t||'ok'); e.style.display='block';
|
|
setTimeout(()=>e.style.display='none',3500);
|
|
}
|
|
async function f(url,opts) {
|
|
try { const r=await fetch(url,{headers:{'Content-Type':'application/json'},...opts}); return await r.json(); }
|
|
catch(e){ notify('API error: '+e.message,'err'); return null; }
|
|
}
|
|
async function load() {
|
|
const [jd,sd]=await Promise.all([f(API.jobs,{}),f(API.stats,{})]);
|
|
if(jd&&jd.jobs) state.jobs=jd.jobs;
|
|
if(sd) state.stats=sd;
|
|
filter(); render();
|
|
}
|
|
async function upd(id,up) { const r=await f(API.update(id),{method:'POST',body:JSON.stringify(up)}); if(r&&r.ok){notify('Saved','ok');load();}else notify('Save failed','err'); }
|
|
async function bulk(ids,up) { if(!ids.length)return; const r=await f(API.bulk,{method:'POST',body:JSON.stringify({ids,updates:up})}); if(r&&r.ok){notify('Updated '+(r.results?r.results.filter(x=>x.ok).length+' jobs':'')+'','ok');load();} }
|
|
async function refresh() { const r=await f(API.refresh,{method:'POST'}); if(r&&r.ok){notify('Refreshed — '+r.total+' jobs','ok');load();} }
|
|
|
|
function filter() {
|
|
let fi=[...state.jobs];
|
|
if(state.filter==='__urgent'){ fi=fi.filter(j=>j.closing_status==='urgent'); }
|
|
else if(state.filter==='__past'){ fi=fi.filter(j=>j.closing_status==='past'); }
|
|
else if(state.filter==='__strong'){ fi=fi.filter(j=>j.strong_match); }
|
|
else if(state.filter!=='all') fi=fi.filter(j=>j.status===state.filter);
|
|
if(state.search){ const q=state.search.toLowerCase(); fi=fi.filter(j=>(j.name+(j.role||'')+(j.location||'')+(j.notes||'')).toLowerCase().includes(q)); }
|
|
const[field,dir]=state.sort.split('-');
|
|
fi.sort((a,b)=>{ let va,vb; switch(field){
|
|
case'date': va=a.date_added||'0'; vb=b.date_added||'0'; break;
|
|
case'days': va=a.days_since||0; vb=b.days_since||0; break;
|
|
case'name': va=(a.name||'').toLowerCase(); vb=(b.name||'').toLowerCase(); break;
|
|
case'salary': va=a.salary||0; vb=b.salary||0; break;
|
|
default: va=a.date_added||'0'; vb=b.date_added||'0';
|
|
} return va<vb?dir==='asc'?-1:1:va>vb?dir==='asc'?1:-1:0; });
|
|
state.filtered=fi; state.selected=new Set();
|
|
}
|
|
|
|
function render() {
|
|
const st=state.stats||{};
|
|
document.getElementById('kt').textContent=st.total||0; document.getElementById('km').textContent=st.strong_matches||0;
|
|
document.getElementById('kw').textContent=st.this_week||0; document.getElementById('ks').textContent=st.stale_14d||0;
|
|
document.getElementById('ku').textContent=st.closing_urgent||0; document.getElementById('kp').textContent=st.closing_past||0;
|
|
document.getElementById('kr').textContent=st.refreshed_at?new Date(st.refreshed_at).toLocaleString('en-AU',{timeZone:'Australia/Perth'}):'-';
|
|
const total=state.jobs.length||1, counts={}; state.jobs.forEach(j=>{counts[j.status]=(counts[j.status]||0)+1;});
|
|
document.getElementById('funnel').innerHTML=SORDER.map(s=>{const c=counts[s]||0; if(!c)return''; return'<div class="fi"><div class="fl"><span>'+s+'</span><span class="fc">'+c+'</span></div><div class="ftr"><div class="ff" style="width:'+(c/total*100).toFixed(0)+'%;background:'+(SCOLORS[s]||'#64748b')+'"></div></div></div>';}).join('');
|
|
const tb=document.getElementById('tb');
|
|
if(!state.filtered.length){ tb.innerHTML='<tr><td colspan="11" style="text-align:center;padding:48px 0;color:#64748b">No jobs match this filter</td></tr>'; return; }
|
|
tb.innerHTML=state.filtered.map(j=>{
|
|
const sel=state.selected.has(j.id)?'checked':'';
|
|
const sCol=SCOLORS[j.status]||'#64748b', sal=j.salary?'$'+Number(j.salary).toLocaleString():'—', age=(j.days_since||0)+'d';
|
|
const stale=(j.days_since||0)>14?'stale-14':(j.days_since||0)>7?'stale-7':'';
|
|
// Closing date urgency
|
|
let closeHtml = '—', clsCls = '';
|
|
if(j.closing_date){
|
|
const cd=new Date(j.closing_date);
|
|
const now=new Date();
|
|
const days=Math.round((cd-now)/86400000);
|
|
const ds=j.closing_date.split('-');
|
|
const fmt=ds[2]+'/'+ds[1];
|
|
closeHtml=fmt;
|
|
if(j.closing_status==='past'){ clsCls='cls-past'; }
|
|
else if(j.closing_status==='urgent'){ clsCls='cls-urgent'; }
|
|
else if(j.closing_status==='warning'){ clsCls='cls-warn'; }
|
|
}
|
|
const rowCls=[stale,clsCls].filter(Boolean).join(' ');
|
|
const name=j.url?'<a href="'+j.url+'" target="_blank">'+j.name+'</a>':j.name;
|
|
const mb=j.strong_match?'<span class="badge badge-sm">★</span>':'';
|
|
return '<tr class="'+rowCls+'" data-id="'+j.id+'"><td class="c-chk"><input type="checkbox" '+sel+' onchange="tog(\''+j.id+'\')"></td><td class="c-co">'+name+mb+'</td><td class="c-ro">'+(j.role||"—")+'</td><td class="c-st"><span class="stat-pill" style="background:'+sCol+'18;color:'+sCol+';border-color:'+sCol+'40" onclick="showMenu(event,\''+j.id+'\',\''+j.status+'\')">'+j.status+'</span></td><td class="c-lo">'+(j.location||"—")+'</td><td class="c-sr">'+(j.source||"—")+'</td><td class="c-sa">'+sal+'</td><td class="c-cls">'+closeHtml+'</td><td class="c-ag">'+age+'</td><td class="c-cl">'+(j.cover_letter_url?'<a href="'+j.cover_letter_url+'" target="_blank" class="cl-link" title="View cover letter">📝</a><button class="cl-rg" onclick="regenerateCL(\''+j.id+'\')" title="Regenerate">🔄</button>':'<button class="cl-rg" onclick="regenerateCL(\''+j.id+'\')" title="Generate cover letter">+CL</button>')+'</td><td class="c-no"><button class="nb" onclick="editNote(\''+j.id+'\')">'+(j.notes?'✎':'+')+'</button></td></tr>';
|
|
}).join('');
|
|
document.getElementById('rc').textContent=state.filtered.length+' of '+state.jobs.length;
|
|
document.getElementById('bc').textContent=state.selected.size;
|
|
document.getElementById('bb').style.display=state.selected.size?'flex':'none';
|
|
}
|
|
|
|
function tog(id){ state.selected.has(id)?state.selected.delete(id):state.selected.add(id); render(); }
|
|
function selA(){ state.filtered.forEach(j=>state.selected.add(j.id)); render(); }
|
|
function desel(){ state.selected.clear(); render(); }
|
|
function setFilter(f){
|
|
state.filter=f;
|
|
// Update tab active states
|
|
document.querySelectorAll('.tab').forEach(t=>t.classList.toggle('a',t.dataset.f===f));
|
|
// Update KPI card active states
|
|
document.getElementById('kf-strong').style.borderColor=f==='__strong'?'var(--cy)':'';
|
|
document.getElementById('kf-urgent').style.borderColor=f==='__urgent'?'var(--cy)':'#f59e0b';
|
|
document.getElementById('kf-past').style.borderColor=f==='__past'?'var(--cy)':'#64748b';
|
|
filter(); render();
|
|
}
|
|
function doSearch(){ state.search=document.getElementById('s').value; filter(); render(); }
|
|
function setSort(s){ state.sort=s; document.querySelectorAll('.sort-btn').forEach(b=>b.classList.toggle('a',b.dataset.s===s)); filter(); render(); }
|
|
function applyBulk(){ const v=document.getElementById('bs').value; bulk([...state.selected],{status:v}); }
|
|
function showMenu(ev,id,cur){
|
|
const m=document.getElementById('sm');
|
|
m.innerHTML=STATUSES.map(s=>'<div class="sm-item'+(s===cur?' sm-cur':'')+'" onclick="changeStat(\''+id+'\',\''+s+'\')">'+s+'</div>').join('');
|
|
m.style.display='block'; m.style.left=Math.min(ev.clientX,window.innerWidth-200)+'px'; m.style.top=ev.clientY+'px';
|
|
setTimeout(()=>document.addEventListener('click',function(){m.style.display='none';},{once:true}),10);
|
|
}
|
|
async function changeStat(id,s){ await upd(id,{status:s}); document.getElementById('sm').style.display='none'; }
|
|
function editNote(id){ const j=state.jobs.find(x=>x.id===id); const n=prompt('Edit notes / JD summary:',j&&j.notes?j.notes:''); if(n!==null) upd(id,{notes:n}); }
|
|
|
|
async function regenerateCL(id){
|
|
const j=state.jobs.find(x=>x.id===id);
|
|
const hasCL=j&&j.cover_letter_url;
|
|
const defaultFB=hasCL?'Make it stronger':'Generate a professional cover letter for this role';
|
|
const pText=hasCL?'How should the cover letter change? (e.g. "focus on CRM experience", "more professional tone"):':'What should the cover letter emphasise? (e.g. "highlight budget management", "reference government experience"):';
|
|
const fb=prompt(pText,defaultFB);
|
|
if(!fb)return;
|
|
const btn=event&&event.target||document.querySelector('[onclick*="'+id+'"]');
|
|
if(btn)btn.textContent='...';
|
|
const r=await f(API.clRegen(id),{method:'POST',body:JSON.stringify({feedback:fb})});
|
|
if(r&&r.ok){ notify('Cover letter '+(hasCL?'regenerated':'generated')+' ✅','ok'); load(); }
|
|
else{ notify('Generation failed: '+(r&&r.error||'unknown error'),'err'); if(btn)btn.textContent=hasCL?'🔄':'+CL'; }
|
|
}
|
|
document.addEventListener('DOMContentLoaded',function(){
|
|
// Theme init
|
|
if(localStorage.getItem('theme')==='light'){ document.documentElement.classList.add('light'); document.getElementById('themeBtn').textContent='🌙'; }
|
|
load(); setInterval(load,120000);
|
|
});
|
|
function toggleTheme(){
|
|
const h=document.documentElement; const b=document.getElementById('themeBtn');
|
|
h.classList.toggle('light');
|
|
const isLight=h.classList.contains('light');
|
|
b.textContent=isLight?'🌙':'☀️';
|
|
localStorage.setItem('theme',isLight?'light':'dark');
|
|
}
|
|
</script>
|
|
<style>
|
|
:root{--bg:#0b1120;--srf:#141d30;--srf2:#1e2a45;--bdr:#1e3050;--txt:#e2e8f0;--m:#64748b;--cy:#22d3ee;--gr:#22c55e;--am:#f59e0b;--rd:#ef4444;--cz:#0b1120;--r-hover:rgba(34,211,238,0.02);--r-td:rgba(30,46,80,0.3)}
|
|
.light{--bg:#f8fafc;--srf:#ffffff;--srf2:#f1f5f9;--bdr:#e2e8f0;--txt:#0f172a;--cy:#0891b2;--cz:#ffffff;--r-hover:rgba(8,145,178,0.04);--r-td:rgba(15,23,42,0.06)}
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--txt);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;transition:background .25s,color .25s}
|
|
.c{max-width:1440px;margin:0 auto;padding:16px}
|
|
.h{display:flex;justify-content:space-between;align-items:center;padding:8px 0 16px;border-bottom:1px solid var(--bdr);margin-bottom:20px;flex-wrap:wrap;gap:8px}
|
|
.h h1{font-size:18px;font-weight:700;color:var(--txt)}
|
|
.h .sub{font-size:12px;color:var(--m);margin-top:2px}
|
|
.h .rh{display:flex;align-items:center;gap:10px;font-size:12px;color:var(--m)}
|
|
.h .rh button{background:var(--srf2);color:var(--m);border:1px solid var(--srf2);padding:5px 12px;border-radius:6px;cursor:pointer;font-size:11px;font-weight:500}
|
|
.h .rh button:hover{border-color:var(--cy);color:var(--cy)}
|
|
.n{display:none;padding:10px 14px;border-radius:6px;margin-bottom:14px;font-size:13px}
|
|
.n.ok{background:rgba(34,197,94,0.12);color:var(--gr);border:1px solid rgba(34,197,94,0.2)}
|
|
.n.err{background:rgba(239,68,68,0.12);color:var(--rd);border:1px solid rgba(239,68,68,0.2)}
|
|
.k{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin-bottom:18px}
|
|
.kc{background:var(--srf);border-radius:8px;padding:14px;border:1px solid var(--bdr)}
|
|
.kc .kv{font-size:26px;font-weight:700;letter-spacing:-0.02em;line-height:1.1}
|
|
.kc .kl{font-size:11px;color:var(--m);margin-top:3px;font-weight:500}
|
|
.kc.cy .kv{color:var(--cy)} .kc.gr .kv{color:var(--gr)} .kc.am .kv{color:var(--am)} .kc.rd .kv{color:var(--rd)}
|
|
.fu{background:var(--srf);border-radius:8px;padding:16px;margin-bottom:18px;border:1px solid var(--bdr)}
|
|
.fi{margin-bottom:5px} .fl{display:flex;justify-content:space-between;font-size:12px;margin-bottom:3px} .fc{color:var(--m)}
|
|
.ftr{height:5px;background:var(--srf2);border-radius:3px;overflow:hidden} .ff{height:100%;border-radius:3px}
|
|
.bulk{display:none;align-items:center;gap:10px;padding:8px 14px;background:rgba(34,211,238,0.06);border:1px solid rgba(34,211,238,0.15);border-radius:6px;margin-bottom:12px;font-size:12px}
|
|
.bulk select{background:var(--srf2);color:var(--txt);border:1px solid var(--bdr);padding:4px 8px;border-radius:4px;font-size:12px}
|
|
.bulk .b-btn{background:var(--cy);color:var(--cz);border:none;padding:5px 12px;border-radius:4px;cursor:pointer;font-size:11px;font-weight:600}
|
|
.bulk .b-btn2{background:var(--srf2);color:var(--m);border:1px solid var(--bdr);padding:5px 12px;border-radius:4px;cursor:pointer;font-size:11px}
|
|
.ct{display:flex;gap:10px;margin-bottom:14px;flex-wrap:wrap;align-items:center}
|
|
.tabs{display:flex;gap:3px;flex-wrap:wrap}
|
|
.tab{padding:5px 14px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:500;background:var(--srf);color:var(--m);border:1px solid transparent}
|
|
.tab:hover{color:var(--txt)} .tab.a{background:var(--cy);color:var(--cz)}
|
|
.sbox{background:var(--srf);border:1px solid var(--bdr);padding:5px 10px;border-radius:6px;color:var(--txt);font-size:12px;width:180px}
|
|
.sbox:focus{outline:none;border-color:var(--cy)}
|
|
.sg{display:flex;gap:2px;margin-left:auto}
|
|
.sort-btn{padding:4px 10px;border-radius:4px;cursor:pointer;font-size:11px;color:var(--m);border:1px solid transparent}
|
|
.sort-btn:hover{color:var(--txt)} .sort-btn.a{border-color:var(--cy);color:var(--cy)}
|
|
.tw{background:var(--srf);border-radius:8px;overflow:hidden;border:1px solid var(--bdr)}
|
|
.th{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;border-bottom:1px solid var(--bdr)}
|
|
.th h2{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--m)}
|
|
.th .rc{color:var(--m);font-size:11px}
|
|
.ts{overflow-x:auto}
|
|
table{width:100%;border-collapse:collapse;font-size:12px}
|
|
th{text-align:left;padding:8px 10px;font-weight:600;color:var(--m);font-size:10px;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--bdr);position:sticky;top:0;background:var(--srf);white-space:nowrap}
|
|
td{padding:6px 10px;border-bottom:1px solid var(--r-td);vertical-align:middle}
|
|
tr:hover td{background:var(--r-hover)}
|
|
.stale-14 td{opacity:.65;color:var(--rd)} .stale-7 td{opacity:.8;color:var(--am)}
|
|
/* Closing date urgency */
|
|
.cls-past td{opacity:0.4}
|
|
.cls-urgent .c-cls{color:var(--rd);font-weight:700}
|
|
.cls-warn .c-cls{color:var(--am)}
|
|
.c-chk{width:28px} .c-co{min-width:160px} .c-co a{color:var(--cy);text-decoration:none;font-weight:500}
|
|
.c-co a:hover{text-decoration:underline} .c-ro{min-width:170px;color:var(--m)} .c-lo{min-width:100px;color:var(--m)}
|
|
.c-sr{min-width:65px;color:var(--m)} .c-sa{text-align:right;white-space:nowrap} .c-cls{text-align:right;white-space:nowrap;font-size:11px}
|
|
.c-ag{text-align:right;white-space:nowrap;color:var(--m)}
|
|
.c-no{width:30px;text-align:center}
|
|
.c-cl{width:55px;text-align:center;white-space:nowrap}
|
|
.cl-link{text-decoration:none;font-size:15px;cursor:pointer;margin-right:4px}
|
|
.cl-link:hover{filter:brightness(1.3)}
|
|
.cl-rg{background:none;border:none;color:var(--m);cursor:pointer;font-size:11px;padding:2px 5px;border-radius:4px}
|
|
.cl-rg:hover{background:var(--srf2);color:var(--cy)}
|
|
.stat-pill{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:500;cursor:pointer;border:1px solid transparent}
|
|
.stat-pill:hover{filter:brightness(1.2)}
|
|
.nb{background:none;border:none;color:var(--m);cursor:pointer;font-size:13px;padding:2px 4px;border-radius:3px;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center}
|
|
.nb:hover{background:var(--srf2);color:var(--txt)}
|
|
.badge{display:inline-block;font-size:9px;padding:1px 5px;border-radius:3px;font-weight:600;margin-left:4px}
|
|
.badge-sm{background:rgba(34,211,238,0.12);color:var(--cy)}
|
|
.sm{display:none;position:fixed;z-index:100;background:var(--srf);border:1px solid var(--bdr);border-radius:6px;padding:3px;box-shadow:0 8px 16px rgba(0,0,0,.3);min-width:140px}
|
|
.sm-item{padding:6px 10px;border-radius:4px;cursor:pointer;font-size:12px}
|
|
.sm-item:hover{background:var(--srf2)} .sm-cur{background:var(--cy);color:var(--cz);font-weight:600}
|
|
.ft{text-align:center;padding:20px 0 12px;color:var(--m);font-size:10px} .ft a{color:var(--cy);text-decoration:none}
|
|
@media(max-width:768px){.c{padding:10px}.k{grid-template-columns:repeat(2,1fr)}.sbox{width:140px}.c-lo,.c-sr,.c-sa{display:none}}
|
|
</style>
|
|
</head>
|
|
<body><div class="c">
|
|
<div class="h"><div><h1>Job Tracker</h1><div class="sub">Anthony Martin · Perth, WA</div></div><div class="rh"><span id="kr">—</span><button onclick="toggleTheme()" id="themeBtn" title="Toggle light/dark mode">☀️</button><button onclick="refresh()">↻ Refresh</button></div></div>
|
|
<div id="n" class="n"></div>
|
|
<div class="k"><div class="kc cy" onclick="setFilter('all')" style="cursor:pointer"><div class="kv" id="kt">—</div><div class="kl">Total Jobs</div></div><div class="kc gr" id="kf-strong" onclick="setFilter('__strong')" style="cursor:pointer"><div class="kv" id="km">—</div><div class="kl">Strong Matches</div></div><div class="kc am"><div class="kv" id="kw">—</div><div class="kl">This Week</div></div><div class="kc rd"><div class="kv" id="ks">—</div><div class="kl">Stale (14d+)</div></div><div class="kc" id="kf-urgent" onclick="setFilter('__urgent')" style="border-color:#f59e0b;cursor:pointer"><div class="kv" id="ku">—</div><div class="kl" style="color:var(--am)">Expiring Soon</div></div><div class="kc" id="kf-past" onclick="setFilter('__past')" style="border-color:#64748b;cursor:pointer"><div class="kv" id="kp">—</div><div class="kl" style="color:var(--m)">Past Closing</div></div></div>
|
|
<div class="fu" id="funnel"></div>
|
|
<div class="bulk" id="bb"><span><strong id="bc">0</strong> selected</span><span>Status:</span><select id="bs"></select><button class="b-btn" onclick="applyBulk()">Apply</button><button class="b-btn2" onclick="desel()">Clear</button></div>
|
|
<div class="ct"><div class="tabs" id="tabs"><span class="tab a" data-f="all" onclick="setFilter('all')">All</span><span class="tab" data-f="Backlog" onclick="setFilter('Backlog')">Backlog</span><span class="tab" data-f="Applied" onclick="setFilter('Applied')">Applied</span><span class="tab" data-f="Interview" onclick="setFilter('Interview')">Interviews</span><span class="tab" data-f="Rejected" onclick="setFilter('Rejected')">Rejected</span></div>
|
|
<input class="sbox" id="s" placeholder="Search..." oninput="doSearch()">
|
|
<div class="sg"><span class="sort-btn a" data-s="date-desc" onclick="setSort('date-desc')">Newest</span><span class="sort-btn" data-s="days-desc" onclick="setSort('days-desc')">Stalest</span><span class="sort-btn" data-s="salary-desc" onclick="setSort('salary-desc')">Salary</span></div></div>
|
|
<div class="tw"><div class="th"><h2>Opportunities</h2><span class="rc" id="rc">—</span></div>
|
|
<div class="ts"><table><thead><tr><th><input type="checkbox" onchange="if(this.checked)selA();else desel();"></th><th>Company</th><th>Role</th><th>Status</th><th>Location</th><th>Source</th><th style="text-align:right">Salary</th><th style="text-align:right">Closing</th><th style="text-align:right">Age</th><th>CL</th><th></th></tr></thead>
|
|
<tbody id="tb"><tr><td colspan="11" style="text-align:center;padding:48px 0;color:var(--m)">Loading...</td></tr></tbody></table></div></div>
|
|
<div class="ft">The Rhino in the Green Jacket · <a href="/api/jobs">API</a></div></div>
|
|
<div id="sm" class="sm"></div>
|
|
<script>
|
|
// Populate bulk status dropdown after STATUSES is defined
|
|
document.getElementById('bs').innerHTML=STATUSES.map(s=>'<option value="'+s+'">'+s+'</option>').join('');
|
|
</script>
|
|
</body></html> |