// approval-workflow.jsx — Module 2: multi-step invoice approval workflow

// ── Constants ───────────────────────────────────────────────────────────────
const ROLES = ['ESTIMATOR', 'PROJECT_MANAGER', 'ACCOUNTING', 'EXECUTIVE', 'ADMIN'];

const APPROVAL_STATUS = {
  PARSED:          'PARSED',
  PENDING_REVIEW:  'PENDING_REVIEW',
  DISPUTED:        'DISPUTED',
  APPROVED_L1:     'APPROVED_L1',
  APPROVED_L2:     'APPROVED_L2',
  APPROVED_L3:     'APPROVED_L3',
  APPROVED_FINAL:  'APPROVED_FINAL',
  PAID:            'PAID',
};

const STATUS_LABEL = {
  PARSED:         'Parsed',
  PENDING_REVIEW: 'Pending Review',
  DISPUTED:       'Disputed',
  APPROVED_L1:    'Approved L1',
  APPROVED_L2:    'Approved L2',
  APPROVED_L3:    'Approved L3',
  APPROVED_FINAL: 'Approved',
  PAID:           'Paid',
};

const STATUS_TONE = {
  PARSED:         'info',
  PENDING_REVIEW: 'warn',
  DISPUTED:       'bad',
  APPROVED_L1:    'info',
  APPROVED_L2:    'info',
  APPROVED_L3:    'info',
  APPROVED_FINAL: 'ok',
  PAID:           'ok',
};

const ROLE_LABEL = {
  ESTIMATOR:       'Estimator',
  PROJECT_MANAGER: 'Project Manager',
  ACCOUNTING:      'Accounting',
  EXECUTIVE:       'Executive',
  ADMIN:           'Admin',
};

// ── Routing helpers ─────────────────────────────────────────────────────────
function getRequiredSteps(totalCents, hasHardFlag, hasNewChargesOver1000) {
  const total = totalCents / 100;
  const steps = [{ level: 1, role: 'PROJECT_MANAGER' }];
  if (total > 5000 || hasHardFlag) steps.push({ level: 2, role: 'ACCOUNTING' });
  if (total > 25000)               steps.push({ level: 3, role: 'EXECUTIVE' });
  if (hasNewChargesOver1000 && steps.findIndex((s) => s.role === 'ACCOUNTING') === -1) {
    steps.push({ level: steps.length + 1, role: 'ACCOUNTING' });
  }
  return steps;
}

function canApprove(userRole, requiredRole) {
  if (!userRole || !requiredRole) return false;
  if (userRole === 'ADMIN') return true;
  return userRole === requiredRole;
}

function getNextStatus(current, completedSteps, requiredSteps) {
  if (completedSteps.length >= requiredSteps.length) return 'APPROVED_FINAL';
  const next = completedSteps.length + 1;
  if (next <= 3) return `APPROVED_L${next}`;
  return 'APPROVED_FINAL';
}

function getCurrentRequiredRole(doc) {
  const done = (doc.completedSteps || []).length;
  const req  = doc.requiredSteps || [];
  if (done >= req.length) return null;
  return req[done]?.role || null;
}

function isOverdue(doc) {
  if (!doc.submittedAt || doc.status === 'PAID' || doc.status === 'APPROVED_FINAL') return false;
  const submittedMs = doc.submittedAt?.toDate?.()?.getTime() || new Date(doc.submittedAt).getTime();
  return Date.now() - submittedMs > 5 * 24 * 60 * 60 * 1000;
}

function makeAuditEntry(action, userInfo, note) {
  return {
    action,
    actor: { uid: userInfo.uid, displayName: userInfo.displayName || userInfo.email, role: userInfo.role || 'UNKNOWN' },
    timestamp: new Date().toISOString(),
    note: note || '',
  };
}

// ── Firestore operations ─────────────────────────────────────────────────────
function approvalColRef(userId) {
  return fbDb.collection('users').doc(userId).collection('approvalInvoices');
}

async function submitForApproval(userId, userInfo, jobId, jobNumber, invoice) {
  const totalCents       = Math.round(grandTotal(invoice) * 100);
  const varianceResult   = computeVariances(invoice, [invoice], DEFAULT_TOLERANCES);
  const hasHardFlag      = (varianceResult?.summary?.byStatus?.HARD_FLAG || 0) > 0
                        || (varianceResult?.summary?.byStatus?.CRITICAL   || 0) > 0;
  const newChargesOver1k = invoice.items?.some((it) => it._new && it.qty * it.unit > 1000) || false;
  const requiredSteps    = getRequiredSteps(totalCents, hasHardFlag, newChargesOver1k);
  const docId            = `${jobId}__${invoice.id}`;

  await approvalColRef(userId).doc(docId).set({
    jobId, jobNumber,
    invoiceId:    invoice.id,
    invoiceData:  JSON.parse(JSON.stringify(invoice, (k, v) => v === undefined ? null : v)),
    status:       APPROVAL_STATUS.PENDING_REVIEW,
    totalCents,
    submittedAt:  firebase.firestore.FieldValue.serverTimestamp(),
    submittedBy:  { uid: userInfo.uid, displayName: userInfo.displayName || userInfo.email },
    requiredSteps,
    completedSteps:      [],
    comments:            [],
    auditTrail:          [makeAuditEntry('SUBMITTED', userInfo)],
    hasHardFlag,
    newChargesOver1k,
    changeOrderRequired: newChargesOver1k,
    changeOrderDoc:      null,
    paymentRef:          null,
    disputeReason:       null,
  });
  return docId;
}

