/* ===== admin-report.jsx — Aruanne: the outputs page and the Word export ======

   Modelled on RP's own report, GL26016_01_Tekst.docx, which is the only honest
   specification there is for this. What that document contains splits cleanly in
   two:

     FACTS, which the database holds and this generates - the title page, how
     many holes were drilled and when, which rigs, how many lab samples, the
     per-soil paragraphs (which holes, what thickness, what depth, what colour
     and density), and the water levels.

     JUDGEMENT, which it does not and must not invent - Reljeef, Geoloogiline
     ehitus, the bearing-capacity and founding recommendations, and Tabel 1's
     normative parameters. Those come out as headings with a marked placeholder,
     so the engineer fills them in and nothing reads as if the software decided
     it.

   Writing a plausible-sounding geotechnical conclusion nobody checked would be
   the worst thing this feature could do, so it does not.
--------------------------------------------------------------------------- */

/* "PA 21, 22, 24…30, 44, 61, 67…68" - RP's own way of naming a set of holes,
   read off the report: a run of three or more consecutive numbers collapses to
   a range, anything shorter is listed. Non-numeric ids are listed as they are.

   `conj` puts "ja" before the last item, which is how the sentences read
   ("Puuraukudes nr 38, 54, 60 ja 82"). */
function bhRangeText(ids, opts) {
  opts = opts || {};
  const nums = [];
  const other = [];
  (ids || []).forEach((id) => {
    const m = String(id).match(/^(?:PA|KP|LP)?[\s-]*(\d+)$/i);
    if (m) nums.push(+m[1]); else other.push(String(id));
  });
  nums.sort((a, b) => a - b);

  const parts = [];
  let i = 0;
  while (i < nums.length) {
    let j = i;
    while (j + 1 < nums.length && nums[j + 1] === nums[j] + 1) j++;
    const run = j - i + 1;
    if (run >= 3) { parts.push(nums[i] + "…" + nums[j]); i = j + 1; }
    else { for (let k = i; k <= j; k++) parts.push(String(nums[k])); i = j + 1; }
  }
  const all = parts.concat(other);
  if (!all.length) return "";
  if (!opts.conj || all.length === 1) return all.join(", ");
  return all.slice(0, -1).join(", ") + " ja " + all[all.length - 1];
}

/* Estonian decimals, and a range written the way the reports write it. Under a
   metre the reports use centimetres ("paksusega 2…13 cm"), at a metre and over
   they switch to metres ("paksusega 0,8…0,9 m") - so the unit follows the
   thickest occurrence, not each one. */
function num2(v, dp) {
  return (+v).toFixed(dp === undefined ? 2 : dp).replace(".", ",");
}
function rangeText(minV, maxV, dp) {
  const a = num2(minV, dp), b = num2(maxV, dp);
  return a === b ? a : a + "…" + b;
}
function thicknessPhrase(minM, maxM) {
  if (maxM < 1) return rangeText(minM * 100, maxM * 100, 0) + " cm";
  return rangeText(minM, maxM, 2) + " m";
}

/* The distinct soils in a project, each with where and how it occurs. Keyed by
   the report name - the ISO name when there is one, else the Estonian name -
   because that is the heading each paragraph carries. */
