import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
import {
    Calendar, Check, ChevronDown, Eye, Inbox,
    Maximize2, Minimize2, RotateCcw, Search, SlidersHorizontal, 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 { Switch } from '@/Components/ui/switch';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/Components/ui/dialog';
import { Textarea } from '@/Components/ui/textarea';
import { Link, useForm } from '@inertiajs/react';
import { ListFooter, StatsDot } from '@/Components/Table/ListFooter';
import { useResizableColumns, ColumnResizeGrip } from '@/lib/useResizableColumns';
import { useClientSort, SortButton } from '@/lib/ClientSort';
import { useToast } from '@/Components/Toast';
import { SELECTED_BG, SELECTED_HOVER_TR } from '@/lib/rowTint';

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' },
];

// ── Column customization (hide + drag-reorder) — managed INLINE in the
// Configuration panel (no modal). PM wants the legacy-complete view by
// default, so everything except Creator (all "System") and the mostly empty
// Links column starts visible — hide, not delete.
const COLUMN_DEFS = [
    { id: 'lwr', label: 'LWR', groupId: 'main', required: true },
    { id: 'projectTitle', label: 'Project Title', groupId: 'main' },
    { id: 'product', label: 'Product', groupId: 'main', required: true },
    { id: 'spec', label: 'Test Spec', groupId: 'main' },
    { id: 'value', label: 'Potential Value', groupId: 'main' },
    { id: 'remark', label: 'Product Remark', groupId: 'main' },
    { id: 'remarkPm', label: 'Remark PM', groupId: 'main' },
    { id: 'creator', label: 'Creator', groupId: 'extra' },
    { id: 'sales', label: 'Sales', groupId: 'extra' },
    { id: 'links', label: 'Links', groupId: 'extra' },
];
// Creator is hidden by default — every queue header is created by "System".
const DEFAULT_VISIBLE = new Set(['lwr', 'projectTitle', 'product', 'spec', 'value', 'remark', 'remarkPm', 'sales']);
const defaultColumnState = () => COLUMN_DEFS.map((d) => ({ id: d.id, visible: DEFAULT_VISIBLE.has(d.id) }));
const STORAGE_KEY = 'lwrApprovalPmColumnsState_v8';
function loadStoredState() {
    try {
        const raw = localStorage.getItem(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));
        COLUMN_DEFS.forEach((d) => { if (!ids.has(d.id)) parsed.push({ id: d.id, visible: DEFAULT_VISIBLE.has(d.id) }); });
        return parsed;
    }
    catch {
        return defaultColumnState();
    }
}
const RIGHT_COLS = new Set(['value']);
const CELL_CLASS = {
    value: 'text-right tabular-nums',
    remark: 'max-w-[220px] truncate',
    remarkPm: 'max-w-[220px] truncate',
    projectTitle: 'max-w-[200px] truncate',
};
// Fixed per-column pixel widths for the table-fixed + <colgroup> layout (resizable
// columns — drag a header's right edge). Mirrors the Quotation Approval PM reference;
// LWR has NO frozen/pinned columns, so the only non-column cols are the leading
// checkbox and the trailing Actions cell.
const CHECKBOX_W = 48;
const ACTIONS_W = 116;
const COL_W_DEFAULT = 130;
const COL_W = {
    lwr: 200, projectTitle: 200, product: 220, spec: 180, value: 150,
    remark: 220, remarkPm: 200, creator: 120, sales: 130, links: 90,
};
// Sortable columns — id → RAW row value for useClientSort (A→Z on first click,
// numbers sort numerically). One row = one product line here, so every data
// column is scalar per row and all of them sort.
const SORT_GETTERS = {
    lwr: (r) => r.lwrId,
    projectTitle: (r) => r.projectTitle,
    product: (r) => r.productName,
    spec: (r) => r.typeForm,
    value: (r) => parseNum(r.potentialValues),
    remark: (r) => r.productRemark,
    remarkPm: (r) => r.remarkPM,
    creator: (r) => r.creator,
    sales: (r) => r.sales,
    links: (r) => r.linkCount,
};

// ── Grouping (BnT Approval PM pattern) — rows group under reorderable dims.
// Default: Principal only; add/remove/reorder via the Configuration panel.
const GROUP_DIMS = ['principal', 'company', 'sales', 'creator', 'division', 'productFrom'];
const GROUP_META = {
    principal: { label: 'Principal', val: (q) => q.principalName || 'No Principal' },
    company: { label: 'Company', val: (q) => q.company || 'No Company' },
    sales: { label: 'Sales', val: (q) => q.sales || 'No Sales' },
    creator: { label: 'Creator', val: (q) => q.creator || 'No Creator' },
    division: { label: 'Division', val: (q) => q.division || 'No Division' },
    productFrom: { label: 'Product From', val: (q) => q.productFrom || 'NA' },
};
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));
};
// Tree node → stable expansion key (dim:value), shared by toggle + collapse-all.
const nodePath = (parentPath, node) => `${parentPath}¦${node.dim}:${node.key}`;

