(async () => {
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
const fmt = (n) => (n == null ? '' : Number(n).toLocaleString('en-US'));
const usd = (n) => (n == null ? '' : n >= 1e9 ? `$${(n / 1e9).toFixed(2)}B` : n >= 1e6 ? `$${(n / 1e6).toFixed(1)}M` : `$${fmt(Math.round(n))}`);
let d;
try { d = await (await fetch('data.json')).json(); } catch (e) { $('count').textContent = 'Could not load data.'; return; }
const idx = (cols) => Object.fromEntries(cols.map((c, i) => [c, i]));
// decode the compact payload: dictionary-encoded columns and URL suffixes (see build.py compact())
for (const t of [d.contracts, d.opps]) {
const di = Object.entries(t.dicts || {}).map(([c, vals]) => [t.cols.indexOf(c), vals]);
const ui = Object.keys(d.prefix || {}).map((c) => [t.cols.indexOf(c), d.prefix[c], c === 'sam_url' ? '/view' : '/']).filter((x) => x[0] > -1);
for (const r of t.rows) {
for (const [i, vals] of di) if (r[i] != null) r[i] = vals[r[i]];
for (const [i, pre, suf] of ui) if (r[i]) r[i] = pre + r[i] + suf;
}
}
const C = idx(d.contracts.cols), O = idx(d.opps.cols);
const CR = d.contracts.rows, OR = d.opps.rows;
$('built').textContent = `Built ${d.built.slice(0, 10)} from USAspending.gov and SAM.gov.`;
const total = CR.reduce((a, r) => a + (r[C.obligated] || 0), 0);
$('stats').innerHTML = [
[fmt(CR.length), 'contracts ≥ $1M ending in 12 months'],
[usd(total), 'obligated on them so far'],
[fmt(CR.filter((r) => r[C.days_to_end] <= 90).length), 'ending within 90 days'],
[fmt(OR.length), 'open bids on SAM.gov'],
].map(([b, s]) => `
${b}${s}
`).join('');
let mode = 'c', page = 0, cur = [];
const PER = 50;
const p = new URLSearchParams(location.search);
if (p.get('tab') === 'open') mode = 'o';
function fillSelects() {
const rows = mode === 'c' ? CR : OR, ai = mode === 'c' ? C.agency : O.department, si = mode === 'c' ? C.place_state : O.place_state;
const reset = (sel, label, vals) => { sel.innerHTML = ''; sel.add(new Option(label, '')); for (const v of vals) sel.add(new Option(v, v)); };
const count = (i) => { const m = new Map(); for (const r of rows) if (r[i]) m.set(r[i], (m.get(r[i]) || 0) + 1); return [...m.entries()].sort((a, b) => b[1] - a[1]).map((x) => x[0]); };
reset($('agency'), mode === 'c' ? 'All agencies' : 'All departments', count(ai));
reset($('state'), 'All states', count(si).sort());
$('when').parentElement && ($('when').style.display = mode === 'c' ? '' : 'none');
$('q').placeholder = mode === 'c' ? 'Search incumbent, agency, NAICS title, PIID' : 'Search title, agency, solicitation number';
$('t').tHead.innerHTML = mode === 'c'
? '| Ends | Incumbent | Agency | NAICS | Obligated | Potential value | State | Set-aside |
'
: '| Respond by | Title | Agency | NAICS | Type | Set-aside | State |
';
$('tab-c').classList.toggle('on', mode === 'c'); $('tab-o').classList.toggle('on', mode === 'o');
}
function apply() {
const q = $('q').value.trim().toUpperCase(), na = $('naics').value.trim(), ag = $('agency').value, st = $('state').value, w = +$('when').value || 0;
if (mode === 'c') {
cur = CR.filter((r) => (!ag || r[C.agency] === ag) && (!st || r[C.place_state] === st) && (!na || String(r[C.naics] || '').startsWith(na)) && (!w || r[C.days_to_end] <= w)
&& (!q || [r[C.incumbent], r[C.agency], r[C.sub_agency], r[C.naics_title], r[C.piid], r[C.naics]].some((v) => v && String(v).toUpperCase().includes(q))));
} else {
cur = OR.filter((r) => (!ag || r[O.department] === ag) && (!st || r[O.place_state] === st) && (!na || String(r[O.naics] || '').startsWith(na))
&& (!q || [r[O.title], r[O.department], r[O.sub_tier], r[O.solicitation_number], r[O.naics]].some((v) => v && String(v).toUpperCase().includes(q))));
}
page = 0; render();
const u = new URLSearchParams();
if (mode === 'o') u.set('tab', 'open');
for (const k of ['q', 'naics', 'agency', 'state', 'when']) if ($(k).value) u.set(k, $(k).value);
history.replaceState(null, '', u.toString() ? `?${u}` : location.pathname);
}
function render() {
const pages = Math.max(1, Math.ceil(cur.length / PER));
const sum = mode === 'c' ? ` · ${usd(cur.reduce((a, r) => a + (r[C.obligated] || 0), 0))} obligated` : '';
$('count').textContent = `${fmt(cur.length)} ${mode === 'c' ? 'contracts' : 'open notices'} match${sum}.`;
$('page').textContent = `Page ${page + 1} of ${pages}`;
$('prev').disabled = page === 0; $('next').disabled = page >= pages - 1;
const slice = cur.slice(page * PER, page * PER + PER);
$('t').tBodies[0].innerHTML = mode === 'c' ? slice.map((r) => {
const soon = r[C.days_to_end] <= 90 ? ' soon' : '';
const opt = r[C.options_beyond_current_end] ? ' options' : '';
return `| ${esc(r[C.current_end_date])} ${r[C.days_to_end]} days${opt} | `
+ `${r[C.usaspending_url] ? `${esc(r[C.incumbent])}` : esc(r[C.incumbent])} ${esc(r[C.piid])} | `
+ `${esc(r[C.agency])} ${esc(r[C.sub_agency])} | ${esc(r[C.naics])} ${esc(r[C.naics_title])} | `
+ `${usd(r[C.obligated])} | ${usd(r[C.potential_value])} | ${esc(r[C.place_state])} | ${esc(r[C.set_aside] || '')} |
`;
}).join('') : slice.map((r) => `| ${esc(r[O.response_deadline])} | `
+ `${esc(r[O.title])} ${esc(r[O.solicitation_number] || '')} | `
+ `${esc(r[O.department])} ${esc(r[O.sub_tier] || '')} | ${esc(r[O.naics] || '')} | ${esc(r[O.notice_type])} | ${esc(r[O.set_aside] || '')} | ${esc(r[O.place_state] || '')} |
`).join('');
}
function setMode(m) { mode = m; fillSelects(); apply(); }
for (const k of ['q', 'naics']) $(k).addEventListener('input', apply);
for (const k of ['agency', 'state', 'when']) $(k).addEventListener('change', apply);
$('tab-c').onclick = () => setMode('c');
$('tab-o').onclick = () => setMode('o');
$('prev').onclick = () => { page--; render(); };
$('next').onclick = () => { page++; render(); };
$('csv').onclick = () => {
const cols = mode === 'c' ? d.contracts.cols : d.opps.cols;
const q = (v) => (v == null ? '' : /[",\n]/.test(String(v)) ? `"${String(v).replaceAll('"', '""')}"` : String(v));
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([[cols.join(','), ...cur.map((r) => r.map(q).join(','))].join('\n')], { type: 'text/csv' }));
a.download = `awardtide-${mode === 'c' ? 'contracts-ending' : 'open-bids'}-${d.built.slice(0, 10)}.csv`;
a.click();
};
fillSelects();
for (const k of ['q', 'naics', 'agency', 'state', 'when']) if (p.get(k)) $(k).value = p.get(k);
apply();
})();