function soilSummary(pid) {
  const holes = (BOREHOLES[pid] || []).slice().sort(bhCompare);
  const by = {};
  holes.forEach((b) => {
    const log = SAMPLE_LOG[b.key];
    if (!log || !log.geos) return;
    log.geos.forEach((g) => {
      const est = (window.layerSoilName ? layerSoilName(g) : "").trim();
      const iso = (window.layerAgsName ? layerAgsName(g) : "").trim();
      /* GROUPED BY THE ISO NAME, not the Estonian one. Typed Estonian names
         differ in case and word order for the same soil - "Väga ühtlane
         PeenLIIV" and "Väga ühtlane peenLIIV", "PeenLIIV orgaanikaga" and
         "Orgaanikaga peenLIIV" - and grouping on those produced 25 paragraphs
         for 21 soils. The ISO symbol IS the identity, and case matters in it:
         FSa and FSaU are different soils under Tabel 2. The heading then uses
         the commonest Estonian spelling. */
      const key = iso || est.toLowerCase();
      if (!key) return;
      const top = parseFloat(g.top), base = parseFloat(g.base);
      if (!isFinite(top) || !isFinite(base)) return;
      const t = base - top;
      const s = by[key] || (by[key] = {
        name: est || iso, iso: iso, names: {}, holes: [], thinnest: Infinity, thickest: 0,
        top: Infinity, base: 0, colours: {}, density: {}, moisture: {},
        organic: [], surface: 0, count: 0,
      });
      if (est) s.names[est] = (s.names[est] || 0) + 1;
      if (s.holes.indexOf(b.id) === -1) s.holes.push(b.id);
      s.thinnest = Math.min(s.thinnest, t);
      s.thickest = Math.max(s.thickest, t);
      s.top = Math.min(s.top, top);
      s.base = Math.max(s.base, base);
      s.count += 1;
      if (top === 0) s.surface += 1;
      /* trimmed: a stored "pruun " printed as "pruun , niiske" */
      const col = (g.colour || "").trim();
      const den = (g.density || "").trim();
      const moi = (g.moisture || "").trim();
      if (col) s.colours[col] = (s.colours[col] || 0) + 1;
      if (den) s.density[den] = (s.density[den] || 0) + 1;
      if (moi) s.moisture[moi] = (s.moisture[moi] || 0) + 1;
      const op = parseFloat(g.organicPct);
      if (isFinite(op)) s.organic.push(op);
      if (!s.iso && iso) s.iso = iso;
    });
  });
  /* shallowest first, which is the order the reports describe them in:
     "kihi kaupa ülevalt alla" */
  Object.keys(by).forEach((k) => {
    const s = by[k];
    const spellings = Object.keys(s.names);
    if (spellings.length) {
      spellings.sort((a, b) => s.names[b] - s.names[a] || a.localeCompare(b, "et"));
      s.name = spellings[0];
    }
  });
  return Object.keys(by).map((k) => by[k]).sort((a, b) => a.top - b.top || a.name.localeCompare(b.name, "et"));
}

/* the commonest value of a tally, when one clearly dominates */
function dominant(tally, share) {
  const keys = Object.keys(tally || {});
  if (!keys.length) return "";
  const total = keys.reduce((n, k) => n + tally[k], 0);
  keys.sort((a, b) => tally[b] - tally[a]);
  return tally[keys[0]] / total >= (share || 0.6) ? keys[0] : "";
}

function reportFacts(pid) {
  const proj = (PROJECTS || []).find((p) => p.id === pid) || {};
  const holes = (BOREHOLES[pid] || []).slice().sort(bhCompare);
  const dates = holes.map((b) => b.date).filter(Boolean).sort();
  const rigs = {};
  const workers = {};
  let sampled = 0, sounded = 0, deepest = 0;
  const water = [];

  holes.forEach((b) => {
    if (b.rig) rigs[b.rig] = (rigs[b.rig] || 0) + 1;
    const log = SAMPLE_LOG[b.key];
    if (!log) return;
    (log.loca && log.loca.fieldWorkers ? log.loca.fieldWorkers : []).forEach((w) => {
      workers[w] = (workers[w] || 0) + 1;
    });
    const samp = (log.samp || []).filter((s) => (s.type || "").trim().toUpperCase() === "K");
    if (samp.length) sampled += samp.length;
    if (window.lpSoundings && lpSoundings(log.sounding).some((x) => x.readings && x.readings.length)) sounded += 1;
    const d = parseFloat(b.depth);
    if (isFinite(d)) deepest = Math.max(deepest, d);
    const v = parseFloat(log.loca && log.loca.veeStrike);
    if (isFinite(v) && v >= 0) water.push({ id: b.id, v: v });
  });

  return {
    proj: proj, holes: holes, dates: dates,
    rigs: Object.keys(rigs).sort((a, b) => rigs[b] - rigs[a]),
    workers: Object.keys(workers).sort((a, b) => workers[b] - workers[a]),
    sampled: sampled, sounded: sounded, deepest: deepest, water: water,
    soils: soilSummary(pid),
  };
}

