File size: 7,479 Bytes
e9316e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d9ff790
e9316e9
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
(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();
})();