async function approveStep(userId, docId, doc, userInfo, note) {
  const completedSteps = [
    ...(doc.completedSteps || []),
    {
      level: (doc.completedSteps || []).length + 1,
      role:  getCurrentRequiredRole(doc),
      approvedBy: { uid: userInfo.uid, displayName: userInfo.displayName || userInfo.email, role: userInfo.role },
      approvedAt: new Date().toISOString(),
      note: note || '',
    },
  ];
  const newStatus = getNextStatus(doc.status, completedSteps, doc.requiredSteps || []);
  await approvalColRef(userId).doc(docId).update({
    status: newStatus,
    completedSteps,
    auditTrail: firebase.firestore.FieldValue.arrayUnion(makeAuditEntry('APPROVED', userInfo, note)),
  });
}

async function disputeApproval(userId, docId, reason, userInfo) {
  await approvalColRef(userId).doc(docId).update({
    status:        APPROVAL_STATUS.DISPUTED,
    disputeReason: reason,
    auditTrail:    firebase.firestore.FieldValue.arrayUnion(makeAuditEntry('DISPUTED', userInfo, reason)),
  });
}

async function resubmitToReview(userId, docId, userInfo) {
  await approvalColRef(userId).doc(docId).update({
    status:        APPROVAL_STATUS.PENDING_REVIEW,
    disputeReason: null,
    auditTrail:    firebase.firestore.FieldValue.arrayUnion(makeAuditEntry('RESUBMITTED', userInfo)),
  });
}

async function markPaid(userId, docId, paymentRef, userInfo) {
  await approvalColRef(userId).doc(docId).update({
    status:     APPROVAL_STATUS.PAID,
    paymentRef: paymentRef || '',
    paidAt:     firebase.firestore.FieldValue.serverTimestamp(),
    auditTrail: firebase.firestore.FieldValue.arrayUnion(makeAuditEntry('PAID', userInfo, paymentRef)),
  });
}

async function uploadChangeOrder(userId, docId, docName, userInfo) {
  await approvalColRef(userId).doc(docId).update({
    changeOrderDoc:      { name: docName, uploadedAt: new Date().toISOString() },
    changeOrderRequired: false,
    auditTrail:          firebase.firestore.FieldValue.arrayUnion(makeAuditEntry('CHANGE_ORDER_UPLOADED', userInfo, docName)),
  });
}

// ── ApprovalStatusPill ──────────────────────────────────────────────────────
function ApprovalStatusPill({ status, size }) {
  const tone  = STATUS_TONE[status] || 'info';
  const label = STATUS_LABEL[status] || status;
  return (
    <span className={`status status-${tone}${size === 'sm' ? ' status-sm' : ''}`}>
      <span className="status-dot" />{label}
    </span>
  );
}

// ── ApprovalChain ───────────────────────────────────────────────────────────
function ApprovalChain({ requiredSteps, completedSteps, status }) {
  if (!requiredSteps || requiredSteps.length === 0) return null;
  const completed = completedSteps || [];
  const isDisputed = status === 'DISPUTED';
  const isFinal    = status === 'APPROVED_FINAL' || status === 'PAID';

  return (
    <div className="ach">
      {requiredSteps.map((step, idx) => {
        const done = completed[idx];
        const isCurrent = !isDisputed && !isFinal && completed.length === idx;
        const state = done ? 'done' : isCurrent ? 'current' : 'pending';
        return (
          <React.Fragment key={idx}>
            {idx > 0 && <div className={`ach-line ${completed.length > idx ? 'ach-line-done' : ''}`} />}
            <div className={`ach-step ach-step-${state}`}>
              <div className="ach-dot">
                {done
                  ? <Icon name="check" size={10} />
                  : isCurrent ? <span className="ach-dot-pulse" /> : null
                }
              </div>
              <div className="ach-info">
                <div className="ach-role">{ROLE_LABEL[step.role] || step.role}</div>
                {done
                  ? <div className="ach-who">{done.approvedBy?.displayName} · {done.approvedAt ? new Date(done.approvedAt).toLocaleDateString() : ''}</div>
                  : <div className="ach-who">{isCurrent ? 'Awaiting approval' : 'Pending'}</div>
                }
                {done?.note && <div className="ach-note">"{done.note}"</div>}
              </div>
            </div>
          </React.Fragment>
        );
      })}
      {(isFinal || status === 'PAID') && (
        <>
          <div className="ach-line ach-line-done" />
          <div className="ach-step ach-step-done">
            <div className="ach-dot"><Icon name="check" size={10} /></div>
            <div className="ach-info">
              <div className="ach-role">{status === 'PAID' ? 'Payment Recorded' : 'Final Approval'}</div>
            </div>
          </div>
        </>
      )}
    </div>
  );
}