const REPORT_TODO = "‹ täiendab geoloogiainsener ›";

/* One soil's paragraph: the bold name, then the factual sentence. Shared by the
   Word export and the on-screen check list so they cannot drift. */
function soilSentence(s) {
  let txt = " – ";
  txt += s.surface === s.count ? "esineb pindmise kihina "
    : s.surface > 0 ? "esineb nii pindmise kui sügavama kihina "
      : "esineb ";
  /* "puuraugus nr 41", not "puuraukudes nr 41" - one hole takes the singular */
  txt += (s.holes.length === 1 ? "puuraugus nr " : "puuraukudes nr ")
    + bhRangeText(s.holes, { conj: true });
  txt += ", paksusega " + thicknessPhrase(s.thinnest, s.thickest);
  if (s.top > 0) txt += ", sügavusel " + rangeText(s.top, s.base) + " m";
  txt += ".";
  const col = dominant(s.colours), den = dominant(s.density), moi = dominant(s.moisture);
  const bits = [];
  if (col) bits.push("värvuselt " + col.toLowerCase());
  if (den) bits.push(den.toLowerCase());
  if (moi) bits.push(moi.toLowerCase());
  if (bits.length) txt += " Pinnas on valdavalt " + bits.join(", ") + ".";
  if (s.organic.length) {
    const lo = Math.min.apply(null, s.organic), hi = Math.max.apply(null, s.organic);
    txt += " Laboris määratud orgaanika sisaldus " + rangeText(lo, hi, 1) + "%.";
  }
  return txt;
}

function soilHeading(s) {
  return s.name + (s.iso && s.iso !== s.name ? " (" + s.iso + ")" : "");
}

/* The title page splits `location` on the first comma: "Harju maakond, Anija
   vald" fills both lines. One field, because that is what the project form has,
   and two columns that could disagree with it would be worse. */
function splitLocation(loc) {
  const i = String(loc || "").indexOf(",");
  if (i === -1) return { county: String(loc || "").trim(), muni: "" };
  return { county: loc.slice(0, i).trim(), muni: loc.slice(i + 1).trim() };
}

