import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
import {
    Ban, Calendar, Check, ChevronDown, ChevronRight, History, Inbox,
    Eye, EyeOff, Link2, Maximize2, MessageSquare, Minimize2, RotateCcw, Search, Settings, X,
} from 'lucide-react';
import AppLayout from '@/Layouts/AppLayout';
import { DecisionBar } from '@/Components/MenuSampleOrders/DecisionBar';
import { AddFilterMenu, FilterPill } from '@/Components/ui/filter-pill';
import { CheckBox } from '@/Components/Proto/UI/CheckBox';
import { Button } from '@/Components/ui/button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/Components/ui/dialog';
import { Textarea } from '@/Components/ui/textarea';
import { router, useForm, usePage } from '@inertiajs/react';
import { ListFooter, StatsDot } from '@/Components/Table/ListFooter';
import { useClientSort, SortButton } from '@/lib/ClientSort';
import { SELECTED_BG, SELECTED_HOVER_CELL, SELECTED_HOVER_TR } from '@/lib/rowTint';
import { useToast } from '@/Components/Toast';

// Sample Order Approval PM console, mirroring the Quotation Approval PM dense grouped
// table (2026-06-22). Every PM action is LINE-grained: approve / revise / reject act on
// the checked lines the server marked `canAct`, never on the whole order.
//
// This page is PM-only. It briefly carried a `stage` prop + STAGE_META so it could also
// render Approval SM; nothing ever passed stage='sm' (Approval SM renders
// MenuSampleOrders/ApprovalSm/Detail), so the switch was removed 2026-08-07.

// The three PM decisions. All three are line-grained and all three require a comment.
// `reject` was implemented server-side from the start (header 5 / lines 11 + the legacy
// "has been rejected" mail) but had no button anywhere until 2026-08-07 — it was only
// reachable by hand-crafting a POST.
const ACTION_META = {
    approve: {
        label: 'Approve',
        verb: 'Approved',
        icon: Check,
        badge: 'bg-success/12 text-success-text',
        emphasis: 'text-success-text',
        // Primary/violet gradient comes from <Button variant="default"> — no className.
        button: '',
        placeholder: 'Reason / approval note…',
        note: '.',
    },
    revise: {
        label: 'Revise',
        verb: 'sent back for revision',
        icon: RotateCcw,
        badge: 'bg-warning-bg text-warning-text',
        emphasis: 'text-warning-text',
        button: 'border border-warning bg-warning-bg text-warning-text hover:bg-warning-bg/80',
        placeholder: 'Apa yang perlu direvisi…',
        note: ' — revise akan mengembalikan order ke pembuat.',
    },
    reject: {
        label: 'Reject',
        verb: 'Rejected',
        icon: Ban,
        badge: 'bg-danger/10 text-danger-text',
        emphasis: 'text-danger-text',
        button: 'border border-danger/40 bg-card text-danger-text hover:bg-danger/10',
        placeholder: 'Alasan penolakan…',
        note: ' — reject menutup line ini; order tidak lanjut ke packing.',
    },
};

const DATE_RANGE_OPTIONS = [
    { value: 'all', label: 'All time' },
    { value: '7', label: 'Last 7 days' },
    { value: '30', label: 'Last 30 days' },
    { value: '90', label: 'Last 90 days' },
    { value: '365', label: 'Last year' },
];

// Muted "NA" placeholder (no dashes — same convention as the quotation page).
const NA = () => <span className="font-normal text-muted-foreground/60">NA</span>;

// Truncated text that reveals its full content on click (toggles wrapping); the
// native title still shows the full value on hover. Click again to collapse.
function ExpandableText({ children, title, className = '' }) {
    const [open, setOpen] = useState(false);
    const toggle = (e) => { e.stopPropagation(); setOpen((o) => !o); };
    return (
        <span
            className={`block cursor-pointer ${open ? 'whitespace-normal break-words' : 'truncate'} ${className}`}
            title={title}
            role="button"
            tabIndex={0}
            onClick={toggle}
            onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(e); } }}
        >
            {children}
        </span>
    );
}

// Instant hover popover. Uses position:fixed measured from the trigger rect so it
// escapes the table's overflow-x-auto clipping. Flips below the trigger when
// there's no room above, and clamps the left edge inside the viewport.
function Tip({ content, children, className = '', width = 300 }) {
    const [pos, setPos] = useState(null);
    const ref = useRef(null);
    const show = () => {
        const r = ref.current?.getBoundingClientRect();
        if (!r) return;
        let left = r.left;
        const max = window.innerWidth - width - 8;
        if (left > max) left = max;
        if (left < 8) left = 8;
        const below = r.top < 220;
        setPos({ left, top: below ? r.bottom + 6 : r.top - 6, below });
    };
    return (
        <span ref={ref} className={className} onMouseEnter={show} onMouseLeave={() => setPos(null)}>
            {children}
            {pos && (
                <span
                    className={`pointer-events-none fixed z-[100] block rounded-lg border border-border bg-card p-2.5 text-left text-[11px] font-normal leading-snug text-foreground shadow-modal ${pos.below ? '' : '-translate-y-full'}`}
                    style={{ left: pos.left, top: pos.top, width, maxWidth: '85vw' }}>
                    {content}
                </span>
            )}
        </span>
    );
}

// Shared layout for a hover popover: a small title + a stacked list of records.
function RecordPanel({ title, rows, empty = 'No records' }) {
    return (
        <span className="flex flex-col gap-1">
            <span className="border-b border-border/60 pb-1 text-[10px] font-bold uppercase tracking-wide text-muted-foreground">{title}</span>
            {rows.length
                ? rows.map((r, i) => <span key={i} className="block leading-snug">{r}</span>)
                : <span className="text-muted-foreground">{empty}</span>}
        </span>
    );
}

function daysAgo(dateStr) {
    if (!dateStr || dateStr === '—' || dateStr === '-') return null;
    const t = Date.parse(dateStr);
    if (Number.isNaN(t)) return null;
    return (Date.now() - t) / (1000 * 60 * 60 * 24);
}

const MONTHS_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
function shortDate(d) {
    if (!d || d === '—' || d === '-') return null;
    const t = Date.parse(d);
    if (Number.isNaN(t)) return String(d).slice(0, 10);
    const dt = new Date(t);
    return `${dt.getDate()} ${MONTHS_SHORT[dt.getMonth()]} '${String(dt.getFullYear()).slice(-2)}`;
}

// ── Activity record builders ──
// ACTUAL records only. When the real log is empty they return an empty array and the
// cell renders nothing.
//
// ⛔ DO NOT re-add a "demo"/seeded fallback here. Until 2026-08-07 these builders
// synthesised approver names, remarks and dates from the order id whenever the real log
// was empty — and because every order in this queue is still at Request, the PM/SM
// thread was empty by definition, so what an approver read was ALWAYS invented. A PM
// signs off on samples here; a blank cell is correct, a plausible fake one is not.
function distinctLineVals(q, key) {
    return Array.from(new Set((q.lineItems ?? []).map((li) => li[key]).filter((v) => v && v !== '—')));
}
function remarkRecords(q) {
    return [
        ...distinctLineVals(q, 'remarks').map((t) => ({ tag: 'SJ', text: t })),
        ...distinctLineVals(q, 'remarkCoverLetter').map((t) => ({ tag: 'Cover Letter', text: t })),
        ...distinctLineVals(q, 'remarkInternal').map((t) => ({ tag: 'Internal', text: t })),
    ];
}
function historyRecords(q) {
    return (q.history?.entries ?? [])
        .filter((e) => (e.Status && e.Status !== '—') || (e.Comment && e.Comment !== '—'))
        .map((e) => ({
            status: e.Status || '—',
            date: shortDate(e.Tanggal),
            user: e.User,
            comment: e.Comment && e.Comment !== '—' ? e.Comment : '',
        }));
}
// Real linked records from the backend (sampleorderlink). Empty array → no links.
const linkedRecords = (q) => q.links ?? [];
// Linked-record kind → list page to open (placeholder deep-link target).
const LINK_ROUTES = {
    'Lab Work Request': 'lwrs.index',
    Quotation: 'quotations.index',
    'Visit Report': null,
};
function goToLink(rec) {
    const name = LINK_ROUTES[rec?.kind];
    if (name) {
        try { router.visit(route(name)); } catch { /* route not registered — no-op */ }
    }
}

// ⛔ There is deliberately NO "Latest from PM & SM" column here. It was removed
// 2026-08-07: Sample Order has no cross-order PM/SM comment engine (that is a Quotation
// feature, served by quotations.products.last-pm/sm-comments), so the column could only
// ever show this order's own assignment log — which, for a queue of Request-status
// orders, has no PM or SM entry in it at all. It filled the gap with invented text.
// The real log is still one hover away in the Activity column.

