/* =====================================================================
   AI-COS Essential — production work modules (loads after the legacy
   demo files and overrides them):
   - JobsModule        calibration job lifecycle (M3)
   - Worksheet         per-instrument calibration worksheet per ADAM form
   - CertsModule       certificate register + ADAM printable cert (M4)
   - QMSModule         NCR & CAPA register with lifecycle (M5)
   - DocsModule        controlled-document register (M5)
   - ReportsModule     live reporting + CSV export (M6)
   ===================================================================== */

/* ---- calibration reference values per gas (span/alarms/range per ADAM sheet) ---- */
const GAS_CAL = {
  'H₂S': { span: 25, unit: 'ppm', zeroTol: 2, tolPct: 10, low: 10, high: 15, range: '0–100 ppm' },
  'CO':  { span: 100, unit: 'ppm', zeroTol: 2, tolPct: 10, low: 35, high: 100, range: '0–500 ppm' },
  'O₂':  { span: 18.0, unit: '%vol', fresh: 20.9, absTol: 0.5, low: 19.5, high: 23.5, range: '0–30 %vol' },
  'LEL': { span: 50, unit: '%LEL', zeroTol: 2, tolPct: 10, low: 10, high: 20, range: '0–100 %LEL' },
  'SO₂': { span: 10, unit: 'ppm', zeroTol: 2, tolPct: 10, low: 2, high: 5, range: '0–20 ppm' },
};

/* =====================================================================
   Intelligence layer — computed audit readiness + step-by-step guidance
   (deterministic, from live laboratory data — no canned numbers)
   ===================================================================== */
function computeReadiness() {
  const DB = window.DB;
  const act = DB.monitors.filter((m) => m.status !== 'Retired');
  const st = (s) => act.filter((m) => (m.status === 'Active' ? 'Valid' : m.status) === s).length;
  const overdue = st('Overdue');
  const failedRepair = st('Failed') + st('Under repair');
  const openCapa = DB.capa.filter((x) => x.status !== 'Closed');
  const majorOpen = openCapa.filter((x) => x.severity === 'Major').length;
  const capaOverdue = openCapa.filter((x) => new Date(x.due) < DB.TODAY).length;
  const badCyls = DB.cylinders.filter((c) => c.status === 'Expired' || c.status === 'Low pressure' || (c.exp && new Date(c.exp) < DB.TODAY)).length;
  const docsDue = DB.docReg.filter((d) => d.status === 'Published' && d.review && new Date(d.review) < DB.TODAY).length;
  const docsDraft = DB.docReg.filter((d) => d.status === 'Draft').length;
  const stuckReview = DB.jobs.filter((j) => _jstage(j) === "QA/QC Review").length;
  const factors = [
    { label: 'Overdue calibrations', value: -Math.min(24, overdue), detail: overdue + ' of ' + act.length + ' assets' },
    { label: 'Failed / under repair', value: -Math.min(10, failedRepair * 2), detail: failedRepair + ' instruments' },
    { label: 'Open major NCRs', value: -Math.min(12, majorOpen * 4), detail: majorOpen + ' major' },
    { label: 'Overdue CAPAs', value: -Math.min(9, capaOverdue * 3), detail: capaOverdue + ' past due' },
    { label: 'Reference-gas stock', value: -Math.min(9, badCyls * 3), detail: badCyls + ' cylinders expired/low' },
    { label: 'Document control', value: -Math.min(8, (docsDue + docsDraft) * 2), detail: docsDue + ' past review · ' + docsDraft + ' draft' },
    { label: 'Approvals pending', value: -Math.min(4, stuckReview), detail: stuckReview + ' jobs in review' },
  ];
  const score = Math.max(5, 100 + factors.reduce((a, f) => a + f.value, 0));
  const verdict = score >= 85 ? 'Audit-ready' : score >= 70 ? 'Minor gaps' : 'Action needed';
  return { score, verdict, factors };
}

function calGuide() {
  const DB = window.DB; const g = [];
  const act = DB.monitors.filter((m) => m.status !== 'Retired');
  const overdue = act.filter((m) => (m.status === 'Active' ? 'Valid' : m.status) === 'Overdue');
  const failed = act.filter((m) => m.status === 'Failed');
  // jobs pipeline
  DB.jobs.filter((j) => _jstage(j) === "Received").slice(0, 2).forEach((j) =>
    g.push({ module: 'jobs', level: 'warn', text: j.id + ' received — record the incoming inspection to begin', go: 'jobs' }));
  DB.jobs.filter((j) => _jstage(j) === "Inspection").slice(0, 2).forEach((j) =>
    g.push({ module: 'jobs', level: 'warn', text: j.id + ' inspected — assign a technician', go: 'jobs' }));
  DB.jobs.filter((j) => _jstage(j) === "In Calibration").forEach((j) => {
    const pend = (j.assets || []).length - Object.keys(j.worksheets || {}).length;
    if (pend > 0 && j.assets) g.push({ module: 'jobs', level: 'info', text: j.id + ': ' + pend + ' calibration worksheet(s) still to complete', go: 'jobs' });
  });
  DB.jobs.filter((j) => _jstage(j) === "QA/QC Review").slice(0, 2).forEach((j) =>
    g.push({ module: 'jobs', level: canApprove() ? 'warn' : 'info', text: j.id + ' awaits ' + (canApprove() ? 'your approval — certificates issue automatically' : 'Quality/Admin approval'), go: 'jobs' }));
  DB.jobs.filter((j) => _jstage(j) === "Certified").slice(0, 1).forEach((j) =>
    g.push({ module: 'jobs', level: 'ok', text: j.id + ' approved — dispatch instruments & certificates to the customer', go: 'jobs' }));
  // assets
  if (overdue.length) g.push({ module: 'assets', level: 'bad', text: overdue.length + ' assets overdue for calibration — create a job to bring them in', go: 'jobs' });
  if (failed.length) g.push({ module: 'assets', level: 'bad', text: failed.length + ' failed instrument(s) quarantined — repair and recalibrate', go: 'assets' });
  // reminders
  const S = DB.reminderSettings || { windows: [30], includeOverdue: true };
  const maxW = Math.max.apply(null, S.windows || [30]);
  const dueSet = new Set(act.filter((m) => { const d = Math.ceil((new Date(m.due) - DB.TODAY) / 864e5); return (d < 0 && S.includeOverdue) || (d >= 0 && d <= maxW); }).map((m) => m.customer));
  if (dueSet.size) g.push({ module: 'schedule', level: 'warn', text: 'Calibration reminders pending for ' + dueSet.size + ' customer(s)', go: 'schedule' });
  // quality
  const capaOd = DB.capa.filter((x) => x.status !== 'Closed' && new Date(x.due) < DB.TODAY);
  capaOd.slice(0, 2).forEach((x) => g.push({ module: 'qms', level: 'bad', text: x.id + ' overdue — ' + (x.title || '').slice(0, 50), go: 'qms' }));
  // documents
  const docsDue = DB.docReg.filter((d) => d.status === 'Published' && d.review && (new Date(d.review) - DB.TODAY) / 864e5 < 90);
  if (docsDue.length) g.push({ module: 'documents', level: 'info', text: docsDue.length + ' controlled document(s) due for review within 90 days', go: 'documents' });
  const drafts = DB.docReg.filter((d) => d.status === 'Draft');
  if (drafts.length) g.push({ module: 'documents', level: 'info', text: drafts.length + ' draft document(s) awaiting publication', go: 'documents' });
  if (!g.length) g.push({ module: 'dashboard', level: 'ok', text: 'All processes on track — nothing needs your attention', go: 'dashboard' });
  const rank = { bad: 0, warn: 1, info: 2, ok: 3 };
  return g.sort((a, b) => rank[a.level] - rank[b.level]);
}

const GUIDE_TONE = { bad: ['var(--bad)', 'var(--bad-bg)', 'alert'], warn: ['var(--warn)', 'var(--warn-bg)', 'clock'], info: ['var(--primary-600)', 'var(--info-bg)', 'flag'], ok: ['var(--ok)', 'var(--ok-bg)', 'check-circle'] };

function GuidePanel({ onNav, limit = 6 }) {
  const items = calGuide().slice(0, limit);
  return (
    <div className="card">
      <div className="card-head"><Icon name="shield" size={16} style={{ color: 'var(--primary-600)' }} /><div className="h-sec" style={{ fontSize: 14 }}>Guided actions</div><span className="spacer" /><Badge kind="info">{items.length}</Badge></div>
      <div className="card-body" style={{ display: 'flex', flexDirection: 'column', gap: 8, paddingTop: 8 }}>
        {items.map((it, i) => {
          const t = GUIDE_TONE[it.level];
          return (
            <div key={i} className="row" style={{ gap: 10, padding: '8px 10px', borderRadius: 10, background: t[1], alignItems: 'flex-start' }}>
              <span style={{ color: t[0], flex: 'none', marginTop: 1 }}><Icon name={t[2]} size={15} /></span>
              <span style={{ flex: 1, fontSize: 12.5, lineHeight: 1.4 }}>{it.text}</span>
              {it.go && it.go !== 'dashboard' && <Btn size="sm" variant="ghost" iconR="arrowR" onClick={() => onNav(it.go)}>Go</Btn>}
            </div>
          );
        })}
      </div>
    </div>
  );
}

function ModuleGuide({ module, onNav }) {
  const items = calGuide().filter((it) => it.module === module).slice(0, 2);
  if (!items.length) return null;
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 14 }}>
      {items.map((it, i) => {
        const t = GUIDE_TONE[it.level];
        return (
          <div key={i} className="row" style={{ gap: 10, padding: '10px 14px', borderRadius: 11, background: t[1], border: '1px solid ' + t[0].replace(')', ',.25)').replace('var(--bad)', 'var(--bad-line)').replace('var(--warn)', 'var(--warn-line)') }}>
            <span style={{ color: t[0], flex: 'none' }}><Icon name={t[2]} size={15} /></span>
            <span style={{ flex: 1, fontSize: 12.5, fontWeight: 600 }}>{it.text}</span>
          </div>
        );
      })}
    </div>
  );
}

const STAGE_HINTS = {
  'Received': 'Next: record the incoming inspection — check every instrument for physical damage before assigning (notify the customer and offer return if damage is found).',
  'Inspection': 'Next: inspection recorded — assign a technician (handling of calibration items — QMS §7.4).',
  'Assigned': 'Next: the technician begins calibration — the job and its instruments move to In Calibration.',
  'In Calibration': 'Next: complete the ADAM calibration worksheet per instrument — Fresh Air Zero (±2 ppm), Span Check (±10%), Alarm Test Low/High and the five functional checks.',
  'QA/QC Review': 'Next: QA/QC reviews the worksheets (Reviewed By — QA/QC), then the Technical Manager approves — certificates issue automatically for passing instruments (QMS §7.8).',
  'Certified': 'Next: certificates are approved by the Technical Manager — notify the customer that the instruments are ready for collection, then dispatch.',
  'Dispatched': 'Job complete — certificates are in the register and each asset shows its next calibration due date.',
};
/* Job process strictly per the customer documents (Master Dashboard workflow):
   Received (receive instrument — confirmation by mail) -> Inspection (physical
   damage check; notify customer / return if requested) -> Assigned -> In
   Calibration -> QA/QC Review (QMS 7.8) -> Certified (Approved By Technical
   Manager) -> Dispatched (ready for collection — customer notified). */