// ── AuditTrailPanel ─────────────────────────────────────────────────────────
function AuditTrailPanel({ trail }) {
  if (!trail || trail.length === 0) {
    return <div className="atp-empty">No audit events yet.</div>;
  }
  const sorted = [...trail].sort((a, b) => {
    const ta = a.timestamp?.toDate?.()?.getTime() || 0;
    const tb = b.timestamp?.toDate?.()?.getTime() || 0;
    return ta - tb;
  });
  const ACTION_ICON = {
    SUBMITTED: 'upload', APPROVED: 'check', DISPUTED: 'flag',
    RESUBMITTED: 'reset', PAID: 'arrow', CHANGE_ORDER_UPLOADED: 'file',
  };
  return (
    <div className="atp">
      {sorted.map((ev, i) => (
        <div key={i} className="atp-row">
          <div className="atp-icon-wrap">
            <Icon name={ACTION_ICON[ev.action] || 'history'} size={12} />
          </div>
          <div className="atp-body">
            <div className="atp-action">{ev.action.replace(/_/g, ' ')}</div>
            <div className="atp-meta">
              <span>{ev.actor?.displayName || ev.actor?.uid || '—'}</span>
              {ev.actor?.role && <span className="atp-role">{ROLE_LABEL[ev.actor.role] || ev.actor.role}</span>}
              {ev.timestamp?.toDate && (
                <span className="mono atp-ts">{ev.timestamp.toDate().toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}</span>
              )}
            </div>
            {ev.note && <div className="atp-note">"{ev.note}"</div>}
          </div>
        </div>
      ))}
    </div>
  );
}

// ── InvoiceDetailDrawer ──────────────────────────────────────────────────────
function InvoiceDetailDrawer({ doc, userInfo, onClose, onAction }) {
  const [tab, setTab]         = React.useState('detail');
  const [note, setNote]       = React.useState('');
  const [payRef, setPayRef]   = React.useState('');
  const [coName, setCoName]   = React.useState('');
  const [working, setWorking] = React.useState(false);
  const [dispReason, setDispReason] = React.useState('');
  const [showDisp, setShowDisp]     = React.useState(false);
  const [showPay, setShowPay]       = React.useState(false);

  const inv         = doc.invoiceData || {};
  const requiredRole = getCurrentRequiredRole(doc);
  const userCanApprove = canApprove(userInfo.role, requiredRole);
  const isAccounting   = userInfo.role === 'ACCOUNTING' || userInfo.role === 'ADMIN';
  const overdue        = isOverdue(doc);

  const act = async (fn) => {
    setWorking(true);
    try { await fn(); onAction(); }
    catch (e) { alert('Action failed: ' + e.message); }
    finally { setWorking(false); }
  };

  const fmtDate = (ts) => ts?.toDate?.()?.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) || '—';

  return (
    <div className="idd-overlay" onClick={(e) => e.target === e.currentTarget && onClose()}>
      <div className="idd" role="dialog" aria-modal="true">
        {/* Header */}
        <div className="idd-hd">
          <div className="idd-hd-l">
            <div className="idd-tag mono">INVOICE {doc.invoiceId}</div>
            <div className="idd-job mono">{doc.jobNumber}</div>
          </div>
          <div className="idd-hd-r">
            <ApprovalStatusPill status={doc.status} />
            {overdue && <span className="idd-overdue">Overdue</span>}
            <button className="btn btn-ghost btn-sm" onClick={onClose}><Icon name="x" size={13}/></button>
          </div>
        </div>

        {/* Key info row */}
        <div className="idd-meta">
          <span className="num idd-total">{fmtUSD(doc.totalCents / 100)}</span>
          <span className="idd-sep">·</span>
          <span>{inv.vendor || '—'}</span>
          <span className="idd-sep">·</span>
          <span className="mono">{inv.date || '—'}</span>
          <span className="idd-sep">·</span>
          <span>Submitted {fmtDate(doc.submittedAt)}</span>
          {doc.hasHardFlag && <span className="idd-flag-chip">Hard Flag</span>}
          {doc.changeOrderRequired && !doc.changeOrderDoc && (
            <span className="idd-flag-chip idd-co-warn">Change order required</span>
          )}
        </div>

        {/* Approval chain */}
        <div className="idd-chain-wrap">
          <ApprovalChain
            requiredSteps={doc.requiredSteps}
            completedSteps={doc.completedSteps}
            status={doc.status}
          />
        </div>

        {/* Tabs */}
        <div className="idd-tabs">
          {[['detail', 'Line Items'], ['audit', 'Audit Trail']].map(([k, l]) => (
            <button key={k} className={`idd-tab ${tab === k ? 'is-on' : ''}`} onClick={() => setTab(k)}>{l}</button>
          ))}
        </div>

        {/* Tab content */}
        <div className="idd-body">
          {tab === 'detail' && (
            <div className="idd-items">
              <div className="idd-items-head">
                <div>Description</div>
                <div className="num">Qty</div>
                <div className="num">Unit</div>
                <div className="num">Total</div>
              </div>
              {(inv.items || []).map((it, i) => (
                <div key={i} className={`idd-item-row ${it._new ? 'idd-item-new' : ''}`}>
                  <div className="idd-item-desc">
                    {it.desc || it.sku || '—'}
                    {it._new && <span className="idd-new-badge">New</span>}
                  </div>
                  <div className="num">{it.qty}</div>
                  <div className="num">{fmtUSD(it.unit)}</div>
                  <div className="num">{fmtUSD(it.qty * it.unit)}</div>
                </div>
              ))}
              <div className="idd-items-foot">
                <div>Total</div>
                <div className="num">{fmtUSD(doc.totalCents / 100)}</div>
              </div>
            </div>
          )}
          {tab === 'audit' && <AuditTrailPanel trail={doc.auditTrail} />}
        </div>

        {/* Dispute inline */}
        {showDisp && (
          <div className="idd-disp">
            <textarea className="textarea" placeholder="Reason for dispute…" value={dispReason} onChange={(e) => setDispReason(e.target.value)} />
            <div className="idd-disp-btns">
              <button className="btn btn-ghost btn-sm" onClick={() => setShowDisp(false)}>Cancel</button>
              <button className="btn btn-bad btn-sm" disabled={!dispReason.trim() || working}
                onClick={() => act(() => disputeApproval(userInfo.userId, doc._id, dispReason.trim(), userInfo))}>
                Confirm Dispute
              </button>
            </div>
          </div>
        )}

        {/* Pay inline */}
        {showPay && (
          <div className="idd-disp">
            <input className="input" placeholder="Payment reference # (optional)" value={payRef} onChange={(e) => setPayRef(e.target.value)} />
            <div className="idd-disp-btns">
              <button className="btn btn-ghost btn-sm" onClick={() => setShowPay(false)}>Cancel</button>
              <button className="btn btn-ok btn-sm" disabled={working}
                onClick={() => act(() => markPaid(userInfo.userId, doc._id, payRef, userInfo))}>
                Confirm Payment
              </button>
            </div>
          </div>
        )}

        {/* Change order upload */}
        {doc.changeOrderRequired && !doc.changeOrderDoc && (
          <div className="idd-co">
            <Icon name="flag" size={13} />
            <span>Change order document required for new charges &gt;$1,000</span>
            <input
              className="input idd-co-input"
              placeholder="Document name / reference"
              value={coName}
              onChange={(e) => setCoName(e.target.value)}
            />
            <button className="btn btn-sm" disabled={!coName.trim() || working}
              onClick={() => act(() => uploadChangeOrder(userInfo.userId, doc._id, coName.trim(), userInfo))}>
              <Icon name="save" size={12}/> Submit
            </button>
          </div>
        )}

        {/* Action bar */}
        <div className="idd-actions">
          {doc.status === 'PENDING_REVIEW' && !showDisp && !showPay && (
            <>
              {userCanApprove && !doc.changeOrderRequired && (
                <>
                  <input
                    className="input idd-note-input"
                    placeholder="Approval note (optional)"
                    value={note}
                    onChange={(e) => setNote(e.target.value)}
                  />
                  <button className="btn btn-ok" disabled={working}
                    onClick={() => act(() => approveStep(userInfo.userId, doc._id, doc, userInfo, note))}>
                    <Icon name="check" size={14}/> Approve
                  </button>
                </>
              )}
              {!showDisp && (
                <button className="btn btn-bad" disabled={working} onClick={() => setShowDisp(true)}>
                  <Icon name="flag" size={14}/> Dispute
                </button>
              )}
              {!userCanApprove && requiredRole && (
                <span className="idd-role-hint">
                  Awaiting {ROLE_LABEL[requiredRole] || requiredRole} approval — your role is {ROLE_LABEL[userInfo.role] || userInfo.role || 'unset'}.
                </span>
              )}
            </>
          )}
          {doc.status === 'DISPUTED' && (
            <>
              <div className="idd-disp-reason"><Icon name="flag" size={13}/> "{doc.disputeReason}"</div>
              <button className="btn" disabled={working}
                onClick={() => act(() => resubmitToReview(userInfo.userId, doc._id, userInfo))}>
                <Icon name="reset" size={13}/> Acknowledge &amp; Resubmit
              </button>
            </>
          )}
          {doc.status === 'APPROVED_FINAL' && isAccounting && !showPay && (
            <button className="btn btn-ok" onClick={() => setShowPay(true)}>
              <Icon name="arrow" size={14}/> Record Payment
            </button>
          )}
          {doc.status === 'PAID' && (
            <div className="idd-paid">
              <Icon name="check" size={14}/> Paid{doc.paymentRef ? ` · Ref: ${doc.paymentRef}` : ''}
            </div>
          )}
        </div>

        <ApprovalWorkflowStyles />
      </div>
    </div>
  );
}