/* ---------------------------------------------------------------------------
   The Word export. Patches RP's own template; see the file header.
--------------------------------------------------------------------------- */
function exportReportDocx(pid) {
  const T = window.DocxTemplate;
  if (!T) return Promise.reject(new Error("docx-template.js ei ole laetud"));
  const f = reportFacts(pid);
  const p = f.proj;
  if (!f.holes.length) return Promise.reject(new Error("Projektil ei ole puurauke"));
  if (!f.soils.length) return Promise.reject(new Error("Projektil ei ole kihiandmeid"));

  const code = (p.code || "") + (p.part ? "-" + p.part : "");
  const loc = splitLocation(p.location);

  return RPStore.reportTemplate().then((buf) => {
    const entries = T.readZip(buf);
    const need = ["word/document.xml", "word/header1.xml", "word/header2.xml", "word/footer1.xml"];
    const byName = {};
    entries.forEach((e) => { byName[e.name] = e; });
    if (!byName["word/document.xml"]) throw new Error("Mall ei ole korrektne .docx");

    return Promise.all(need.filter((n) => byName[n]).map((n) =>
      T.inflate(byName[n]).then((u8) => ({ name: n, xml: new TextDecoder("utf-8").decode(u8) }))
    )).then((parts) => {
      const xmlOf = {};
      parts.forEach((x) => { xmlOf[x.name] = x.xml; });
      let doc = xmlOf["word/document.xml"];
      const missed = [];

      /* --- the title page. Twice each: mc:Choice and mc:Fallback. --- */
      const title = [
        ["GLxxxx", "Töö nr " + code],
        ["XXX maakond", loc.county],
        ["XXX vald", loc.muni],
        ["Tellija nimi", p.client || ""],
        ["Vastutav töötäitja: XXXX", "Vastutav töötäitja: " + (p.engineer || p.lead || "")],
        ["Eelprojekt / PÕhiprojekt", p.designStage || ""],
        ["TÖÖ / PROJEKTI NIMETUS", p.name || ""],
      ];
      title.forEach(([needle, value]) => {
        if (!value) return;                 /* nothing known - leave the placeholder */
        const r = T.replaceWhere(doc, needle, [{ t: value }], { leaf: true });
        doc = r.xml;
        if (!r.count) missed.push(needle);
      });

      /* --- what the field work was, from the register --- */
      const dateSpan = f.dates.length
        ? (f.dates[0] === f.dates[f.dates.length - 1]
          ? etDate(f.dates[0])
          : etDate(f.dates[0]) + "–" + etDate(f.dates[f.dates.length - 1]))
        : "";
      const kinds = {};
      f.holes.forEach((b) => { kinds[b.type || "PA"] = (kinds[b.type || "PA"] || 0) + 1; });
      let fw = "Geotehnilise uuringu välitöö toimus " + (dateSpan || REPORT_TODO)
        + ". Rajati kokku " + f.holes.length + " uuringupunkti ("
        + Object.keys(kinds).sort().map((k) => kinds[k] + " " + k).join(", ")
        + "), suurim uuritud sügavus " + num2(f.deepest) + " m.";
      if (f.rigs.length) fw += " Uuringu teostamiseks kasutati puuragregaate " + f.rigs.join(", ") + ".";
      const rFw = T.replaceWhere(doc, "Geotehnilise uuringu välitöö toimus", [{ t: fw }], { depth: 0 });
      doc = rFw.xml;
      if (!rFw.count) missed.push("välitöö kirjeldus");

      if (f.sampled) {
        const rLab = T.replaceWhere(doc, "pinnaseproovi", [{
          t: "Puuraukudest võeti " + f.sampled + " pinnaseproovi. " + REPORT_TODO
            + " Labor, katsemetoodika ja standardid.",
        }], { depth: 0 });
        doc = rLab.xml;
      }

      /* --- EVERY layer from the geology profiles, in the text ---
         "every geological layer that has been input in the geology profiles
         needs to be presented in the text as well" (Christopher, 02.09). The
         template's example layers are replaced, not appended to. */
      const blocks = f.soils.map((s) => [
        { t: soilHeading(s), b: true },
        { t: soilSentence(s) },
      ]);
      const rB = T.replaceBlock(doc, "Järgnevalt on iseloomustatud",
        "Hüdrogeoloogilised", blocks);
      doc = rB.xml;

      /* --- water --- */
      if (f.water.length) {
        const vs = f.water.map((w) => w.v);
        const lo = Math.min.apply(null, vs), hi = Math.max.apply(null, vs);
        const rW = T.replaceWhere(doc, "Vett esines", [{
          t: "Vett esines välitöö käigus " + f.water.length + " puuraugus, kus see asus"
            + " maapinnast " + rangeText(lo, hi) + " meetri sügavusel.",
        }], { depth: 0 });
        doc = rW.xml;
      }

      /* --- the running header names the project on EVERY page ---
         The template's still says "Kalakasvatus;Saarepeedi vald", and so does
         RP's delivered GL26016 report - nobody updated it by hand. */
      const edits = { "word/document.xml": doc };
      ["word/header1.xml", "word/header2.xml"].forEach((n) => {
        if (!xmlOf[n] || !p.name) return;
        let h = xmlOf[n];
        T.scanParas(h).filter((q) => q.leaf && T.textOf(h.slice(q.a, q.b)).trim())
          .reverse()
          .forEach((q) => {
            h = h.slice(0, q.a) + T.setRuns(h.slice(q.a, q.b), [{ t: p.name }]) + h.slice(q.b);
          });
        edits[n] = h;
      });
      /* the footer's file name is a FILENAME field; set the cached text too so
         it reads right before Word's first refresh */
      if (xmlOf["word/footer1.xml"] && code) {
        edits["word/footer1.xml"] = xmlOf["word/footer1.xml"]
          .split("GLxxxx 01 Tekst.docx").join(code + "_01_Tekst.docx");
      }

      const blob = T.build(entries, edits);
      RPStore.download(code.replace(/[^\wÀ-ž-]+/g, "_") + "_01_Tekst.docx", blob);
      return {
        holes: f.holes.length, soils: f.soils.length,
        replaced: rB.replaced, missed: missed,
      };
    });
  });
}