// Single-select pill (PillSelect look, but one value) — used for the date range.
function RangePill({ label, value, options, onChange }) {
    const [open, setOpen] = useState(false);
    const ref = useRef(null);
    useEffect(() => {
        const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
        if (open) document.addEventListener('mousedown', h);
        return () => document.removeEventListener('mousedown', h);
    }, [open]);
    const current = options.find((o) => o.value === value);
    const hasVal = value !== options[0].value;
    return (
        <div ref={ref} className="relative">
            <button type="button" onClick={() => setOpen((o) => !o)}
                className={`inline-flex h-8 items-center gap-1.5 whitespace-nowrap rounded-full border px-3 text-[12.5px] font-semibold shadow-sm transition-colors hover:border-primary hover:text-primary ${hasVal ? 'border-border-soft-strong bg-accent text-primary' : open ? 'border-primary bg-card text-primary' : 'border-border/50 bg-card text-muted-foreground'}`}>
                <Calendar aria-hidden="true" className="size-[13px] shrink-0 opacity-80" />
                <span>{hasVal ? `${label}: ${current?.label}` : `${label}: All`}</span>
                {hasVal ? (
                    <span role="button" aria-label="Clear" onClick={(e) => { e.stopPropagation(); onChange(options[0].value); }}
                        className="inline-flex size-4 items-center justify-center rounded-full bg-primary/18 text-[0.75rem] leading-none text-primary hover:bg-danger hover:text-white">×</span>
                ) : (
                    <ChevronDown aria-hidden="true" strokeWidth={2.5} className={`size-2.5 transition-transform ${open ? 'rotate-180' : ''}`} />
                )}
            </button>
            {open && (
                <div className="absolute left-0 top-full z-50 mt-1.5 w-[180px] overflow-hidden rounded-xl border border-border bg-surface py-1.5 shadow-modal">
                    {options.map((o) => (
                        <button key={o.value} type="button" onClick={() => { onChange(o.value); setOpen(false); }}
                            className={`flex w-full items-center justify-between px-3.5 py-2 text-left text-[0.8rem] transition-colors hover:bg-surface-tint ${o.value === value ? 'font-bold text-primary' : 'font-medium text-foreground'}`}>
                            {o.label}
                            {o.value === value && <Check className="size-3.5" strokeWidth={3} />}
                        </button>
                    ))}
                </div>
            )}
        </div>
    );
}

const emptyFilters = () => ({
    q: '', dateRange: 'all',
    company: [], principal: [], sales: [], industry: [], product: [], application: [],
});
const inSel = (arr, v) => !arr.length || arr.includes(v);

// Merged columns — each cell packs related fields; rows stay multi-line, dense.
const COLUMN_DEFS = [
    // No fixed column widths — each column hugs its content; the cells cap their own
    // width via max-w on the inner div, so the only inter-column gap is the uniform
    // td padding (pl-4 pr-4) → consistent margins everywhere.
    { id: 'entity', label: 'Company', required: true, w: '' },
    { id: 'address', label: 'Address', w: '' },
    { id: 'product', label: 'Product', required: true, w: '' },
    { id: 'qty', label: 'Qty', w: '' },
    { id: 'creator', label: 'Creator', w: '' },
    { id: 'sales', label: 'Sales', w: '' },
    { id: 'soBy', label: 'SO By', w: '' },
    { id: 'meta', label: 'Activity', w: '' },
];
const GROUP_EMPHASIS = {};
// Frozen (sticky-left) columns — checkbox + Company + Product stay put on scroll.
// Cumulative offsets: checkbox 48px, Company (w-64) 256px → Product at 304px.
const FROZEN = {
    entity: 'sticky left-12 z-20',
};
// Pinned (can't reorder) + fixed pixel widths for the table-fixed/resizable layout.
const PINNED = ['entity'];
const CHECKBOX_W = 48;
// 4 × size-6 icon buttons (🔗 Approve Revise Reject) + gap-1 + the td's px-4.
const ACTIONS_W = 148;
const COL_W_DEFAULT = 130;
const COL_W = { entity: 260, address: 240, product: 200, qty: 90, creator: 110, sales: 130, soBy: 170, meta: 92 };
// Sortable columns — id → RAW record (sample order) value for useClientSort. Only
// order-level scalars sort: Product/Qty hold one value PER LINE ITEM (an array at
// the record level) and meta is history-derived, so those stay unsorted.
const SORT_GETTERS = {
    entity: (q) => q.company,
    address: (q) => q.address,
    creator: (q) => q.creator,
    sales: (q) => q.sales,
    soBy: (q) => q.sampleOrderBy,
    // Product sorts by the order's alphabetically FIRST product (user 2026-08-24), the same
    // rule as the Quotation Approval PM twin. useClientSort orders RECORDS and one order can
    // carry several line items, so a record has no single product to sort on; taking the
    // minimum makes the position predictable. Note the rows of one order stay together, so the
    // column does not read strictly A-Z.
    product: (q) => {
        const names = (q.lineItems ?? []).map((li) => li.productName || li.barang).filter(Boolean);
        return names.length ? names.slice().sort()[0] : '';
    },
};
// What the toolbar's search box looks through. Product names are included (user 2026-08-24)
// — they live on the LINE ITEMS, so an order matches when ANY of its products does. Both the
// suggestion dropdown and the row filter read this, so they cannot disagree about a match.
const searchHaystack = (q) => [
    q.id,
    q.company ?? '',
    ...(q.lineItems ?? []).flatMap((li) => [li.productName, li.barang]),
].filter(Boolean).join(' ').toLowerCase();

const itemsOf = (q) => (q.lineItems && q.lineItems.length) ? q.lineItems : [{}];
const liKey = (q, li, idx) => 'd' + (li.id || `${q.id}_${idx}`);

// ── Grouping — every header-level dim listed; only CHECKED pills nest the table.
// Default checked = Principal.
const GROUP_DIMS = ['principal', 'company', 'sales', 'industry', 'application', 'creator'];
const GROUP_META = {
    company: { label: 'Company', val: (q) => q.company || 'No Company' },
    // Principal lives on the line items — a mixed order joins them ("ARKEMA + ...").
    principal: {
        label: 'Principal',
        val: (q) => {
            const names = Array.from(new Set((q.lineItems ?? []).map((li) => li.principalName).filter(Boolean))).sort();
            return names.length ? names.join(' + ') : 'No Principal';
        },
    },
    sales: { label: 'Sales', val: (q) => q.sales || 'No Sales' },
    industry: { label: 'Industry', val: (q) => q.industry || 'No Industry' },
    application: { label: 'Application', val: (q) => q.division || 'No Application' },
    creator: { label: 'Creator', val: (q) => q.creator || 'System' },
};
const DIM_COL = {};
const buildTree = (rows, dims) => {
    const [dim, ...rest] = dims;
    const map = new Map();
    rows.forEach((q) => { const k = GROUP_META[dim].val(q); if (!map.has(k)) map.set(k, []); map.get(k).push(q); });
    return [...map.entries()]
        .map(([key, rs]) => ({ dim, key, label: key, rows: rs, children: rest.length ? buildTree(rs, rest) : null }))
        .sort((a, b) => a.label.localeCompare(b.label));
};
const nodePath = (parentPath, node) => `${parentPath}¦${node.dim}:${node.key}`;

// Native HTML5 drag reorder (react-sortablejs crashes under React 19).
function SortableList({ ids, onReorder, className, renderItem }) {
    const [dragIdx, setDragIdx] = useState(null);
    const move = (from, to) => {
        if (from === null || to < 0 || to >= ids.length || from === to) return;
        const n = [...ids]; const [m] = n.splice(from, 1); n.splice(to, 0, m); onReorder(n);
    };
    return (
        <div className={className}>
            {ids.map((id, i) => (
                <div
                    key={id} draggable
                    onDragStart={() => setDragIdx(i)}
                    onDragEnter={() => { if (dragIdx !== null && dragIdx !== i) { move(dragIdx, i); setDragIdx(i); } }}
                    onDragOver={(e) => e.preventDefault()}
                    onDragEnd={() => setDragIdx(null)}
                    className={`cursor-grab active:cursor-grabbing ${dragIdx === i ? 'opacity-40' : ''}`}
                >
                    {renderItem(id)}
                </div>
            ))}
        </div>
    );
}

// "Rows" pill in the Configuration panel — grip + label + group checkbox + remove.
function GroupPill({ label, grouped, onToggleGroup, onRemove }) {
    return (
        <div className="flex h-9 items-center gap-2 rounded-lg border border-primary/20 bg-primary/10 px-2.5 text-[12px] font-bold text-primary shadow-sm">
            <span className="grid place-items-center opacity-60 transition-opacity hover:opacity-100" aria-hidden="true">
                <svg width="10" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><circle cx="9" cy="5" r="1" /><circle cx="9" cy="12" r="1" /><circle cx="9" cy="19" r="1" /><circle cx="15" cy="5" r="1" /><circle cx="15" cy="12" r="1" /><circle cx="15" cy="19" r="1" /></svg>
            </span>
            <span className="flex-1 truncate">{label}</span>
            <span className="flex items-center" title={grouped ? 'Grouped — uncheck to ungroup' : 'Check to group by this field'}>
                <CheckBox size="sm" checked={!!grouped} onChange={onToggleGroup} ariaLabel={`Group by ${label}`} />
            </span>
            <button type="button" onClick={onRemove} title="Remove from list" aria-label={`Remove ${label}`}
                className="grid size-5 place-items-center rounded-md opacity-60 transition-all hover:bg-danger/15 hover:text-danger-text hover:opacity-100">
                <X className="size-3" strokeWidth={3} />
            </button>
        </div>
    );
}