// Relative age for the anchor block — old requests turn amber, ancient ones red.
function timeAgo(dateStr) {
    const days = daysAgo(dateStr);
    if (days === null) return { label: 'NA', cls: 'text-muted-foreground' };
    const cls = days > 365 ? 'text-danger-text' : days > 90 ? 'text-warning-text' : 'text-muted-foreground';
    if (days < 1) return { label: 'today', cls };
    if (days < 30) return { label: `${Math.floor(days)} d ago`, cls };
    if (days < 365) return { label: `${Math.floor(days / 30)} mo ago`, cls };
    return { label: `${(days / 365).toFixed(1)} yr ago`, cls };
}

// Maps labworkrequestdetailstatus.StatusName → a calm badge tone (token-based, no hex).
const STATUS_TONES = {
    'request': 'bg-primary/10 text-primary',
    'approval sm': 'bg-warning/10 text-warning-text',
    'approval pm': 'bg-warning/10 text-warning-text',
    'revise': 'bg-warning-bg text-warning-text',
    'revised': 'bg-warning-bg text-warning-text',
    'feedback': 'bg-warning/10 text-warning-text',
    'reject': 'bg-danger/10 text-danger-text',
    'rejected': 'bg-danger/10 text-danger-text',
    'cancel': 'bg-danger/10 text-danger-text',
    'approved': 'bg-success/10 text-success-text',
    'print': 'bg-success/10 text-success-text',
    'lab processing': 'bg-success/10 text-success-text',
};
const statusTone = (s) => STATUS_TONES[(s || '').toLowerCase()] || 'bg-primary/10 text-primary';

function StatusBadge({ status }) {
    return (
        <span className={`inline-flex items-center gap-1.5 whitespace-nowrap rounded-md px-2.5 py-1 text-[11.5px] font-bold capitalize ${statusTone(status)}`}>
            <span aria-hidden="true" className="size-1.5 rounded-full bg-current" />{status || 'NA'}
        </span>
    );
}

