cybermax / bidscope /app.js
CyberMax-tools's picture
Deploy bidscope to the hub
d9ff790 verified
Raw History Blame Contribute Delete
7.48 kB
(async () => {
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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]) => `<div class="stat"><b>${b}</b><span>${s}</span></div>`).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'
? '<tr><th>Ends</th><th>Incumbent</th><th>Agency</th><th>NAICS</th><th>Obligated</th><th>Potential value</th><th>State</th><th>Set-aside</th></tr>'
: '<tr><th>Respond by</th><th>Title</th><th>Agency</th><th>NAICS</th><th>Type</th><th>Set-aside</th><th>State</th></tr>';
$('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] ? ' <span class="pill" title="Potential end date is later: the agency may exercise options instead of re-competing">options</span>' : '';
return `<tr><td><span class="pill${soon}">${esc(r[C.current_end_date])}</span><div class="sub">${r[C.days_to_end]} days${opt}</div></td>`
+ `<td>${r[C.usaspending_url] ? `<a href="${esc(r[C.usaspending_url])}" target="_blank" rel="noopener">${esc(r[C.incumbent])}</a>` : esc(r[C.incumbent])}<div class="sub">${esc(r[C.piid])}</div></td>`
+ `<td>${esc(r[C.agency])}<div class="sub">${esc(r[C.sub_agency])}</div></td><td>${esc(r[C.naics])}<div class="sub">${esc(r[C.naics_title])}</div></td>`
+ `<td class="num">${usd(r[C.obligated])}</td><td class="num">${usd(r[C.potential_value])}</td><td>${esc(r[C.place_state])}</td><td>${esc(r[C.set_aside] || '')}</td></tr>`;
}).join('') : slice.map((r) => `<tr><td>${esc(r[O.response_deadline])}</td>`
+ `<td><a href="${esc(r[O.sam_url])}" target="_blank" rel="noopener">${esc(r[O.title])}</a><div class="sub">${esc(r[O.solicitation_number] || '')}</div></td>`
+ `<td>${esc(r[O.department])}<div class="sub">${esc(r[O.sub_tier] || '')}</div></td><td>${esc(r[O.naics] || '')}</td><td>${esc(r[O.notice_type])}</td><td>${esc(r[O.set_aside] || '')}</td><td>${esc(r[O.place_state] || '')}</td></tr>`).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();
})();