// ── ApprovalQueueCard ────────────────────────────────────────────────────────
function ApprovalQueueCard({ doc, onClick }) {
  const overdue  = isOverdue(doc);
  const tone     = STATUS_TONE[doc.status] || 'info';
  const fmtDate  = (ts) => ts?.toDate?.()?.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) || '—';
  const reqRole  = getCurrentRequiredRole(doc);

  return (
    <div
      className={`aqc ${overdue ? 'aqc-overdue' : ''}`}
      onClick={onClick}
      role="button"
      tabIndex={0}
      onKeyDown={(e) => e.key === 'Enter' && onClick()}
    >
      <div className="aqc-top">
        <span className="aqc-id mono">{doc.invoiceId}</span>
        <div className="aqc-top-r">
          <ApprovalStatusPill status={doc.status} size="sm" />
          {overdue && <span className="aqc-overdue-chip">Overdue</span>}
        </div>
      </div>
      <div className="aqc-job mono">{doc.jobNumber}</div>
      <div className="aqc-meta">
        <span className="num aqc-total">{fmtUSD(doc.totalCents / 100)}</span>
        {doc.hasHardFlag && <span className="aqc-flag"><Icon name="flag" size={10}/> Flagged</span>}
        {doc.changeOrderRequired && !doc.changeOrderDoc && (
          <span className="aqc-flag aqc-co">CO Required</span>
        )}
      </div>
      <div className="aqc-foot">
        <span>Submitted {fmtDate(doc.submittedAt)}</span>
        {reqRole && doc.status === 'PENDING_REVIEW' && (
          <span className="aqc-awaiting">Awaiting {ROLE_LABEL[reqRole] || reqRole}</span>
        )}
        <Icon name="chev" size={12} style={{ color: 'var(--fg-dim)', marginLeft: 'auto' }} />
      </div>
    </div>
  );
}