/* dd.mm.yyyy, as the reports write dates */
function etDate(iso) {
  if (!iso) return "";
  const p = String(iso).slice(0, 10).split("-");
  return p.length === 3 ? p[2] + "." + p[1] + "." + p[0] : String(iso);
}

Object.assign(window, {
  bhRangeText, soilSummary, reportFacts, exportReportDocx, etDate, REPORT_TODO,
});

/* ---------------------------------------------------------------------------
   The Aruanne page. Everything the report needs already exists somewhere in the
   app; what was missing is a place that gathers it, so the four Väljundid
   buttons in Protsess have somewhere real to go.

   Deliberately shows WHAT WILL BE WRITTEN before writing it. A report is signed
   by an engineer, so it matters that they can see the facts the generator found
   - how many holes, which soils, what water - and notice a wrong one before it
   reaches a Word file rather than after.
--------------------------------------------------------------------------- */
function ReportScreen({ canEdit, onToast, projectId }) {
  /* seeded from the site-wide selection, so arriving here keeps the project you
     were already working in rather than resetting to whatever is first */
  const first = (PROJECTS || [])[0];
  const [pid, setPid] = React.useState(
    projectId && projectId !== "all" ? projectId : (first ? first.id : ""));
  React.useEffect(() => {
    if (projectId && projectId !== "all") setPid(projectId);
  }, [projectId]);
  const [perSheet, setPerSheet] = React.useState(10);

  const facts = React.useMemo(() => (pid ? reportFacts(pid) : null), [pid]);
  /* reportFacts already defaults this to {} - taking it from there means one
     source, and no crash while PROJECTS is still loading. A bare `proj.code` on
     an undefined proj is the same class of error that blanked this page.
     Declared AFTER facts: a const is not hoisted, so reading it above would be
     a temporal dead zone. */
  const proj = (facts && facts.proj) || {};
  const holes = facts ? facts.holes : [];
  const withLogs = holes.filter((b) => SAMPLE_LOG[b.key] && (SAMPLE_LOG[b.key].geos || []).length);
  const blockers = pid && window.RPStore && RPStore.agsBlockers ? RPStore.agsBlockers(pid) : [];

  const [busy, setBusy] = React.useState("");
  const [tmpl, setTmpl] = React.useState(undefined);   /* undefined = checking */
  React.useEffect(() => {
    let live = true;
    RPStore.reportTemplateInfo().then((t) => { if (live) setTmpl(t); });
    return () => { live = false; };
  }, []);

  /* AWAITED. The Word export fetches the template from Storage, so it is a
     Promise now; reporting success before it resolves would put "laaditud" over
     a failure - the same blindness that hid the Asfalt bug for three rounds. */
  const run = (label, fn) => {
    setBusy(label);
    Promise.resolve()
      .then(fn)
      .then((r) => {
        let msg = label + " laaditud";
        if (r && r.holes) msg += " · " + r.holes + " puurauku";
        if (r && r.soils) msg += " · " + r.soils + " pinnast";
        onToast && onToast(msg);
        if (r && r.missed && r.missed.length) {
          onToast && onToast("Mallis jäid asendamata: " + r.missed.join(", ")
            + " — kontrolli malli", true);
        }
      })
      .catch((e) => onToast && onToast(e && e.message ? e.message : String(e), true))
      .then(() => setBusy(""));
  };

  const pickTemplate = () => {
    const inp = document.createElement("input");
    inp.type = "file";
    inp.accept = ".docx";
    inp.onchange = () => {
      const file = inp.files && inp.files[0];
      if (!file) return;
      setBusy("Mall");
      RPStore.uploadReportTemplate(file)
        .then(() => RPStore.reportTemplateInfo())
        .then((t) => { setTmpl(t); onToast && onToast("Aruande mall uuendatud"); })
        .catch((e) => onToast && onToast(e && e.message ? e.message : String(e), true))
        .then(() => setBusy(""));
    };
    inp.click();
  };

  return (
    <div className="page-inner fade-in">
      <div className="page-head">
        <div>
          <h1 className="page-title">Aruanne</h1>
          <p className="page-sub">Väljundid ühest kohast · faktid andmebaasist, järeldused insenerilt</p>
        </div>
      </div>

      <div className="fence-bar">
        <ProjectPicker value={pid} onChange={setPid} />
        <span className="fence-bar-gap"></span>
        {holes.length > 0 && (
          <span className="badge">{withLogs.length}/{holes.length} puurauku kihiandmetega</span>
        )}
      </div>

      {!pid && <Panel title="Vali projekt"><p className="muted">Aruande koostamiseks vali projekt.</p></Panel>}

      {pid && (
        <div className="grid-2-1">
          <Panel title="Mida aruandesse kirjutatakse" sub="Kontrolli enne eksporti">
            {!facts || !holes.length ? (
              <p className="muted">Projektil ei ole veel puurauke.</p>
            ) : (
              <div className="rep-facts">
                <div className="rep-row"><span>Töö nr</span><b>{(proj.code || "") + (proj.part ? "-" + proj.part : "")}</b></div>
                <div className="rep-row"><span>Staadium</span>
                  <b>{proj.designStage || <span className="muted">määramata</span>}</b></div>
                <div className="rep-row"><span>Asukoht</span>
                  <b>{proj.location || <span className="muted">määramata</span>}</b></div>
                <div className="rep-row"><span>Uuringupunktid</span><b>{holes.length}</b></div>
                <div className="rep-row"><span>Välitöö</span><b>{facts.dates.length
                  ? etDate(facts.dates[0]) + (facts.dates.length > 1 ? "–" + etDate(facts.dates[facts.dates.length - 1]) : "")
                  : "—"}</b></div>
                <div className="rep-row"><span>Suurim sügavus</span><b>{num2(facts.deepest)} m</b></div>
                <div className="rep-row"><span>Puuragregaadid</span><b>{facts.rigs.join(", ") || "—"}</b></div>
                <div className="rep-row"><span>Laboriproovid</span><b>{facts.sampled || "—"}</b></div>
                <div className="rep-row"><span>Penetratsioonikatsed</span><b>{facts.sounded || "—"}</b></div>
                <div className="rep-row"><span>Vett esines</span><b>{facts.water.length
                  ? facts.water.length + " augus" : "ei esinenud"}</b></div>
                <div className="rep-sep">Eraldatud pinnased ({facts.soils.length})</div>
                {facts.soils.map((s) => (
                  <div className="rep-soil" key={s.name}>
                    <b>{s.name}</b>
                    <span className="mono">
                      {" nr " + bhRangeText(s.holes) + " · " + thicknessPhrase(s.thinnest, s.thickest)}
                    </span>
                  </div>
                ))}
                {!facts.soils.length && <p className="muted">Kihiandmeid ei ole.</p>}
              </div>
            )}
          </Panel>

          <Panel title="Väljundid" sub="Aruanne ja lisad">
            <div className="outputs">
              <button type="button" className="output"
                disabled={!withLogs.length || !!busy || tmpl === null}
                onClick={() => run("Aruanne", () => exportReportDocx(pid))}>
                <span className="output-ico"><Icon name="table" size={17} /></span>
                <div>
                  <div className="output-t">Aruanne (Word)</div>
                  <div className="output-s mono">
                    {tmpl === undefined ? "malli kontrollin…"
                      : tmpl === null ? "mall puudub — laadi ülesse"
                        : "RP mall · " + (facts ? facts.soils.length : 0) + " kihikirjeldust"}
                  </div>
                </div>
                <Icon name="download" size={15} />
              </button>

              <button type="button" className="output" disabled={!withLogs.length || !!busy}
                onClick={() => run("Läbilõiked", () => {
                  exportAnalysisXls(pid, withLogs.map((b) => b.key), { perSheet: perSheet });
                  return { holes: withLogs.length };
                })}>
                <span className="output-ico"><Icon name="activity" size={17} /></span>
                <div>
                  <div className="output-t">Geoloogilised profiilid (Excel)</div>
                  <div className="output-s mono">{perSheet} puurauku lehel</div>
                </div>
                <Icon name="download" size={15} />
              </button>

              <button type="button" className="output" disabled={!withLogs.length || !!busy}
                onClick={() => run("AGS4", () => { RPStore.exportAGS4(pid); return {}; })}>
                <span className="output-ico"><Icon name="cloud" size={17} /></span>
                <div>
                  <div className="output-t">AGS4 andmepakett</div>
                  <div className="output-s mono">
                    {blockers.length ? blockers.length + " kihil puudub ISO nimi" : "kõik kihid nimetatud"}
                  </div>
                </div>
                <Icon name="download" size={15} />
              </button>
            </div>

            {/* The template is the format. It lives in a private bucket so it
                reaches signed-in staff only, and so it can be replaced here
                without a deploy when RP change the report. */}
            <div className="rep-tmpl">
              <div>
                <div className="output-t">Aruande mall</div>
                <div className="output-s mono">
                  {tmpl === undefined ? "kontrollin…"
                    : tmpl === null ? "puudub — aruannet ei saa koostada"
                      : RPStore.REPORT_TEMPLATE_PATH
                        + (tmpl.updated_at ? " · " + etDate(tmpl.updated_at) : "")}
                </div>
              </div>
              {canEdit && (
                <button type="button" className="btn btn-ghost btn-sm"
                  disabled={!!busy} onClick={pickTemplate}>
                  <Icon name="upload" size={14} /> {tmpl ? "Asenda" : "Laadi ülesse"}
                </button>
              )}
            </div>

            <label className="fence-per mono" style={{ marginTop: 12 }}
              title="Mitu puurauku ühele Exceli lehele">
              <span>profiilid: lehel</span>
              <select className="select" value={perSheet} onChange={(e) => setPerSheet(+e.target.value)}>
                {Array.from({ length: 15 }, (_, i) => i + 1).map((n) => <option key={n} value={n}>{n}</option>)}
              </select>
            </label>

            {/* Said plainly, because a report that quietly invented its own
                conclusions would be worse than one with gaps. */}
            <p className="muted" style={{ marginTop: 14, lineHeight: 1.5 }}>
              Word-fail sisaldab tiitellehe, koosseisu, teostatud tööde kirjelduse,
              pinnaste kihikirjeldused ja veetasemed — kõik andmebaasist.
              Reljeef, geoloogiline ehitus, geotehnilised tingimused ja tabel 1 on
              märgitud <b>{REPORT_TODO}</b> ja need täidab geoloogiainsener.
              Asendiplaan ja läbilõigete joonised tehakse CAD-is.
            </p>
          </Panel>
        </div>
      )}
    </div>
  );
}

Object.assign(window, { ReportScreen });