function parseNum(v) {
    if (v === null || v === undefined || v === '') return 0;
    const n = Number(String(v).replace(/[^0-9.-]/g, ''));
    return Number.isNaN(n) ? 0 : n;
}
function formatUsd(n) {
    if (!n) return 'USD 0';
    return 'USD ' + new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(n);
}
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);
}
// Period pill — quick presets plus the legacy Early Date / End Date range.
// Custom dates take precedence over the preset.
function PeriodPill({ preset, from, to, 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 hasCustom = Boolean(from || to);
    const hasVal = hasCustom || preset !== 'all';
    const label = hasCustom
        ? `Period: ${from || '…'} → ${to || '…'}`
        : `Period: ${hasVal ? DATE_RANGE_OPTIONS.find((o) => o.value === preset)?.label : 'All'}`;
    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>{label}</span>
                {hasVal ? (
                    <span role="button" aria-label="Clear" onClick={(e) => { e.stopPropagation(); onChange({ preset: 'all', from: '', to: '' }); }}
                        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-[230px] overflow-hidden rounded-xl border border-border bg-surface py-1.5 shadow-modal">
                    {DATE_RANGE_OPTIONS.map((o) => (
                        <button key={o.value} type="button" onClick={() => { onChange({ preset: o.value, from: '', to: '' }); 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 ${!hasCustom && o.value === preset ? 'font-bold text-primary' : 'font-medium text-foreground'}`}>
                            {o.label}
                            {!hasCustom && o.value === preset && <Check className="size-3.5" strokeWidth={3} />}
                        </button>
                    ))}
                    <div className="mt-1 flex flex-col gap-2 border-t border-border/60 px-3.5 py-2.5">
                        <span className="text-[10px] font-extrabold uppercase tracking-wider text-muted-foreground">Custom range</span>
                        <label className="flex items-center justify-between gap-2 text-[11.5px] font-semibold text-muted-foreground">
                            Early Date
                            <input type="date" value={from} onChange={(e) => onChange({ preset: 'all', from: e.target.value, to })}
                                className="h-7 rounded-md border border-input bg-card px-1.5 text-[11.5px] text-foreground outline-none focus-visible:border-primary" />
                        </label>
                        <label className="flex items-center justify-between gap-2 text-[11.5px] font-semibold text-muted-foreground">
                            End Date
                            <input type="date" value={to} onChange={(e) => onChange({ preset: 'all', from, to: e.target.value })}
                                className="h-7 rounded-md border border-input bg-card px-1.5 text-[11.5px] text-foreground outline-none focus-visible:border-primary" />
                        </label>
                    </div>
                </div>
            )}
        </div>
    );
}

// Native HTML5 drag reorder (same approach as the BnT panel — react-sortablejs
// crashes under React 19, so we roll our own).
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, same contract as the BnT Approval PM PivotPill: every dim sits in the
// list; only the CHECKED ones actually nest the table.
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 emptyFilters = () => ({
    q: '', dateRange: 'all', dateFrom: '', dateTo: '',
    company: [], principal: [], sales: [], product: [], creator: [], companyCp: [], status: [],
});
const inSel = (arr, v) => !arr.length || arr.includes(v);

// One row per PRODUCT (labworkrequestdetails) — PM approves each product line
// individually, matching the legacy listlwrapprovalpm.php. Rows group under
// reorderable dims (default Principal) with a BnT-style Configuration panel.
export default function LwrApprovalPmIndex({ items = [] }) {
    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 (BnT Approval PM rules): the Rows list starts with EVERY
    // dim; "grouped" = which pills are checked, and only those nest the table.
    // Default: Principal first and the only grouped one.
    const [groupBy, setGroupBy] = useState(['principal', 'company', 'sales', 'creator', 'division', 'productFrom']);
    const [grouped, setGrouped] = useState(() => new Set(['principal']));
    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(20);
    const [page, setPage] = useState(1);
    const [selectedIds, setSelectedIds] = useState(new Set());

    // Approve/revise dialog → real Inertia POST to lwrs.approval-pm.act. The
    // action label stays past-tense ('approved'/'revised') for the dialog copy;
    // it maps to the route's 'approve'/'revise' at submit time.
    const [commentDialog, setCommentDialog] = useState({ open: false, action: 'approved', ids: [] });
    const actForm = useForm({ comment: '' });

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

    // Column customization — hide via the modal, reorder via modal drag or
    // dragging the table headers themselves. Persisted per page in localStorage.
    const [columnState, setColumnState] = useState(() => loadStoredState());
    const [dragColIdx, setDragColIdx] = useState(null);
    const [dragOverColIdx, setDragOverColIdx] = useState(null);
    // Per-column widths (resizable — drag a header's right edge). Seeded from COL_W;
    // `resizeRef` lets onDragStart bail while a resize is in progress so the grip
    // never starts a column reorder.
    const { widthOf, startResize, resizingId, resizeRef } = useResizableColumns(COL_W, COL_W_DEFAULT);
    const visibleCols = useMemo(() => columnState
        .filter((c) => c.visible)
        .map((c) => COLUMN_DEFS.find((d) => d.id === c.id))
        .filter(Boolean), [columnState]);
    const handleApplyColumns = (next) => {
        setColumnState(next);
        try { localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); } catch {}
    };
    const handleResetColumns = () => {
        const def = defaultColumnState();
        try { localStorage.removeItem(STORAGE_KEY); } catch {}
        return def;
    };
    const reorderColumns = (fromIdx, toIdx) => {
        if (fromIdx === null || fromIdx === toIdx) return;
        const fromId = visibleCols[fromIdx]?.id;
        const toId = visibleCols[toIdx]?.id;
        if (!fromId || !toId) return;
        setColumnState((prev) => {
            const next = [...prev];
            const fi = next.findIndex((c) => c.id === fromId);
            const ti = next.findIndex((c) => c.id === toId);
            const [moved] = next.splice(fi, 1);
            next.splice(ti, 0, moved);
            try { localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); } catch {}
            return next;
        });
    };

    const renderCell = (q, colId, currentStatus) => {
        switch (colId) {
            // Anchor — kept LIGHT (BnT leaf rule): the number always shows (sales
            // ask PM to "approve number xxxx") with the age inline, and the rest
            // only when its dim is NOT already a group header above the row.
            // No status badge while pending — it appears once the line is decided.
            case 'lwr': {
                const age = timeAgo(q.tanggal);
                const decided = currentStatus !== 'request';
                return (
                    <span className="flex flex-col items-start gap-0.5">
                        <span className="inline-flex items-baseline gap-2">
                            <Link href={route('lwrs.approval-pm.show', q.lwrId)} title={`View LWR #${q.lwrId}`} className="text-[13px] font-bold tracking-[-0.005em] text-primary hover:underline">#{q.lwrId}</Link>
                            <span className={`text-[11px] tabular-nums ${age.cls}`} title={q.tanggal || undefined}>{age.label}</span>
                        </span>
                        {!effGroup.includes('company') && <span className="text-[12px] font-semibold text-foreground">{q.company}</span>}
                        {decided && <StatusBadge status={currentStatus} />}
                    </span>
                );
            }
            // Product block — identity only: name + principal/origin. The subtitle
            // skips dims that are already group headers (no repetition).
            case 'product': {
                const sub1 = [
                    effGroup.includes('principal') ? null : q.principalName,
                    effGroup.includes('productFrom') ? null : q.productFrom,
                ].filter(Boolean).join(' · ');
                return (
                    <span className="flex flex-col gap-0.5">
                        <span className="text-[13px] font-semibold text-foreground">{q.productName || <span className="font-normal text-muted-foreground/70">NA</span>}</span>
                        {sub1 && <span className="text-[11.5px] text-muted-foreground">{sub1}</span>}
                    </span>
                );
            }
            // Test spec block — merged but fully labeled.
            case 'spec': {
                const vol = `${q.volumeTest}${q.satuanVolume && q.satuanVolume !== '-' ? ` ${q.satuanVolume}` : ''}`;
                return (
                    <span className="flex flex-col gap-0.5 tabular-nums">
                        <span className="text-foreground">{q.typeForm || <span className="text-muted-foreground/70">NA</span>}</span>
                        <span className="text-[11px] text-muted-foreground">Vol test: <span className="text-foreground">{vol}</span></span>
                        {q.colour && <span className="text-[11px] text-muted-foreground">Colour: <span className="text-foreground">{q.colour}</span></span>}
                    </span>
                );
            }
            // Value block — bold total on top, labeled price & potential volume under it.
            case 'value':
                return (
                    <span className="flex flex-col items-end gap-0.5 tabular-nums">
                        <span className="text-[13px] font-bold text-foreground">${q.potentialValues}</span>
                        <span className="whitespace-nowrap text-[11px] text-muted-foreground">Price: <span className="text-foreground">${q.unitPriceUSD}{q.satuanPrice ? ` ${q.satuanPrice}` : ''}</span></span>
                        <span className="whitespace-nowrap text-[11px] text-muted-foreground">Pot. vol: <span className="text-foreground">{q.potentialVolume}{q.satuanPotential ? ` ${q.satuanPotential}` : ''}</span></span>
                    </span>
                );
            case 'projectTitle': return q.projectTitle || <span className="text-muted-foreground/70">NA</span>;
            case 'creator': return q.creator || <span className="text-muted-foreground/70">NA</span>;
            case 'sales': return q.sales || <span className="text-muted-foreground/70">NA</span>;
            case 'remark': return q.productRemark || <span className="text-muted-foreground/70">NA</span>;
            case 'remarkPm': return q.remarkPM || <span className="text-muted-foreground/70">-</span>;
            case 'links': return q.linkCount ? q.linkCount : <span className="text-muted-foreground/70">NA</span>;
            default: return null;
        }
    };

    const source = items;

    // Search autocomplete (BnT pattern) — typing surfaces matching LWRs (deduped
    // per LWR No, since sales ask PM to "approve number xxxx"); picking one fills
    // the box with that number and the table narrows to its product lines.
    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 [];
        const byLwr = new Map();
        source.forEach((it) => {
            const hay = `${it.lwrId} ${it.company ?? ''} ${it.projectTitle ?? ''} ${it.productName ?? ''} ${it.principalName ?? ''}`.toLowerCase();
            if (!hay.includes(sq)) return;
            if (!byLwr.has(it.lwrId)) byLwr.set(it.lwrId, { lwrId: it.lwrId, company: it.company, tanggal: it.tanggal, products: 0 });
            byLwr.get(it.lwrId).products += 1;
        });
        return [...byLwr.values()].slice(0, 8);
    }, [source, sq]);

    const effStatus = (q) => q.status?.toLowerCase() || 'request';
    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 creatorOptions = useMemo(() => uniq(source.map((q) => q.creator)), [source]);
    const companyCpOptions = useMemo(() => uniq(source.map((q) => q.companyCp)), [source]);
    const principalOptions = useMemo(() => uniq(source.map((q) => q.principalName)), [source]);
    const productOptions = useMemo(() => uniq(source.map((q) => q.productName)), [source]);
    const statusOptions = useMemo(() => uniq(source.map(effStatus)), [source]);

    const rows = useMemo(() => {
        const qq = filters.q.trim().toLowerCase();
        return source.filter((q) => {
            if (qq) {
                const haystack = `${q.lwrId} ${q.company} ${q.projectTitle ?? ''} ${q.productName ?? ''} ${q.principalName ?? ''}`.toLowerCase();
                if (!haystack.includes(qq)) return false;
            }
            if (!inSel(filters.company, q.company)) return false;
            if (!inSel(filters.sales, q.sales)) return false;
            if (!inSel(filters.creator, q.creator)) return false;
            if (!inSel(filters.companyCp, q.companyCp)) return false;
            if (!inSel(filters.status, effStatus(q))) return false;
            if (!inSel(filters.principal, q.principalName)) return false;
            if (!inSel(filters.product, q.productName)) return false;
            // Custom Early/End Date (legacy) wins over the relative preset.
            if (filters.dateFrom || filters.dateTo) {
                const d = (q.tanggal || '').slice(0, 10);
                if (!d) return false;
                if (filters.dateFrom && d < filters.dateFrom) return false;
                if (filters.dateTo && d > filters.dateTo) return false;
            }
            else 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 (BnT rule).
    const effGroup = useMemo(() => groupBy.filter((d) => grouped.has(d)), [groupBy, grouped]);

    // Header sort (ClientSort house pattern) — applied to the filtered rows BEFORE
    // the grouping order + pagination slice, so it holds within groups and across pages.
    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), else
    // newest LWR first (the backend's order).
    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.lwrId) - Number(a.lwrId)) || (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);

    // Grouping tree over the current page (paths are dim:value, so the
    // collapsed state survives paging and filtering).
    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));

    const allOnPageSelected = pageRows.length > 0 && pageRows.every((q) => selectedIds.has(q.id));
    const toggleOne = (id) => {
        const next = new Set(selectedIds);
        if (next.has(id)) next.delete(id);
        else next.add(id);
        setSelectedIds(next);
    };
    const toggleAll = () => {
        const next = new Set(selectedIds);
        if (allOnPageSelected) pageRows.forEach((q) => next.delete(q.id));
        else pageRows.forEach((q) => next.add(q.id));
        setSelectedIds(next);
    };
    const toggleGroup = (ids) => {
        const all = ids.every((id) => selectedIds.has(id));
        setSelectedIds((prev) => { const n = new Set(prev); ids.forEach((id) => (all ? n.delete(id) : n.add(id))); return n; });
    };
    const groupState = (ids) => {
        const c = ids.reduce((n, id) => n + (selectedIds.has(id) ? 1 : 0), 0);
        return { checked: c > 0 && c === ids.length, indeterminate: c > 0 && c < ids.length };
    };

    const openComment = (action, ids) => {
        if (!ids || ids.length === 0) return;
        actForm.clearErrors();
        actForm.setData('comment', '');
        setCommentDialog({ open: true, action, ids });
    };
    const submitComment = () => {
        if (!actForm.data.comment.trim() || actForm.processing) return;
        const { action, ids } = commentDialog;
        // Dialog action ('approved'/'revised') → route action ('approve'/'revise').
        const routeAction = action === 'approved' ? 'approve' : 'revise';
        // @inertiajs/react's transform() SETS the transform and returns undefined
        // (NOT chainable) — chaining `.transform(...).post(...)` throws
        // "Cannot read properties of undefined (reading 'post')" inside the click
        // handler, so the button silently does nothing. Call them separately.
        actForm.transform((d) => ({ comment: d.comment, details: ids }));
        actForm.post(
            route('lwrs.approval-pm.act', { action: routeAction }),
            {
                preserveScroll: true,
                // On success the server reloads the queue: decided lines have left
                // status Request, so they drop out of `items` (legacy behavior).
                onSuccess: () => { setCommentDialog((d) => ({ ...d, open: false })); setSelectedIds(new Set()); },
                onError: () => showToast('Please check the form and try again.', 'error'),
            },
        );
    };

    const stats = useMemo(() => {
        const total = rows.length;
        const lwrs = new Set(rows.map((q) => q.lwrId)).size;
        const companies = new Set(rows.map((q) => q.company).filter(Boolean)).size;
        const potential = rows.reduce((s, q) => s + parseNum(q.potentialValues), 0);
        return { total, lwrs, companies, potential };
    }, [rows]);

    const activeFilterCount =
        (filters.q.trim() ? 1 : 0) +
        ((filters.dateFrom || filters.dateTo) ? 1 : (filters.dateRange !== 'all' ? 1 : 0)) +
        ['company', 'principal', 'sales', 'product', 'creator', 'companyCp', 'status']
            .reduce((n, k) => n + (filters[k].length ? 1 : 0), 0);

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

    // Core filter pills always shown in the panel; the rest behind "+ Add filter".
    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: 'creator', label: 'Creator', opts: creatorOptions },
        { key: 'companyCp', label: 'Company CP', opts: companyCpOptions },
        { key: 'status', label: 'Status', opts: statusOptions },
    ];
    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));

    // BnT rule: the first column header names the GROUPING ("Principal", or
    // "Principal / Company" for two levels) — it falls back to "LWR" only when
    // nothing is grouped.
    const lwrHeaderLabel = effGroup.length ? effGroup.map((d) => GROUP_META[d].label).join(' / ') : 'LWR';

    // Dialog subject line: single product vs N products.
    const dialogSubject = commentDialog.ids.length === 1
        ? (() => { const it = source.find((x) => x.id === commentDialog.ids[0]); return it ? `"${it.productName}" (LWR #${it.lwrId})` : '1 product'; })()
        : `${commentDialog.ids.length} products`;

    // ── Row renderers (BnT Approval PM tree pattern) ──
    const renderLeaf = (q, depth) => {
        const currentStatus = effStatus(q);
        const isApproved = currentStatus === 'approved';
        const isRevised = currentStatus === 'revised';
        const decided = isApproved || isRevised;
        return (
            <tr key={q.id}
                className={`group transition-colors ${selectedIds.has(q.id) ? `${SELECTED_BG} ${SELECTED_HOVER_TR}` : 'bg-card hover:bg-secondary/60'} ${decided ? 'opacity-55' : ''}`}>
                <td className="pl-6 pr-2">
                    <CheckBox checked={selectedIds.has(q.id)} onChange={() => toggleOne(q.id)} />
                </td>
                {visibleCols.map((col, i) => (
                    <td key={col.id} className={CELL_CLASS[col.id] || ''}
                        style={i === 0 && depth > 0 ? { paddingLeft: 16 + depth * 16 } : undefined}
                        title={col.id === 'remark' ? (q.productRemark || undefined) : col.id === 'remarkPm' ? (q.remarkPM || undefined) : undefined}>
                        {renderCell(q, col.id, currentStatus)}
                    </td>
                ))}
                <td>
                    {/* ACTIONS — icon buttons (✓ Approve · ↺ Revise), matching the
                        Quotation Approval PM design system. */}
                    <div className="flex items-center justify-end gap-1.5">
                        <Link href={route('lwrs.approval-pm.show', q.lwrId)} title="View detail" aria-label="View detail"
                            className="grid size-6 place-items-center rounded-md border border-border bg-transparent text-muted-foreground transition-colors hover:border-primary hover:bg-primary/5 hover:text-primary"><Eye className="size-3.5" strokeWidth={2.5} /></Link>
                        {decided ? (
                            <span className={`grid size-6 place-items-center ${isApproved ? 'text-success-text' : 'text-warning-text'}`}>
                                {isApproved ? <Check className="size-3.5" strokeWidth={3} /> : <RotateCcw className="size-3.5" strokeWidth={2.5} />}
                            </span>
                        ) : (
                            <>
                                <button type="button" onClick={() => openComment('approved', [q.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('revised', [q.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>
                            </>
                        )}
                    </div>
                </td>
            </tr>
        );
    };

    const renderNodes = (nodes, depth, parentPath) => nodes.map((node) => {
        const path = nodePath(parentPath, node);
        const isCollapsed = collapsedPaths.has(path);
        const ids = node.rows.map((r) => r.id);
        const sel = groupState(ids);
        const lwrCount = new Set(node.rows.map((r) => r.lwrId)).size;
        const potential = node.rows.reduce((s, r) => s + parseNum(r.potentialValues), 0);
        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="pl-6 pr-2">
                        <CheckBox {...sel} onChange={() => toggleGroup(ids)} ariaLabel={`Select all in ${node.label}`} />
                    </td>
                    <td colSpan={visibleCols.length + 1}>
                        <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-muted-foreground transition-transform ${isCollapsed ? '-rotate-90' : ''}`}>
                                <ChevronDown className={isTop ? 'size-4' : 'size-3.5'} strokeWidth={2.5} />
                            </span>
                            {/* name left; one quiet stats string flush right — stays tidy
                                no matter how long the principal name is */}
                            <span className="text-[12.5px] font-bold 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] tabular-nums text-muted-foreground">
                                {node.rows.length} product{node.rows.length !== 1 ? 's' : ''} · {lwrCount} LWR{lwrCount !== 1 ? 's' : ''}
                                <span aria-hidden="true" className="text-muted-foreground/40">·</span>
                                <span className="font-bold text-foreground" title="Total potential value">{formatUsd(potential)}</span>
                            </span>
                        </div>
                    </td>
                </tr>
                {!isCollapsed && (node.children ? renderNodes(node.children, depth + 1, path) : node.rows.map((r) => renderLeaf(r, depth + 1)))}
            </Fragment>
        );
    });

    return (
      <section className="flex min-w-0 flex-col gap-[18px]" id="lastLwrPmView">
        <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">
              <Link href={route('lwrs.index')} className="no-underline hover:text-primary">Lab Work Request</Link>
              <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">Lab Work Request PM</h1>
          </div>
        </header>

        <div className="flex flex-col lg:flex-row lg:items-start gap-5">
          {/* Main list — one row per product line, grouped under the configured dims */}
          <article className="min-w-0 flex-1 overflow-hidden rounded-xl border border-border bg-card">
            {/* Toolbar — search + period stay here; field filters live in the panel */}
            <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 LWR No, Company, Project, 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.lwrId} type="button" onMouseDown={(e) => { e.preventDefault(); set('q', String(m.lwrId)); 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.lwrId} · {m.company}</span>
                        <span className="text-[11px] tabular-nums text-muted-foreground">{m.products} product{m.products !== 1 ? 's' : ''}{m.tanggal ? ` · ${m.tanggal}` : ''}</span>
                      </button>
                    ))}
                  </div>
                )}
              </div>

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

              <PeriodPill preset={filters.dateRange} from={filters.dateFrom} to={filters.dateTo}
                onChange={(p) => { setFiltersRaw((f) => ({ ...f, dateRange: p.preset, dateFrom: p.from, dateTo: p.to })); setPage(1); }} />

              {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'}`}>
                  <SlidersHorizontal className="size-3.5" strokeWidth={2.5} />
                </button>
              </div>
            </div>

            <div className="overflow-x-auto">
              <table
                style={{ minWidth: CHECKBOX_W + ACTIONS_W + visibleCols.reduce((s, c) => s + widthOf(c.id), 0) }}
                className="w-full table-fixed border-separate border-spacing-0 [&_tbody_td]:whitespace-nowrap [&_tbody_td]:p-[12px_14px] [&_tbody_td]:align-top [&_tbody_td]:text-[12.5px] [&_tbody_td]:text-card-foreground [&_tbody_td]:border-b [&_tbody_td]:border-border/60 [&_thead_th]:cursor-default [&_thead_th]:select-none [&_thead_th]:whitespace-nowrap [&_thead_th]:bg-[color-mix(in_srgb,var(--color-secondary)_50%,var(--color-card))] [&_thead_th]:p-[14px_14px] [&_thead_th]:text-left [&_thead_th]:text-[11px] [&_thead_th]:font-semibold [&_thead_th]:uppercase [&_thead_th]:tracking-wide [&_thead_th]:text-muted-foreground [&_thead_th:first-child]:rounded-l-full [&_thead_th:last-child]:rounded-r-full">
                <colgroup>
                  <col style={{ width: CHECKBOX_W }} />
                  {visibleCols.map((col) => <col key={col.id} style={{ width: widthOf(col.id) }} />)}
                  <col style={{ width: ACTIONS_W }} />
                </colgroup>
                <thead>
                  <tr>
                    <th className="w-12 pl-6 pr-2">
                       <CheckBox checked={allOnPageSelected} onChange={toggleAll} />
                    </th>
                    {visibleCols.map((col, i) => (
                      <th
                        key={col.id}
                        draggable
                        onDragStart={(e) => { if (resizeRef.current) { e.preventDefault(); return; } setDragColIdx(i); }}
                        onDragOver={(e) => { e.preventDefault(); setDragOverColIdx(i); }}
                        onDrop={() => { reorderColumns(dragColIdx, i); setDragColIdx(null); setDragOverColIdx(null); }}
                        onDragEnd={() => { setDragColIdx(null); setDragOverColIdx(null); }}
                        title="Drag to reorder · drag right edge to resize"
                        className={`group/col relative cursor-grab active:cursor-grabbing transition-colors ${RIGHT_COLS.has(col.id) ? 'text-right!' : ''} ${dragColIdx === i ? 'opacity-45' : ''} ${dragOverColIdx === i && dragColIdx !== i ? 'bg-accent text-accent-foreground' : ''}`}
                      >
                        {SORT_GETTERS[col.id]
                          ? <SortButton id={col.id} label={col.id === 'lwr' ? lwrHeaderLabel : col.label} sortKey={sortKey} sortDir={sortDir} onToggle={toggleSort} />
                          : (col.id === 'lwr' ? lwrHeaderLabel : col.label)}
                        <ColumnResizeGrip onMouseDown={(e) => startResize(e, col.id)} active={resizingId === col.id} />
                      </th>
                    ))}
                    <th className="!text-right!">Actions</th>
                  </tr>
                </thead>
                <tbody>
                  {pageRows.length === 0 ? (
                    <tr>
                      <td colSpan={visibleCols.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 product lines 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>

            <ListFooter page={currentPage} totalPages={totalPages} onPage={setPage}
               pageSize={pageSize} onPageSize={(n) => { setPageSize(n); setPage(1); }}
               pageSizeOptions={[10, 20, 50, 100]} total={sortedRows.length} itemLabel="products"
               stats={<>
                  <StatsDot />
                  <span><b className="font-semibold text-foreground">{stats.total}</b> products</span>
                  <StatsDot />
                  <span><b className="font-semibold text-foreground">{stats.lwrs}</b> LWRs</span>
                  <StatsDot />
                  <span><b className="font-semibold text-foreground">{stats.companies}</b> companies</span>
                  <StatsDot />
                  <span className="font-semibold text-foreground" title="Total potential value">{formatUsd(stats.potential)}</span>
               </>} />
          </article>

          {/* Configuration panel — BnT Approval PM pattern: filters + group-by */}
          {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 filter it.</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 — mirrors the toolbar pills, BnT-style chips */}
                <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.dateFrom || filters.dateTo || 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: {(filters.dateFrom || filters.dateTo) ? `${filters.dateFrom || '…'} → ${filters.dateTo || '…'}` : DATE_RANGE_OPTIONS.find((o) => o.value === filters.dateRange)?.label}
                        <button type="button" onClick={() => { setFiltersRaw((f) => ({ ...f, dateRange: 'all', dateFrom: '', dateTo: '' })); setPage(1); }} 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 — BnT rule: all dims listed; the checkbox makes one a group */}
                <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 — inline, same grammar as Rows: drag = order, check = show */}
                <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={() => handleApplyColumns(handleResetColumns())} className="text-[11px] font-bold text-primary hover:underline">Reset</button>
                  </div>
                  <SortableList
                    ids={columnState.map((c) => c.id)}
                    onReorder={(ids) => { const byId = new Map(columnState.map((c) => [c.id, c])); handleApplyColumns(ids.map((id) => byId.get(id)).filter(Boolean)); }}
                    className="flex flex-col gap-2"
                    renderItem={(id) => {
                      const def = COLUMN_DEFS.find((d) => d.id === id);
                      const st = columnState.find((c) => c.id === id);
                      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 ${st?.visible ? 'text-foreground' : 'text-muted-foreground/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>
                          <span className="flex items-center" title={def?.required ? 'Required column' : (st?.visible ? 'Shown — switch off to hide' : 'Hidden — switch on to show')}>
                            <Switch size="sm" checked={!!st?.visible} disabled={def?.required}
                              onCheckedChange={() => handleApplyColumns(columnState.map((c) => c.id === id ? { ...c, visible: !c.visible } : c))}
                              aria-label={`Show column ${def?.label || id}`} />
                          </span>
                        </div>
                      );
                    }}
                  />
                  <p className="m-0 px-1 text-[11px] leading-snug text-muted-foreground/70">Drag to reorder; uncheck to hide. You can also drag the table headers directly.</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', 'creator', 'division', 'productFrom']); setGrouped(new Set(['principal'])); 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>

        <Dialog open={commentDialog.open} onOpenChange={(o) => setCommentDialog((d) => ({ ...d, open: o }))}>
          <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 ${commentDialog.action === 'approved' ? 'bg-success/12 text-success-text' : 'bg-warning-bg text-warning-text'}`}>
                  {commentDialog.action === 'approved'
                    ? <Check className="size-4" strokeWidth={3} />
                    : <RotateCcw className="size-4" strokeWidth={2.5} />}
                </span>
                <span>
                  {commentDialog.action === 'approved' ? 'Approve' : 'Revise'} {dialogSubject}?
                </span>
              </DialogTitle>
              <DialogDescription>
                {commentDialog.ids.length === 1 ? 'This product' : `${commentDialog.ids.length} products`} will be{' '}
                <b className={commentDialog.action === 'approved' ? 'text-success-text' : 'text-warning-text'}>
                  {commentDialog.action === 'approved' ? 'Approved' : 'sent back for revision'}
                </b>{' '}and recorded in the LWR history.
              </DialogDescription>
            </DialogHeader>
            <div>
              <label htmlFor="lwrApprovalComment" className="mb-1.5 block text-xs font-semibold text-foreground">
                Comment <span className="text-danger-text">*</span>
              </label>
              <Textarea
                id="lwrApprovalComment"
                autoFocus
                value={actForm.data.comment}
                onChange={(e) => actForm.setData('comment', e.target.value)}
                placeholder={commentDialog.action === 'approved' ? 'Approval note…' : 'Apa yang perlu direvisi…'}
                className="min-h-[96px] resize-none"
              />
              {(actForm.errors.comment || actForm.errors.details) ? (
                <p className="mt-1.5 text-[11px] text-danger-text">{actForm.errors.comment || actForm.errors.details}</p>
              ) : (
                <p className="mt-1.5 text-[11px] text-muted-foreground">Required — cannot continue with an empty comment.</p>
              )}
            </div>
            <DialogFooter>
              <Button
                size="sm"
                variant="default"
                className={commentDialog.action === 'approved' ? '' : 'border border-warning bg-warning-bg text-warning-text hover:bg-warning-bg/80'}
                disabled={!actForm.data.comment.trim() || actForm.processing}
                onClick={submitComment}
              >
                Yes, {commentDialog.action === 'approved' ? 'Approve' : 'Revise'}{commentDialog.ids.length > 1 ? ` ${commentDialog.ids.length}` : ''}
              </Button>
              <Button variant="outline" size="sm" onClick={() => setCommentDialog((d) => ({ ...d, open: false }))}>Cancel</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('approved', [...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('revised', [...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>
          </div>
        </DecisionBar>
      </section>
    );
}

LwrApprovalPmIndex.layout = [AppLayout]