// ── NotificationBell ─────────────────────────────────────────────────────────
function NotificationBell({ userId, onNavigate }) {
  const [docs, setDocs] = React.useState([]);
  const [open, setOpen] = React.useState(false);
  const wrapRef = React.useRef(null);

  React.useEffect(() => {
    if (!userId) return;
    const unsub = approvalColRef(userId)
      .where('status', 'in', ['PENDING_REVIEW', 'DISPUTED'])
      .onSnapshot((snap) => setDocs(snap.docs.map((d) => ({ _id: d.id, ...d.data() }))), () => {});
    return unsub;
  }, [userId]);

  // Close on click outside
  React.useEffect(() => {
    if (!open) return;
    const handler = (e) => {
      if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
    };
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, [open]);

  const unread = docs.filter((d) => isOverdue(d) || d.status === 'DISPUTED').length;
  const pending = docs.filter((d) => d.status === 'PENDING_REVIEW').length;
  const total = unread + pending;

  const handleRowClick = () => {
    setOpen(false);
    onNavigate?.();
  };

  return (
    <div className="nbell" ref={wrapRef} style={{ position: 'relative' }}>
      <button className="nbell-btn" onClick={() => setOpen((v) => !v)} aria-label="Notifications">
        <Icon name="flag" size={16} />
        {total > 0 && (
          <span className="nbell-count">{total > 9 ? '9+' : total}</span>
        )}
      </button>
      {open && (
        <div className="nbell-panel">
          <div className="nbell-hd">
            <span>Approval Queue</span>
            <button className="btn btn-ghost btn-sm" onClick={() => setOpen(false)}><Icon name="x" size={12}/></button>
          </div>
          {docs.length === 0 ? (
            <div className="nbell-empty">No pending items</div>
          ) : (
            docs.slice(0, 6).map((d) => (
              <button key={d._id} className="nbell-row nbell-row-btn" onClick={handleRowClick}>
                <span className="mono nbell-id">{d.invoiceId}</span>
                <span className="nbell-job">{d.jobNumber}</span>
                <ApprovalStatusPill status={d.status} size="sm" />
              </button>
            ))
          )}
          {docs.length > 6 && (
            <button className="nbell-more nbell-more-btn" onClick={handleRowClick}>
              +{docs.length - 6} more — view all in Approvals
            </button>
          )}
        </div>
      )}
    </div>
  );
}

// ── ApprovalView ─────────────────────────────────────────────────────────────
function ApprovalView({ user, userInfo }) {
  const [docs, setDocs]     = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [selected, setSelected] = React.useState(null);
  const [statusFilter, setStatusFilter] = React.useState('active');
  const [refresh, setRefresh] = React.useState(0);

  React.useEffect(() => {
    if (!user?.uid) return;
    setLoading(true);
    const unsub = approvalColRef(user.uid)
      .orderBy('submittedAt', 'desc')
      .onSnapshot(
        (snap) => { setDocs(snap.docs.map((d) => ({ _id: d.id, ...d.data() }))); setLoading(false); },
        ()     => setLoading(false)
      );
    return unsub;
  }, [user?.uid, refresh]);

  const filtered = React.useMemo(() => {
    if (statusFilter === 'active') return docs.filter((d) => !['APPROVED_FINAL','PAID'].includes(d.status));
    if (statusFilter === 'approved') return docs.filter((d) => ['APPROVED_FINAL','PAID'].includes(d.status));
    return docs.filter((d) => d.status === statusFilter);
  }, [docs, statusFilter]);

  const grouped = React.useMemo(() => {
    const g = {};
    for (const d of filtered) {
      if (!g[d.status]) g[d.status] = [];
      g[d.status].push(d);
    }
    return g;
  }, [filtered]);

  const counts = React.useMemo(() => ({
    active:   docs.filter((d) => !['APPROVED_FINAL','PAID'].includes(d.status)).length,
    disputed: docs.filter((d) => d.status === 'DISPUTED').length,
    approved: docs.filter((d) => ['APPROVED_FINAL','PAID'].includes(d.status)).length,
  }), [docs]);

  const STATUS_ORDER = ['DISPUTED','PENDING_REVIEW','APPROVED_L1','APPROVED_L2','APPROVED_L3'];

  return (
    <div className="av">
      <div className="av-hd">
        <div>
          <div className="av-eyebrow mono">APPROVALS</div>
          <h1 className="av-title">Invoice Approval Queue</h1>
        </div>
      </div>

      {/* Filter tabs */}
      <div className="av-tabs">
        {[['active', 'Active', counts.active], ['disputed', 'Disputed', counts.disputed], ['approved', 'Completed', counts.approved]].map(([k, l, c]) => (
          <button key={k} className={`av-tab ${statusFilter === k ? 'is-on' : ''}`} onClick={() => setStatusFilter(k)}>
            {l}
            {c > 0 && <span className="av-tab-count num">{c}</span>}
          </button>
        ))}
      </div>

      {loading ? (
        <div className="av-state"><div className="av-spin" /></div>
      ) : docs.length === 0 ? (
        <div className="av-state">
          <div className="av-empty-icon"><Icon name="check" size={32} /></div>
          <div className="av-empty-h">No invoices submitted for approval yet</div>
          <div className="av-empty-sub">Use the Submit for Approval button on any invoice in the Budget tab.</div>
        </div>
      ) : filtered.length === 0 ? (
        <div className="av-state">
          <div className="av-empty-sub">No items in this category.</div>
        </div>
      ) : (
        <div className="av-groups">
          {STATUS_ORDER.filter((s) => grouped[s]).map((s) => (
            <div key={s} className="av-group">
              <div className="av-group-hd">
                <ApprovalStatusPill status={s} />
                <span className="av-group-count num">{grouped[s].length}</span>
                {s === 'DISPUTED' && (
                  <span className="av-group-warn">
                    <Icon name="flag" size={12}/> Requires attention
                  </span>
                )}
              </div>
              <div className="av-cards">
                {[...grouped[s]]
                  .sort((a, b) => (isOverdue(b) ? 1 : 0) - (isOverdue(a) ? 1 : 0)
                               || (b.submittedAt?.toDate?.()?.getTime() || 0) - (a.submittedAt?.toDate?.()?.getTime() || 0))
                  .map((d) => (
                    <ApprovalQueueCard key={d._id} doc={d} onClick={() => setSelected(d)} />
                  ))
                }
              </div>
            </div>
          ))}
          {/* completed group */}
          {grouped['APPROVED_FINAL'] && (
            <div className="av-group">
              <div className="av-group-hd">
                <ApprovalStatusPill status="APPROVED_FINAL" />
                <span className="av-group-count num">{(grouped['APPROVED_FINAL'] || []).length + (grouped['PAID'] || []).length}</span>
              </div>
              <div className="av-cards">
                {[...(grouped['APPROVED_FINAL'] || []), ...(grouped['PAID'] || [])].map((d) => (
                  <ApprovalQueueCard key={d._id} doc={d} onClick={() => setSelected(d)} />
                ))}
              </div>
            </div>
          )}
        </div>
      )}

      {selected && (
        <InvoiceDetailDrawer
          doc={selected}
          userInfo={{ ...userInfo, userId: user.uid }}
          onClose={() => setSelected(null)}
          onAction={() => { setSelected(null); setRefresh((v) => v + 1); }}
        />
      )}

      <ApprovalWorkflowStyles />
    </div>
  );
}

const ApprovalWorkflowStyles = () => (
  <style>{`
    /* ── ApprovalStatusPill size override ────────────────────────── */
    .status-sm { height: 18px; padding: 0 7px; font-size: 10px; }

    /* ── ApprovalChain ───────────────────────────────────────────── */
    .ach { display: flex; align-items: flex-start; gap: 0; padding: 16px 0; }
    .ach-line { flex: 1; height: 2px; background: var(--border); margin: 10px 0 0; min-width: 16px; }
    .ach-line-done { background: var(--ok); }
    .ach-step { display: flex; flex-direction: column; align-items: center; gap: 6px; min-width: 90px; max-width: 130px; }
    .ach-dot {
      width: 22px; height: 22px; border-radius: 50%; border: 2px solid var(--border);
      background: var(--surface-2); display: grid; place-items: center;
      flex-shrink: 0;
    }
    .ach-step-done .ach-dot  { background: var(--ok);    border-color: var(--ok);   color: #fff; }
    .ach-step-current .ach-dot { border-color: var(--accent); background: oklch(from var(--accent) l c h / 0.15); }
    .ach-dot-pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: nbell-pulse .9s ease-in-out infinite; }
    @keyframes nbell-pulse { 0%,100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.4); opacity: .7; } }
    .ach-info { text-align: center; }
    .ach-role { font-size: 11px; font-weight: 600; color: var(--fg); }
    .ach-who  { font-size: 10.5px; color: var(--fg-dim); margin-top: 2px; }
    .ach-note { font-size: 10px; color: var(--fg-dim); font-style: italic; margin-top: 2px; max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

    /* ── Audit Trail ─────────────────────────────────────────────── */
    .atp { display: flex; flex-direction: column; gap: 0; padding: 4px 0; }
    .atp-empty { font-size: 13px; color: var(--fg-dim); padding: 16px; text-align: center; }
    .atp-row { display: flex; gap: 10px; align-items: flex-start; padding: 10px 0; border-bottom: 1px dashed var(--border); }
    .atp-row:last-child { border-bottom: none; }
    .atp-icon-wrap { width: 26px; height: 26px; border-radius: 7px; background: var(--surface-2); border: 1px solid var(--border); display: grid; place-items: center; color: var(--fg-dim); flex-shrink: 0; margin-top: 1px; }
    .atp-body { flex: 1; }
    .atp-action { font-size: 12.5px; font-weight: 500; text-transform: capitalize; }
    .atp-meta { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--fg-dim); margin-top: 2px; flex-wrap: wrap; }
    .atp-role { background: var(--surface-3); border: 1px solid var(--border); padding: 0 5px; border-radius: 3px; font-size: 10px; color: var(--fg-dim); }
    .atp-ts { font-size: 10.5px; }
    .atp-note { font-size: 11.5px; color: var(--fg-muted); margin-top: 4px; font-style: italic; }

    /* ── Invoice Detail Drawer ───────────────────────────────────── */
    .idd-overlay {
      position: fixed; inset: 0; background: oklch(0 0 0 / .55);
      z-index: 100; display: flex; align-items: stretch; justify-content: flex-end;
    }
    .idd {
      width: min(620px, 100vw); background: var(--bg);
      border-left: 1px solid var(--border); display: flex; flex-direction: column;
      overflow-y: auto; box-shadow: var(--shadow-2);
    }
    .idd-hd { display: flex; align-items: flex-start; justify-content: space-between; padding: 18px 20px 14px; border-bottom: 1px solid var(--border); }
    .idd-hd-r { display: flex; align-items: center; gap: 8px; }
    .idd-tag { font-size: 10px; letter-spacing: 0.12em; color: var(--fg-dim); }
    .idd-job { font-size: 16px; font-weight: 600; margin-top: 2px; }
    .idd-overdue { font-size: 10.5px; background: var(--bad-bg); color: var(--bad); padding: 2px 8px; border-radius: 999px; border: 1px solid oklch(from var(--bad) l calc(c*.5) h / .4); font-weight: 600; }
    .idd-meta { display: flex; align-items: center; gap: 8px; padding: 12px 20px; border-bottom: 1px solid var(--border); font-size: 13px; flex-wrap: wrap; }
    .idd-total { font-size: 17px; font-weight: 700; }
    .idd-sep { color: var(--fg-dim); }
    .idd-flag-chip { font-size: 10.5px; padding: 2px 8px; border-radius: 999px; background: var(--bad-bg); color: var(--bad); border: 1px solid oklch(from var(--bad) l calc(c*.5) h / .35); font-weight: 600; }
    .idd-co-warn { background: var(--warn-bg); color: var(--warn); border-color: oklch(from var(--warn) l calc(c*.5) h / .35); }
    .idd-chain-wrap { padding: 8px 20px 0; }
    .idd-tabs { display: flex; gap: 2px; padding: 0 20px; border-bottom: 1px solid var(--border); margin-top: 8px; }
    .idd-tab { appearance: none; border: 0; border-bottom: 2px solid transparent; background: transparent; color: var(--fg-muted); padding: 8px 14px; font: inherit; font-size: 13px; cursor: pointer; }
    .idd-tab.is-on { color: var(--fg); border-bottom-color: var(--accent); }
    .idd-body { flex: 1; padding: 16px 20px; overflow-y: auto; }
    .idd-items-head, .idd-item-row, .idd-items-foot { display: grid; grid-template-columns: 1fr 60px 80px 80px; gap: 8px; padding: 7px 0; }
    .idd-items-head { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--fg-dim); border-bottom: 1px solid var(--border); }
    .idd-item-row { font-size: 12.5px; border-bottom: 1px dashed var(--border); }
    .idd-item-row:last-child { border-bottom: none; }
    .idd-item-new { background: oklch(from var(--warn) l c h / 0.06); }
    .idd-item-desc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: flex; align-items: center; gap: 6px; }
    .idd-new-badge { font-size: 9.5px; padding: 1px 5px; border-radius: 3px; background: var(--warn-bg); color: var(--warn); flex-shrink: 0; }
    .idd-items-foot { font-weight: 600; border-top: 1px solid var(--border); margin-top: 4px; padding-top: 8px; font-size: 13.5px; display: grid; grid-template-columns: 1fr 80px; }
    .idd-disp { padding: 12px 20px; border-top: 1px solid var(--border); background: var(--surface-2); display: flex; flex-direction: column; gap: 8px; }
    .idd-disp-btns { display: flex; gap: 8px; justify-content: flex-end; }
    .idd-disp-reason { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--bad); }
    .idd-co { display: flex; align-items: center; gap: 8px; padding: 10px 20px; border-top: 1px solid var(--border); background: oklch(from var(--warn) l c h / 0.06); font-size: 12.5px; color: var(--warn); flex-wrap: wrap; }
    .idd-co-input { height: 30px; font-size: 12px; flex: 1; min-width: 180px; }
    .idd-actions { display: flex; align-items: center; gap: 10px; padding: 14px 20px; border-top: 1px solid var(--border); background: var(--surface); flex-wrap: wrap; }
    .idd-note-input { flex: 1; height: 34px; min-width: 180px; }
    .idd-role-hint { font-size: 12px; color: var(--fg-dim); font-style: italic; }
    .idd-paid { display: flex; align-items: center; gap: 8px; color: var(--ok); font-weight: 500; font-size: 13.5px; }

    /* ── Approval Queue Card ─────────────────────────────────────── */
    .aqc {
      background: var(--surface); border: 1px solid var(--border);
      border-radius: var(--r-lg); padding: 14px 16px; cursor: pointer;
      display: flex; flex-direction: column; gap: 6px;
      transition: border-color .12s, background .12s;
    }
    .aqc:hover { border-color: var(--accent); background: var(--surface-2); }
    .aqc:focus-visible { box-shadow: 0 0 0 2px var(--accent); outline: none; }
    .aqc-overdue { border-color: oklch(from var(--bad) l calc(c*.5) h / .45); }
    .aqc-overdue:hover { border-color: var(--bad); }
    .aqc-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
    .aqc-id { font-size: 13px; font-weight: 600; }
    .aqc-top-r { display: flex; align-items: center; gap: 6px; }
    .aqc-job { font-size: 11.5px; color: var(--fg-dim); }
    .aqc-meta { display: flex; align-items: center; gap: 8px; font-size: 12.5px; }
    .aqc-total { font-size: 14px; font-weight: 600; }
    .aqc-flag { display: inline-flex; align-items: center; gap: 4px; font-size: 10.5px; color: var(--warn); background: var(--warn-bg); padding: 1px 7px; border-radius: 999px; border: 1px solid oklch(from var(--warn) l calc(c*.4) h / .3); }
    .aqc-co { color: var(--bad); background: var(--bad-bg); border-color: oklch(from var(--bad) l calc(c*.4) h / .3); }
    .aqc-overdue-chip { font-size: 10px; background: var(--bad-bg); color: var(--bad); padding: 1px 6px; border-radius: 999px; border: 1px solid oklch(from var(--bad) l calc(c*.5) h / .4); font-weight: 600; }
    .aqc-foot { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--fg-dim); }
    .aqc-awaiting { margin-left: 4px; font-size: 10.5px; color: var(--info); background: var(--info-bg); padding: 1px 7px; border-radius: 999px; border: 1px solid oklch(from var(--info) l calc(c*.4) h / .3); }

    /* ── Notification Bell ───────────────────────────────────────── */
    .nbell-btn {
      appearance: none; border: 1px solid var(--border); background: var(--surface-2);
      border-radius: var(--r); padding: 5px 8px; cursor: pointer; color: var(--fg-muted);
      display: inline-flex; align-items: center; gap: 6px; position: relative;
      transition: background .12s, border-color .12s;
    }
    .nbell-btn:hover { background: var(--surface-3); border-color: var(--border-strong); color: var(--fg); }
    .nbell-count {
      position: absolute; top: -4px; right: -4px;
      min-width: 16px; height: 16px; border-radius: 999px;
      background: var(--bad); color: #fff; font-size: 9px; font-weight: 700;
      display: grid; place-items: center; padding: 0 3px; line-height: 1;
    }
    .nbell-panel {
      position: absolute; right: 0; top: calc(100% + 8px);
      width: 280px; background: var(--surface);
      border: 1px solid var(--border); border-radius: var(--r-lg);
      box-shadow: var(--shadow-2); z-index: 50;
    }
    .nbell-hd { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-bottom: 1px solid var(--border); font-size: 13px; font-weight: 600; }
    .nbell-row { display: flex; align-items: center; gap: 8px; padding: 8px 14px; border-bottom: 1px solid var(--border); font-size: 12px; }
    .nbell-row-btn {
      appearance: none; border: 0; background: transparent; width: 100%;
      text-align: left; cursor: pointer; font: inherit;
    }
    .nbell-row-btn:hover { background: var(--surface-2); }
    .nbell-row:last-child { border-bottom: none; }
    .nbell-id { font-size: 12px; font-weight: 500; flex-shrink: 0; }
    .nbell-job { font-size: 11px; color: var(--fg-dim); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .nbell-empty { padding: 16px; text-align: center; font-size: 13px; color: var(--fg-dim); }
    .nbell-more { padding: 8px 14px; font-size: 11px; color: var(--fg-dim); text-align: center; border-top: 1px solid var(--border); }
    .nbell-more-btn {
      appearance: none; border: 0; background: transparent; width: 100%;
      cursor: pointer; font: inherit; color: var(--accent);
    }
    .nbell-more-btn:hover { background: var(--surface-2); }

    /* ── Approval View ───────────────────────────────────────────── */
    .av { max-width: 1200px; margin: 0 auto; padding: 48px 28px 80px; }
    .av-hd { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 24px; }
    .av-eyebrow { font-size: 10.5px; letter-spacing: 0.14em; color: var(--fg-dim); margin-bottom: 6px; }
    .av-title   { margin: 0; font-size: 30px; font-weight: 600; letter-spacing: -0.02em; }
    .av-tabs { display: flex; gap: 2px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); padding: 3px; margin-bottom: 24px; width: fit-content; }
    .av-tab { appearance: none; border: 0; background: transparent; color: var(--fg-muted); padding: 6px 14px; border-radius: 4px; font: inherit; font-size: 13px; cursor: pointer; display: flex; align-items: center; gap: 6px; }
    .av-tab.is-on { background: var(--surface-3); color: var(--fg); }
    .av-tab:hover:not(.is-on) { color: var(--fg); }
    .av-tab-count { font-size: 11px; background: var(--surface-2); color: var(--fg-dim); padding: 1px 7px; border-radius: 999px; }
    .av-state { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; min-height: 280px; }
    .av-spin { width: 28px; height: 28px; border-radius: 50%; border: 2.5px solid var(--border); border-top-color: var(--accent); animation: dz-spin .7s linear infinite; }
    .av-empty-icon { color: var(--fg-dim); opacity: 0.3; }
    .av-empty-h { font-size: 15px; font-weight: 500; color: var(--fg-muted); }
    .av-empty-sub { font-size: 13px; color: var(--fg-dim); max-width: 340px; text-align: center; line-height: 1.55; }
    .av-groups { display: flex; flex-direction: column; gap: 32px; }
    .av-group-hd { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
    .av-group-count { font-size: 10.5px; padding: 1px 8px; border-radius: 999px; background: var(--surface-2); color: var(--fg-dim); border: 1px solid var(--border); }
    .av-group-warn { display: flex; align-items: center; gap: 5px; font-size: 12px; color: var(--warn); }
    .av-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px,1fr)); gap: 12px; }
  `}</style>
);

Object.assign(window, {
  ROLES, APPROVAL_STATUS, STATUS_LABEL, STATUS_TONE, ROLE_LABEL,
  getRequiredSteps, canApprove, getCurrentRequiredRole, isOverdue,
  submitForApproval, approveStep, disputeApproval, resubmitToReview, markPaid, uploadChangeOrder,
  ApprovalStatusPill, ApprovalChain, AuditTrailPanel,
  InvoiceDetailDrawer, ApprovalQueueCard, NotificationBell, ApprovalView, ApprovalWorkflowStyles,
});