const JOB_STAGES = ["Received", "Inspection", "Assigned", "In Calibration", "QA/QC Review", "Certified", "Dispatched"];
const _jstage = (j) => ({ "In progress": "In Calibration", "Review": "QA/QC Review", "Approved": "Certified" }[j.stage] || j.stage);
const _dstr = (d) => { if (!d) return ''; const x = new Date(d); return isNaN(x) ? '' : x.toISOString().slice(0, 10); };
const canApprove = () => window.CAL && CAL.user && ['Admin', 'Quality', 'Super Admin'].includes(CAL.user.role);
/* Staff = the real named users (mirrored from cal_users at boot). These
   helpers keep every form usable regardless of how many users exist. */
const _staffOpts = () => {
  const DB = window.DB;
  if (DB.staff && DB.staff.length) return DB.staff.map((s) => s.name);
  return [window.CAL && CAL.user ? CAL.user.name : '—'];
};
const _staffId = (name) => {
  const s = (window.DB.staff || []).find((x) => x.name === name);
  return s ? s.id : (window.CAL && CAL.user ? CAL.user.id : null);
};
const _staffName = (v, fb) => {
  if (!v) return fb || '—';
  const s = (window.DB.staff || []).find((x) => x.id === v);
  if (s) return s.name;
  return typeof v === 'string' && v.indexOf('U-') !== 0 ? v : (fb || '—');
};

function csvExport(filename, cols, rows) {
  const esc = (v) => { const s = v == null ? '' : String(v); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; };
  const csv = [cols.join(',')].concat(rows.map((r) => cols.map((c) => esc(r[c])).join(','))).join('\n');
  const a = document.createElement('a');
  a.href = URL.createObjectURL(new Blob(['﻿' + csv], { type: 'text/csv' }));
  a.download = filename; a.click(); URL.revokeObjectURL(a.href);
}

/* =====================================================================
   M3 — Calibration Jobs
   ===================================================================== */
function newJobFlow() {
  const DB = window.DB;
  if (!DB.customers.length) {
    window.toast('Add a customer before booking a job — opening the customer form for you.', 'info', { label: 'Add customer', onClick: () => window.openCustomerForm && window.openCustomerForm() });
    if (window.openCustomerForm) window.openCustomerForm();
    return;
  }
  const NEW_CUST = '＋ Register a new customer…';
  openForm({
    title: 'New calibration job', icon: 'workflow', sub: 'Step 1 of 2 — job details',
    submitLabel: 'Next — add instruments', done: 'Now add the instruments',
    fields: [
      { key: 'customer', label: 'Customer', type: 'select', options: DB.customers.map((c) => c.name).concat(NEW_CUST), value: DB.customers[0].name, half: true },
      { key: 'type', label: 'Job type', type: 'select', options: calOpt('jobTypes'), value: calOpt('jobTypes')[0], half: true },
      { key: 'priority', label: 'Priority', type: 'radio', options: calOpt('priorities'), value: calOpt('priorities')[1] || calOpt('priorities')[0], half: true },
      { key: 'due', label: 'Target date', type: 'date', value: _dstr(new Date(+DB.TODAY + 3 * 864e5)), half: true },
    ],
    onSave: (v) => {
      if (v.customer === NEW_CUST) { window.toast('Register the new customer, then book their job.', 'info'); if (window.openCustomerForm) window.openCustomerForm(); return; }
      const cust = DB.customers.find((c) => c.name === v.customer) || DB.customers[0];
      const eligible = DB.monitors.filter((m) => m.customer === cust.id && m.status !== 'Retired');
      const NEW_INST = '＋ Register a new instrument…';
      const startStep2 = () => setTimeout(() => {
        const elig = DB.monitors.filter((m) => m.customer === cust.id && m.status !== 'Retired');
        openForm({
          title: 'New calibration job — ' + cust.short, icon: 'cpu', sub: 'Step 2 of 2 — add the instruments the customer delivered',
          submitLabel: 'Create job', done: 'Job created',
          fields: [
            { key: 'assets', label: 'Registered instruments (' + elig.length + ')', type: 'checks', options: elig.map((m) => m.id + ' · ' + m.model), value: elig.filter((m) => _statusOf(m) === 'Overdue' || _statusOf(m) === 'Due soon').map((m) => m.id + ' · ' + m.model) },
            { type: 'section', label: 'Not on the list?', hint: 'Register an instrument the customer just delivered — it will be added to this job.' },
            { key: '_addnew', label: ' ', type: 'checks', options: [NEW_INST], value: [] },
          ],
          onSave: (v2) => {
            if ((v2._addnew || []).includes(NEW_INST)) {
              window.openAssetForm(null, { customerId: cust.id, onCreated: () => { window.toast('Instrument registered — add more or create the job.', 'ok'); startStep2(); } });
              return;
            }
            const assets = (v2.assets || []).map((s) => String(s).split(' · ')[0]);
            if (!assets.length) { window.toast('Add at least one instrument — tick the registered ones or register a new one.', 'warn'); return; }
            const seq = 413 + DB.jobs.length;
            const jid = 'JOB-26-0' + seq;
            dbAdd('jobs', {
              id: jid, customer: cust.id, assets, items: assets.length,
              type: v.type, stage: 'Received', tech: null, priority: v.priority,
              due: v.due ? new Date(v.due) : new Date(), sla: 5, recvd: new Date(), worksheets: {},
            });
            calNotifyCustomer(cust, 'Instruments received — ' + jid, 'Dear ' + (cust.contact || cust.name) + ',\n\nWe confirm receipt of ' + assets.length + ' instrument(s) under calibration job ' + jid + '. You will be notified when they are ready for collection.\n\n' + DB.company.lab + '\n' + DB.company.name, 'Receive confirmation');
          },
        });
      }, 60);
      if (!eligible.length) {
        window.toast(cust.name + ' has no instruments registered yet — register the first one to add to the job.', 'info');
        window.openAssetForm(null, { customerId: cust.id, onCreated: () => { window.toast('Instrument registered.', 'ok'); startStep2(); } });
        return;
      }
      startStep2();
    },
  });
}