const DEFAULT_VISIBLE = new Set(['entity', 'product', 'qty', 'creator', 'sales', 'soBy', 'meta']);
// _v4: the 'pmsm' column is gone — bumped so a saved v3 layout cannot resurrect its id.
const COLUMN_STORAGE_KEY = 'sampleOrderApprovalPmColumns_v4';
const defaultColumnState = () => COLUMN_DEFS.map((d) => ({ id: d.id, visible: DEFAULT_VISIBLE.has(d.id) }));
function loadColumnState() {
    try {
        const raw = localStorage.getItem(COLUMN_STORAGE_KEY);
        if (!raw) return defaultColumnState();
        const parsed = JSON.parse(raw).filter((c) => COLUMN_DEFS.some((d) => d.id === c.id));
        const ids = new Set(parsed.map((c) => c.id));
        // Insert any newly-added column at its canonical position (after its preceding
        // sibling), not at the end — so e.g. Address stays next to Company.
        COLUMN_DEFS.forEach((d, i) => {
            if (ids.has(d.id)) return;
            let insertAt = parsed.length;
            for (let j = i - 1; j >= 0; j--) {
                const pos = parsed.findIndex((c) => c.id === COLUMN_DEFS[j].id);
                if (pos >= 0) { insertAt = pos + 1; break; }
            }
            parsed.splice(insertAt, 0, { id: d.id, visible: DEFAULT_VISIBLE.has(d.id) });
            ids.add(d.id);
        });
        return parsed;
    } catch {
        return defaultColumnState();
    }
}

