(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' ? 'EndsIncumbentAgencyNAICSObligatedPotential valueStateSet-aside' : 'Respond byTitleAgencyNAICSTypeSet-asideState'; $('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(); })();