function openWorksheet(job, mon, onDone) {
  const DB = window.DB;
  const cyls = DB.cylinders.filter((c) => c.status === 'Active' && (!c.exp || new Date(c.exp) > DB.TODAY));
  if (!cyls.length) { window.toast('You need an in-date calibration gas cylinder to record a worksheet. Add one to continue.', 'warn', { label: 'Add cylinder', onClick: () => window.openCylinderForm && window.openCylinderForm() }); return; }
  const staffNames = _staffOpts();
  const cust = DB.custById(mon.customer) || {};
  const rangeStr = (mon.sensors || []).map((g) => g + ' ' + ((GAS_CAL[g] || {}).range || '')).join(' · ');
  const OKNOT = ['OK', 'NOT OK'];
  const ALARM_OPTS = ['Alarm activated (OK)', 'Did not activate (NOT OK)'];
  const sensorFields = [];
  (mon.sensors || []).forEach((g) => {
    const R = GAS_CAL[g] || GAS_CAL['H₂S'];
    const zeroLbl = R.fresh != null ? `Fresh air ${R.fresh} ${R.unit} (±${R.absTol})` : `Fresh air 0 ${R.unit} (±${R.zeroTol})`;
    sensorFields.push({ type: 'section', label: g + ' sensor — calibration results', hint: `Range ${R.range} · ${zeroLbl} · span ${R.span} ${R.unit} (±${R.tolPct || 10}%)` });
    sensorFields.push({ key: 'zero_' + g, label: 'Fresh Air Zero — actual reading (' + R.unit + ')', type: 'number', value: R.fresh != null ? String(R.fresh) : '0', half: true });
    sensorFields.push({ key: 'span_' + g, label: 'Span Check — applied ' + R.span + ' ' + R.unit, type: 'number', value: String(R.span), half: true });
    sensorFields.push({ key: 'low_' + g, label: 'Alarm Test Low — applied ' + R.low + ' ' + R.unit, type: 'select', options: ALARM_OPTS, value: ALARM_OPTS[0], half: true });
    sensorFields.push({ key: 'high_' + g, label: 'Alarm Test High — applied ' + R.high + ' ' + R.unit, type: 'select', options: ALARM_OPTS, value: ALARM_OPTS[0], half: true });
  });
  // Functional checks come from the checklist matching this instrument's type
  const _cl = window.calChecklistFor(mon.equipmentType);
  const FUNCTIONAL = _cl.items;
  const isFixed = window.calIsFixed(mon.equipmentType);
  const wsTitle = isFixed ? 'Fixed detector calibration worksheet — ' + mon.id : 'Calibration worksheet — ' + mon.id;
  openForm({
    title: wsTitle, icon: 'clipboard', wide: true,
    sub: (isFixed ? 'Per Fixed Gas Detector Calibration Worksheet · ' : 'Per ADAM Gas Monitor Calibration Worksheet · ') + mon.model + ' · S/N ' + mon.serial,
    submitLabel: 'Complete calibration', done: 'Worksheet recorded',
    fields: [
      { type: 'section', label: 'Instrument & client' },
      { key: '_client', label: 'Client name', type: 'readonly', value: cust.name || '—' },
      ...(isFixed ? [{ key: '_site', label: 'Site / plant', type: 'readonly', value: cust.site || mon.location || '—' }] : []),
      { key: '_wo', label: 'Work order no.', type: 'readonly', value: job.id },
      { key: '_inst', label: 'Instrument make / model', type: 'readonly', value: mon.model },
      { key: '_serial', label: 'Serial number', type: 'readonly', value: mon.serial },
      { key: '_asset', label: 'Asset ID', type: 'readonly', value: mon.id },
      ...(isFixed ? [
        { key: 'tag', label: 'Detector tag no.', type: 'text', value: mon.tag || '', placeholder: 'e.g. GD-101', half: true },
        { key: 'plcTag', label: 'Controller / PLC tag', type: 'text', value: mon.plcTag || '', placeholder: 'e.g. PLC-A2', half: true },
        { key: 'loopNo', label: 'Loop no.', type: 'text', value: mon.loopNo || '', placeholder: 'e.g. L-07', half: true },
      ] : []),
      { key: 'firmware', label: 'Firmware version', type: 'text', value: mon.firmware || '', placeholder: 'e.g. v2.14', half: true },
      { key: '_range', label: 'Sensor type & range', type: 'readonly', value: rangeStr },
      { key: '_proc', label: 'Procedure reference', type: 'readonly', value: 'CAL-MD-014 Rev 4' },
      { key: '_certno', label: 'Certificate No.', type: 'readonly', value: 'Assigned on Technical Manager approval' },
      { key: '_nextdue', label: 'Next calibration due', type: 'readonly', value: DB.fmt(DB.addM(DB.TODAY, mon.interval || 6)) + ' (' + (mon.interval || 6) + '-month interval)' },
      { type: 'section', label: 'Conditions' },
      { key: 'calDate', label: 'Calibration date', type: 'date', value: _dstr(DB.TODAY), half: true },
      { key: 'location', label: isFixed ? 'Location / area' : 'Location', type: 'text', value: mon.location || (isFixed ? '' : 'Laboratory'), half: true },
      { key: 'temp', label: 'Ambient temperature (°C)', type: 'number', value: '24', half: true },
      { key: 'humidity', label: 'Humidity (%RH)', type: 'number', value: '45', half: true },
      { type: 'section', label: 'Calibration gas', hint: 'Concentration and cylinder expiry are recorded on the worksheet & certificate' },
      { key: 'cylinder', label: 'Gas cylinder', type: 'select', options: cyls.map((c) => `${c.id} — ${c.mix} (${c.conc}) · exp ${DB.fmt(c.exp)}`), full: true },
      { key: 'flowRate', label: 'Flow rate', type: 'text', value: '0.3–0.5 L/min', half: true },
      { key: 'gasSupplier', label: 'Gas supplier', type: 'text', value: cyls[0].supplier || '', half: true },
    ].concat(sensorFields, isFixed ? [
      { type: 'section', label: '4–20 mA output verification', hint: 'Loop output at zero and full scale · expected 4.00 / 20.00 mA · tolerance ±0.2 mA' },
      { key: 'mAZero', label: 'Output at zero — actual (mA)', type: 'number', value: '4.00', half: true },
      { key: 'mASpan', label: 'Output at full scale — actual (mA)', type: 'number', value: '20.00', half: true },
    ] : [], [
      { type: 'section', label: 'Functional checks', hint: 'Checklist: ' + _cl.name + (mon.equipmentType ? ' · ' + mon.equipmentType : '') + ' — mark NOT OK to fail the item' },
    ], FUNCTIONAL.map((f, i) => ({ key: 'fn_' + i, label: f, type: 'select', options: OKNOT, value: 'OK', half: true })), [
      { type: 'section', label: 'Personnel & remarks' },
      { key: 'tech', label: 'Calibrated by (signature)', type: 'select', options: staffNames, value: (CAL.user && staffNames.includes(CAL.user.name)) ? CAL.user.name : staffNames[0], half: true },
      { key: 'verifiedBy', label: 'Verified by (signature)', type: 'select', options: staffNames, value: staffNames.find((n) => !CAL.user || n !== CAL.user.name) || staffNames[0], half: true },
      { key: 'clientRep', label: 'Client representative (witness)', type: 'text', value: '', placeholder: 'Name — optional', half: true },
      { key: 'remarks', label: 'Remarks / observations', type: 'textarea', full: true },
    ]),
    onSave: (v) => {
      const readings = (mon.sensors || []).map((g) => {
        const R = GAS_CAL[g] || GAS_CAL['H₂S'];
        const zero = parseFloat(v['zero_' + g]); const span = parseFloat(v['span_' + g]);
        const zeroOk = R.fresh != null ? Math.abs(zero - R.fresh) <= R.absTol : Math.abs(zero) <= R.zeroTol;
        const spanOk = R.fresh != null ? Math.abs(span - R.span) <= R.absTol : Math.abs(span - R.span) <= R.span * R.tolPct / 100;
        const lowOk = v['low_' + g] === 'Alarm activated (OK)';
        const highOk = v['high_' + g] === 'Alarm activated (OK)';
        return { gas: g, unit: R.unit, range: R.range, zero, span, expected: R.span, low: R.low, high: R.high, zeroOk, spanOk, lowOk, highOk, ok: zeroOk && spanOk && lowOk && highOk };
      });
      const functional = {}; FUNCTIONAL.forEach((f, i) => functional[f] = v['fn_' + i] || 'OK');
      const checksOk = Object.values(functional).every((x) => x === 'OK');
      // 4–20 mA loop output (fixed detectors) — ±0.2 mA per the fixed worksheets
      let loopOut = null;
      if (isFixed) {
        const z = parseFloat(v.mAZero), s = parseFloat(v.mASpan);
        loopOut = { zero: z, span: s, zeroOk: !isNaN(z) && Math.abs(z - 4) <= 0.2, spanOk: !isNaN(s) && Math.abs(s - 20) <= 0.2 };
        loopOut.ok = loopOut.zeroOk && loopOut.spanOk;
      }
      const pass = readings.every((r) => r.ok) && checksOk && (!loopOut || loopOut.ok);
      const calDate = v.calDate ? new Date(v.calDate) : new Date();
      const due = DB.addM(calDate, mon.interval || 6);
      const cylId = (v.cylinder || '').split(' — ')[0];
      const cylRec = DB.cylinders.find((c) => c.id === cylId) || {};
      const ws = {
        id: 'WS-26-' + (3200 + (DB.worksheets || []).length), job: job.id, monitor: mon.id, model: mon.model, serial: mon.serial,
        customer: mon.customer, calDate, location: v.location, temp: v.temp, humidity: v.humidity, range: rangeStr,
        cylinder: cylId, cylinderDesc: v.cylinder || '', gasConc: cylRec.conc || '', gasMix: cylRec.mix || '', gasExpiry: cylRec.exp || null,
        flowRate: v.flowRate || '', gasSupplier: v.gasSupplier || '', firmware: v.firmware || mon.firmware || '',
        workOrder: job.id, isFixed, tag: v.tag || mon.tag || '', plcTag: v.plcTag || mon.plcTag || '', loopNo: v.loopNo || mon.loopNo || '',
        sitePlant: isFixed ? (cust.site || '') : '', loopOut, clientRep: v.clientRep || '',
        readings, functional, checklist: _cl.name, equipmentType: mon.equipmentType || '', result: pass ? 'Pass' : 'Fail',
        tech: v.tech, verifiedBy: v.verifiedBy, remarks: v.remarks || '', nextDue: due,
      };
      dbAdd('worksheets', ws);
      const wsMap = Object.assign({}, job.worksheets, { [mon.id]: { ws: ws.id, result: ws.result } });
      dbUpdate('jobs', 'id', job.id, { worksheets: wsMap });
      dbUpdate('monitors', 'id', mon.id, Object.assign(
        { last: calDate, due, dueRel: DB.rel(due), status: pass ? 'Valid' : 'Failed', firmware: ws.firmware },
        isFixed ? { tag: ws.tag, plcTag: ws.plcTag, loopNo: ws.loopNo } : {}
      ));
      if (!pass) {
        dbAdd('capa', {
          id: 'CAPA-26-0' + (30 + DB.capa.length), title: 'Failed calibration — ' + mon.id + ' (' + mon.model + ')',
          source: 'Failed calibration', severity: 'Major', owner: (CAL.user || {}).id || null,
          due: new Date(+DB.TODAY + 7 * 864e5), status: 'Open', stage: 'Open',
          desc: 'Instrument failed calibration: ' + readings.filter((r) => !r.ok).map((r) => r.gas).join(', ') + (checksOk ? '' : ' + functional checks') + '. Quarantine and investigate.',
        });
        window.toast(mon.id + ' FAILED — NCR raised automatically', 'bad');
      }
      if (onDone) onDone(ws);
    },
  });
}

function issueCertificate(job, wsId) {
  const DB = window.DB;
  const ws = (DB.worksheets || []).find((w) => w.id === wsId);
  if (!ws || ws.result !== 'Pass') return null;
  if (DB.certificates.some((c) => c.worksheet === ws.id)) return null; // idempotent
  const nums = DB.certificates.map((c) => parseInt(String(c.id).replace(/^\D+/g, '').slice(-5), 10) || 0);
  const next = Math.max.apply(null, nums.concat(4188)) + 1;
  const id = 'ADM-CAL-26-' + String(next).padStart(5, '0');
  // supersede older valid certs for the same instrument
  DB.certificates.filter((c) => c.monitor === ws.monitor && c.status === 'Valid')
    .forEach((c) => dbUpdate('certificates', 'id', c.id, { status: 'Superseded' }));
  const cert = {
    id, worksheet: ws.id, job: job.id, monitor: ws.monitor, model: ws.model, serial: ws.serial,
    customer: ws.customer, issued: new Date(), calDate: ws.calDate, due: ws.nextDue, certExpiry: ws.nextDue,
    method: 'CAL-MD-014 Rev 4', mu: '±0.6 ppm', signatory: (CAL.user || {}).id || null, technician: ws.tech,
    reviewedBy: ws.verifiedBy || null, status: 'Valid', result: 'Pass',
    readings: ws.readings, range: ws.range, cylinder: ws.cylinderDesc, cylinderNo: ws.cylinder,
    gasConc: ws.gasConc, gasMix: ws.gasMix, gasExpiry: ws.gasExpiry,
    temp: ws.temp, humidity: ws.humidity, location: ws.location,
    // fixed-detector & doc-conformance fields (per gas-type worksheets/certificates)
    isFixed: !!ws.isFixed, equipmentType: ws.equipmentType || '', tag: ws.tag || '', plcTag: ws.plcTag || '',
    loopNo: ws.loopNo || '', firmware: ws.firmware || '', sitePlant: ws.sitePlant || '', workOrder: ws.workOrder || ws.job,
    flowRate: ws.flowRate || '', gasSupplier: ws.gasSupplier || '', loopOut: ws.loopOut || null, clientRep: ws.clientRep || '',
  };
  dbAdd('certificates', cert);
  return cert;
}