export default function SampleOrderApprovalPmIndex({ sampleOrders = [] }) {
    const { show: showToast } = useToast();
    const [filters, setFiltersRaw] = useState(() => {
        // Deep-link dari dialog 🔗 modul lain: /…/approval-pm?q={nomor dokumen}.
        const q = new URLSearchParams(window.location.search).get('q') || '';
        return { ...emptyFilters(), q };
    });
    const [extras, setExtras] = useState([]);

    // Grouping + panel — default ungrouped (flat), same as quotation Approval PM.
    const [groupBy, setGroupBy] = useState(['principal', 'company', 'sales', 'industry', 'application', 'creator']);
    const [grouped, setGrouped] = useState(() => new Set());
    const toggleGrouped = (id) => setGrouped((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; });
    const [collapsedPaths, setCollapsedPaths] = useState(() => new Set());
    const [showConfig, setShowConfig] = useState(false);

    const [pageSize, setPageSize] = useState(10);
    const [page, setPage] = useState(1);
    // Selection operates on LINE ITEMS (one checkbox per line). Keys are liKey(...).
    // The bulk toolbar / per-line actions submit the detail ids of the selected
    // actionable (canAct) lines.
    const [selectedIds, setSelectedIds] = useState(new Set());

    // Comment dialog drives the real Inertia POST. `ids` holds the selected LINE keys.
    const [commentDialog, setCommentDialog] = useState({ open: false, action: 'approve', ids: [] });
    const actForm = useForm({ comment: '', details: [] });
    // Linked-data viewer (opened by the 🔗 icon).
    const [linkedQ, setLinkedQ] = useState(null);

    usePage(); // keep the hook for flash/auth context parity (no direct read needed)

    // Column order + visibility (persisted); resize widths live in colW (session only).
    const [columnState, setColumnState] = useState(loadColumnState);
    const [dragColId, setDragColId] = useState(null);
    // Per-column widths (resizable) + active-resize id for the hover handle.
    const [colW, setColW] = useState(() => ({ ...COL_W }));
    const [resizingId, setResizingId] = useState(null);
    const resizeRef = useRef(null);
    const startResize = (e, id) => {
        e.preventDefault();
        e.stopPropagation();
        resizeRef.current = { id, startX: e.clientX, startW: colW[id] ?? COL_W_DEFAULT };
        setResizingId(id);
        const onMove = (ev) => {
            const r = resizeRef.current;
            if (!r) return;
            const next = Math.max(64, r.startW + (ev.clientX - r.startX));
            setColW((w) => ({ ...w, [r.id]: next }));
        };
        const onUp = () => {
            resizeRef.current = null;
            setResizingId(null);
            document.removeEventListener('mousemove', onMove);
            document.removeEventListener('mouseup', onUp);
        };
        document.addEventListener('mousemove', onMove);
        document.addEventListener('mouseup', onUp);
    };
    const colDefById = useMemo(() => new Map(COLUMN_DEFS.map((d) => [d.id, d])), []);
    const visibleCols = useMemo(
        () => columnState
            .filter((c) => { const d = colDefById.get(c.id); return d && (d.required || c.visible !== false); })
            .map((c) => colDefById.get(c.id)),
        [columnState, colDefById],
    );
    const toggleColumnVisible = (id) => {
        setColumnState((prev) => {
            const next = prev.map((c) => (c.id === id ? { ...c, visible: c.visible === false } : c));
            try { localStorage.setItem(COLUMN_STORAGE_KEY, JSON.stringify(next)); } catch { /* ignore */ }
            return next;
        });
    };
    const reorderCols = (fromId, toId) => {
        if (!fromId || fromId === toId) return;
        if (PINNED.includes(fromId)) return;
        setColumnState((prev) => {
            const fi = prev.findIndex((c) => c.id === fromId);
            const ti = prev.findIndex((c) => c.id === toId);
            if (fi < 0 || ti < 0) return prev;
            const next = [...prev];
            const [moved] = next.splice(fi, 1);
            next.splice(ti, 0, moved);
            try { localStorage.setItem(COLUMN_STORAGE_KEY, JSON.stringify(next)); } catch { /* ignore */ }
            return next;
        });
    };

    const set = (key, val) => { setFiltersRaw((f) => ({ ...f, [key]: val })); setPage(1); };

    const source = sampleOrders || [];

    // Search autocomplete — typing surfaces matching orders; picking fills the box.
    const [searchOpen, setSearchOpen] = useState(false);
    const searchRef = useRef(null);
    useEffect(() => {
        if (!searchOpen) return;
        const h = (e) => { if (searchRef.current && !searchRef.current.contains(e.target)) setSearchOpen(false); };
        document.addEventListener('mousedown', h);
        return () => document.removeEventListener('mousedown', h);
    }, [searchOpen]);
    const sq = filters.q.trim().toLowerCase();
    const searchMatches = useMemo(() => {
        if (!sq) return [];
        return source
            .filter((q) => searchHaystack(q).includes(sq))
            .slice(0, 8);
    }, [source, sq]);

    const uniq = (arr) => Array.from(new Set(arr.filter(Boolean))).sort();
    const companyOptions = useMemo(() => uniq(source.map((q) => q.company)), [source]);
    const salesOptions = useMemo(() => uniq(source.map((q) => q.sales)), [source]);
    const industryOptions = useMemo(() => uniq(source.map((q) => q.industry)), [source]);
    const applicationOptions = useMemo(() => uniq(source.map((q) => q.division)), [source]);
    const principalOptions = useMemo(() => uniq(source.flatMap((q) => (q.lineItems ?? []).map((li) => li.principalName))), [source]);
    const productOptions = useMemo(() => uniq(source.flatMap((q) => (q.lineItems ?? []).map((li) => li.productName || li.barang))), [source]);

    const rows = useMemo(() => {
        const qq = filters.q.trim().toLowerCase();
        return source.filter((q) => {
            if (qq) {
                if (!searchHaystack(q).includes(qq)) return false;
            }
            if (!inSel(filters.company, q.company)) return false;
            if (!inSel(filters.sales, q.sales)) return false;
            if (!inSel(filters.industry, q.industry)) return false;
            if (!inSel(filters.application, q.division)) return false;
            if (filters.principal.length && !(q.lineItems ?? []).some((li) => filters.principal.includes(li.principalName))) return false;
            if (filters.product.length && !(q.lineItems ?? []).some((li) => filters.product.includes(li.productName) || filters.product.includes(li.barang))) return false;
            if (filters.dateRange !== 'all') {
                const limit = Number(filters.dateRange);
                const ago = daysAgo(q.tanggal);
                if (ago === null || ago > limit) return false;
            }
            return true;
        });
    }, [source, filters]);

    // Only the CHECKED Rows pills nest the table, in pill order.
    const effGroup = useMemo(() => groupBy.filter((d) => grouped.has(d)), [groupBy, grouped]);

    // Header sort (ClientSort house pattern) — applied to the filtered ORDERS before
    // the grouping order + pagination slice, so it holds within groups and across
    // pages (each order's line-item rows stay together).
    const { sorted: clientSorted, sortKey, sortDir, toggleSort } = useClientSort(rows, SORT_GETTERS);

    // Order by the grouping dims first so groups stay contiguous across pages;
    // within a group, an active header sort wins (Array.sort is stable).
    const sortedRows = useMemo(() => {
        if (!effGroup.length) return clientSorted;
        return [...clientSorted].sort((a, b) => {
            for (const d of effGroup) {
                const c = GROUP_META[d].val(a).localeCompare(GROUP_META[d].val(b));
                if (c) return c;
            }
            return sortKey ? 0 : Number(b.id) - Number(a.id);
        });
    }, [clientSorted, effGroup, sortKey]);

    const totalPages = Math.max(1, Math.ceil(sortedRows.length / pageSize));
    const currentPage = Math.min(page, totalPages);
    const startIdx = (currentPage - 1) * pageSize;
    const pageRows = sortedRows.slice(startIdx, startIdx + pageSize);

    const tree = useMemo(() => (effGroup.length ? buildTree(pageRows, effGroup) : []), [pageRows, effGroup]);
    const allGroupPaths = useMemo(() => {
        const acc = [];
        const walk = (nodes, parent) => nodes.forEach((n) => { const p = nodePath(parent, n); acc.push(p); if (n.children) walk(n.children, p); });
        walk(tree, '');
        return acc;
    }, [tree]);
    const toggleCollapse = (path) => setCollapsedPaths((prev) => { const n = new Set(prev); n.has(path) ? n.delete(path) : n.add(path); return n; });
    const expandAll = () => setCollapsedPaths(new Set());
    const collapseAll = () => setCollapsedPaths(new Set(allGroupPaths));

    // Per-line actionability — PM is line-grained (server marks each `li.canAct`).
    const lineActable = (li) => !!li?.canAct;

    // Selection is per LINE now. A flat map from line key → its sampleorderdetails id,
    // so the act payload's `details` is built directly from the selected line keys.
    // Only actionable lines are mapped (non-actionable lines never get selected).
    const lineDetailId = useMemo(() => {
        const m = new Map();
        source.forEach((q) => itemsOf(q).forEach((li, idx) => {
            if (lineActable(li)) m.set(liKey(q, li, idx), li.id);
        }));
        return m;
    }, [source]);
    // Selected line keys → their detail ids (the PM act payload). Filters to keys we
    // can actually act on (defensive: a stale selection never sends a non-canAct id).
    const detailIdsFor = (keys) => keys.map((k) => lineDetailId.get(k)).filter((v) => v != null);

    // Page selection: only actionable lines on the page participate in the checkboxes.
    const pageItemIds = useMemo(
        () => pageRows.flatMap((q) => itemsOf(q).filter(lineActable).map((li, idx) => liKey(q, li, idx))),
        [pageRows],
    );
    const allOnPageSelected = pageItemIds.length > 0 && pageItemIds.every((id) => selectedIds.has(id));
    const toggleOne = (id) => setSelectedIds((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; });
    const toggleAll = () => {
        const next = new Set(selectedIds);
        if (allOnPageSelected) pageItemIds.forEach((id) => next.delete(id));
        else pageItemIds.forEach((id) => next.add(id));
        setSelectedIds(next);
    };
    // Group select operates on the actionable LINE keys under the group's orders.
    const toggleGroupSel = (keys) => {
        const all = keys.length > 0 && keys.every((id) => selectedIds.has(id));
        setSelectedIds((prev) => { const n = new Set(prev); keys.forEach((id) => (all ? n.delete(id) : n.add(id))); return n; });
    };
    const groupSelState = (keys) => {
        const c = keys.reduce((n, id) => n + (selectedIds.has(id) ? 1 : 0), 0);
        return { checked: c > 0 && c === keys.length, indeterminate: c > 0 && c < keys.length, disabled: keys.length === 0 };
    };

    const openComment = (action, ids) => {
        if (!ids || ids.length === 0) return;
        actForm.reset();
        actForm.clearErrors();
        actForm.setData('comment', '');
        setCommentDialog({ open: true, action, ids });
    };
    const openLinks = (q) => setLinkedQ(q);

    const submitComment = () => {
        if (!actForm.data.comment.trim()) return;
        const { action, ids } = commentDialog;
        const done = () => { setCommentDialog((d) => ({ ...d, open: false })); setSelectedIds(new Set()); };
        // PM is line-grained: submit the detail ids of the selected actionable lines.
        const details = detailIdsFor(ids);
        // @inertiajs/react's transform() sets the transform and returns undefined (not chainable) —
        // call it on its own line, then post, like every other form page in this app.
        actForm.transform((d) => ({ comment: d.comment, details }));
        actForm.post(
            route('sample-orders.approval-pm.act', { action }),
            {
                preserveScroll: true,
                onSuccess: done,
                onError: () => showToast('Please check the form and try again.', 'error'),
            },
        );
    };

    const stats = useMemo(() => {
        const total = rows.length;
        const items = rows.reduce((s, q) => s + (q.lineItems?.length ?? 0), 0);
        const companies = new Set(rows.map((q) => q.company).filter(Boolean)).size;
        // Actionable now counts actionable LINE ITEMS (one selectable row each), not orders.
        const actionable = rows.reduce((s, q) => s + (q.lineItems ?? []).filter(lineActable).length, 0);
        return { total, items, companies, actionable };
    }, [rows]);

    const activeFilterCount =
        (filters.q.trim() ? 1 : 0) +
        (filters.dateRange !== 'all' ? 1 : 0) +
        ['company', 'principal', 'sales', 'industry', 'product', 'application']
            .reduce((n, k) => n + (filters[k].length ? 1 : 0), 0);

    const resetFilters = () => {
        setFiltersRaw(emptyFilters());
        setExtras([]);
        setPage(1);
    };

    const coreFields = [
        { key: 'company', label: 'Company', opts: companyOptions },
        { key: 'principal', label: 'Principal', opts: principalOptions },
        { key: 'product', label: 'Product', opts: productOptions },
        { key: 'sales', label: 'Sales', opts: salesOptions },
    ];
    const extraFields = [
        { key: 'industry', label: 'Industry', opts: industryOptions },
        { key: 'application', label: 'Application', opts: applicationOptions },
    ];
    const visibleExtras = extraFields.filter((f) => extras.includes(f.key) || filters[f.key]?.length);
    const hiddenExtras = extraFields.filter((f) => !visibleExtras.some((v) => v.key === f.key));
    const removeExtra = (key) => { setExtras((e) => e.filter((x) => x !== key)); set(key, []); };
    const inactiveDims = GROUP_DIMS.filter((d) => !groupBy.includes(d));

    const displayCols = useMemo(
        () => visibleCols.filter((c) => !effGroup.some((d) => DIM_COL[d] === c.id)),
        [visibleCols, effGroup],
    );

    // Per-line columns get a specific line item `li`; per-order columns read `q`.
    const renderCell = (q, colId, li = (q.lineItems ?? [])[0] || {}) => {
        switch (colId) {
            // 1. COMPANY · #SO · DIVISION · INDUSTRY · CATEGORY — company with #SO hugging it;
            //    division · industry · category below + T&C chip. (Creator & Sales = own columns.)
            case 'entity': {
                const companyShown = !effGroup.includes('company');
                const company = q.company || 'NA';
                const head = companyShown ? company : (q.sales || company);
                const ctx = [q.division, q.industry, q.companyCategory].filter(Boolean).join(' · ');
                const tcEntries = Object.entries(q.terms ?? {}).filter(([, v]) => v && v !== '—');
                return (
                    <div className="flex w-full min-w-0 flex-col gap-0.5 leading-tight">
                        <div className="flex items-baseline justify-between gap-2 pr-1">
                            <ExpandableText className="min-w-0 font-semibold text-foreground" title={q.address ? `${head}\n${q.address}` : head}>{head}</ExpandableText>
                            <span className="shrink-0 text-[11px] font-bold text-primary">#{q.id}</span>
                        </div>
                        {(ctx || tcEntries.length > 0) && (
                            <div className="flex items-center justify-between gap-2 pr-1 text-[11px]">
                                <ExpandableText className="min-w-0 text-muted-foreground" title={ctx}>{ctx || <NA />}</ExpandableText>
                                {tcEntries.length > 0 && (
                                    <Tip width={320} className="inline-flex shrink-0 cursor-help items-center gap-1 rounded border border-border/70 bg-muted/60 px-1 py-px text-[9px] font-bold uppercase tracking-wide leading-none text-muted-foreground hover:border-border hover:text-foreground"
                                        content={
                                            <RecordPanel title="Terms & Conditions" rows={tcEntries.map(([k, v]) => (
                                                <span><span className="font-semibold text-foreground">{k}</span>: <span className="text-muted-foreground">{v}</span></span>
                                            ))} />
                                        }>
                                        T&C
                                    </Tip>
                                )}
                            </div>
                        )}
                    </div>
                );
            }
            // 1b. ADDRESS — company address (default-hidden column; muted, hover for full text).
            case 'address':
                return q.address
                    ? <ExpandableText className="block w-full text-[12px] text-muted-foreground" title={q.address}>{q.address}</ExpandableText>
                    : <NA />;
            // 2. PRODUCT · PRINCIPAL · APPLICATION — same principle as quotation Approval PM:
            //    product (orig/print) + lot on top; principal · application below.
            case 'product': {
                const orig = li.barang;
                const print = li.productName;
                const showPrint = print && print !== orig;
                const lot = li.listBarang || li.requestLot || (li.requestLot === null && li.listBarang === null ? 'NoLotNumber' : '');
                const principalShown = !effGroup.includes('principal');
                const principal = li.principalName;
                const application = li.application;
                return (
                    <div className="flex w-full min-w-0 flex-col gap-0.5 leading-tight">
                        <ExpandableText className="font-semibold text-foreground" title={`${orig || print || ''}${showPrint ? ` (${print})` : ''}`}>
                            {orig || print || <NA />}
                            {lot && <span className="ml-1 font-normal text-muted-foreground">{lot}</span>}
                        </ExpandableText>
                        {principalShown && (principal || application) && (
                            <ExpandableText className="text-[11px] text-muted-foreground" title={[principal, application].filter(Boolean).join(' · ')}>
                                {principal || ''}
                                {application && <span className="text-muted-foreground/80">{principal ? ' · ' : ''}{application}</span>}
                            </ExpandableText>
                        )}
                    </div>
                );
            }
            // 3. QTY — qty + satuan (bold). qtReceived muted, only when > 0.
            case 'qty': {
                const qty = [li.qty, li.satuan].filter(Boolean).join(' ');
                const received = parseFloat(li.qtReceived);
                return (
                    <div className="flex flex-col items-start leading-tight tabular-nums">
                        <span className="whitespace-nowrap font-semibold text-foreground">{qty || <NA />}</span>
                        {received > 0 && <span className="whitespace-nowrap text-[11px] text-muted-foreground" title="Qty received">rcv {li.qtReceived}</span>}
                    </div>
                );
            }
            // People — Creator & Sales as their own columns (per request).
            case 'creator':
                return <span className="block whitespace-nowrap text-[12px] text-foreground">{q.creator || <NA />}</span>;
            case 'sales':
                return <span className="block whitespace-nowrap text-[12px] text-foreground">{q.sales || <NA />}</span>;
            // 5. SO BY — sampleOrderBy · date; project (muted). Delivery on hover.
            case 'soBy': {
                const top = [q.sampleOrderBy, shortDate(q.tanggalSOBy)].filter(Boolean).join(' · ');
                if (!top && !q.project) return <NA />;
                return (
                    <div className="flex w-full min-w-0 flex-col leading-tight text-[11px] text-muted-foreground"
                        title={q.delivery ? `Delivery: ${q.delivery}` : undefined}>
                        <span className="whitespace-nowrap text-foreground">{top || <NA />}</span>
                        {q.project && <ExpandableText className="text-muted-foreground/80" title={q.project}>{q.project}</ExpandableText>}
                    </div>
                );
            }
            // 6. ACTIVITY — R:n · H:n. R = remarks, H = history. (Links live in the Actions
            //    column next to Approve, so they're not duplicated here.)
            case 'meta': {
                const remarks = remarkRecords(q);
                const history = historyRecords(q);
                const codeCls = 'inline-flex cursor-help items-center gap-0.5 hover:text-foreground';
                return (
                    <span className="flex items-center gap-2.5 whitespace-nowrap text-[11px] tabular-nums text-muted-foreground/80">
                        <Tip className={codeCls} width={300} content={
                            <RecordPanel title={`Remarks (${remarks.length})`} rows={remarks.map((rec) => (
                                <span><span className="mr-1.5 rounded bg-muted px-1 py-px text-[9px] font-bold text-muted-foreground">{rec.tag}</span><span className="text-foreground">{rec.text}</span></span>
                            ))} />
                        }><MessageSquare className="size-3.5" aria-label="Remarks" />{remarks.length}</Tip>
                        <Tip className={codeCls} width={320} content={
                            <RecordPanel title={`History (${history.length})`} rows={history.map((h) => (
                                <span className="text-muted-foreground"><span className="font-semibold text-foreground">{h.status}</span>{h.date ? ` · ${h.date}` : ''}{h.user ? ` · ${h.user}` : ''}{h.comment ? ` — “${h.comment}”` : ''}</span>
                            ))} />
                        }><History className="size-3.5" aria-label="History" />{history.length}</Tip>
                    </span>
                );
            }
            default: return null;
        }
    };

    // ── Row renderers (BnT/LWR Approval PM tree pattern) ──
    // FULL per-line rows: each line item is its own independent <tr>. Order-level
    // columns (Company / SO By / Latest PM&SM / Activity) repeat on every line row;
    // line-level columns (Product / Qty / …) render that specific line via renderCell.
    // Selection + Approve/Revise are per LINE — PM is line-grained (server `canAct`).
    const renderLeaf = (q, depth) => {
        const links = linkedRecords(q);
        return itemsOf(q).map((li, idx) => {
            const id = liKey(q, li, idx);
            const actable = lineActable(li);
            // Non-actionable lines (outside this PM's principal scope, or already
            // decided) are dimmed and show a static "done" mark instead of buttons.
            const decided = !actable;
            const isSel = selectedIds.has(id);
            // Opaque backgrounds for every state + opaque hover — the sticky frozen column
            // must never be translucent or columns bleed through on horizontal scroll
            // (same fix the quotation Approval PM uses; `opacity` on the row broke it).
            // ⚠️ Selected is NOT `bg-accent`: dark mode resolves `--accent` to 16% alpha, which
            // makes the sticky frozen column see-through while the table scrolls under it.
            const rowBg = isSel ? SELECTED_BG : decided ? 'bg-secondary' : 'bg-card';
            const rowHoverTr = isSel ? SELECTED_HOVER_TR : 'hover:bg-muted';
            const rowHoverCell = isSel ? SELECTED_HOVER_CELL : 'group-hover:bg-muted';
            return (
                <tr key={id} className={`group ${rowBg} transition-colors ${rowHoverTr} ${decided ? 'text-muted-foreground' : ''}`}>
                    <td className={`sticky left-0 z-20 ${rowBg} transition-colors py-2.5 pl-6 pr-2 align-top ${rowHoverCell}`}>
                        <CheckBox checked={selectedIds.has(id)} disabled={!actable} onChange={() => actable && toggleOne(id)} />
                    </td>
                    {displayCols.map((col, i) => (
                        <td key={col.id} className={[
                            'py-2.5 pl-4 pr-4 align-top',
                            col.w || '',
                            col.align ? 'text-right tabular-nums' : '',
                            GROUP_EMPHASIS[col.groupId] || '',
                            FROZEN[col.id] ? `${FROZEN[col.id]} ${rowBg} transition-colors ${rowHoverCell}` : '',
                        ].filter(Boolean).join(' ')}
                            style={i === 0 && depth > 0 ? { paddingLeft: 16 + depth * 16 } : undefined}>
                            {renderCell(q, col.id, li)}
                        </td>
                    ))}
                    {/* ACTIONS — per line item: 🔗 Linked (hover) · ✓ Approve · ↺ Revise. */}
                    <td className="px-4 py-2.5 align-top">
                        <div className="flex items-center justify-end gap-1">
                            <Tip width={260} className="inline-flex" content={
                                <RecordPanel title={`Linked (${links.length})`} rows={links.map((l) => (
                                    <span><span className="font-semibold text-foreground">{l.kind}</span> <span className="font-medium text-primary">{l.ref}</span></span>
                                ))} />
                            }>
                                <button type="button" onClick={() => openLinks(q)} aria-label={`Linked records (${links.length})`}
                                    className="relative grid size-6 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-primary">
                                    <Link2 className="size-5" strokeWidth={1.5} />
                                    {links.length > 0 && <span className="pointer-events-none absolute -top-1 right-0 grid h-3.5 min-w-3.5 place-items-center rounded-full bg-muted px-0.5 text-[8px] font-bold leading-none text-muted-foreground tabular-nums ring-1 ring-card">{links.length}</span>}
                                </button>
                            </Tip>
                            {!actable ? (
                                <span className="grid size-6 place-items-center text-muted-foreground/50"
                                    title="Line ini di luar scope Approval PM Anda (atau sudah diproses)">
                                    <Check className="size-3.5" strokeWidth={2.5} />
                                </span>
                            ) : (
                                <>
                                    <button type="button" onClick={() => openComment('approve', [id])} title="Approve" aria-label="Approve"
                                        className="grid size-6 place-items-center rounded-md border border-success/30 bg-success/5 text-success-text transition-colors hover:bg-success/15"><Check className="size-3.5" strokeWidth={3} /></button>
                                    <button type="button" onClick={() => openComment('revise', [id])} title="Revise" aria-label="Revise"
                                        className="grid size-6 place-items-center rounded-md border border-border bg-transparent text-muted-foreground transition-colors hover:border-warning hover:bg-warning-bg hover:text-warning-text"><RotateCcw className="size-3.5" strokeWidth={2.5} /></button>
                                    <button type="button" onClick={() => openComment('reject', [id])} title="Reject" aria-label="Reject"
                                        className="grid size-6 place-items-center rounded-md border border-border bg-transparent text-muted-foreground transition-colors hover:border-danger hover:bg-danger/10 hover:text-danger-text"><Ban className="size-3.5" strokeWidth={2.5} /></button>
                                </>
                            )}
                        </div>
                    </td>
                </tr>
            );
        });
    };

    const renderNodes = (nodes, depth, parentPath) => nodes.map((node) => {
        const path = nodePath(parentPath, node);
        const isCollapsed = collapsedPaths.has(path);
        // Group selection operates on the actionable LINE keys under this group's orders.
        const lineKeys = node.rows.flatMap((q) => itemsOf(q).filter(lineActable).map((li, idx) => liKey(q, li, idx)));
        const sel = groupSelState(lineKeys);
        const actableCount = lineKeys.length;
        const isTop = depth === 0;
        return (
            <Fragment key={path}>
                <tr className={`transition-colors ${isTop ? 'bg-muted/40' : 'bg-muted/20'} hover:bg-muted/60`}>
                    <td className={`py-3 pl-6 pr-2 align-middle ${isTop && !isCollapsed ? 'shadow-[inset_3px_0_0_var(--color-primary)]' : ''}`}>
                        <CheckBox checked={sel.checked} indeterminate={sel.indeterminate} disabled={sel.disabled} onChange={() => toggleGroupSel(lineKeys)} ariaLabel={`Select all actionable lines in ${node.label}`} />
                    </td>
                    <td colSpan={displayCols.length + 1} className="py-3 pl-4 pr-4 align-middle">
                        <div className="flex w-full cursor-pointer select-none items-center gap-2.5" style={{ paddingLeft: depth * 16 }} onClick={() => toggleCollapse(path)}>
                            <span className={`grid place-items-center text-primary transition-transform ${isCollapsed ? '-rotate-90' : ''}`}>
                                <ChevronDown className={isTop ? 'size-4' : 'size-3.5'} strokeWidth={2.5} />
                            </span>
                            <span className="text-[12.5px] font-bold normal-case tracking-tight text-foreground">{node.label}</span>
                            <span className="ml-auto inline-flex items-center gap-1.5 whitespace-nowrap pr-2 text-[11.5px] font-medium normal-case tabular-nums text-muted-foreground">
                                {node.rows.length} order{node.rows.length !== 1 ? 's' : ''}
                                <span aria-hidden="true" className="text-muted-foreground/40">·</span>
                                <span className="font-bold text-foreground">{actableCount} actionable line{actableCount !== 1 ? 's' : ''}</span>
                            </span>
                        </div>
                    </td>
                </tr>
                {!isCollapsed && (node.children ? renderNodes(node.children, depth + 1, path) : node.rows.map((r) => renderLeaf(r, depth + 1)))}
            </Fragment>
        );
    });

    const act = ACTION_META[commentDialog.action] ?? ACTION_META.approve;
    // Selected actionable LINE count driving the dialog count text + button label.
    const dialogLineCount = detailIdsFor(commentDialog.ids).length;

    return (
        <section className="flex min-w-0 flex-col gap-[18px]" id="lastSampleOrderPmView">
            <header className="flex items-start justify-between gap-4">
                <div>
                    <p className="m-0 mb-1.5 flex items-center gap-2 text-xs font-medium text-muted-foreground">
                        <span>Sample Order</span>
                        <span aria-hidden="true">›</span>
                        <span>Approval</span>
                        <span aria-hidden="true">›</span>
                        <span className="text-foreground">Approval PM</span>
                    </p>
                    <h1 className="m-0 text-2xl font-bold leading-[1.15] tracking-tight text-card-foreground">Approval PM</h1>
                </div>
            </header>

            <div className="flex flex-col lg:flex-row lg:items-start gap-5">
                {/* Main list */}
                <article className="min-w-0 flex-1 overflow-hidden rounded-xl border border-border bg-card">
                    {/* Filter bar */}
                    <div className="flex flex-wrap items-center gap-2.5 border-b border-border/50 bg-card px-6 py-5">
                        <div ref={searchRef} className="relative min-w-[200px] max-w-[320px] flex-1">
                            <label className="inline-flex h-8 w-full items-center gap-2 rounded-full border border-transparent bg-muted/60 px-3.5 text-muted-foreground transition-colors hover:bg-muted focus-within:border-primary/40 focus-within:bg-card">
                                <Search aria-hidden="true" className="size-3.5 shrink-0" />
                                <input type="search" placeholder="Search SO No, Company, or Product" autoComplete="off" value={filters.q}
                                    onChange={(e) => { set('q', e.target.value); setSearchOpen(true); }}
                                    onFocus={() => setSearchOpen(true)}
                                    className="min-w-0 flex-1 bg-transparent text-[12.5px] font-medium text-foreground outline-none placeholder:text-muted-foreground/70" />
                            </label>
                            {searchOpen && searchMatches.length > 0 && (
                                <div className="absolute left-0 top-full z-50 mt-1.5 max-h-[280px] w-full overflow-y-auto rounded-xl border border-border bg-surface py-1.5 shadow-modal">
                                    {searchMatches.map((m) => (
                                        <button key={m.id} type="button" onMouseDown={(e) => { e.preventDefault(); set('q', String(m.id)); setSearchOpen(false); }}
                                            className="flex w-full flex-col items-start px-3.5 py-2 text-left transition-colors hover:bg-surface-tint">
                                            <span className="text-[12.5px] font-semibold leading-tight text-foreground">#{m.id} · {m.company}</span>
                                            <span className="text-[11px] tabular-nums text-muted-foreground">{m.tanggal || ''}{m.division ? ` · ${m.division}` : ''}</span>
                                        </button>
                                    ))}
                                </div>
                            )}
                        </div>

                        <span aria-hidden="true" className="h-5 w-px shrink-0 bg-border/70" />

                        <RangePill label="Period" value={filters.dateRange} options={DATE_RANGE_OPTIONS} onChange={(v) => set('dateRange', v)} />

                        {coreFields.map((f) => (
                            <FilterPill key={f.key} label={f.label} value={filters[f.key]} options={f.opts} onChange={(v) => set(f.key, v)} />
                        ))}
                        {visibleExtras.map((f) => (
                            <FilterPill key={f.key} label={f.label} value={filters[f.key]} options={f.opts} onChange={(v) => set(f.key, v)} onRemove={() => removeExtra(f.key)} />
                        ))}

                        <AddFilterMenu fields={hiddenExtras} onAdd={(k) => setExtras((e) => [...e, k])} />

                        {activeFilterCount > 0 && (
                            <button type="button" onClick={resetFilters} className="inline-flex h-8 cursor-pointer items-center gap-1.5 rounded-full px-2.5 text-[12.5px] font-semibold text-muted-foreground transition-colors hover:bg-danger/10 hover:text-danger-text">
                                Reset filters
                            </button>
                        )}

                        <div className="ml-0 sm:ml-auto inline-flex items-center gap-0.5">
                            <button type="button" onClick={expandAll} title="Expand all" aria-label="Expand all"
                                className="grid size-7 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground">
                                <Maximize2 className="size-3.5" strokeWidth={2.5} />
                            </button>
                            <button type="button" onClick={collapseAll} title="Collapse all" aria-label="Collapse all"
                                className="grid size-7 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground">
                                <Minimize2 className="size-3.5" strokeWidth={2.5} />
                            </button>
                            <button type="button" onClick={() => setShowConfig((v) => !v)} title="Configuration" aria-label="Configuration" aria-pressed={showConfig}
                                className={`grid size-7 place-items-center rounded-md transition-colors ${showConfig ? 'bg-accent text-primary' : 'text-muted-foreground hover:bg-muted hover:text-foreground'}`}>
                                <Settings className="size-3.5" strokeWidth={2.5} />
                            </button>
                        </div>
                    </div>

                    <div className="overflow-x-auto">
                        <table
                            style={{ minWidth: CHECKBOX_W + ACTIONS_W + displayCols.reduce((s, c) => s + (colW[c.id] ?? COL_W_DEFAULT), 0) }}
                            className="w-full table-fixed border-separate border-spacing-0 [&_tbody_td]:whitespace-nowrap [&_tbody_td]:align-top [&_tbody_td]:text-[13px] [&_tbody_td]:text-card-foreground [&_tbody_td]:border-b [&_tbody_td]:border-border/40 [&_thead_th]:cursor-default [&_thead_th]:select-none [&_thead_th]:whitespace-nowrap [&_thead_th]:bg-transparent [&_thead_th]:text-left [&_thead_th]:text-[11px] [&_thead_th]:font-semibold [&_thead_th]:uppercase [&_thead_th]:tracking-wide [&_thead_th]:text-muted-foreground/70">
                            <colgroup>
                                <col style={{ width: CHECKBOX_W }} />
                                {displayCols.map((col) => <col key={col.id} style={{ width: colW[col.id] ?? COL_W_DEFAULT }} />)}
                                <col />
                            </colgroup>
                            <thead>
                                <tr>
                                    <th className="sticky left-0 z-20 w-12 bg-card! py-3 pl-6 pr-2">
                                        <CheckBox checked={allOnPageSelected} disabled={pageItemIds.length === 0} onChange={toggleAll} />
                                    </th>
                                    {displayCols.map((col) => {
                                        const pinned = PINNED.includes(col.id);
                                        return (
                                        <th key={col.id}
                                            draggable={!pinned}
                                            onDragStart={(e) => { if (pinned || resizeRef.current) { e.preventDefault(); return; } setDragColId(col.id); }}
                                            onDragOver={(e) => e.preventDefault()}
                                            onDrop={() => { if (!pinned) reorderCols(dragColId, col.id); setDragColId(null); }}
                                            onDragEnd={() => setDragColId(null)}
                                            title={pinned ? 'Pinned · drag right edge to resize' : 'Drag to reorder · drag right edge to resize'}
                                            className={`group/col relative py-3 pl-4 pr-4 ${pinned ? 'cursor-default' : 'cursor-grab active:cursor-grabbing'} ${FROZEN[col.id] ? `${FROZEN[col.id]} bg-card!` : ''} ${dragColId === col.id ? 'opacity-40' : ''}`}>
                                            {SORT_GETTERS[col.id]
                                                ? <SortButton id={col.id} label={col.label} sortKey={sortKey} sortDir={sortDir} onToggle={toggleSort} />
                                                : col.align ? <span className="block text-right">{col.label}</span> : col.label}
                                            <span onMouseDown={(e) => startResize(e, col.id)} onClick={(e) => e.stopPropagation()} title="Drag to resize column"
                                                className="group/resize absolute -right-1.5 top-0 z-10 flex h-full w-3 cursor-col-resize select-none items-center justify-center" aria-hidden="true">
                                                <span className={`w-0.5 rounded-full transition-all ${resizingId === col.id ? 'h-2/3 bg-primary' : 'h-1/3 bg-transparent group-hover/col:h-2/3 group-hover/col:bg-muted-foreground/30 group-hover/resize:h-2/3 group-hover/resize:bg-primary'}`} />
                                            </span>
                                        </th>
                                        );
                                    })}
                                    <th className="px-4 py-3 !text-right!">Actions</th>
                                </tr>
                            </thead>
                            <tbody>
                                {pageRows.length === 0 ? (
                                    <tr>
                                        <td colSpan={displayCols.length + 2} className="px-4 py-16 text-center">
                                            <Inbox aria-hidden="true" className="mx-auto mb-2 size-8 text-muted-foreground/40" />
                                            <p className="m-0 text-[13px] text-muted-foreground">No sample orders match the current filters.</p>
                                            {activeFilterCount > 0 && (
                                                <button type="button" onClick={resetFilters} className="mt-1.5 text-xs font-bold text-primary hover:underline">Reset filters</button>
                                            )}
                                        </td>
                                    </tr>
                                ) : (
                                    effGroup.length ? renderNodes(tree, 0, '') : pageRows.map((q) => renderLeaf(q, 0))
                                )}
                            </tbody>
                        </table>
                    </div>

                    {/* Footer — design-system ListFooter (Showing · ghost pager · rows-per-page) */}
                    <ListFooter
                        page={currentPage} totalPages={totalPages} onPage={setPage}
                        pageSize={pageSize} onPageSize={(n) => { setPageSize(n); setPage(1); }} pageSizeOptions={[5, 10, 15, 20]}
                        total={rows.length} itemLabel="sample orders"
                        stats={<>
                            <StatsDot /><span><b className="font-semibold text-foreground">{stats.total}</b> orders</span>
                            <StatsDot /><span><b className="font-semibold text-foreground">{stats.items}</b> items</span>
                            <StatsDot /><span><b className="font-semibold text-foreground">{stats.companies}</b> companies</span>
                            <StatsDot /><span><b className="font-semibold text-foreground">{stats.actionable}</b> actionable</span>
                        </>}
                    />
                </article>

                {/* Configuration panel */}
                {showConfig && (
                    <div className="lg:sticky lg:top-6 flex h-[70vh] lg:h-[calc(100vh-3rem)] w-full lg:w-[340px] shrink-0 flex-col overflow-hidden rounded-2xl border border-border bg-card shadow-sm">
                        <div className="flex shrink-0 items-start justify-between border-b border-border/60 px-5 py-5">
                            <div>
                                <h3 className="m-0 text-[14.5px] font-extrabold tracking-tight text-foreground">Configuration</h3>
                                <p className="m-0 mt-1 text-[12.5px] font-medium text-muted-foreground">Group the queue and arrange columns.</p>
                            </div>
                            <button type="button" onClick={() => setShowConfig(false)} aria-label="Close configuration"
                                className="-mr-1.5 -mt-1 rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground">
                                <X className="size-[18px]" strokeWidth={2.5} />
                            </button>
                        </div>

                        <div className="flex min-h-0 flex-1 flex-col gap-6 overflow-y-auto p-5">
                            {/* APPLIED FILTERS */}
                            <div className="flex flex-col gap-3">
                                <div className="flex items-center justify-between">
                                    <div className="flex items-center gap-2 text-[11px] font-extrabold uppercase tracking-widest text-muted-foreground">
                                        Applied filters <span className="text-[10px] font-semibold opacity-70">({activeFilterCount})</span>
                                    </div>
                                    {activeFilterCount > 0 && (
                                        <button type="button" onClick={resetFilters} className="text-[11px] font-bold text-primary hover:underline">Clear all</button>
                                    )}
                                </div>
                                <div className="flex flex-wrap gap-2">
                                    {filters.q.trim() && (
                                        <div className="inline-flex h-[26px] items-center gap-1.5 rounded border border-border/80 bg-surface px-2.5 text-[11.5px] font-semibold text-foreground shadow-sm">
                                            Search: {filters.q.trim()}
                                            <button type="button" onClick={() => set('q', '')} className="ml-0.5 text-muted-foreground hover:text-danger-text">×</button>
                                        </div>
                                    )}
                                    {filters.dateRange !== 'all' ? (
                                        <div className="inline-flex h-[26px] items-center gap-1.5 rounded border border-border/80 bg-surface px-2.5 text-[11.5px] font-semibold text-foreground shadow-sm">
                                            Period: {DATE_RANGE_OPTIONS.find((o) => o.value === filters.dateRange)?.label}
                                            <button type="button" onClick={() => set('dateRange', 'all')} className="ml-0.5 text-muted-foreground hover:text-danger-text">×</button>
                                        </div>
                                    ) : (
                                        <div className="inline-flex h-[26px] items-center gap-1.5 rounded border border-border/80 bg-muted/40 px-2.5 text-[11.5px] font-semibold text-muted-foreground">Period: All</div>
                                    )}
                                    {[...coreFields, ...extraFields].map((f) => {
                                        const vals = filters[f.key] || [];
                                        if (!vals.length) {
                                            return coreFields.some((c) => c.key === f.key) ? (
                                                <div key={`${f.key}-all`} className="inline-flex h-[26px] items-center gap-1.5 rounded border border-border/80 bg-muted/40 px-2.5 text-[11.5px] font-semibold text-muted-foreground">{f.label}: All</div>
                                            ) : null;
                                        }
                                        return vals.map((v) => (
                                            <div key={`${f.key}-${v}`} className="inline-flex h-[26px] items-center gap-1.5 rounded border border-border/80 bg-surface px-2.5 text-[11.5px] font-semibold text-foreground shadow-sm">
                                                {f.label}: {v}
                                                <button type="button" onClick={() => set(f.key, vals.filter((x) => x !== v))} className="ml-0.5 text-muted-foreground hover:text-danger-text">×</button>
                                            </div>
                                        ));
                                    })}
                                </div>
                            </div>

                            {/* ROWS */}
                            <div className="flex flex-col gap-3">
                                <div className="text-[11px] font-extrabold uppercase tracking-widest text-muted-foreground">Rows</div>
                                <div className="flex flex-col gap-2">
                                    <SortableList ids={groupBy} onReorder={setGroupBy} className="flex flex-col gap-2" renderItem={(id) => (
                                        <GroupPill
                                            label={GROUP_META[id]?.label || id}
                                            grouped={grouped.has(id)}
                                            onToggleGroup={() => toggleGrouped(id)}
                                            onRemove={() => { setGroupBy(groupBy.filter((x) => x !== id)); setGrouped((prev) => { const n = new Set(prev); n.delete(id); return n; }); }}
                                        />
                                    )} />
                                    {inactiveDims.length > 0 && (
                                        <select
                                            className="h-[34px] w-full cursor-pointer appearance-none rounded-lg border border-dashed border-input bg-muted/30 px-3 text-center text-[12.5px] font-semibold text-muted-foreground transition-colors hover:bg-muted focus:outline-none"
                                            value="" onChange={(e) => { if (e.target.value) { setGroupBy([...groupBy, e.target.value]); e.target.value = ''; } }}>
                                            <option value="" disabled>+ Add field</option>
                                            {inactiveDims.map((d) => <option key={d} value={d}>{GROUP_META[d].label}</option>)}
                                        </select>
                                    )}
                                    <p className="m-0 px-1 text-[11px] leading-snug text-muted-foreground/70">Check a field to group the table by it (pill order = nesting); drag to reorder, × to remove.</p>
                                </div>
                            </div>

                            {/* COLUMNS */}
                            <div className="flex flex-col gap-3">
                                <div className="flex items-center justify-between">
                                    <div className="text-[11px] font-extrabold uppercase tracking-widest text-muted-foreground">Columns</div>
                                    <button type="button" onClick={() => { try { localStorage.removeItem(COLUMN_STORAGE_KEY); } catch { /* ignore */ } setColumnState(defaultColumnState()); }} className="text-[11px] font-bold text-primary hover:underline">Reset</button>
                                </div>
                                <SortableList
                                    ids={columnState.map((c) => c.id)}
                                    onReorder={(ids) => {
                                        const byColId = new Map(columnState.map((c) => [c.id, c]));
                                        const next = ids.map((id) => byColId.get(id)).filter(Boolean);
                                        try { localStorage.setItem(COLUMN_STORAGE_KEY, JSON.stringify(next)); } catch { /* ignore */ }
                                        setColumnState(next);
                                    }}
                                    className="flex flex-col gap-2"
                                    renderItem={(id) => {
                                        const def = colDefById.get(id);
                                        const isVisible = columnState.find((c) => c.id === id)?.visible !== false;
                                        const required = def?.required;
                                        return (
                                            <div className={`flex h-9 items-center gap-2 rounded-lg border border-border/80 bg-muted/30 px-2.5 text-[12px] font-bold shadow-sm transition-opacity ${isVisible ? 'text-foreground' : 'text-muted-foreground opacity-60'}`}>
                                                <span className="grid place-items-center opacity-60 transition-opacity hover:opacity-100" aria-hidden="true">
                                                    <svg width="10" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><circle cx="9" cy="5" r="1" /><circle cx="9" cy="12" r="1" /><circle cx="9" cy="19" r="1" /><circle cx="15" cy="5" r="1" /><circle cx="15" cy="12" r="1" /><circle cx="15" cy="19" r="1" /></svg>
                                                </span>
                                                <span className="flex-1 truncate">{def?.label || id}</span>
                                                <button type="button" disabled={required} onPointerDown={(e) => e.stopPropagation()} onClick={() => toggleColumnVisible(id)}
                                                    title={required ? 'Required column' : isVisible ? 'Hide column' : 'Show column'}
                                                    className="inline-grid size-6 shrink-0 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-primary disabled:opacity-30">
                                                    {isVisible ? <Eye className="size-3.5" /> : <EyeOff className="size-3.5" />}
                                                </button>
                                            </div>
                                        );
                                    }}
                                />
                                <p className="m-0 px-1 text-[11px] leading-snug text-muted-foreground/70">Drag to reorder · eye toggles visibility · drag a header's right edge to resize.</p>
                            </div>
                        </div>

                        <div className="mt-auto flex shrink-0 items-center gap-3 border-t border-border/60 p-5">
                            <button type="button" onClick={() => { resetFilters(); setGroupBy(['principal', 'company', 'sales', 'industry', 'application', 'creator']); setGrouped(new Set()); setCollapsedPaths(new Set()); }}
                                className="h-9 flex-1 rounded-lg border border-input bg-card text-[13px] font-bold text-foreground transition-colors hover:border-primary hover:text-primary">Reset</button>
                            <button type="button" onClick={() => setShowConfig(false)}
                                className="h-9 flex-1 rounded-lg border border-transparent bg-linear-to-br from-violet-500 to-primary text-[13px] font-bold text-white shadow-sm transition-[filter] hover:brightness-105">Apply</button>
                        </div>
                    </div>
                )}
            </div>

            {/* COMMENT dialog — drives the real PM act POST (details = selected line ids). */}
            <Dialog open={commentDialog.open} onOpenChange={(o) => { if (!o) setCommentDialog((d) => ({ ...d, open: false })); }}>
                <DialogContent className="sm:max-w-[440px]">
                    <DialogHeader>
                        <DialogTitle className="flex items-center gap-2.5">
                            <span className={`grid size-8 shrink-0 place-items-center rounded-full ${act.badge}`}>
                                <act.icon className="size-4" strokeWidth={act.label === 'Approve' ? 3 : 2.5} />
                            </span>
                            <span>
                                {act.label}{' '}
                                {dialogLineCount === 1 ? '1 line item' : `${dialogLineCount} line items`}?
                            </span>
                        </DialogTitle>
                        <DialogDescription>
                            {dialogLineCount === 1 ? 'This line item' : `These ${dialogLineCount} line items`} will be{' '}
                            <b className={act.emphasis}>{act.verb}</b>{' '}and recorded in the sample order history.
                        </DialogDescription>
                    </DialogHeader>
                    <div>
                        <label htmlFor="approvalComment" className="mb-1.5 block text-xs font-semibold text-foreground">
                            Comment <span className="text-danger-text">*</span>
                        </label>
                        <Textarea
                            id="approvalComment"
                            autoFocus
                            value={actForm.data.comment}
                            onChange={(e) => { actForm.setData('comment', e.target.value); if (actForm.errors.comment) actForm.clearErrors('comment'); }}
                            placeholder={act.placeholder}
                            className="min-h-[96px] resize-none"
                        />
                        <p className="mt-1.5 text-[11px] font-medium italic text-muted-foreground">
                            Berlaku untuk {dialogLineCount} line Request dalam scope principal Anda{act.note}
                        </p>
                        {actForm.errors.comment && <p className="mt-1.5 text-[11px] font-semibold text-danger-text">{actForm.errors.comment}</p>}
                        {actForm.errors.details && <p className="mt-1.5 text-[11px] font-semibold text-danger-text">{actForm.errors.details}</p>}
                        <p className="mt-1.5 text-[11px] text-muted-foreground">Required — you can’t continue while it’s empty.</p>
                    </div>
                    <DialogFooter>
                        <Button
                            size="sm"
                            variant="default"
                            className={act.button}
                            disabled={!actForm.data.comment.trim() || actForm.processing}
                            onClick={submitComment}
                        >
                            Yes, {act.label}{dialogLineCount > 1 ? ` ${dialogLineCount}` : ''}
                        </Button>
                        <Button variant="outline" size="sm" onClick={() => setCommentDialog((d) => ({ ...d, open: false }))}>Cancel</Button>
                    </DialogFooter>
                </DialogContent>
            </Dialog>

            {/* LINKED DATA — real sampleorderlink records (kind + ref). */}
            <Dialog open={!!linkedQ} onOpenChange={(o) => !o && setLinkedQ(null)}>
                <DialogContent className="sm:max-w-[460px]">
                    <DialogHeader>
                        <DialogTitle className="flex items-center gap-2.5">
                            <span className="grid size-8 shrink-0 place-items-center rounded-full bg-primary/10 text-primary">
                                <Link2 className="size-4" />
                            </span>
                            <span>Linked data{linkedQ ? ` — sample order #${linkedQ.id}` : ''}</span>
                        </DialogTitle>
                        <DialogDescription>
                            Records linked to {linkedQ ? `sample order #${linkedQ.id}` : 'this sample order'}
                            {linkedQ ? ` (${linkedRecords(linkedQ).length})` : ''}.
                        </DialogDescription>
                    </DialogHeader>
                    <div className="flex flex-col divide-y divide-border/60 rounded-lg border border-border">
                        {(linkedQ ? linkedRecords(linkedQ) : []).length === 0 ? (
                            <div className="px-3.5 py-6 text-center text-[12.5px] italic text-muted-foreground">No linked records.</div>
                        ) : (linkedQ ? linkedRecords(linkedQ) : []).map((l, i) => (
                            <button key={i} type="button" onClick={() => { setLinkedQ(null); goToLink(l); }}
                                className="group flex items-center justify-between gap-3 px-3.5 py-2.5 text-left transition-colors hover:bg-muted/50">
                                <span className="flex items-center gap-2.5">
                                    <span className="grid size-7 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground group-hover:bg-primary/10 group-hover:text-primary"><Link2 className="size-3.5" /></span>
                                    <span className="flex flex-col leading-tight">
                                        <span className="text-[13px] font-semibold text-foreground">{l.kind}</span>
                                        <span className="text-[11px] font-medium text-primary tabular-nums">{l.ref}</span>
                                    </span>
                                </span>
                                <ChevronRight className="size-4 shrink-0 text-muted-foreground/50 transition-colors group-hover:text-primary" />
                            </button>
                        ))}
                    </div>
                    <DialogFooter>
                        <Button variant="outline" size="sm" onClick={() => setLinkedQ(null)}>Close</Button>
                    </DialogFooter>
                </DialogContent>
            </Dialog>

        {/* Floating decision pill — ui-conventions.md: the action row on an approval page is a
            centred pinned pill, never a right-aligned row. On a queue you tick rows going DOWN,
            so buttons above the table mean scrolling back UP to act. Approve first, heavier
            actions to its right. Rendered even with nothing picked, disabled: the checkbox
            column needs a visible verb. */}
        <DecisionBar>
          {selectedIds.size > 0 ? (
            <span className="inline-flex items-center gap-2 text-[12.5px] font-medium text-foreground">
              <span className="grid size-5 place-items-center rounded-full bg-primary text-[11px] font-bold text-primary-foreground tabular-nums">{selectedIds.size}</span>
              selected
              <button type="button" onClick={() => setSelectedIds(new Set())}
                className="text-[12px] font-medium text-muted-foreground transition-colors hover:text-foreground">Clear</button>
            </span>
          ) : (
            <span className="text-[12.5px] font-medium text-muted-foreground">Select rows to approve</span>
          )}
          <div className="flex items-center gap-2.5">
            <Button size="sm" disabled={selectedIds.size === 0} onClick={() => openComment('approve', [...selectedIds])} className="h-9 gap-1.5 px-4 text-xs font-bold">
              <Check className="size-3.5" strokeWidth={3} />Approve
            </Button>
            <Button variant="outline" size="sm" disabled={selectedIds.size === 0} onClick={() => openComment('revise', [...selectedIds])}
              className="h-9 gap-1.5 border-warning/40 px-4 text-xs font-bold text-warning-text hover:border-warning hover:bg-warning-bg hover:text-warning-text">
              <RotateCcw className="size-3.5" strokeWidth={2.5} />Revise
            </Button>
            <Button variant="outline" size="sm" disabled={selectedIds.size === 0} onClick={() => openComment('reject', [...selectedIds])}
              className="h-9 gap-1.5 border-danger/30 px-4 text-xs font-bold text-danger-text hover:border-danger hover:bg-danger/10 hover:text-danger-text">
              <Ban className="size-3.5" strokeWidth={2.5} />Reject
            </Button>
          </div>
        </DecisionBar>
        </section>
    );
}

SampleOrderApprovalPmIndex.layout = [AppLayout];