function JobsModule({ jobId, setJobId }) {
  const DB = window.DB;
  const [sel, setSel] = useState(jobId || null);
  // Technicians land on their own queue by default; others see the full board.
  const [scope, setScope] = useState(window.CAL && CAL.user && /technician/i.test(CAL.user.role || '') ? 'mine' : 'all');
  useEffect(() => { if (jobId) setSel(jobId); }, [jobId]);
  const job = sel ? DB.jobs.find((j) => j.id === sel) : null;
  if (job) return <JobDetail job={job} onBack={() => { setSel(null); setJobId && setJobId(null); }} />;

  const me = window.CAL && CAL.user ? CAL.user.id : null;
  const myCount = me ? DB.jobs.filter((j) => j.tech === me).length : 0;
  const scoped = scope === 'mine' ? DB.jobs.filter((j) => j.tech === me) : DB.jobs;
  const counts = JOB_STAGES.map((s) => ({ s, n: scoped.filter((j) => j.stage === s).length }));
  return (
    <div className="page page-wide rise">
      <PageHead title="Calibration Jobs" sub="Received → assigned → calibrated → reviewed → approved → dispatched · worksheets per ADAM form">
        <Btn variant="ghost" icon="download" size="sm" onClick={() => csvExport('jobs.csv', ['id', 'customer', 'type', 'stage', 'items', 'priority'], scoped.map((j) => ({ ...j, customer: (DB.custById(j.customer) || {}).name })))}>Export</Btn>
        <Btn variant="primary" icon="plus" onClick={newJobFlow}>New job</Btn>
      </PageHead>
      <ModuleGuide module="jobs" onNav={()=>{}} />
      {DB.jobs.length > 0 && (
        <div className="tabs" style={{ marginBottom: 14 }}>
          {[['all', 'All jobs', DB.jobs.length], ['mine', 'Assigned to me', myCount]].map(([id, label, n]) => (
            <div key={id} className={`tab${scope === id ? ' on' : ''}`} onClick={() => setScope(id)}>{label} <span style={{ opacity: .55 }}>· {n}</span></div>
          ))}
        </div>
      )}
      <div className="kpi-grid" style={{ gridTemplateColumns: 'repeat(6,1fr)', marginBottom: 16 }}>
        {counts.map((c, i) => <Kpi key={c.s} label={c.s} value={c.n} icon={['box', 'users', 'workflow', 'eye', 'check-circle', 'truck'][i]} tone={['neutral', 'info', 'info', 'warn', 'ok', 'ok'][i] === 'neutral' ? 'info' : ['neutral', 'info', 'info', 'warn', 'ok', 'ok'][i]} />)}
      </div>
      {DB.jobs.length === 0 ? (
        <div className="card">
          <EmptyState icon="workflow"
            title="No calibration jobs yet"
            desc="Book a job when a customer drops off gas detectors for calibration. We'll walk it through receiving, inspection, calibration, QA/QC review and certification."
            actionLabel="Book a calibration job"
            onAction={newJobFlow} />
        </div>
      ) : scoped.length === 0 ? (
        <div className="card">
          <EmptyState icon="users"
            title="Nothing assigned to you right now"
            desc="Jobs assigned to you appear here so you can jump straight to your work. Switch to “All jobs” to see the whole board."
            secondaryLabel="View all jobs"
            onSecondary={() => setScope('all')} />
        </div>
      ) : (
      <div className="card">
        <div className="card-head"><Icon name="workflow" size={16} style={{ color: 'var(--primary-600)' }} /><div className="h-sec" style={{ fontSize: 14 }}>{scope === 'mine' ? 'Assigned to me' : 'Job register'}</div><span className="spacer" /><span className="faint" style={{ fontSize: 11.5 }}>{scoped.length} {scope === 'mine' ? 'assigned to you' : 'jobs'}</span></div>
        <table className="tbl">
          <thead><tr><th>Job</th><th>Customer</th><th>Type</th><th>Items</th><th>Technician</th><th>Priority</th><th>Received</th><th>Target</th><th>Stage</th><th>Progress</th></tr></thead>
          <tbody>
            {scoped.map((j) => {
              const c = DB.custById(j.customer);
              const done = Object.keys(j.worksheets || {}).length;
              return (
                <tr key={j.id} className="clickable" onClick={() => setSel(j.id)}>
                  <td className="mono cell-strong" style={{ fontSize: 12 }}>{j.id}</td>
                  <td><div className="row" style={{ gap: 6 }}><span className="avatar avatar-sm" style={{ background: c.color, borderRadius: 6 }}>{c.short}</span>{c.short}</div></td>
                  <td style={{ fontSize: 12.5 }}>{j.type}</td>
                  <td className="mono" style={{ fontSize: 12.5 }}>{j.items}</td>
                  <td>{j.tech ? <div className="row" style={{ gap: 6 }}><Avatar id={j.tech} size="sm" /><span style={{ fontSize: 12 }}>{(DB.staffById(j.tech) || {}).name}</span></div> : <span className="faint">—</span>}</td>
                  <td><Badge kind={j.priority === 'High' ? 'bad' : j.priority === 'Low' ? 'neutral' : 'info'}>{j.priority}</Badge></td>
                  <td className="faint" style={{ fontSize: 12 }}>{DB.fmt(j.recvd)}</td>
                  <td className="faint" style={{ fontSize: 12 }}>{DB.fmt(j.due)}</td>
                  <td><StatusBadge status={_jstage(j)} /></td>
                  <td style={{ width: 110 }}>{j.assets ? <div className="row" style={{ gap: 6 }}><div style={{ flex: 1 }}><Bar pct={j.items ? done / j.items * 100 : 0} color={done === j.items ? 'var(--ok)' : 'var(--primary-600)'} /></div><span className="mono faint" style={{ fontSize: 10.5 }}>{done}/{j.items}</span></div> : <span className="faint" style={{ fontSize: 11 }}>legacy</span>}</td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
      )}
    </div>
  );
}

function JobDetail({ job, onBack }) {
  const DB = window.DB;
  const c = DB.custById(job.customer);
  const assets = (job.assets || []).map((id) => DB.monitors.find((m) => m.id === id)).filter(Boolean);
  const wsOf = (mid) => (job.worksheets || {})[mid];
  const allDone = assets.length > 0 && assets.every((m) => wsOf(m.id));
  const certsForJob = DB.certificates.filter((x) => x.job === job.id);
  const jstage = _jstage(job);
  const stageIdx = JOB_STAGES.indexOf(jstage);
  const advance = (stage, extra) => dbUpdate('jobs', 'id', job.id, Object.assign({ stage }, extra || {}));
  const addInstruments = () => {
    const onJob = new Set(job.assets || []);
    const avail = DB.monitors.filter((m) => m.customer === job.customer && m.status !== 'Retired' && !onJob.has(m.id));
    const NEW_INST = '＋ Register a new instrument…';
    const commit = (ids) => { const next = (job.assets || []).concat(ids.filter((id) => !onJob.has(id))); dbUpdate('jobs', 'id', job.id, { assets: next, items: next.length }); };
    openForm({
      title: 'Add instruments — ' + job.id, icon: 'cpu',
      sub: 'Add more instruments the customer delivered (same job or another batch)',
      submitLabel: 'Add to job', done: 'Instruments added',
      fields: [
        { key: 'assets', label: avail.length ? c.name + '\'s instruments not yet on this job (' + avail.length + ')' : 'No other registered instruments for this customer', type: 'checks', options: avail.map((m) => m.id + ' · ' + m.model), value: [] },
        { type: 'section', label: 'Not on the list?', hint: 'Register a newly delivered instrument and add it straight to this job.' },
        { key: '_addnew', label: ' ', type: 'checks', options: [NEW_INST], value: [] },
      ],
      onSave: (v) => {
        if ((v._addnew || []).includes(NEW_INST)) { window.openAssetForm(null, { customerId: job.customer, onCreated: (nid) => { commit([nid]); window.toast('Instrument registered & added to ' + job.id, 'ok'); } }); return; }
        const ids = (v.assets || []).map((s) => String(s).split(' · ')[0]);
        if (!ids.length) { window.toast('Pick at least one instrument to add, or register a new one.', 'warn'); return; }
        commit(ids);
      },
    });
  };
  const startCalibration = () => {
    advance('In Calibration');
    // per the dashboard document, instruments in work show status "In calibration"
    assets.forEach((m) => { if (m.status !== 'Retired') dbUpdate('monitors', 'id', m.id, { status: 'In calibration' }); });
  };
  const approveAll = () => {
    let issued = 0;
    assets.forEach((m) => { const w = wsOf(m.id); if (w && w.result === 'Pass') { if (issueCertificate(job, w.ws)) issued++; } });
    advance('Certified');
    window.toast(issued + ' certificate(s) issued & approved (Technical Manager)', 'ok');
  };
  const pickTech = (keepStage) => {
    const techs = DB.staff.filter((s) => /tech/i.test(s.role));
    const opts = (techs.length ? techs : DB.staff).map((s) => s.name);
    if (!opts.length) { window.toast('No team members to assign yet. Add a user with the Technician role in Users & Settings.', 'info', { label: 'Open Users', onClick: () => window._navTo && window._navTo('users') }); return; }
    openForm({
      title: keepStage ? 'Reassign technician' : 'Assign technician', icon: 'users',
      submitLabel: keepStage ? 'Reassign' : 'Assign', done: keepStage ? 'Technician reassigned' : 'Technician assigned',
      fields: [{ key: 'tech', label: 'Technician', type: 'select', options: opts, value: (job.tech && (DB.staffById(job.tech) || {}).name) || opts[0] }],
      onSave: (v) => {
        const tid = _staffId(v.tech);
        if (keepStage) dbUpdate('jobs', 'id', job.id, { tech: tid }); else advance('Assigned', { tech: tid });
        // notify the assigned technician (unless you assigned it to yourself)
        if (tid && !(window.CAL && CAL.user && CAL.user.id === tid)) {
          const cn = (DB.custById(job.customer) || {}).name || 'a customer';
          const n = (job.items || (job.assets || []).length);
          window.calNotifyUser(tid, { title: 'Job ' + job.id + ' assigned to you', body: 'You have been assigned calibration job ' + job.id + ' for ' + cn + ' (' + n + ' instrument' + (n === 1 ? '' : 's') + '). Open it in AI-COS to begin.', jobId: job.id });
          window.toast(v.tech + ' has been notified of the assignment.', 'ok');
        }
      },
    });
  };
  return (
    <div className="page page-wide rise">
      <div className="row" style={{ gap: 10, marginBottom: 16 }}>
        <button className="icon-btn" onClick={onBack}><Icon name="chevL" size={17} /></button>
        <span className="kpi-ico" style={{ width: 36, height: 36, background: 'var(--bg)', color: 'var(--primary-600)' }}><Icon name="workflow" size={18} /></span>
        <div>
          <div className="row" style={{ gap: 9 }}><span className="h-page" style={{ fontSize: 20 }}>{job.id}</span><StatusBadge status={jstage} /><Badge kind={job.priority === 'High' ? 'bad' : 'info'}>{job.priority}</Badge></div>
          <div className="h-sub">{c.name} · {job.type} · {job.items} instrument(s) · received {DB.fmt(job.recvd)}</div>
        </div>
        <span className="spacer" />
        {jstage === 'Received' && <Btn variant="primary" icon="eye" onClick={() => {
          // Fixed detectors get the installation inspection per the fixed ERP workflow
          const hasFixed = assets.some((m) => window.calIsFixed(m.equipmentType));
          const FIXED_INSP = ['Detector Location', 'Detector Condition', 'Mounting', 'Cabling', 'Junction Box', 'Display', 'Environmental Damage', 'Sensor Expiry'];
          openForm({
          title: (hasFixed ? 'Site / installation inspection — ' : 'Incoming inspection — ') + job.id, icon: 'eye',
          sub: hasFixed
            ? 'Inspect the fixed detector installation before calibration (per the fixed gas detector ERP workflow)'
            : 'Check every instrument for physical damage before assigning (per the calibration tracking workflow)',
          submitLabel: 'Record inspection', done: 'Inspection recorded',
          fields: [
            ...(hasFixed ? [{ type: 'section', label: 'Installation checklist', hint: 'Mark NOT OK to note a finding — record details in the notes' }]
              .concat(FIXED_INSP.map((f, i) => ({ key: 'ins_' + i, label: f, type: 'select', options: ['OK', 'NOT OK'], value: 'OK', half: true })),
              [{ key: 'bump', label: 'Bump test', type: 'select', options: ['Passed', 'Failed — troubleshoot before calibration', 'Not performed'], value: 'Passed', full: true }]) : []),
            { key: 'result', label: 'Inspection result', type: 'select', full: true, options: hasFixed
              ? ['Suitable for calibration — proceed', 'Findings noted — customer notified, proceed', 'Not suitable — raise inspection report, recommend repair (close job)']
              : ['No physical damage — proceed', 'Damage found — customer notified, proceed with repair', 'Damage found — return to customer (close job)'], value: hasFixed ? 'Suitable for calibration — proceed' : 'No physical damage — proceed' },
            { key: 'notes', label: 'Inspection notes', type: 'textarea', full: true, placeholder: hasFixed ? 'Installation condition, findings, access/permit notes…' : 'Condition on receipt, damage observed, accessories received…' },
          ],
          onSave: (v) => {
            const checklist = hasFixed ? FIXED_INSP.reduce((a, f, i) => (a[f] = v['ins_' + i] || 'OK', a), {}) : null;
            const insp = { result: v.result, notes: v.notes || '', by: (CAL.user || {}).name, date: new Date(), checklist, bump: hasFixed ? v.bump : undefined };
            if (/return to customer|recommend repair/i.test(v.result)) {
              advance('Dispatched', { inspection: insp, returned: true });
              calNotifyCustomer(c, (hasFixed ? 'Inspection report — ' : 'Instruments returned — ') + job.id, 'Dear ' + (c.contact || c.name) + ',\n\n' + (hasFixed
                ? 'The fixed detector installation was found not suitable for calibration during our site inspection (job ' + job.id + '). An inspection report has been raised and repair is recommended before calibration can proceed.'
                : 'Physical damage was found during the incoming inspection of job ' + job.id + '. As requested, the instruments are being returned without calibration.') + '\n\nNotes: ' + (v.notes || '—') + '\n\n' + DB.company.lab + '\n' + DB.company.name, hasFixed ? 'Inspection report' : 'Return notice');
              window.toast(job.id + ' closed — ' + (hasFixed ? 'inspection report raised, repair recommended' : 'instruments returned to the customer'), 'info');
            } else {
              advance('Inspection', { inspection: insp });
              if (/customer notified/i.test(v.result)) calNotifyCustomer(c, 'Findings noted — ' + job.id, 'Dear ' + (c.contact || c.name) + ',\n\nDuring the ' + (hasFixed ? 'site inspection' : 'incoming inspection') + ' of job ' + job.id + ' findings were noted. We will proceed with calibration and repair as agreed.\n\nNotes: ' + (v.notes || '—') + '\n\n' + DB.company.lab + '\n' + DB.company.name, 'Damage notice');
            }
          },
        }); }}>{'Record ' + (assets.some((m) => window.calIsFixed(m.equipmentType)) ? 'site inspection' : 'incoming inspection')}</Btn>}
        {jstage === 'Inspection' && <Btn variant="primary" icon="users" onClick={() => pickTech(false)}>Assign technician</Btn>}
        {['Assigned', 'In Calibration', 'QA/QC Review'].includes(jstage) && <Btn variant="ghost" icon="users" onClick={() => pickTech(true)}>Reassign</Btn>}
        {jstage === 'Assigned' && <Btn variant="primary" icon="workflow" onClick={startCalibration}>Start calibration</Btn>}
        {jstage === 'In Calibration' && <Btn variant="primary" icon="eye" onClick={() => allDone ? advance('QA/QC Review') : window.toast('Finish the calibration worksheet for each instrument before sending the job to QA/QC review.', 'warn')} style={!allDone ? { opacity: .6 } : null}>Submit for QA/QC review</Btn>}
        {jstage === 'QA/QC Review' && (canApprove()
          ? <Btn variant="primary" icon="check-circle" onClick={approveAll}>Approve (Technical Manager) &amp; issue certificates</Btn>
          : <Badge kind="warn">Awaiting QA/QC review &amp; Technical Manager approval</Badge>)}
        {jstage === 'Certified' && <Btn variant="primary" icon="truck" onClick={() => {
          calNotifyCustomer(c, 'Ready for collection — ' + job.id, 'Dear ' + (c.contact || c.name) + ',\n\nCalibration job ' + job.id + ' is complete. ' + job.items + ' instrument(s) and their calibration certificates are ready for collection.\n\n' + DB.company.lab + '\n' + DB.company.name, 'Collection notice');
          advance('Dispatched');
        }}>Notify customer &amp; dispatch</Btn>}
        {jstage === 'Received' && <Btn variant="ghost" style={{ color: 'var(--bad)' }} onClick={() => { if (window.confirm('Delete ' + job.id + '?')) { dbRemove('jobs', 'id', job.id); onBack(); } }}>Delete</Btn>}
      </div>

      {/* stage strip */}
      <div className="card card-pad" style={{ marginBottom: 16, overflowX: 'auto' }}>
        <div className="row" style={{ gap: 0, minWidth: 700 }}>
          {JOB_STAGES.map((s, i) => (
            <React.Fragment key={s}>
              <div className="row" style={{ gap: 8, flex: 'none' }}>
                <span style={{ width: 28, height: 28, borderRadius: '50%', display: 'grid', placeItems: 'center', fontSize: 12, fontWeight: 800, background: i < stageIdx ? 'var(--ok)' : i === stageIdx ? 'var(--primary-600)' : 'var(--bg)', color: i <= stageIdx ? '#fff' : 'var(--text-3)' }}>{i < stageIdx ? <Icon name="check" size={14} /> : i + 1}</span>
                <span style={{ fontSize: 12.5, fontWeight: i === stageIdx ? 750 : 600, color: i === stageIdx ? 'var(--primary-700)' : i < stageIdx ? 'var(--text)' : 'var(--text-3)' }}>{s}</span>
              </div>
              {i < JOB_STAGES.length - 1 && <div style={{ flex: 1, height: 2, margin: '0 10px', background: i < stageIdx ? 'var(--ok)' : 'var(--border-2)' }} />}
            </React.Fragment>
          ))}
        </div>
        <div className="row" style={{ gap: 8, marginTop: 12, padding: '9px 12px', borderRadius: 10, background: 'var(--info-bg)', alignItems: 'flex-start' }}>
          <span style={{ color: 'var(--primary-600)', flex: 'none', marginTop: 1 }}><Icon name="flag" size={14} /></span>
          <span style={{ fontSize: 12.5, lineHeight: 1.45 }}><b>What's next:</b> {STAGE_HINTS[_jstage(job)]}</span>
        </div>
      </div>

      <div className="dash-ops" style={{ display: 'grid', gridTemplateColumns: '1.6fr 1fr', gap: 16, alignItems: 'start' }}>
        <div className="card">
          <div className="card-head"><Icon name="cpu" size={16} style={{ color: 'var(--primary-600)' }} /><div className="h-sec" style={{ fontSize: 14 }}>Instruments &amp; worksheets</div><span className="spacer" />{['Received', 'Inspection', 'Assigned'].includes(jstage) && <Btn size="sm" icon="plus" onClick={addInstruments}>Add instruments</Btn>}</div>
          <table className="tbl">
            <thead><tr><th>Asset</th><th>Model</th><th>Serial</th><th>Sensors</th><th>Result</th><th style={{ width: 150 }}></th></tr></thead>
            <tbody>
              {assets.map((m) => {
                const w = wsOf(m.id);
                return (
                  <tr key={m.id}>
                    <td className="mono cell-strong" style={{ fontSize: 12 }}>{m.id}</td>
                    <td style={{ fontSize: 12.5 }}>{m.model}</td>
                    <td className="mono faint" style={{ fontSize: 11.5 }}>{m.serial}</td>
                    <td><div className="row wrap" style={{ gap: 4 }}>{(m.sensors || []).map((s) => <span key={s} className="tag" style={{ padding: '1px 6px', fontSize: 10.5 }}>{s}</span>)}</div></td>
                    <td>{w ? <Badge kind={w.result === 'Pass' ? 'ok' : 'bad'} dot>{w.result}</Badge> : <Badge kind="neutral">Pending</Badge>}</td>
                    <td>
                      {_jstage(job) === 'In Calibration' && !w && <Btn size="sm" variant="primary" icon="clipboard" onClick={() => openWorksheet(job, m)}>Worksheet</Btn>}
                      {w && <Btn size="sm" variant="ghost" onClick={() => window.toast('Worksheet ' + w.ws + ' — ' + w.result, 'info')}>{w.ws}</Btn>}
                    </td>
                  </tr>
                );
              })}
              {assets.length === 0 && <tr><td colSpan={6} className="faint" style={{ textAlign: 'center', padding: 22 }}>Legacy job — no linked instruments.</td></tr>}
            </tbody>
          </table>
        </div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
          <div className="card card-pad">
            <div className="eyebrow" style={{ marginBottom: 8 }}>Job details</div>
            {[['Customer', c.name], ['Contact', (c.contact || '—') + (c.email ? ' · ' + c.email : '')], ['Technician', job.tech ? (DB.staffById(job.tech) || {}).name : 'Unassigned'], ['Received', DB.fmt(job.recvd)], ['Target', DB.fmt(job.due)]].map(([k, v]) => (
              <div key={k} className="row" style={{ justifyContent: 'space-between', padding: '5px 0', fontSize: 12.5 }}><span className="faint">{k}</span><b style={{ textAlign: 'right' }}>{v}</b></div>
            ))}
          </div>
          <div className="card">
            <div className="card-head"><Icon name="certificate" size={15} style={{ color: 'var(--primary-600)' }} /><div className="h-sec" style={{ fontSize: 13.5 }}>Certificates from this job</div></div>
            <div className="card-body" style={{ paddingTop: 4 }}>
              {certsForJob.length === 0
                ? <div className="faint" style={{ fontSize: 12, padding: '8px 0' }}>Issued on approval of passing worksheets.</div>
                : certsForJob.map((ct) => (
                  <div key={ct.id} className="row" style={{ justifyContent: 'space-between', padding: '7px 0', borderBottom: '1px solid var(--border)', fontSize: 12 }}>
                    <span className="mono" style={{ color: 'var(--primary-700)' }}>{ct.id}</span>
                    <span className="faint">{ct.monitor}</span>
                    <Btn size="sm" variant="ghost" icon="documents" onClick={() => printCertificate(ct)}>Print</Btn>
                  </div>
                ))}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

/* =====================================================================
   M4 — Certificates: register + ADAM printable certificate
   ===================================================================== */
function printCertificate(ct) {
  const DB = window.DB;
  const c = DB.custById(ct.customer) || {};
  const co = DB.company;
  const tech = _staffName(ct.technician);
  const reviewer = _staffName(ct.reviewedBy);
  const approver = _staffName(ct.signatory);
  // Title & extra result rows per the gas-type certificate documents
  const _gases = (ct.readings || []).map((r) => r.gas);
  const _gasLabel = _gases.length > 1 ? 'MULTI GAS' : _gases[0] === 'LEL' ? 'COMBUSTIBLE GAS' : (_gases[0] || 'GAS') + ' GAS';
  const title = (ct.isFixed ? 'FIXED ' : '') + _gasLabel + ' DETECTOR CALIBRATION CERTIFICATE';
  const lo = ct.loopOut;
  const loopRow = lo ? `
      <tr><td>4–20 mA Output — Zero</td><td>Zero gas</td><td>4.00 mA</td><td>${lo.zero != null && !isNaN(lo.zero) ? lo.zero.toFixed(2) : '—'} mA</td><td>±0.2 mA</td><td class="${lo.zeroOk ? 'ok' : 'bad'}">${lo.zeroOk ? 'Pass' : 'Fail'}</td></tr>
      <tr><td>4–20 mA Output — Full Scale</td><td>Span gas</td><td>20.00 mA</td><td>${lo.span != null && !isNaN(lo.span) ? lo.span.toFixed(2) : '—'} mA</td><td>±0.2 mA</td><td class="${lo.spanOk ? 'ok' : 'bad'}">${lo.spanOk ? 'Pass' : 'Fail'}</td></tr>` : '';
  const rows = (ct.readings || []).map((r) => `
      <tr><td>${r.gas} — Fresh Air Zero Check</td><td>${r.gas === 'O₂' ? '20.9 %vol' : '0 ' + r.unit}</td><td>${r.gas === 'O₂' ? '20.9 %vol' : '0 ' + r.unit}</td><td>${r.zero} ${r.unit}</td><td>${r.gas === 'O₂' ? '±0.5 %vol' : '±2 ' + r.unit}</td><td class="${r.zeroOk ? 'ok' : 'bad'}">${r.zeroOk ? 'Pass' : 'Fail'}</td></tr>
      <tr><td>${r.gas} — Span Calibration</td><td>${r.expected} ${r.unit}</td><td>${r.expected} ${r.unit}</td><td>${r.span} ${r.unit}</td><td>±10%</td><td class="${r.spanOk ? 'ok' : 'bad'}">${r.spanOk ? 'Pass' : 'Fail'}</td></tr>
      ${r.low != null ? `<tr><td>${r.gas} — Low Alarm Test</td><td>${r.low} ${r.unit}</td><td>Alarm Activated</td><td>${r.lowOk === false ? 'No alarm' : 'Alarm Activated'}</td><td>N/A</td><td class="${r.lowOk !== false ? 'ok' : 'bad'}">${r.lowOk !== false ? 'Pass' : 'Fail'}</td></tr>
      <tr><td>${r.gas} — High Alarm Test</td><td>${r.high} ${r.unit}</td><td>Alarm Activated</td><td>${r.highOk === false ? 'No alarm' : 'Alarm Activated'}</td><td>N/A</td><td class="${r.highOk !== false ? 'ok' : 'bad'}">${r.highOk !== false ? 'Pass' : 'Fail'}</td></tr>` : ''}`).join('');
  const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${ct.id}</title>
  <style>
    body{font-family:'Helvetica Neue',Arial,sans-serif;color:#16283c;margin:0;padding:36px 44px;font-size:12px}
    .head{display:flex;justify-content:space-between;align-items:center;border-bottom:3px solid #16314a;padding-bottom:14px}
    .head img{height:52px}.head .co{text-align:right;font-size:10.5px;color:#4a5b70;line-height:1.5}
    h1{text-align:center;font-size:19px;letter-spacing:1.5px;margin:22px 0 2px;color:#16314a}
    .subt{text-align:center;font-size:12.5px;color:#4a5b70;margin-bottom:18px}
    table{width:100%;border-collapse:collapse;margin:10px 0}
    td,th{border:1px solid #c9d4e0;padding:6px 9px;font-size:11px;text-align:left}
    th{background:#eef2f7;font-weight:700;color:#16314a}
    .grid td:first-child{background:#f6f8fb;font-weight:600;width:24%}
    .ok{color:#0f7a4f;font-weight:700}.bad{color:#b91c1c;font-weight:700}
    .stmt{font-size:11px;line-height:1.6;margin:14px 0;color:#33465c}
    .sig{display:grid;grid-template-columns:1fr 1fr 1fr;gap:18px;margin-top:26px}
    .sig div{border-top:1.5px solid #16314a;padding-top:6px;font-size:10.5px}
    .sig b{display:block;font-size:11.5px}
    .foot{margin-top:26px;font-size:9.5px;color:#7a8ba0;border-top:1px solid #d7e0ea;padding-top:8px;display:flex;justify-content:space-between}
    .print-btn{position:fixed;top:12px;right:12px;padding:9px 18px;background:#2f6fed;color:#fff;border:none;border-radius:8px;font-weight:700;cursor:pointer}
    @media print{.print-btn{display:none}}
  </style></head><body>
  <button class="print-btn" onclick="window.print()">Print / Save PDF</button>
  <div class="head">
    <div style="font-size:21px;font-weight:800;color:#16314a;letter-spacing:.2px;line-height:1.2">${co.name}</div>
    <div class="co">${co.lab}<br>${co.address}<br>${co.standards}</div>
  </div>
  <h1>${title}</h1>
  <div class="subt">${ct.isFixed ? 'Fixed / installed gas detector' : 'Portable Gas Monitor'} · Certificate No. <b>${ct.id}</b></div>
  <table class="grid">
    <tr><td>Certificate No.</td><td>${ct.id}</td><td style="background:#f6f8fb;font-weight:600">Calibration Date</td><td>${DB.fmt(ct.calDate)}</td></tr>
    <tr><td>Client Name</td><td>${c.name || '—'}</td><td style="background:#f6f8fb;font-weight:600">${ct.isFixed ? 'Site / Plant' : 'Location'}</td><td>${(ct.isFixed ? (ct.sitePlant || c.site) : ct.location) || ct.location || c.site || '—'}</td></tr>
    ${ct.isFixed ? `<tr><td>Location / Area</td><td>${ct.location || '—'}</td><td style="background:#f6f8fb;font-weight:600">Work Order No.</td><td>${ct.workOrder || ct.job || '—'}</td></tr>
    <tr><td>Detector Tag No.</td><td>${ct.tag || '—'}</td><td style="background:#f6f8fb;font-weight:600">Controller / PLC Tag</td><td>${ct.plcTag || '—'}</td></tr>
    <tr><td>Loop No.</td><td>${ct.loopNo || '—'}</td><td style="background:#f6f8fb;font-weight:600">Firmware Version</td><td>${ct.firmware || '—'}</td></tr>` : `<tr><td>Work Order No.</td><td>${ct.workOrder || ct.job || '—'}</td><td style="background:#f6f8fb;font-weight:600">Firmware Version</td><td>${ct.firmware || '—'}</td></tr>`}
    <tr><td>Instrument Make / Model</td><td>${ct.model}</td><td style="background:#f6f8fb;font-weight:600">Serial Number</td><td>${ct.serial || '—'}</td></tr>
    <tr><td>Asset ID</td><td>${ct.monitor}</td><td style="background:#f6f8fb;font-weight:600">Sensor Type(s)</td><td>${(ct.readings || []).map((r) => r.gas).join(', ') || 'H₂S'}</td></tr>
    <tr><td>Measurement Range</td><td colspan="3">${ct.range || (ct.readings || []).map((r) => r.gas + ' ' + (r.range || '')).join(' · ') || '—'}</td></tr>
    <tr><td>Calibration Gas Used</td><td>${ct.gasMix || ct.cylinder || '—'}</td><td style="background:#f6f8fb;font-weight:600">Gas Cylinder No.</td><td>${ct.cylinderNo || '—'}</td></tr>
    <tr><td>Gas Concentration</td><td>${ct.gasConc || '—'}</td><td style="background:#f6f8fb;font-weight:600">Gas Expiry Date</td><td>${ct.gasExpiry ? DB.fmt(ct.gasExpiry) : '—'}</td></tr>
    <tr><td>Flow Rate</td><td>${ct.flowRate || '—'}</td><td style="background:#f6f8fb;font-weight:600">Gas Supplier</td><td>${ct.gasSupplier || '—'}</td></tr>
    <tr><td>Ambient Temperature</td><td>${ct.temp || '—'} °C</td><td style="background:#f6f8fb;font-weight:600">Humidity</td><td>${ct.humidity || '—'} %RH</td></tr>
    <tr><td>Next Calibration Due</td><td>${DB.fmt(ct.due)}</td><td style="background:#f6f8fb;font-weight:600">Certificate Expiry</td><td>${DB.fmt(ct.certExpiry || ct.due)}</td></tr>
    <tr><td>Procedure Ref.</td><td>${ct.method || 'CAL-MD-014 Rev 4'}</td><td style="background:#f6f8fb;font-weight:600">Technician</td><td>${tech}</td></tr>
  </table>
  <h3 style="font-size:13px;margin:16px 0 4px;color:#16314a">Calibration Results</h3>
  <table><tr><th>Test Description</th><th>Applied Value</th><th>Expected Reading</th><th>Actual Reading</th><th>Tolerance</th><th>Status</th></tr>${rows}${loopRow}${rows || loopRow ? '' : '<tr><td colspan="6">Zero &amp; span within tolerance — see worksheet.</td></tr>'}</table>
  <div style="border:1.5px solid #16314a;border-radius:6px;padding:10px 14px;margin:14px 0;font-size:12px">
    <b style="letter-spacing:.5px">STATEMENT OF CONFORMITY&nbsp;&nbsp;</b>
    <span style="margin-right:18px">${/pass/i.test(ct.result) ? '☑' : '☐'} <b>PASS</b> — Detector complies with specified calibration acceptance criteria.</span>
    <span>${/pass/i.test(ct.result) ? '☐' : '☑'} <b>FAIL</b> — Detector does not comply.</span>
  </div>
  <p class="stmt">This is to certify that the above-mentioned ${ct.isFixed ? 'fixed gas detector' : 'portable gas monitor'} has been calibrated using certified
  reference gas and calibrated equipment${ct.isFixed ? ' in accordance with the manufacturer recommendations and applicable laboratory procedures' : ''}. The instrument was found to be operating within acceptable tolerance limits
  at the time of calibration. This certificate remains valid until the certificate expiry date provided the instrument
  is maintained and used in accordance with manufacturer recommendations.</p>
  <div class="sig" style="${ct.clientRep ? 'grid-template-columns:1fr 1fr 1fr 1fr' : ''}">
    <div><b>${tech}</b>Calibrated By<br>Date: ${DB.fmt(ct.calDate)}</div>
    <div><b>${reviewer}</b>Verified By (QA/QC)<br>Date: ${DB.fmt(ct.issued)}</div>
    <div><b>${approver}</b>Approved By (Technical Manager)<br>Date: ${DB.fmt(ct.issued)}</div>
    ${ct.clientRep ? `<div><b>${ct.clientRep}</b>Client Representative<br>Date: ${DB.fmt(ct.calDate)}</div>` : ''}
  </div>
  <div class="foot"><span>${co.name} · ${co.lab}</span><span>${ct.id} · Issued ${DB.fmt(ct.issued)} · Generated by AI-COS Essential</span></div>
  </body></html>`;
  const w = window.open('', '_blank');
  if (!w) { window.toast('Allow pop-ups to open the certificate', 'bad'); return; }
  w.document.write(html); w.document.close();
}

function CertsModule() {
  const DB = window.DB;
  const [q, setQ] = useState('');
  const [status, setStatus] = useState('All');
  const list = DB.certificates.filter((ct) =>
    (status === 'All' || ct.status === status) &&
    (!q || (ct.id + ' ' + ct.monitor + ' ' + (ct.model || '') + ' ' + (ct.serial || '')).toLowerCase().includes(q.toLowerCase()))
  );
  const nMonth = DB.certificates.filter((ct) => new Date(ct.issued).getMonth() === DB.TODAY.getMonth()).length;
  const sel = { fontFamily: 'inherit', fontSize: 12.5, padding: '7px 11px', borderRadius: 9, border: '1px solid var(--border-2)', background: 'var(--surface)', fontWeight: 600 };
  return (
    <div className="page page-wide rise">
      <PageHead title="Certificates" sub="Issued calibration certificates · ADAM template · print / PDF / withdraw">
        <Btn variant="ghost" icon="download" size="sm" onClick={() => csvExport('certificates.csv', ['id', 'monitor', 'model', 'serial', 'customer', 'status'], DB.certificates.map((x) => ({ ...x, customer: (DB.custById(x.customer) || {}).name })))}>Export</Btn>
      </PageHead>
      <ModuleGuide module="certs" onNav={()=>{}} />
      <div className="kpi-grid" style={{ gridTemplateColumns: 'repeat(4,1fr)', marginBottom: 16 }}>
        <Kpi label="Certificates issued" value={DB.certificates.length} icon="certificate" tone="info" />
        <Kpi label="Issued this month" value={nMonth} icon="calendar" tone="ok" />
        <Kpi label="Valid" value={DB.certificates.filter((x) => x.status === 'Valid').length} icon="check-circle" tone="ok" />
        <Kpi label="Superseded / withdrawn" value={DB.certificates.filter((x) => x.status !== 'Valid').length} icon="history" tone="warn" />
      </div>
      {DB.certificates.length === 0 ? (
        <div className="card">
          <EmptyState icon="certificate"
            title="No certificates issued yet"
            desc="Certificates are generated automatically when a calibration job passes QA/QC review and the Technical Manager approves it. Book a job to get started."
            actionLabel="Go to Calibration Jobs" actionIcon="arrowR"
            onAction={() => window._navTo && window._navTo('jobs')} />
        </div>
      ) : (
      <div className="card">
        <div className="card-head" style={{ gap: 10 }}>
          <div className="h-sec" style={{ fontSize: 14 }}>Certificate register</div><span className="spacer" />
          <div className="searchbox" style={{ width: 220 }}><Icon name="search" size={14} /><input placeholder="Cert no, asset, serial…" value={q} onChange={(e) => setQ(e.target.value)} style={{ fontSize: 12.5 }} /></div>
          <select value={status} onChange={(e) => setStatus(e.target.value)} style={sel}><option value="All">All statuses</option><option>Valid</option><option>Superseded</option><option>Withdrawn</option></select>
        </div>
        <table className="tbl">
          <thead><tr><th>Certificate No.</th><th>Asset</th><th>Model</th><th>Customer</th><th>Calibrated</th><th>Valid until</th><th>Result</th><th>Status</th><th style={{ width: 170 }}></th></tr></thead>
          <tbody>
            {list.map((ct) => {
              const c = DB.custById(ct.customer);
              return (
                <tr key={ct.id}>
                  <td className="mono cell-strong" style={{ fontSize: 11.5, color: 'var(--primary-700)' }}>{ct.id}</td>
                  <td className="mono" style={{ fontSize: 11.5 }}>{ct.monitor}</td>
                  <td style={{ fontSize: 12.5 }}>{ct.model}</td>
                  <td><div className="row" style={{ gap: 6 }}><span className="avatar avatar-sm" style={{ background: c.color, borderRadius: 6 }}>{c.short}</span>{c.short}</div></td>
                  <td className="faint" style={{ fontSize: 12 }}>{DB.fmt(ct.calDate || ct.issued)}</td>
                  <td className="faint" style={{ fontSize: 12 }}>{ct.due ? DB.fmt(ct.due) : '—'}</td>
                  <td><Badge kind={/pass/i.test(ct.result) ? 'ok' : 'warn'}>{ct.result}</Badge></td>
                  <td><StatusBadge status={ct.status} /></td>
                  <td>
                    <div className="row" style={{ gap: 6, justifyContent: 'flex-end' }}>
                      <Btn size="sm" variant="ghost" icon="documents" onClick={() => printCertificate(ct)}>Print</Btn>
                      {ct.status === 'Valid' && canApprove() && <Btn size="sm" variant="ghost" style={{ color: 'var(--bad)' }} onClick={() => { if (window.confirm('Withdraw ' + ct.id + '?')) dbUpdate('certificates', 'id', ct.id, { status: 'Withdrawn' }); }}>Withdraw</Btn>}
                    </div>
                  </td>
                </tr>
              );
            })}
            {list.length === 0 && <tr><td colSpan={9} className="faint" style={{ textAlign: 'center', padding: 24 }}>No certificates match these filters — try clearing the search or status filter.</td></tr>}
          </tbody>
        </table>
      </div>
      )}
    </div>
  );
}

/* =====================================================================
   M5 — NCR & CAPA register with lifecycle
   ===================================================================== */
const NCR_STAGES = ['Open', 'Root cause', 'Action', 'Verification', 'Closed'];
function openNcrForm(rec) {
  const DB = window.DB; const e = rec || {};
  openForm({
    title: rec ? 'Edit ' + e.id : 'Raise NCR', icon: 'flag',
    sub: 'Nonconformity & corrective action — quality register',
    submitLabel: rec ? 'Save changes' : 'Raise NCR', done: rec ? 'NCR updated' : 'NCR raised',
    fields: [
      { key: 'title', label: 'Nonconformity', type: 'text', full: true, value: e.title },
      { key: 'desc', label: 'Description / containment', type: 'textarea', full: true, value: e.desc },
      { key: 'source', label: 'Source', type: 'select', options: calOpt('ncrSources'), value: e.source || calOpt('ncrSources')[0], half: true },
      { key: 'severity', label: 'Severity', type: 'radio', options: ['Minor', 'Major'], value: e.severity || 'Minor', half: true },
      { key: 'owner', label: 'Owner', type: 'select', options: _staffOpts(), value: e.owner ? _staffName(e.owner) : ((CAL.user || {}).name || _staffOpts()[0]), half: true },
      { key: 'due', label: 'Due date', type: 'date', value: _dstr(e.due || new Date(+DB.TODAY + 14 * 864e5)), half: true },
    ],
    onSave: (v) => {
      const owner = _staffId(v.owner);
      const patch = { title: v.title, desc: v.desc, source: v.source, severity: v.severity, owner, due: v.due ? new Date(v.due) : new Date() };
      if (rec) dbUpdate('capa', 'id', e.id, patch);
      else dbAdd('capa', Object.assign({ id: 'CAPA-26-0' + (30 + DB.capa.length), status: 'Open', stage: 'Open' }, patch));
    },
  });
}
function QMSModule() {
  const DB = window.DB;
  const [status, setStatus] = useState('All');
  const list = DB.capa.filter((x) => status === 'All' || (status === 'Open' ? x.status !== 'Closed' : x.status === 'Closed'));
  const open = DB.capa.filter((x) => x.status !== 'Closed');
  const overdue = open.filter((x) => new Date(x.due) < DB.TODAY);
  const nextStage = (x) => {
    const i = NCR_STAGES.indexOf(x.stage);
    if (i < 0 || i >= NCR_STAGES.length - 1) return;
    const stage = NCR_STAGES[i + 1];
    dbUpdate('capa', 'id', x.id, { stage, status: stage === 'Closed' ? 'Closed' : 'In progress' });
    window.toast(x.id + ' → ' + stage, 'ok');
  };
  const sel = { fontFamily: 'inherit', fontSize: 12.5, padding: '7px 11px', borderRadius: 9, border: '1px solid var(--border-2)', background: 'var(--surface)', fontWeight: 600 };
  return (
    <div className="page page-wide rise">
      <PageHead title="NCR &amp; CAPA" sub="Nonconformities · corrective & preventive actions · Open → Root cause → Action → Verification → Closed">
        <Btn variant="ghost" icon="download" size="sm" onClick={() => csvExport('ncr-capa.csv', ['id', 'title', 'source', 'severity', 'stage', 'status'], DB.capa)}>Export</Btn>
        <Btn variant="primary" icon="plus" onClick={() => openNcrForm()}>Raise NCR</Btn>
      </PageHead>
      <ModuleGuide module="qms" onNav={()=>{}} />
      <div className="kpi-grid" style={{ gridTemplateColumns: 'repeat(4,1fr)', marginBottom: 16 }}>
        <Kpi label="Open NCRs" value={open.length} icon="flag" tone="warn" />
        <Kpi label="Overdue" value={overdue.length} icon="alert" tone={overdue.length ? 'bad' : 'ok'} />
        <Kpi label="Major open" value={open.filter((x) => x.severity === 'Major').length} icon="x-circle" tone="bad" />
        <Kpi label="Closed" value={DB.capa.filter((x) => x.status === 'Closed').length} icon="check-circle" tone="ok" />
      </div>
      <div className="card">
        <div className="card-head" style={{ gap: 10 }}>
          <div className="h-sec" style={{ fontSize: 14 }}>Quality register</div><span className="spacer" />
          <select value={status} onChange={(e) => setStatus(e.target.value)} style={sel}><option value="All">All</option><option>Open</option><option>Closed</option></select>
        </div>
        <table className="tbl">
          <thead><tr><th>Ref</th><th>Nonconformity</th><th>Source</th><th>Severity</th><th>Owner</th><th>Due</th><th>Stage</th><th style={{ width: 210 }}></th></tr></thead>
          <tbody>
            {list.map((x) => (
              <tr key={x.id}>
                <td className="mono cell-strong" style={{ fontSize: 11.5 }}>{x.id}</td>
                <td style={{ fontSize: 12.5, maxWidth: 300 }}>{x.title}{x.desc ? <div className="faint" style={{ fontSize: 11, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 300 }} title={x.desc}>{x.desc}</div> : null}</td>
                <td><span className="tag">{x.source}</span></td>
                <td><Badge kind={x.severity === 'Major' ? 'bad' : 'warn'}>{x.severity}</Badge></td>
                <td>{x.owner ? <div className="row" style={{ gap: 6 }}><Avatar id={x.owner} size="sm" /><span style={{ fontSize: 12 }}>{((DB.staffById(x.owner) || {}).name || '').split(' ')[0]}</span></div> : '—'}</td>
                <td><span style={{ fontSize: 12, fontWeight: 600, color: new Date(x.due) < DB.TODAY && x.status !== 'Closed' ? 'var(--bad)' : 'var(--text)' }}>{DB.fmt(x.due)}</span></td>
                <td><Badge kind={x.stage === 'Closed' ? 'ok' : x.stage === 'Open' ? 'warn' : 'info'} dot={x.stage !== 'Closed'}>{x.stage}</Badge></td>
                <td>
                  <div className="row" style={{ gap: 6, justifyContent: 'flex-end' }}>
                    {x.status !== 'Closed' && <Btn size="sm" variant="primary" onClick={() => nextStage(x)}>{NCR_STAGES[NCR_STAGES.indexOf(x.stage) + 1] === 'Closed' ? 'Close' : '→ ' + NCR_STAGES[NCR_STAGES.indexOf(x.stage) + 1]}</Btn>}
                    <Btn size="sm" variant="ghost" onClick={() => openNcrForm(x)}>Edit</Btn>
                    <Btn size="sm" variant="ghost" style={{ color: 'var(--bad)' }} onClick={() => { if (window.confirm('Delete ' + x.id + '? This removes it from the quality register permanently.')) dbRemove('capa', 'id', x.id); }}>Del</Btn>
                  </div>
                </td>
              </tr>
            ))}
            {list.length === 0 && <tr><td colSpan={8} className="faint" style={{ textAlign: 'center', padding: 26 }}>{DB.capa.length === 0 ? 'No nonconformities logged — a clean register. Failed calibrations raise an NCR automatically, or use “Raise NCR” to log one manually.' : 'No records match this filter.'}</td></tr>}
          </tbody>
        </table>
      </div>
    </div>
  );
}

/* =====================================================================
   M5 — Document Control (per ADAM QMS numbering)
   ===================================================================== */
function openDocForm(rec) {
  const DB = window.DB; const e = rec || {};
  openForm({
    title: rec ? 'Edit ' + e.code : 'New controlled document', icon: 'documents',
    sub: 'ADAM document control — ISO/IEC 17025 §8.3 / ISO 9001 §7.5',
    submitLabel: rec ? 'Save changes' : 'Create (draft)', done: rec ? 'Document updated' : 'Draft created',
    fields: [
      { key: 'title', label: 'Document title', type: 'text', full: true, value: e.title },
      { key: 'code', label: 'Document no.', type: 'text', value: e.code, placeholder: 'ADM-QMS-L2-P-011', half: true },
      { key: 'type', label: 'Type', type: 'select', options: calOpt('docTypes'), value: e.type || 'Procedure', half: true },
      { key: 'level', label: 'Level', type: 'select', options: calOpt('docLevels'), value: e.level || 'L2', half: true },
      { key: 'owner', label: 'Owner', type: 'select', options: _staffOpts(), value: e.owner ? _staffName(e.owner) : ((CAL.user || {}).name || _staffOpts()[0]), half: true },
      { key: 'review', label: 'Next review', type: 'date', value: _dstr(e.review || DB.addM(DB.TODAY, 12)), half: true },
    ],
    onSave: (v) => {
      const owner = _staffId(v.owner);
      const patch = { title: v.title, type: v.type, level: v.level, owner, review: v.review ? new Date(v.review) : DB.addM(DB.TODAY, 12) };
      if (rec) dbUpdate('docReg', 'code', e.code, patch);
      else dbAdd('docReg', Object.assign({ code: v.code || ('ADM-QMS-' + v.level + '-P-0' + (10 + DB.docReg.length)), ver: 'v1', status: 'Draft' }, patch));
    },
  });
}
function DocsModule() {
  const DB = window.DB;
  const [q, setQ] = useState('');
  const list = DB.docReg.filter((d) => !q || (d.code + ' ' + d.title).toLowerCase().includes(q.toLowerCase()));
  const bump = (v) => 'v' + ((parseInt(String(v).replace(/\D/g, ''), 10) || 1) + 1);
  return (
    <div className="page page-wide rise">
      <PageHead title="Document Control" sub={'Controlled documents of ' + DB.company.short + ' · draft → published → revision → obsolete'}>
        <Btn variant="ghost" icon="download" size="sm" onClick={() => csvExport('documents.csv', ['code', 'title', 'type', 'level', 'ver', 'status'], DB.docReg)}>Export</Btn>
        <Btn variant="primary" icon="plus" onClick={() => openDocForm()}>New document</Btn>
      </PageHead>
      <ModuleGuide module="documents" onNav={()=>{}} />
      <div className="kpi-grid" style={{ gridTemplateColumns: 'repeat(4,1fr)', marginBottom: 16 }}>
        <Kpi label="Controlled documents" value={DB.docReg.length} icon="documents" tone="info" />
        <Kpi label="Published" value={DB.docReg.filter((d) => d.status === 'Published').length} icon="check-circle" tone="ok" />
        <Kpi label="Draft / in review" value={DB.docReg.filter((d) => d.status !== 'Published' && d.status !== 'Obsolete').length} icon="clock" tone="warn" />
        <Kpi label="Review due 90d" value={DB.docReg.filter((d) => d.review && (new Date(d.review) - DB.TODAY) / 864e5 < 90 && d.status === 'Published').length} icon="calendar" tone="warn" />
      </div>
      <div className="card">
        <div className="card-head" style={{ gap: 10 }}>
          <div className="h-sec" style={{ fontSize: 14 }}>Document register</div><span className="spacer" />
          <div className="searchbox" style={{ width: 220 }}><Icon name="search" size={14} /><input placeholder="Number or title…" value={q} onChange={(e) => setQ(e.target.value)} style={{ fontSize: 12.5 }} /></div>
        </div>
        <table className="tbl">
          <thead><tr><th>Document no.</th><th>Title</th><th>Type</th><th>Level</th><th>Version</th><th>Owner</th><th>Next review</th><th>Status</th><th style={{ width: 220 }}></th></tr></thead>
          <tbody>
            {list.map((d) => (
              <tr key={d.code}>
                <td className="mono cell-strong" style={{ fontSize: 11.5 }}>{d.code}</td>
                <td style={{ fontSize: 12.5, maxWidth: 280 }}>{d.title}</td>
                <td><span className="tag">{d.type}</span></td>
                <td className="mono" style={{ fontSize: 11.5 }}>{d.level}</td>
                <td className="mono" style={{ fontSize: 11.5 }}>{d.ver}</td>
                <td>{d.owner ? <div className="row" style={{ gap: 6 }}><Avatar id={d.owner} size="sm" /><span style={{ fontSize: 12 }}>{((DB.staffById(d.owner) || {}).name || '').split(' ')[0]}</span></div> : '—'}</td>
                <td className="faint" style={{ fontSize: 12 }}>{d.review ? DB.fmt(d.review) : '—'}</td>
                <td><StatusBadge status={d.status} /></td>
                <td>
                  <div className="row" style={{ gap: 6, justifyContent: 'flex-end' }}>
                    {d.status === 'Draft' && <Btn size="sm" variant="primary" onClick={() => { dbUpdate('docReg', 'code', d.code, { status: 'Published' }); window.toast(d.code + ' published', 'ok'); }}>Publish</Btn>}
                    {d.status === 'Published' && <Btn size="sm" variant="ghost" onClick={() => { dbUpdate('docReg', 'code', d.code, { ver: bump(d.ver), review: DB.addM(DB.TODAY, 12) }); window.toast(d.code + ' revised → ' + bump(d.ver), 'ok'); }}>New revision</Btn>}
                    <Btn size="sm" variant="ghost" onClick={() => openDocForm(d)}>Edit</Btn>
                    {d.status !== 'Obsolete' && <Btn size="sm" variant="ghost" style={{ color: 'var(--bad)' }} onClick={() => dbUpdate('docReg', 'code', d.code, { status: 'Obsolete' })}>Obsolete</Btn>}
                  </div>
                </td>
              </tr>
            ))}
            {list.length === 0 && <tr><td colSpan={9} className="faint" style={{ textAlign: 'center', padding: 26 }}>{DB.docReg.length === 0 ? 'No controlled documents yet. Add your calibration procedures, SOPs and manuals with “New document”.' : 'No documents match your search.'}</td></tr>}
          </tbody>
        </table>
      </div>
    </div>
  );
}

/* =====================================================================
   M6 — Reporting (computed live) + CSV exports
   ===================================================================== */
function ReportsModule() {
  const DB = window.DB;
  const norm = DB.monitors.map((m) => Object.assign({}, m, { status: m.status === 'Active' ? 'Valid' : m.status }));
  const compliance = (ms) => { const act = ms.filter((m) => m.status !== 'Retired'); return act.length ? Math.round(act.filter((m) => m.status === 'Valid' || m.status === 'Due soon').length / act.length * 100) : 100; };
  const custRows = DB.customers.map((c) => { const ms = norm.filter((m) => m.customer === c.id); return { c, n: ms.length, od: ms.filter((m) => m.status === 'Overdue').length, pct: compliance(ms) }; }).sort((a, b) => a.pct - b.pct);
  const wsAll = DB.worksheets || [];
  const techRows = DB.staff.filter((s) => /tech/i.test(s.role)).map((s) => {
    const mine = wsAll.filter((w) => w.tech === s.name);
    return { s, done: mine.length, pass: mine.filter((w) => w.result === 'Pass').length };
  });
  const stageRows = JOB_STAGES.map((st) => ({ st, n: DB.jobs.filter((j) => j.stage === st).length }));
  return (
    <div className="page page-wide rise">
      <PageHead title="Reporting" sub="Operational & compliance reporting — computed live from laboratory data">
        <Btn variant="ghost" icon="documents" size="sm" onClick={() => window.print()}>Print</Btn>
      </PageHead>
      <div className="kpi-grid" style={{ gridTemplateColumns: 'repeat(5,1fr)', marginBottom: 16 }}>
        <Kpi label="Fleet compliance" value={compliance(norm)} unit="%" icon="shield" tone="ok" />
        <Kpi label="Certificates issued" value={DB.certificates.length} icon="certificate" tone="info" />
        <Kpi label="Worksheets completed" value={wsAll.length} icon="clipboard" tone="info" />
        <Kpi label="Open NCRs" value={DB.capa.filter((x) => x.status !== 'Closed').length} icon="flag" tone="warn" />
        <Kpi label="Reminders sent" value={(DB.reminderLog || []).filter((l) => l.status === 'Sent').length} icon="bell" tone="info" />
      </div>
      <div className="dash-ops" style={{ display: 'grid', gridTemplateColumns: '1.3fr 1fr', gap: 16, alignItems: 'start', marginBottom: 16 }}>
        <div className="card">
          <div className="card-head"><Icon name="briefcase" size={16} style={{ color: 'var(--primary-600)' }} /><div className="h-sec" style={{ fontSize: 14 }}>Compliance by customer</div></div>
          <table className="tbl">
            <thead><tr><th>Customer</th><th>Assets</th><th>Overdue</th><th>Compliance</th></tr></thead>
            <tbody>{custRows.map((r) => (
              <tr key={r.c.id}>
                <td><div className="row" style={{ gap: 7 }}><span className="avatar avatar-sm" style={{ background: r.c.color, borderRadius: 6 }}>{r.c.short}</span><span style={{ fontSize: 12.5, fontWeight: 600 }}>{r.c.name}</span></div></td>
                <td className="mono" style={{ fontSize: 12.5 }}>{r.n}</td>
                <td>{r.od ? <Badge kind="bad">{r.od}</Badge> : <Badge kind="ok">0</Badge>}</td>
                <td><div className="row" style={{ gap: 8 }}><div style={{ width: 90 }}><Bar pct={r.pct} color={r.pct >= 90 ? 'var(--ok)' : r.pct >= 70 ? 'var(--warn)' : 'var(--bad)'} /></div><span className="mono" style={{ fontSize: 12, fontWeight: 700 }}>{r.pct}%</span></div></td>
              </tr>
            ))}</tbody>
          </table>
        </div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
          <div className="card">
            <div className="card-head"><Icon name="workflow" size={15} style={{ color: 'var(--primary-600)' }} /><div className="h-sec" style={{ fontSize: 13.5 }}>Jobs by stage</div></div>
            <div className="card-body" style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
              {stageRows.map((r) => (
                <div key={r.st} className="row" style={{ gap: 9, fontSize: 12 }}>
                  <span style={{ width: 90, color: 'var(--text-2)', fontWeight: 600 }}>{r.st}</span>
                  <div style={{ flex: 1, height: 14, borderRadius: 4, background: 'var(--bg)', overflow: 'hidden' }}><div style={{ width: (r.n / Math.max(1, DB.jobs.length) * 100) + '%', height: '100%', background: 'var(--primary-600)' }} /></div>
                  <span className="mono cell-strong" style={{ width: 20, textAlign: 'right' }}>{r.n}</span>
                </div>
              ))}
            </div>
          </div>
          <div className="card">
            <div className="card-head"><Icon name="users" size={15} style={{ color: 'var(--primary-600)' }} /><div className="h-sec" style={{ fontSize: 13.5 }}>Technician output</div></div>
            <table className="tbl"><thead><tr><th>Technician</th><th>Worksheets</th><th>Pass rate</th></tr></thead>
              <tbody>{techRows.map((t) => (
                <tr key={t.s.id}><td><div className="row" style={{ gap: 7 }}><Avatar id={t.s.id} size="sm" /><span style={{ fontSize: 12.5 }}>{t.s.name}</span></div></td>
                  <td className="mono" style={{ fontSize: 12.5 }}>{t.done}</td>
                  <td className="mono" style={{ fontSize: 12.5 }}>{t.done ? Math.round(t.pass / t.done * 100) + '%' : '—'}</td></tr>
              ))}</tbody></table>
          </div>
        </div>
      </div>
      <div className="card card-pad">
        <div className="eyebrow" style={{ marginBottom: 10 }}>Data exports (CSV)</div>
        <div className="row" style={{ gap: 10, flexWrap: 'wrap' }}>
          <Btn size="sm" icon="download" onClick={() => csvExport('assets.csv', ['id', 'model', 'serial', 'customer', 'location', 'status'], norm.map((m) => ({ ...m, customer: (DB.custById(m.customer) || {}).name })))}>Asset register</Btn>
          <Btn size="sm" icon="download" onClick={() => csvExport('certificates.csv', ['id', 'monitor', 'model', 'serial', 'customer', 'status'], DB.certificates.map((x) => ({ ...x, customer: (DB.custById(x.customer) || {}).name })))}>Certificates</Btn>
          <Btn size="sm" icon="download" onClick={() => csvExport('ncr-capa.csv', ['id', 'title', 'source', 'severity', 'stage', 'status'], DB.capa)}>NCR / CAPA</Btn>
          <Btn size="sm" icon="download" onClick={() => csvExport('reminder-log.csv', ['id', 'date', 'to', 'subject', 'count', 'status'], DB.reminderLog || [])}>Reminder log</Btn>
          <Btn size="sm" icon="download" onClick={() => csvExport('documents.csv', ['code', 'title', 'type', 'level', 'ver', 'status'], DB.docReg)}>Documents</Btn>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { JobsModule, CertsModule, QMSModule, DocsModule, ReportsModule, printCertificate, openWorksheet, issueCertificate, computeReadiness, calGuide, GuidePanel, ModuleGuide, newJobFlow, openNcrForm, openDocForm, calReadiness: computeReadiness });
