import React, { useState, useEffect, useMemo, useCallback, useRef, createContext, useContext } from 'react';
import { createRoot } from 'react-dom/client';
import { createPortal } from 'react-dom';
import {
  LayoutDashboard, GitBranch, FileText, FlaskConical, Package, Users, Archive, Bell, Search,
  Plus, Trash2, X, ChevronDown, ChevronRight, ChevronLeft, Check, AlertTriangle, Clock, Star,
  MessageSquare, Paperclip, Upload, MoreHorizontal, Menu, ArrowLeft, Sparkles, Send, Building2,
  Globe2, Calendar, TrendingUp, Filter, ExternalLink, Pencil, CircleDot, LogOut, UserCog,
  Download, Info, Award, Mail, Phone, BarChart3, PieChart, Handshake, Flag, CheckCircle2, XCircle,
  RotateCcw, ThumbsUp, ThumbsDown, Sparkle, RefreshCw, ChevronsUpDown, CalendarClock, Truck, Wallet, Settings2,
  Loader2, Inbox, LogIn, UserPlus,
} from 'lucide-react';
import { createClient } from '@supabase/supabase-js';
import { QueryClient, QueryClientProvider, useQuery, useQueryClient } from '@tanstack/react-query';
// Plain JS, no JSX - loads via native ES module resolution, no Babel-transform
// tag needed (unlike IntelligenceReport.jsx, see the comment at its top).
import { selectProjectSlice, computeProgress, computeHealth, computeForecast, computeHistoricalBaseline } from './reportEngine.js';

/* ======================================================================
   0. SUPABASE CLIENT (Phase 4)
   ====================================================================== */

// Project URL + anon/publishable key - safe to ship in client code by design
// (this is what the anon key is *for*; every permission it has is governed by
// RLS, verified working in supabase/tests/phase3_rls_manual.sql). Never put
// the service_role secret key here or anywhere in browser code.
const SUPABASE_URL = 'https://nsxhgxtimqjtmmjzhsww.supabase.co';
const SUPABASE_ANON_KEY = 'sb_publishable_LjJ6kFhIkbl373FRqhCrEg_KS-NNA5p';
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);

const queryClient = new QueryClient({
  defaultOptions: { queries: { staleTime: 30_000, retry: 1 } },
});

/* ======================================================================
   1. OPTION LISTS
   ====================================================================== */

const NEXT_ACTIONS = ['Will arrive soon', 'Production about to finish', 'Arranging shipments', 'Customs clearance', 'Awaiting inspection', 'Ready for dispatch', 'On Hold', 'Under Shipping', 'Complete'];
const CATEGORY_OPTIONS = ['Perfume', 'Gift', 'Diffuser'];
const PRIORITY_OPTIONS = ['Critical', 'High', 'Normal', 'Low'];
const COMPONENT_STATUS_FILTERS = ['Under Production', 'Complete'];
const PROJECT_STATUSES = ['Development', 'Quotation', 'Sampling', 'Production', 'Shipping', 'Complete'];
const CURRENCIES = [
  { code: 'EUR', symbol: '€' },
  { code: 'USD', symbol: '$' },
  { code: 'GBP', symbol: '£' },
  { code: 'AED', symbol: 'AED' },
  { code: 'SAR', symbol: 'SAR' },
  { code: 'CNY', symbol: 'CN¥' },
  { code: 'CHF', symbol: 'CHF' },
  { code: 'JPY', symbol: '¥' },
  { code: 'INR', symbol: '₹' },
  { code: 'EGP', symbol: 'E£' },
];
const EXCHANGE_RATES_TO_USD = {
  EUR: 1.08, USD: 1, GBP: 1.27, AED: 0.272, SAR: 0.267, CNY: 0.14, CHF: 1.12, JPY: 0.0067, INR: 0.012, EGP: 0.021,
};
function toUSD(amount, currency, rates = EXCHANGE_RATES_TO_USD) { return amount * (rates[currency] ?? EXCHANGE_RATES_TO_USD[currency] ?? 1); }
function fmtRelativeFromISO(iso) {
  if (!iso) return 'never';
  const diffMs = Date.now() - new Date(iso).getTime();
  const diffH = Math.floor(diffMs / 3600000);
  if (diffH < 1) return 'just now';
  if (diffH < 24) return `${diffH}h ago`;
  return `${Math.floor(diffH / 24)}d ago`;
}

/* ======================================================================
   2. HELPERS
   ====================================================================== */

let __uidCounter = 0;
function uid(prefix) { __uidCounter += 1; return `${prefix}_${Date.now().toString(36)}${__uidCounter}${Math.random().toString(36).slice(2, 5)}`; }

function todayStr() { return new Date().toISOString().slice(0, 10); }
function addDays(dateStr, days) { const d = new Date(dateStr); d.setDate(d.getDate() + days); return d.toISOString().slice(0, 10); }
function fmtDate(dateStr) {
  if (!dateStr) return '—';
  const d = new Date(dateStr + (dateStr.length <= 10 ? 'T00:00:00' : ''));
  if (isNaN(d.getTime())) return dateStr;
  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
function fmtDateShort(dateStr) {
  if (!dateStr) return '—';
  const d = new Date(dateStr + 'T00:00:00');
  if (isNaN(d.getTime())) return dateStr;
  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
function daysBetween(a, b) { return Math.floor((new Date(b) - new Date(a)) / 86400000); }
function fmtDueRelative(dateStr) {
  if (!dateStr) return '—';
  const diff = daysBetween(todayStr(), dateStr);
  if (diff === 0) return 'Today';
  if (diff === 1) return 'Tomorrow';
  if (diff > 1) return `In ${diff} days`;
  return `${-diff} day${diff === -1 ? '' : 's'} ago`;
}
// Accepts either a plain date string ('2026-08-01') or a full ISO timestamp — both parse
// correctly via `new Date()`, a bare date just resolves to midnight UTC that day.
function hoursSince(dateLike) { return dateLike ? Math.floor((Date.now() - new Date(dateLike).getTime()) / 3600000) : 0; }
function fmtDateTimeShort(iso) {
  if (!iso) return '—';
  const d = new Date(iso);
  if (isNaN(d.getTime())) return iso;
  return `${d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}, ${d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}`;
}
function durationSince(startDate) { return Math.max(0, Math.floor((Date.now() - new Date(startDate + 'T00:00:00').getTime()) / 86400000)); }
function fmtEUR(n, opts = {}) {
  if (n === null || n === undefined || n === '') return '—';
  const num = Number(n);
  const decimals = opts.decimals !== undefined ? opts.decimals : (num < 10 ? 2 : 0);
  return `€${num.toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals })}`;
}
function currencySymbol(code) { return CURRENCIES.find(c => c.code === code)?.symbol || code || '€'; }
function fmtCurrency(n, code = 'EUR', opts = {}) {
  if (n === null || n === undefined || n === '') return '—';
  const num = Number(n);
  const decimals = opts.decimals !== undefined ? opts.decimals : (num < 10 ? 2 : 0);
  const sym = currencySymbol(code);
  const formatted = num.toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
  return ['€', '$', '£', '¥', '₹'].includes(sym) ? `${sym}${formatted}` : `${sym} ${formatted}`;
}
function initials(name) { return (name || '?').split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase(); }
function parseNumeric(str) { const n = parseFloat(String(str ?? '').replace(/[^0-9.]/g, '')); return isNaN(n) ? 0 : n; }
function lineItemAmount(li) {
  return (li.unitPrice || 0) * (li.qty || parseNumeric(li.moq)) + (li.moldCost || 0);
}
function quotationTotalAmount(q) {
  return (q.lineItems || []).reduce((sum, li) => sum + lineItemAmount(li), 0);
}
function quotationAcceptedAmount(q) {
  return (q.lineItems || []).filter(li => li.status === 'Approved' || li.status === 'In Sampling').reduce((sum, li) => sum + lineItemAmount(li), 0);
}
function quotationRejectedAmount(q) {
  return (q.lineItems || []).filter(li => li.status === 'Rejected').reduce((sum, li) => sum + lineItemAmount(li), 0);
}
function deriveQuotationStatus(q) {
  const items = q.lineItems || [];
  if (!items.length) return 'Pending';
  const statuses = items.map(li => li.status || 'Pending');
  if (statuses.every(s => s === 'Pending')) return 'Pending';
  if (statuses.every(s => s === 'Rejected')) return 'Rejected';
  if (statuses.every(s => s === 'Approved' || s === 'In Sampling')) return 'Approved';
  if (statuses.every(s => s === 'Developing')) return 'Developing';
  return 'Partial';
}
function lineItemDecisionSummary(items) {
  const order = ['Approved', 'In Sampling', 'Developing', 'Rejected', 'Pending'];
  const counts = { Approved: 0, 'In Sampling': 0, Developing: 0, Rejected: 0, Pending: 0 };
  (items || []).forEach(li => { const s = li.status || 'Pending'; counts[s] = (counts[s] || 0) + 1; });
  const parts = order.filter(k => counts[k] > 0).map(k => `${k} ${counts[k]}`);
  return parts.length ? parts.join(' · ') : 'Pending 0';
}
function classifyWaiting(waitingStr, suppliers, users) {
  if (!waitingStr) return 'External';
  if (suppliers.some(s => waitingStr.includes(s.name))) return 'Supplier';
  if (users.some(u => waitingStr.includes(u.name))) return 'Internal';
  return 'External';
}
function relativeAgoBucket(createdAtIso) {
  if (!createdAtIso) return 'Earlier';
  const diffH = (Date.now() - new Date(createdAtIso).getTime()) / 3600000;
  if (diffH < 24) return 'Today';
  if (diffH < 48) return 'Yesterday';
  return 'Earlier';
}
function userById(users, id) { return users.find(u => u.id === id); }
function supplierById(suppliers, id) { return suppliers.find(s => s.id === id); }

function canSeeProject(user, project) {
  if (!project) return false;
  if (user.accessRole === 'Admin') return true;
  if (project.owner === user.id) return true;
  if (project.visibleTo?.includes('all')) return true;
  if (project.visibleTo?.includes(user.id)) return true;
  return false;
}

function canEditProject(user, project) {
  if (!user || !project) return false;
  if (user.accessRole === 'Admin') return true;
  if (project.owner === user.id) return true;
  if (project.editableBy?.includes(user.id)) return true;
  return false;
}

// Item-level ownership: whoever actually created/owns a specific quotation, sampling
// track, or component is the only non-admin who can amend it — being the project's
// owner or a fellow collaborator grants visibility and the right to add your OWN new
// work, not the right to touch someone else's. Falls back to the project owner only
// when no per-item owner is recorded at all (defensive — should not normally happen).
function canEditItem(user, itemOwnerId, project) {
  if (!user) return false;
  if (user.accessRole === 'Admin') return true;
  if (itemOwnerId) return user.id === itemOwnerId;
  return canEditProject(user, project);
}

// Legacy quotations created before `createdBy` existed fall back to the project's
// owner, preserving their previous (project-level) edit behavior without a migration.
function quotationOwnerId(quotation, project) {
  return quotation.createdBy || project?.owner;
}

function canEditQuotation(user, quotation, project) {
  if (!user) return false;
  if (user.accessRole === 'Admin') return true;
  const items = quotation.lineItems || [];
  if (items.length && items.every(li => li.locked)) return false;
  return quotationOwnerId(quotation, project) === user.id;
}

function canDeleteQuotation(user, quotation, project) {
  if (!user) return false;
  if (user.accessRole === 'Admin') return true;
  if ((quotation.lineItems || []).some(li => li.locked)) return false;
  return quotationOwnerId(quotation, project) === user.id;
}

function canEditLineItem(user, quotation, project, lineItem) {
  if (!user) return false;
  if (lineItem.locked) return user.accessRole === 'Admin';
  if (user.accessRole === 'Admin') return true;
  return quotationOwnerId(quotation, project) === user.id;
}

const STATUS_TONE = {
  Development: 'info', Quotation: 'purple', Sampling: 'amber', Production: 'gold', Shipping: 'sage', Complete: 'muted',
  Pending: 'muted', Shortlisted: 'info', Selected: 'sage', Rejected: 'oud', Partial: 'amber', 'In Sampling': 'gold',
  Sourcing: 'info', 'Sampling In Progress': 'amber', 'Sample Approved': 'sage', 'Moved to Production': 'gold', 'On Hold': 'amber', Cancelled: 'oud',
  Requested: 'muted', 'In Transit': 'info', Received: 'purple', 'Under Review': 'amber', Approved: 'sage', 'Needs Revision': 'oud',
  'Under Production': 'gold', 'Under Shipping': 'info', Confirmed: 'info', Dispatched: 'purple', Arrived: 'sage',
  Potential: 'muted', Quoted: 'info', 'Sample Requested': 'amber', 'Selected for Production': 'sage',
  Todo: 'muted', 'In Progress': 'amber', Done: 'sage',
  'On Track': 'sage', 'At Risk': 'amber', Behind: 'oud',
  Developing: 'purple',
  Blocked: 'oud', Decision: 'amber', Stale: 'muted',
};

/* oud/sage read their bg/border/text straight off the (now luxury-hex) `oud`/`sage`
   tokens, so they get the "enamel glow" treatment via one added shadow each. `gold` here
   is a status TONE (In Sampling/Production/etc business statuses) and is deliberately
   NOT the `gold` Tailwind token (which still means brand blue for buttons/links) - it's
   hardcoded to the actual luxury gold hexes so the two "gold"s can coexist without
   colliding. */
const TONE_CLASSES = {
  info: 'bg-info/10 hover:bg-info/15 text-info border-info/30',
  purple: 'bg-purple/10 hover:bg-purple/15 text-purple border-purple/30',
  amber: 'bg-amber/10 hover:bg-amber/15 text-amber border-amber/30',
  gold: 'bg-[#C9A66B]/15 hover:bg-[#C9A66B]/20 text-[#8B6B3D] border-[#C9A66B]/30 shadow-[0_0_10px_rgba(201,166,107,0.2)]',
  sage: 'bg-sage/12 hover:bg-sage/20 text-[#4A7046] border-sage/30 shadow-[0_0_10px_rgba(122,155,118,0.2)]',
  oud: 'bg-oud/10 hover:bg-oud/15 text-oud border-oud/30 shadow-[0_0_10px_rgba(163,75,61,0.2)]',
  muted: 'bg-stale/10 hover:bg-stale/15 text-stale border-stale/30',
};
const TONE_DOT = {
  info: 'bg-info', purple: 'bg-purple', amber: 'bg-amber', gold: 'bg-[#C9A66B]', sage: 'bg-sage', oud: 'bg-oud', muted: 'bg-stale',
};
/* Light theme: no ambient glow (reads as dark-theme atmosphere, not "crisp"). Kept as a
   no-op map so the `glow` prop on StatusPill/PriorityTag/etc. stays a safe no-op - the
   enamel glow above is always-on via TONE_CLASSES instead, not gated by this prop. */
const TONE_GLOW = {
  oud: '', sage: '', gold: '', amber: '', purple: '',
};
const TONE_HEX = {
  info: '#66C2FB', purple: '#6D5FE0', amber: '#E5A72E', gold: '#C9A66B', sage: '#7A9B76', oud: '#A34B3D', muted: '#98A2B3',
};

const PRIORITY_TONE = { Critical: 'oud', High: 'amber', Normal: 'muted', Low: 'sage' };
// computeHealth() output (reportEngine.js) -> the same sage/amber/oud tones StatusPill
// and PriorityTag already use elsewhere, so a project's health reads the same color
// no matter which view (Pipeline Timeline, Dashboard, Project Overview) shows it.
const HEALTH_TONE = { 'On Track': 'sage', 'At Risk': 'amber', Behind: 'oud', Complete: 'sage' };

/* Sample round logistics lifecycle */
const SAMPLE_ROUND_FLOW_STEPS = ['Requested', 'Confirmed', 'Under Production', 'Dispatched', 'In Transit', 'Arrived', 'Under Review'];
const SAMPLE_ROUND_TERMINAL_STATUSES = ['Approved', 'Rejected', 'Needs Revision'];
const SAMPLE_ROUND_TRANSITIONS = {
  Requested: ['Confirmed'],
  Confirmed: ['Under Production'],
  'Under Production': ['Dispatched'],
  Dispatched: ['In Transit'],
  'In Transit': ['Arrived'],
  Arrived: ['Under Review'],
  'Under Review': ['Approved', 'Rejected', 'Needs Revision'],
};
const SAMPLE_ROUND_TERMINAL_TONE = { Approved: 'sage', Rejected: 'oud', 'Needs Revision': 'amber' };

/* ======================================================================
   3. CONTEXT
   ====================================================================== */

const AppContext = createContext(null);
const useApp = () => useContext(AppContext);

// Keeps a modal/panel mounted through its exit animation instead of vanishing instantly
// when `open` flips false. Returns 'closed' (don't render), 'open' (entrance state), or
// 'closing' (play the exit animation, then unmount after exitMs).
function useDeferredOpen(open, exitMs = 150) {
  const [phase, setPhase] = useState(open ? 'open' : 'closed');
  const timerRef = useRef(null);
  useEffect(() => {
    clearTimeout(timerRef.current);
    if (open) {
      setPhase('open');
    } else {
      setPhase(p => (p === 'closed' ? 'closed' : 'closing'));
      timerRef.current = setTimeout(() => setPhase('closed'), exitMs);
    }
    return () => clearTimeout(timerRef.current);
  }, [open, exitMs]);
  return phase;
}

// Returns the value from the render before this one (undefined on first render, so
// callers can tell "just mounted" apart from "actually changed").
function usePrevious(value) {
  const ref = useRef(undefined);
  useEffect(() => { ref.current = value; });
  return ref.current;
}

/* ======================================================================
   4. SHARED UI COMPONENTS
   ====================================================================== */

function StatusPill({ status, size = 'md', glow = false }) {
  const tone = STATUS_TONE[status] || 'muted';
  const cls = TONE_CLASSES[tone];
  const sizeCls = size === 'sm' ? 'text-[11px] px-2 py-0.5' : 'text-xs px-2.5 py-1';
  return (
    <span className={`inline-flex items-center gap-1.5 rounded-full border font-body font-medium whitespace-nowrap transition-colors duration-150 ${sizeCls} ${cls} ${glow ? (TONE_GLOW[tone] || '') : ''}`}>
      <span className={`w-1.5 h-1.5 rounded-full shrink-0 ${TONE_DOT[tone]} ${tone === 'oud' ? 'animate-pulseOnce' : ''}`} />
      {status}
    </span>
  );
}

function PriorityTag({ priority }) {
  const tone = PRIORITY_TONE[priority] || 'muted';
  return (
    <span className={`inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-xs font-medium ${TONE_CLASSES[tone]} ${priority === 'Critical' ? TONE_GLOW.oud : ''}`}>
      {priority === 'Critical' && <AlertTriangle className="w-3 h-3" />}
      {priority}
    </span>
  );
}

function PyramidProgress({ progress = 0, height = 'h-2', showLabel = false }) {
  const p = Math.max(0, Math.min(100, progress));
  // Starts at 0 and animates up to the real value on mount (the .pyramid-progress CSS
  // class carries the width transition, so this single mechanism also smoothly
  // re-animates later if `progress` changes after mount).
  const [display, setDisplay] = useState(0);
  useEffect(() => {
    const raf = requestAnimationFrame(() => setDisplay(p));
    return () => cancelAnimationFrame(raf);
  }, [p]);
  return (
    <div className="w-full">
      <div className={`w-full ${height} rounded-full bg-border overflow-hidden relative`}>
        <div className="h-full rounded-full pyramid-progress" style={{ width: `${display}%` }} />
      </div>
      {showLabel && <div className="mt-1 text-xs font-mono text-muted">{p}%</div>}
    </div>
  );
}

function Avatar({ user, size = 'md', ring = true }) {
  if (!user) return null;
  const sizeCls = { sm: 'w-6 h-6 text-[10px]', md: 'w-8 h-8 text-xs', lg: 'w-12 h-12 text-base', xl: 'w-20 h-20 text-2xl' }[size] || 'w-8 h-8 text-xs';
  return (
    <div
      className={`shrink-0 rounded-full flex items-center justify-center font-semibold font-body ${sizeCls} ${ring ? 'ring-2' : ''}`}
      style={{ backgroundColor: `${user.color}22`, color: user.color, ...(ring ? { boxShadow: `0 0 0 2px ${user.color}55` } : {}) }}
      title={user.name}
    >
      {user.avatar || initials(user.name)}
    </div>
  );
}

function Card({ children, className = '', hover = false, onClick, ...rest }) {
  return (
    <div
      onClick={onClick}
      className={`rounded-[14px] border border-border bg-surface shadow-[0_4px_20px_-4px_rgba(18,17,16,0.06),inset_0_1px_0_rgba(201,166,107,0.2)] transition-all duration-200 ease-out ${hover ? 'hover:shadow-[0_12px_30px_-6px_rgba(18,17,16,0.1),0_0_0_1px_rgba(201,166,107,0.3)] hover:-translate-y-0.5 active:scale-[0.995] active:duration-100 cursor-pointer' : ''} ${className}`}
      {...rest}
    >
      {children}
    </div>
  );
}

function Button({ children, variant = 'primary', size = 'md', className = '', icon: Icon, loading = false, disabled, ...rest }) {
  const base = 'inline-flex items-center justify-center gap-2 rounded-lg font-medium transition-all duration-150 active:scale-[0.97] disabled:opacity-40 disabled:grayscale disabled:cursor-not-allowed disabled:active:scale-100 whitespace-nowrap focus-visible:outline-none focus-visible:shadow-[0_0_0_2px_#FFFFFF,0_0_0_4px_#1827F5]';
  const sizes = { sm: 'text-xs px-2.5 py-1.5', md: 'text-sm px-4 py-2', lg: 'text-base px-5 py-2.5' };
  const variants = {
    primary: 'bg-gold text-white border border-gold hover:bg-[#141fd6] hover:border-[#141fd6] shadow-[0_2px_8px_rgba(24,39,245,0.25)]',
    secondary: 'bg-surface border border-gold text-gold hover:bg-elevated',
    ghost: 'text-gold hover:bg-elevated bg-transparent transition-colors duration-150',
    danger: 'bg-oud/10 text-oud border border-oud hover:bg-oud/20',
    success: 'bg-sage/10 text-sage border border-sage/40 hover:bg-sage/20',
    warning: 'bg-amber/10 text-amber border border-amber/40 hover:bg-amber/20',
  };
  const spinnerCls = size === 'sm' ? 'w-3.5 h-3.5' : 'w-4 h-4';
  return (
    <button className={`${base} ${sizes[size]} ${variants[variant]} ${className}`} disabled={disabled || loading} {...rest}>
      {loading ? <Loader2 className={`${spinnerCls} animate-spin`} /> : Icon && <Icon className={spinnerCls} />}
      {children}
    </button>
  );
}

const Input = React.forwardRef(function Input({ className = '', ...rest }, ref) {
  return (
    <input
      ref={ref}
      className={`w-full bg-surface border border-border rounded-lg px-3 py-2 text-sm text-primary placeholder:text-stale focus:outline-none focus:shadow-[0_0_0_3px_rgba(24,39,245,0.12)] focus:border-gold/50 transition-all ${className}`}
      {...rest}
    />
  );
});

function Select({ className = '', children, ...rest }) {
  return (
    <select
      className={`w-full bg-surface border border-border rounded-lg px-3 py-2 text-sm text-primary focus:outline-none focus:shadow-[0_0_0_3px_rgba(24,39,245,0.12)] focus:border-gold/50 transition-all appearance-none cursor-pointer ${className}`}
      {...rest}
    >
      {children}
    </select>
  );
}

function Label({ children }) { return <label className="block text-xs font-medium text-muted mb-1.5">{children}</label>; }

function Modal({ open, onClose, title, children, width = 'max-w-lg' }) {
  const phase = useDeferredOpen(open, 120);
  if (phase === 'closed') return null;
  const closing = phase === 'closing';
  return createPortal(
    <div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
      <div className={`absolute inset-0 bg-black/30 backdrop-blur-xl ${closing ? 'animate-backdropOut' : 'animate-backdropIn'}`} onClick={onClose} />
      <div className={`relative w-full ${width} bg-surface border border-border rounded-2xl shadow-2xl ${closing ? 'animate-dialogOut' : 'animate-dialogIn'} max-h-[90vh] overflow-y-auto`}>
        <div className="flex items-center justify-between px-6 py-4 border-b border-border sticky top-0 bg-surface z-10">
          <h3 className="font-display text-lg text-primary">{title}</h3>
          <button onClick={onClose} className="text-muted hover:text-primary p-1 rounded-lg hover:bg-elevated">
            <X className="w-4 h-4" />
          </button>
        </div>
        <div className="p-6">{children}</div>
      </div>
    </div>,
    document.body
  );
}

function ConfirmDialog({ state, onCancel, onConfirm }) {
  const phase = useDeferredOpen(!!state?.open, 120);
  if (phase === 'closed') return null;
  const closing = phase === 'closing';
  return createPortal(
    <div className="fixed inset-0 z-[200] flex items-center justify-center p-4">
      <div className={`absolute inset-0 bg-black/30 backdrop-blur-xl ${closing ? 'animate-backdropOut' : 'animate-backdropIn'}`} onClick={onCancel} />
      <div className={`relative w-full max-w-sm bg-surface border border-border rounded-2xl shadow-2xl ${closing ? 'animate-dialogOut' : 'animate-dialogIn'} p-6`}>
        <div className="flex items-center gap-3 mb-3">
          <div className="w-9 h-9 rounded-full bg-oud/10 flex items-center justify-center shrink-0">
            <AlertTriangle className="w-4 h-4 text-oud" />
          </div>
          <h3 className="font-display text-lg text-primary">{state?.title}</h3>
        </div>
        <p className="text-sm text-muted mb-6">{state?.message}</p>
        <div className="flex justify-end gap-2">
          <Button variant="secondary" size="sm" onClick={onCancel}>{state?.cancelLabel || 'Cancel'}</Button>
          <Button variant={state?.confirmVariant || 'danger'} size="sm" onClick={onConfirm}>{state?.confirmLabel || 'Confirm'}</Button>
        </div>
      </div>
    </div>,
    document.body
  );
}

// Blocking conflict dialog: shown when a write is rejected because the
// record changed since the client last read it (see reportConflict in
// AppProvider). Deliberately offers no merge/overwrite path - only Dismiss
// (keep looking at the stale view) or Reload (refetch the latest value and
// discard whatever the user was about to submit).
function ConflictDialog({ state, onDismiss, onReload }) {
  const phase = useDeferredOpen(!!state?.open, 120);
  if (phase === 'closed') return null;
  const closing = phase === 'closing';
  return createPortal(
    <div className="fixed inset-0 z-[200] flex items-center justify-center p-4">
      <div className={`absolute inset-0 bg-black/30 backdrop-blur-xl ${closing ? 'animate-backdropOut' : 'animate-backdropIn'}`} onClick={onDismiss} />
      <div className={`relative w-full max-w-sm bg-surface border border-border rounded-2xl shadow-2xl ${closing ? 'animate-dialogOut' : 'animate-dialogIn'} p-6`}>
        <div className="flex items-center gap-3 mb-3">
          <div className="w-9 h-9 rounded-full bg-gold/10 flex items-center justify-center shrink-0">
            <RefreshCw className="w-4 h-4 text-gold" />
          </div>
          <h3 className="font-display text-lg text-primary">Someone else updated this first</h3>
        </div>
        <p className="text-sm text-muted mb-6">{state?.message || 'This record changed since you loaded it. Reload to see the latest version, then reapply your change.'}</p>
        <div className="flex justify-end gap-2">
          <Button variant="secondary" size="sm" onClick={onDismiss}>Dismiss</Button>
          <Button size="sm" icon={RefreshCw} onClick={onReload}>Reload latest</Button>
        </div>
      </div>
    </div>,
    document.body
  );
}

function ToastStack({ toasts, dismiss }) {
  // Mirrors `toasts` locally so a removed toast can still play its 200ms exit slide
  // before actually disappearing from the DOM, instead of vanishing the instant
  // AppProvider drops it from the array.
  const [shown, setShown] = useState([]);
  const prevIds = useRef(new Set());

  useEffect(() => {
    const nextIds = new Set(toasts.map(t => t.id));
    setShown(prev => {
      const stillHere = prev.filter(t => nextIds.has(t.id) || t.closing);
      const additions = toasts.filter(t => !prevIds.current.has(t.id));
      const merged = [...stillHere.filter(t => nextIds.has(t.id)), ...additions];
      const removed = prev.filter(t => !nextIds.has(t.id) && !t.closing).map(t => ({ ...t, closing: true }));
      removed.forEach(t => setTimeout(() => setShown(s => s.filter(x => x.id !== t.id)), 200));
      return [...merged, ...removed];
    });
    prevIds.current = nextIds;
  }, [toasts]);

  const barColor = t => t.type === 'error' ? 'bg-oud' : t.type === 'micro' ? 'bg-stale' : 'bg-sage';
  return (
    <div className="fixed top-4 right-4 z-[300] flex flex-col gap-2 w-80 max-w-[calc(100vw-2rem)]">
      {shown.map(t => (
        <div key={t.id} className={`relative overflow-hidden ${t.closing ? 'animate-toastOut' : 'animate-toastIn'} bg-surface border border-border rounded-lg px-4 py-3 shadow-[0_4px_12px_rgba(0,0,0,0.08)] flex items-start gap-3`}>
          <div className={`mt-0.5 w-2 h-2 rounded-full shrink-0 ${barColor(t)}`} />
          <div className="flex-1 text-sm text-primary">{t.message}</div>
          <button onClick={() => dismiss(t.id)} className="text-muted hover:text-primary"><X className="w-3.5 h-3.5" /></button>
          {t.duration && !t.closing && (
            <div className={`absolute bottom-0 left-0 h-1 ${barColor(t)} animate-shrinkWidth`} style={{ animationDuration: `${t.duration}ms` }} />
          )}
        </div>
      ))}
    </div>
  );
}

function EmptyState({ icon: Icon = Package, title, message, actionLabel, onAction }) {
  return (
    <div className="flex flex-col items-center justify-center text-center py-16 px-6">
      <Icon className="w-12 h-12 text-stale/40 mb-4" strokeWidth={1.5} />
      <h4 className="font-display text-base text-muted mb-1">{title}</h4>
      {message && <p className="text-sm text-stale max-w-sm mb-4">{message}</p>}
      {actionLabel && <Button size="sm" variant="secondary" icon={Plus} onClick={onAction} className="mt-1">{actionLabel}</Button>}
    </div>
  );
}

function fileIcon(type) {
  return { pdf: '📄', jpg: '🖼️', png: '🖼️', xlsx: '📊', docx: '📃' }[type] || '📎';
}

function AttachmentRow({ attachment, onDelete, onOpen, user, sourceLabel }) {
  const [opening, setOpening] = useState(false);
  const handleOpen = async () => {
    if (!onOpen || opening) return;
    setOpening(true);
    const url = await onOpen(attachment.fileUrl);
    setOpening(false);
    if (url) window.open(url, '_blank', 'noopener,noreferrer');
  };
  return (
    <div className="flex items-center gap-3 px-3 py-2 rounded-lg border border-border bg-base hover:bg-elevated transition-colors group">
      <span className="text-lg leading-none">{fileIcon(attachment.fileType)}</span>
      <button type="button" onClick={handleOpen} disabled={!onOpen || opening} className="flex-1 min-w-0 text-left disabled:cursor-default">
        <div className={`text-sm text-primary truncate ${onOpen ? 'hover:underline' : ''}`}>{attachment.fileName}{opening ? ' …' : ''}</div>
        <div className="text-xs text-muted font-mono">{attachment.fileSize} · {user?.name || 'Unknown'} · {attachment.uploadedAt}{sourceLabel ? ` · ${sourceLabel}` : ''}</div>
      </button>
      {onDelete && (
        <button onClick={() => onDelete(attachment.id)} className="opacity-0 group-hover:opacity-100 text-muted hover:text-oud transition-opacity">
          <Trash2 className="w-3.5 h-3.5" />
        </button>
      )}
    </div>
  );
}

// Lightweight @mention autocomplete, like WhatsApp: typing "@" opens a
// filtered list of org members; picking one inserts "@Full Name " verbatim,
// since post_comment() matches mentions by exact full_name substring, not
// a structured id reference.
function CommentComposer({ value, onChange, onSubmit, users, placeholder = 'Add a comment...' }) {
  const [mentionQuery, setMentionQuery] = useState(null); // null = closed, '' or partial text = open
  const inputRef = useRef(null);

  const updateMentionState = (text, caretPos) => {
    const uptoCaret = text.slice(0, caretPos);
    const match = uptoCaret.match(/@([^\s@]*)$/);
    setMentionQuery(match ? match[1] : null);
  };

  const handleChange = (e) => {
    onChange(e.target.value);
    updateMentionState(e.target.value, e.target.selectionStart ?? e.target.value.length);
  };

  const pickMention = (name) => {
    const el = inputRef.current;
    const caret = el ? el.selectionStart ?? value.length : value.length;
    const uptoCaret = value.slice(0, caret);
    const replaced = uptoCaret.replace(/@([^\s@]*)$/, `@${name} `);
    const next = replaced + value.slice(caret);
    onChange(next);
    setMentionQuery(null);
    requestAnimationFrame(() => el?.focus());
  };

  const suggestions = mentionQuery === null ? [] : users.filter(u => u.name.toLowerCase().includes(mentionQuery.toLowerCase())).slice(0, 6);

  return (
    <div className="relative flex-1">
      {suggestions.length > 0 && (
        <div className="absolute bottom-full mb-1 left-0 right-0 bg-surface border border-border rounded-lg shadow-lg overflow-hidden z-10">
          {suggestions.map(u => (
            <button
              key={u.id}
              type="button"
              onMouseDown={e => { e.preventDefault(); pickMention(u.name); }}
              className="w-full flex items-center gap-2 px-3 py-2 text-left text-sm hover:bg-elevated transition-colors"
            >
              <Avatar user={u} size="sm" />
              <span className="text-primary">{u.name}</span>
            </button>
          ))}
        </div>
      )}
      <Input
        ref={inputRef}
        value={value}
        placeholder={placeholder}
        onChange={handleChange}
        onKeyDown={e => {
          if (e.key === 'Escape') { setMentionQuery(null); return; }
          if (e.key === 'Enter' && mentionQuery === null && value.trim()) { onSubmit(); }
        }}
      />
    </div>
  );
}

function CommentBubble({ comment, user }) {
  return (
    <div className="flex gap-3">
      <Avatar user={user} size="sm" />
      <div className="flex-1">
        <div className="flex items-baseline gap-2">
          <span className="text-sm font-medium text-primary">{user?.name}</span>
          <span className="text-xs text-muted font-mono">{comment.createdAt}</span>
        </div>
        <p className="text-sm text-muted mt-0.5">{comment.text}</p>
      </div>
    </div>
  );
}

function ActivityRow({ item, user }) {
  return (
    <div className="flex gap-3 py-2.5">
      <Avatar user={user} size="sm" />
      <div className="flex-1 min-w-0">
        <p className="text-sm text-primary leading-snug">
          <span className="font-medium">{user?.name}</span> {item.action} <span className="text-gold">{item.detail}</span>
        </p>
        <span className="text-xs text-muted font-mono">{fmtRelativeFromISO(item.createdAt)}</span>
      </div>
    </div>
  );
}

const LIFECYCLE_STEPS = ['Quotation', 'Sampling', 'Components', 'Complete'];
function statusStepIndex(status) {
  if (status === 'Development' || status === 'Quotation') return 0;
  if (status === 'Sampling') return 1;
  if (status === 'Production' || status === 'Shipping') return 2;
  if (status === 'Complete') return 3;
  return 0;
}
function LifecycleStepper({ status }) {
  const idx = statusStepIndex(status);
  return (
    <div className="flex items-center w-full">
      {LIFECYCLE_STEPS.map((step, i) => (
        <React.Fragment key={step}>
          <div className="flex flex-col items-center gap-1.5 min-w-[70px]">
            <div className={`w-3 h-3 rounded-full border-2 ${i < idx ? 'bg-gold border-gold' : i === idx ? 'bg-goldbright border-goldbright animate-pulseDot' : 'bg-transparent border-border'}`} />
            <span className={`text-[11px] font-medium ${i <= idx ? 'text-gold' : 'text-muted'}`}>{step}</span>
          </div>
          {i < LIFECYCLE_STEPS.length - 1 && <div className={`flex-1 h-px mb-4 ${i < idx ? 'bg-gold' : 'bg-border'}`} />}
        </React.Fragment>
      ))}
    </div>
  );
}

function SamplingStageTracker({ status }) {
  const steps = ['Sourcing', 'Sampling', 'Approved', 'Production'];
  const map = { Sourcing: 0, 'Sampling In Progress': 1, 'Sample Approved': 2, 'Moved to Production': 3, 'On Hold': 1, Cancelled: 0 };
  const idx = map[status] ?? 0;
  return (
    <div className="flex items-center gap-1">
      {steps.map((s, i) => (
        <React.Fragment key={s}>
          <div className={`w-2 h-2 rounded-full ${i <= idx ? 'bg-gold' : 'bg-border'} ${i === idx ? 'animate-pulseDot' : ''}`} />
          {i < steps.length - 1 && <div className={`w-4 h-px ${i < idx ? 'bg-gold' : 'bg-border'}`} />}
        </React.Fragment>
      ))}
    </div>
  );
}

function Dropdown({ trigger, children, align = 'right' }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    function onDoc(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, []);
  return (
    <div className="relative" ref={ref}>
      <div onClick={() => setOpen(o => !o)}>{trigger}</div>
      {open && (
        <div className={`absolute z-50 mt-2 ${align === 'right' ? 'right-0' : 'left-0'} min-w-[180px] bg-elevated border border-border rounded-lg shadow-2xl py-1 animate-dropdownIn`} onClick={() => setOpen(false)}>
          {React.Children.map(children, (child, i) => (
            <div className="animate-fadeUp" style={{ animationDelay: `${i * 20}ms`, animationDuration: '150ms' }}>{child}</div>
          ))}
        </div>
      )}
    </div>
  );
}

function InlineSelect({ value, options, onChange, tone }) {
  return (
    <select
      value={value}
      onChange={e => onChange(e.target.value)}
      onClick={e => e.stopPropagation()}
      className={`text-xs font-medium rounded-full border px-2.5 py-1 bg-transparent cursor-pointer focus:outline-none focus:ring-2 focus:ring-gold/40 ${TONE_CLASSES[tone || STATUS_TONE[value] || 'muted']}`}
    >
      {options.map(o => <option key={o} value={o} className="bg-elevated text-primary">{o}</option>)}
    </select>
  );
}

/* ======================================================================
   5. APP PROVIDER (STATE + ACTIONS)
   ====================================================================== */

function capitalizeRole(r) { return r ? r.charAt(0).toUpperCase() + r.slice(1) : 'Team'; }

// The prototype's free-text Supplier.moq ("30,000 units", "100L") is
// normalized in the schema as moq_quantity + moq_unit. Parse on write,
// reconstruct on read, so the UI's single free-text MOQ field never has to
// change.
function parseMoq(moqString) {
  if (!moqString || !moqString.trim()) return { quantity: null, unit: null };
  const m = moqString.trim().match(/^([\d,.]+)\s*(.*)$/);
  if (!m) return { quantity: null, unit: moqString.trim() || null };
  const quantity = parseFloat(m[1].replace(/,/g, ''));
  return { quantity: isNaN(quantity) ? null : quantity, unit: m[2].trim() || 'units' };
}
function formatMoq(quantity, unit) {
  if (quantity == null) return '';
  const qtyStr = quantity.toLocaleString('en-US');
  if (!unit) return qtyStr;
  return unit.toUpperCase() === 'L' ? `${qtyStr}${unit}` : `${qtyStr} ${unit}`;
}

// Live-fetched, mapped to the prototype's flat Supplier shape - supplier_contacts
// (normalized per 03-database-schema.md) is folded back into contact/email/phone
// via its one primary-per-supplier row.
function useOrgSuppliers() {
  return useQuery({
    queryKey: ['suppliers'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('suppliers')
        .select('id, name, country, makes, moq_quantity, moq_unit, lead_time_days, terms, rating, is_active, is_potential, row_version, serial_no, supplier_contacts(name, email, phone, is_primary)');
      if (error) throw error;
      return data.map(s => {
        const primary = (s.supplier_contacts || []).find(c => c.is_primary) || s.supplier_contacts?.[0] || {};
        return {
          id: s.id, name: s.name, country: s.country || '', makes: s.makes || '',
          moq: formatMoq(s.moq_quantity, s.moq_unit), lead: s.lead_time_days, terms: s.terms || '',
          rating: s.rating, active: s.is_active, isPotential: s.is_potential, rowVersion: s.row_version, serialNo: s.serial_no,
          contact: primary.name || '', email: primary.email || '', phone: primary.phone || '',
        };
      });
    },
  });
}

// Live-fetched, mapped to the exact {id,name,role,accessRole,avatar,color,email,department}
// shape the hardcoded USERS array used to have (Phase 4) - every existing userById()/
// app.users call site throughout the file keeps working unchanged.
function useOrgUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('profiles')
        .select('id, full_name, role_title, department, avatar_initials, avatar_color, email, organization_members(access_role, sees_all_projects)');
      if (error) throw error;
      return data.map(p => ({
        id: p.id,
        name: p.full_name,
        role: p.role_title || '',
        accessRole: capitalizeRole(p.organization_members?.[0]?.access_role),
        seesAllProjects: !!p.organization_members?.[0]?.sees_all_projects,
        avatar: p.avatar_initials || initials(p.full_name),
        color: p.avatar_color || '#667085',
        email: p.email,
        department: p.department || '',
      }));
    },
  });
}

// This org's id, needed only for inserting new rows (organization_id is
// not-null on every business table); reads never need it since RLS already
// scopes every SELECT to the caller's own org membership.
function useCurrentOrgId(authUserId) {
  return useQuery({
    queryKey: ['org-id', authUserId],
    enabled: !!authUserId,
    queryFn: async () => {
      const { data, error } = await supabase
        .from('organization_members')
        .select('organization_id')
        .eq('profile_id', authUserId)
        .limit(1)
        .single();
      if (error) throw error;
      return data.organization_id;
    },
  });
}

// Live-fetched, mapped to the prototype's Quotation shape. "In Sampling" is
// never a real value in the DB's line_item_status enum (only Pending/
// Approved/Rejected/Developing are) - it's derived here exactly like
// `locked` is, from sampling_project_id being set, matching
// 03-database-schema.md's design note that In Sampling is a display-only
// derived state, not a stored decision.
function useOrgQuotations() {
  return useQuery({
    queryKey: ['quotations'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('quotations')
        .select('id, project_id, supplier_id, supplier_name_snapshot, reference_no, currency, valid_until, created_at, created_by, row_version, quotation_line_items(id, item_name, qty, unit_price, moq_quantity, moq_unit, mold_cost, sampling_cost, prod_lead_time_days, sampling_lead_time_days, status, approved_reason, rejected_reason, decided_at, sampling_project_id, locked)');
      if (error) throw error;
      return data.map(q => ({
        id: q.id, projectId: q.project_id, supplierId: q.supplier_id, supplierName: q.supplier_name_snapshot, referenceNo: q.reference_no || '',
        currency: q.currency, validUntil: q.valid_until, createdAt: q.created_at, createdBy: q.created_by, rowVersion: q.row_version,
        lineItems: (q.quotation_line_items || []).map(li => ({
          id: li.id, itemName: li.item_name, qty: Number(li.qty), unitPrice: Number(li.unit_price),
          moq: formatMoq(li.moq_quantity, li.moq_unit), moldCost: Number(li.mold_cost), samplingCost: Number(li.sampling_cost),
          prodLeadTime: li.prod_lead_time_days, samplingLeadTime: li.sampling_lead_time_days,
          status: li.sampling_project_id ? 'In Sampling' : li.status,
          approvedReason: li.approved_reason || '', rejectedReason: li.rejected_reason || '',
          decidedAt: li.decided_at, samplingProjectId: li.sampling_project_id, locked: li.locked,
        })),
      }));
    },
  });
}

// Live-fetched, mapped to the exact prototype Project shape (Phase 4) - see
// useOrgUsers above for the same pattern. project_members is folded into
// editableBy[] to match the old shape; visibleTo is hardcoded to ['all']
// since there is no visible_to column (per-project visibility restriction
// was decided against - every org member sees every project, enforced by
// RLS on the select itself, not by this field).
function useOrgProjects() {
  return useQuery({
    queryKey: ['projects'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('projects')
        .select('id, name, category, status, priority, owner_id, target_date, start_date, completed_date, completed_by, is_archived, blocking, waiting, progress, launch_wave, row_version, estimated_budget, budget_currency, project_members(profile_id)');
      if (error) throw error;
      return data.map(p => ({
        id: p.id, name: p.name, category: p.category, status: p.status, priority: p.priority,
        owner: p.owner_id, target: p.target_date, startDate: p.start_date,
        completedDate: p.completed_date, completedBy: p.completed_by, isArchived: p.is_archived,
        blocking: p.blocking, waiting: p.waiting, progress: p.progress, launchWave: p.launch_wave, rowVersion: p.row_version,
        estimatedBudget: p.estimated_budget != null ? Number(p.estimated_budget) : null, budgetCurrency: p.budget_currency,
        visibleTo: ['all'],
        editableBy: (p.project_members || []).map(m => m.profile_id),
      }));
    },
  });
}

// Live-fetched, mapped to the prototype's SamplingProject shape. The
// migration dropped the stored `name`/`category` columns (pure duplication -
// see 20260805090300_sampling_and_production.sql) - name is reconstructed
// here via the joined project name, category is always the same constant
// the prototype always used.
function useOrgSamplingProjects() {
  return useQuery({
    queryKey: ['samplingProjects'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('sampling_projects')
        .select('id, project_id, item_name, status, owner_id, target_approval_date, notes, created_at, row_version, projects(name)');
      if (error) throw error;
      return data.map(sp => ({
        id: sp.id, name: `${sp.projects?.name || 'Project'} — ${sp.item_name}`, itemName: sp.item_name, category: 'Component',
        linkedProjectId: sp.project_id, status: sp.status, ownerId: sp.owner_id, rowVersion: sp.row_version,
        targetApprovalDate: sp.target_approval_date, notes: sp.notes || '', createdAt: sp.created_at ? sp.created_at.slice(0, 10) : sp.created_at,
      }));
    },
  });
}

// statusHistory is a separate append-only table (sample_round_status_history)
// - folded back into the prototype's inline array here, sorted oldest-first
// to match how it was always built (push onto the end).
function useOrgSampleRounds() {
  return useQuery({
    queryKey: ['sampleRounds'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('sample_rounds')
        .select('id, sampling_project_id, supplier_id, version, status, held_from_status, date_requested, date_received, date_reviewed, result_notes, next_action, cost, lead_time_days, row_version, sample_round_status_history(status, updated_at, updated_by, note)');
      if (error) throw error;
      return data.map(r => ({
        id: r.id, samplingProjectId: r.sampling_project_id, supplierId: r.supplier_id, version: r.version, status: r.status,
        rowVersion: r.row_version,
        heldFromStatus: r.held_from_status,
        statusHistory: (r.sample_round_status_history || [])
          .slice().sort((a, b) => new Date(a.updated_at) - new Date(b.updated_at))
          .map(h => ({ status: h.status, updatedAt: h.updated_at, updatedBy: h.updated_by, note: h.note || undefined })),
        dateRequested: r.date_requested, dateReceived: r.date_received, dateReviewed: r.date_reviewed,
        resultNotes: r.result_notes || null, nextAction: r.next_action || '', cost: Number(r.cost), leadTimeDays: r.lead_time_days,
      }));
    },
  });
}

function useOrgSupplierBids() {
  return useQuery({
    queryKey: ['supplierBids'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('supplier_bids')
        .select('id, sampling_project_id, supplier_id, bid_status, quoted_price, currency, moq_quantity, moq_unit, quoted_qty, quoted_lead_time_days, quoted_sampling_lead_time_days, rating, mold_cost, sampling_cost');
      if (error) throw error;
      return data.map(b => ({
        id: b.id, samplingProjectId: b.sampling_project_id, supplierId: b.supplier_id, bidStatus: b.bid_status,
        quotedPrice: Number(b.quoted_price), currency: b.currency, quotedMOQ: formatMoq(b.moq_quantity, b.moq_unit),
        quotedQty: Number(b.quoted_qty), quotedLeadTime: b.quoted_lead_time_days, quotedSamplingLeadTime: b.quoted_sampling_lead_time_days,
        rating: b.rating, moldCost: Number(b.mold_cost), samplingCost: Number(b.sampling_cost),
      }));
    },
  });
}

// projectName is derived via the joined project - not stored (matches the
// same normalization reasoning as sampling_projects.name). duration is
// mapped through for shape-parity but is dead in the UI (every view computes
// elapsed time live from startDate instead - see durationSince()).
function useOrgComponents() {
  return useQuery({
    queryKey: ['components'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('components')
        .select('id, project_id, sampling_project_id, name, supplier_id, supplier_country_snapshot, status, price, currency, order_qty, duration_days, next_action, next_action_updated_at, owner_id, start_date, completed_date, row_version, projects(name)');
      if (error) throw error;
      return data.map(c => ({
        id: c.id, name: c.name, projectId: c.project_id, projectName: c.projects?.name || '', samplingProjectId: c.sampling_project_id,
        supplier: c.supplier_id, country: c.supplier_country_snapshot || '', status: c.status,
        price: Number(c.price), currency: c.currency, orderQty: Number(c.order_qty), duration: c.duration_days,
        nextAction: c.next_action || '', nextActionUpdatedAt: c.next_action_updated_at, owner: c.owner_id,
        startDate: c.start_date, completedDate: c.completed_date, rowVersion: c.row_version,
      }));
    },
  });
}

// attachments/comments both use six mutually-exclusive nullable parent
// columns instead of the prototype's free-form (entityType, entityId) pair
// (03-database-schema.md's "explicit parent FK, exactly one set" rule).
// These two helpers translate between the two shapes in both directions.
const PARENT_COLUMN_BY_ENTITY_TYPE = {
  project: 'project_id', quotation: 'quotation_id', samplingProject: 'sampling_project_id',
  sampleRound: 'sample_round_id', component: 'component_id', supplier: 'supplier_id',
};
function resolveParentEntity(row) {
  for (const [type, col] of Object.entries(PARENT_COLUMN_BY_ENTITY_TYPE)) {
    if (row[col]) return { entityType: type, entityId: row[col] };
  }
  return { entityType: null, entityId: null };
}

function useOrgAttachments() {
  return useQuery({
    queryKey: ['attachments'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('attachments')
        .select('id, project_id, quotation_id, sampling_project_id, sample_round_id, component_id, supplier_id, storage_path, file_name, file_size_bytes, file_type, uploaded_by, uploaded_at');
      if (error) throw error;
      return data.map(a => ({
        id: a.id, ...resolveParentEntity(a), fileName: a.file_name, fileUrl: a.storage_path,
        uploadedBy: a.uploaded_by, uploadedAt: a.uploaded_at ? a.uploaded_at.slice(0, 10) : a.uploaded_at,
        fileSize: `${(a.file_size_bytes / 1024 / 1024).toFixed(1)} MB`, fileType: a.file_type || '',
      }));
    },
  });
}

// createdAt is a real timestamp now (the prototype stored the literal string
// "Just now" forever - a documented bug, not behavior worth preserving), so
// it's formatted the same way the activity feed already does.
function useOrgComments() {
  return useQuery({
    queryKey: ['comments'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('comments')
        .select('id, project_id, quotation_id, sampling_project_id, sample_round_id, component_id, supplier_id, author_id, body, created_at');
      if (error) throw error;
      return data.map(c => ({
        id: c.id, ...resolveParentEntity(c), userId: c.author_id, text: c.body,
        // Absolute date+time, not relative ("2h ago") - reported live as
        // confusing during a fast back-and-forth thread where several
        // comments land within the same hour and all show identically as
        // "just now"/"Xh ago" with no way to tell them apart.
        createdAt: fmtDateTimeShort(c.created_at),
      }));
    },
  });
}

// linkedEntityType is always 'project' at every real call site today
// (project_id is the only FK tasks carries - see 20260805090400's comment).
function useOrgTasks() {
  return useQuery({
    queryKey: ['tasks'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('tasks')
        .select('id, title, description, assigned_to, created_by, due_date, status, project_id, completed_at');
      if (error) throw error;
      return data.map(t => ({
        id: t.id, title: t.title, description: t.description || '', assignedTo: t.assigned_to, createdBy: t.created_by,
        dueDate: t.due_date, status: t.status, completedAt: t.completed_at,
        linkedEntityType: t.project_id ? 'project' : null, linkedEntityId: t.project_id,
      }));
    },
  });
}

// Notifications are read-only from the client's perspective today - there is
// no insert policy for ordinary users by design (03-database-schema.md /
// the migration's own comment: real notifications are populated by a
// scheduled job/RPC, Phase 6, not written here). This intentionally means
// the bell now shows nothing until that job exists, instead of the
// prototype's 3 permanently-fake seed rows - a more honest empty state than
// stale alerts that never actually reflected real data.
function useOrgNotifications() {
  return useQuery({
    queryKey: ['notifications'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('notifications')
        .select('id, type, text, read_at, created_at, link_page, link_id')
        .order('created_at', { ascending: false });
      if (error) throw error;
      return data.map(n => ({
        id: n.id, type: n.type, text: n.text, read: !!n.read_at, time: fmtRelativeFromISO(n.created_at),
        linkPage: n.link_page, linkId: n.link_id,
      }));
    },
  });
}

// Every refresh inserts new rows rather than updating in place (a real
// history exists, per 03-database-schema.md), so this keeps only the
// latest row per currency - USD is always 1 and never stored, matching the
// prototype's own EXCHANGE_RATES_TO_USD base assumption.
function useOrgFxRates() {
  return useQuery({
    queryKey: ['fxRates'],
    queryFn: async () => {
      const { data, error } = await supabase.from('fx_rates').select('currency, rate_to_usd, source, fetched_at').order('fetched_at', { ascending: false });
      if (error) throw error;
      const rates = { USD: 1 };
      const seen = new Set();
      let updatedAt = null;
      let source = 'default';
      for (const row of data) {
        if (!seen.has(row.currency)) { seen.add(row.currency); rates[row.currency] = Number(row.rate_to_usd); }
        if (!updatedAt || row.fetched_at > updatedAt) { updatedAt = row.fetched_at; source = row.source; }
      }
      return { rates, updatedAt, source };
    },
  });
}

// activity_log has no client insert policy (only log_activity() and the
// cascade RPCs, all SECURITY DEFINER, may write it - see
// 20260805130000_workflow_rpcs.sql). Capped at 200 rows, newest first,
// matching the per-project substring-match cap already used in Project
// Detail's Audit Log for archived projects.
function useOrgActivity() {
  return useQuery({
    queryKey: ['activity'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('activity_log')
        .select('id, actor_id, project_id, action, detail, created_at')
        .order('created_at', { ascending: false })
        .limit(200);
      if (error) throw error;
      return data.map(a => ({ id: a.id, userId: a.actor_id, projectId: a.project_id, action: a.action, detail: a.detail, createdAt: a.created_at }));
    },
  });
}

function AppProvider({ children, authUserId }) {
  const qc = useQueryClient();
  const { data: users = [], isLoading: usersLoading } = useOrgUsers();
  const { data: projects = [], isLoading: projectsLoading } = useOrgProjects();
  const { data: orgId } = useCurrentOrgId(authUserId);
  const { data: suppliers = [], isLoading: suppliersLoading } = useOrgSuppliers();
  const { data: quotations = [], isLoading: quotationsLoading } = useOrgQuotations();
  const { data: samplingProjects = [], isLoading: samplingProjectsLoading } = useOrgSamplingProjects();
  const { data: sampleRounds = [], isLoading: sampleRoundsLoading } = useOrgSampleRounds();
  const { data: supplierBids = [], isLoading: supplierBidsLoading } = useOrgSupplierBids();
  const { data: components = [], isLoading: componentsLoading } = useOrgComponents();
  const { data: attachments = [], isLoading: attachmentsLoading } = useOrgAttachments();
  const { data: comments = [], isLoading: commentsLoading } = useOrgComments();
  const { data: activity = [], isLoading: activityLoading } = useOrgActivity();
  const { data: tasks = [], isLoading: tasksLoading } = useOrgTasks();
  const { data: notifications = [], isLoading: notificationsLoading } = useOrgNotifications();
  const currentUserId = authUserId; // real Supabase Auth session user - no more switching (Phase 4)
  const { data: fxData } = useOrgFxRates();
  const fxRates = fxData?.rates || EXCHANGE_RATES_TO_USD;
  const fxUpdatedAt = fxData?.updatedAt || null;
  const fxSource = fxData?.source || 'default';

  // Deep-link hydration: reads ?page=X&id=Y from the URL on first load (e.g.
  // the "Open in SAKAN" links in the daily email digest) and seeds route
  // from it instead of the Dashboard default. route itself stays pure
  // in-memory state after this - no ongoing URL sync - same as it always
  // was; this only handles the one-time entry point. Query string survives
  // the login redirect untouched (only the invite/recovery hash gets
  // cleared elsewhere), so this works whether or not the user was already
  // signed in when they clicked the link.
  //
  // ?tab= and ?projects= additionally support deep-linking straight into a
  // specific Report scope (e.g. the Sunday digest's "Weekly Project
  // Overview" link) - projects is a comma-separated list of project ids,
  // mapped to the exact { tab, reportProjectIds } shape ProjectDetail's own
  // "Project Overview" button already produces via app.navigate(), so both
  // entry points land on the identical pre-filtered, auto-generating view.
  const [route, setRoute] = useState(() => {
    const params = new URLSearchParams(window.location.search);
    const page = params.get('page');
    if (!page) return { page: 'dashboard', id: null };
    const tab = params.get('tab');
    const projects = params.get('projects');
    window.history.replaceState(null, '', window.location.pathname);
    return {
      page, id: params.get('id') || null,
      params: (tab || projects) ? { tab: tab || undefined, reportProjectIds: projects ? projects.split(',').filter(Boolean) : undefined } : null,
    };
  });
  const [toasts, setToasts] = useState([]);
  const [confirmState, setConfirmState] = useState(null);
  const confirmResolveRef = useRef(null);
  const [conflictState, setConflictState] = useState(null);
  const [searchOpen, setSearchOpen] = useState(false);
  const [aiOpen, setAiOpen] = useState(false);

  const currentUser = userById(users, currentUserId);

  const navigate = useCallback((page, id = null, params = null) => { setRoute({ page, id, params }); window.scrollTo(0, 0); }, []);

  const toast = useCallback((message, type = 'success') => {
    const id = uid('t');
    const duration = 4000;
    setToasts(ts => [...ts, { id, message, type, duration }]);
    setTimeout(() => setToasts(ts => ts.filter(t => t.id !== id)), duration);
  }, []);
  const microToast = useCallback((message = 'Saved') => {
    const id = uid('mt');
    const duration = 1500;
    setToasts(ts => [...ts, { id, message, type: 'micro', duration }]);
    setTimeout(() => setToasts(ts => ts.filter(t => t.id !== id)), duration);
  }, []);
  const dismissToast = useCallback(id => setToasts(ts => ts.filter(t => t.id !== id)), []);

  // fx_rates only has an insert policy for Admins (a scheduled Edge Function
  // is meant to own the live refresh in production, using the service role
  // which bypasses RLS entirely - see 20260805090400's comment); a non-admin
  // calling this simply gets a silently-failing insert here, matching the
  // existing Admin-only Exchange Rates panel gating in the UI.
  const refreshFxRates = useCallback(async (silent = false) => {
    try {
      const res = await fetch('https://open.er-api.com/v6/latest/USD');
      const data = await res.json();
      if (data.result !== 'success') throw new Error('bad response');
      const rows = [];
      CURRENCIES.forEach(c => {
        const apiRate = data.rates[c.code];
        if (apiRate) rows.push({ currency: c.code, rate_to_usd: 1 / apiRate, source: 'live' });
      });
      if (rows.length) {
        const { error } = await supabase.from('fx_rates').insert(rows);
        if (error) throw error;
      }
      qc.invalidateQueries({ queryKey: ['fxRates'] });
      if (!silent) toast('Exchange rates updated from live market data.');
    } catch (e) {
      if (!silent) toast('Could not fetch live exchange rates. Check your connection.', 'error');
    }
  }, [qc, toast]);

  const updateFxRate = useCallback((code, rate) => {
    (async () => {
      const { error } = await supabase.from('fx_rates').insert({ currency: code, rate_to_usd: rate, source: 'manual' });
      if (error) { toast('Failed to update rate.', 'error'); return; }
      qc.invalidateQueries({ queryKey: ['fxRates'] });
      microToast('Rate updated');
    })();
  }, [qc, microToast, toast]);

  // Team management: inviting genuinely needs Supabase Auth's admin API
  // (service_role only, never a client key), so it's a security definer
  // Postgres function (invite_team_member, 20260810100000_team_invites.sql)
  // rather than a direct client insert. Role changes and removal are plain
  // RLS-gated table writes - organization_members_write already lets an
  // Admin update/delete any membership row in their org.
  const inviteMember = useCallback((form) => {
    if (!orgId) { toast('Still loading your organization — try again in a moment.', 'error'); return; }
    (async () => {
      const { error } = await supabase.rpc('invite_team_member', {
        p_organization_id: orgId,
        p_email: form.email.trim(),
        p_full_name: form.name.trim(),
        p_role_title: form.title?.trim() || null,
        p_access_role: form.accessRole.toLowerCase(),
        p_sees_all_projects: form.accessRole === 'Team' ? !!form.seesAllProjects : false,
      });
      if (error) { toast(error.message || 'Failed to send invite.', 'error'); return; }
      qc.invalidateQueries({ queryKey: ['users'] });
      toast(`Invite sent to ${form.email}.`);
    })();
  }, [orgId, qc, toast]);

  const updateMemberRole = useCallback((profileId, newAccessRole) => {
    if (!orgId) return;
    (async () => {
      const { error } = await supabase.from('organization_members').update({ access_role: newAccessRole.toLowerCase() }).eq('organization_id', orgId).eq('profile_id', profileId);
      if (error) { toast('Failed to update role.', 'error'); return; }
      qc.invalidateQueries({ queryKey: ['users'] });
      microToast('Role updated');
    })();
  }, [orgId, qc, microToast, toast]);

  // "All Tasks" grants org-wide visibility to a Team member (can_view_project,
  // 20260810170000_visibility_scope_and_drop_manager.sql) - it does NOT grant
  // edit rights anywhere they aren't already an owner/collaborator/admin.
  const updateMemberScope = useCallback((profileId, seesAllProjects) => {
    if (!orgId) return;
    (async () => {
      const { error } = await supabase.from('organization_members').update({ sees_all_projects: seesAllProjects }).eq('organization_id', orgId).eq('profile_id', profileId);
      if (error) { toast('Failed to update access scope.', 'error'); return; }
      qc.invalidateQueries({ queryKey: ['users'] });
      microToast('Access scope updated');
    })();
  }, [orgId, qc, microToast, toast]);

  const removeMember = useCallback((profileId) => {
    if (!orgId) return;
    (async () => {
      const { error } = await supabase.from('organization_members').delete().eq('organization_id', orgId).eq('profile_id', profileId);
      if (error) { toast('Failed to remove member.', 'error'); return; }
      qc.invalidateQueries({ queryKey: ['users'] });
      toast('Member removed.');
    })();
  }, [orgId, qc, toast]);

  useEffect(() => {
    const stale = !fxUpdatedAt || daysBetween(fxUpdatedAt, new Date().toISOString()) >= 1;
    if (stale) refreshFxRates(true);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const confirm = useCallback((opts) => {
    return new Promise(resolve => {
      confirmResolveRef.current = resolve;
      setConfirmState({ open: true, ...opts });
    });
  }, []);
  const handleConfirmCancel = () => { setConfirmState(null); confirmResolveRef.current?.(false); };
  const handleConfirmOk = () => { setConfirmState(null); confirmResolveRef.current?.(true); };

  // Optimistic version-conflict handling (Phase 5): every direct table edit
  // and every workflow RPC sends back the row_version it last read, and the
  // database rejects the write (SQLSTATE 40001) if that row changed under
  // it. Chosen UX (developer decision 2026-08-05): block, don't silently
  // overwrite or silently refresh - discard the stale local edit, refetch,
  // and make the user explicitly reload before reapplying their change.
  const VERSION_CONFLICT_CODE = '40001';
  const reportConflict = useCallback((opts) => { setConflictState({ open: true, ...opts }); }, []);
  const isVersionConflict = useCallback((error) => error?.code === VERSION_CONFLICT_CODE, []);
  const handleConflictReload = () => {
    (conflictState?.queryKeys || []).forEach(key => qc.invalidateQueries({ queryKey: [key] }));
    setConflictState(null);
  };
  const handleConflictDismiss = () => setConflictState(null);

  // Writes through activity_log's log_activity() RPC (SECURITY DEFINER - see
  // 20260805130000_workflow_rpcs.sql; ordinary clients have no insert policy
  // on the table itself). The cascade RPCs below log their own activity
  // entries as part of the same transaction instead of calling this.
  const logActivity = useCallback((action, detail) => {
    qc.setQueryData(['activity'], (old = []) => [{ id: crypto.randomUUID(), userId: currentUserId, action, detail, createdAt: new Date().toISOString() }, ...old]);
    (async () => {
      await supabase.rpc('log_activity', { p_action: action, p_detail: detail });
      qc.invalidateQueries({ queryKey: ['activity'] });
    })();
  }, [currentUserId, qc]);

  /* ---- PROJECTS ---- */
  // id is generated client-side (not left to the DB default) so callers that
  // need the new id synchronously (navigate-to-detail, task creation) keep
  // working exactly as before, while the insert itself happens in the
  // background against an optimistically-updated cache.
  const createProject = useCallback((data) => {
    if (!orgId) { toast('Still loading your organization — try again in a moment.', 'error'); return null; }
    const id = crypto.randomUUID();
    const launchWave = (projects.reduce((m, p) => Math.max(m, p.launchWave || 1), 0)) || 1;
    const editableBy = (data.editableBy || []).filter(Boolean);
    const proj = {
      id, name: data.name, status: 'Quotation', owner: data.owner, priority: data.priority || 'Normal',
      blocking: null, waiting: null, progress: 5, category: data.category, target: data.target,
      startDate: todayStr(), completedDate: null, completedBy: null, isArchived: false,
      estimatedBudget: data.estimatedBudget, budgetCurrency: data.budgetCurrency,
      visibleTo: ['all'], editableBy, launchWave,
    };
    qc.setQueryData(['projects'], (old = []) => [...old, proj]);
    (async () => {
      const { error } = await supabase.from('projects').insert({
        id, organization_id: orgId, name: proj.name, category: proj.category, status: proj.status,
        priority: proj.priority, owner_id: proj.owner, target_date: proj.target, start_date: proj.startDate,
        progress: proj.progress, launch_wave: proj.launchWave,
        estimated_budget: proj.estimatedBudget, budget_currency: proj.budgetCurrency,
      });
      if (error) { toast('Failed to create project.', 'error'); qc.invalidateQueries({ queryKey: ['projects'] }); return; }
      if (editableBy.length) {
        const { error: memberError } = await supabase.from('project_members')
          .insert(editableBy.map(profileId => ({ project_id: id, profile_id: profileId, added_by: currentUserId })));
        if (memberError) toast('Project created, but collaborators could not be added (permission denied).', 'error');
      }
      qc.invalidateQueries({ queryKey: ['projects'] });
    })();
    logActivity('created project', proj.name);
    toast(`Project "${proj.name}" created.`);
    return id;
  }, [projects, orgId, currentUserId, qc, logActivity, toast]);

  const deleteProject = useCallback((projectId) => {
    const proj = projects.find(p => p.id === projectId);
    if (!canEditProject(currentUser, proj)) { toast('Only the owner or an admin can delete this project.', 'error'); return; }
    (async () => {
      // canDeleteProject already guarantees zero quotations/sampling
      // projects/components exist, so any attachment on this project can
      // only be a direct project-level upload - clean those Storage objects
      // up first, since the projects row's cascade delete only removes the
      // attachments table row, not the underlying file bytes.
      const paths = attachments.filter(a => a.entityType === 'project' && a.entityId === projectId).map(a => a.fileUrl).filter(Boolean);
      if (paths.length) await supabase.storage.from('attachments').remove(paths);
      const { error, count } = await supabase.from('projects').delete({ count: 'exact' }).eq('id', projectId);
      if (error || !count) {
        toast('Only the owner or an admin can delete this project.', 'error');
        return;
      }
      qc.setQueryData(['projects'], (old = []) => old.filter(p => p.id !== projectId));
      logActivity('deleted', 'a project');
      toast('Project deleted.');
    })();
  }, [projects, attachments, currentUser, qc, logActivity, toast]);

  const canDeleteProject = useCallback((projectId) => {
    return quotations.filter(q => q.projectId === projectId).length === 0 &&
      samplingProjects.filter(sp => sp.linkedProjectId === projectId).length === 0 &&
      components.filter(c => c.projectId === projectId).length === 0;
  }, [quotations, samplingProjects, components]);

  // Maps camelCase patch keys to DB columns and pushes both an optimistic
  // cache update and the real write; `visibleTo` is deliberately excluded
  // from the DB write (no column - see useOrgProjects above).
  const updateProject = useCallback((projectId, patch) => {
    const current = projects.find(p => p.id === projectId);
    qc.setQueryData(['projects'], (old = []) => old.map(p => p.id === projectId ? { ...p, ...patch } : p));
    const fieldMap = {
      name: 'name', category: 'category', status: 'status', priority: 'priority',
      owner: 'owner_id', target: 'target_date', startDate: 'start_date',
      completedDate: 'completed_date', completedBy: 'completed_by', isArchived: 'is_archived',
      blocking: 'blocking', waiting: 'waiting', progress: 'progress', launchWave: 'launch_wave',
      estimatedBudget: 'estimated_budget', budgetCurrency: 'budget_currency',
    };
    const dbPatch = {};
    Object.entries(fieldMap).forEach(([jsKey, dbKey]) => { if (jsKey in patch) dbPatch[dbKey] = patch[jsKey]; });
    (async () => {
      if (Object.keys(dbPatch).length) {
        const { data, error } = await supabase.from('projects').update(dbPatch).eq('id', projectId).eq('row_version', current?.rowVersion).select('id');
        if (error) { toast('Failed to save project changes.', 'error'); }
        else if (!data?.length) {
          reportConflict({ queryKeys: ['projects'] });
          qc.invalidateQueries({ queryKey: ['projects'] });
          return;
        }
      }
      if ('editableBy' in patch) {
        const { error: delError } = await supabase.from('project_members').delete().eq('project_id', projectId);
        const rows = (patch.editableBy || []).filter(Boolean).map(profileId => ({ project_id: projectId, profile_id: profileId, added_by: currentUserId }));
        if (!delError && rows.length) {
          const { error: insError } = await supabase.from('project_members').insert(rows);
          if (insError) toast('Failed to update collaborators (permission denied).', 'error');
        }
      }
      qc.invalidateQueries({ queryKey: ['projects'] });
    })();
  }, [projects, qc, currentUserId, toast, reportConflict]);

  const markProjectComplete = useCallback((projectId) => {
    const proj = projects.find(p => p.id === projectId);
    if (!canEditProject(currentUser, proj)) { toast('Only the owner or an admin can mark this project complete.', 'error'); return; }
    updateProject(projectId, { status: 'Complete', progress: 100, completedDate: todayStr(), completedBy: currentUserId, isArchived: true });
    logActivity('marked complete', proj?.name || 'project');
    toast(`"${proj?.name}" moved to Accomplished Projects.`);
  }, [projects, currentUser, currentUserId, updateProject, logActivity, toast]);

  /* ---- SUPPLIERS ---- */
  const addSupplier = useCallback((data) => {
    if (!data.name?.trim()) { toast('Supplier name is required.', 'error'); return null; }
    if (!orgId) { toast('Still loading your organization — try again in a moment.', 'error'); return null; }
    const id = crypto.randomUUID();
    const lead = data.lead === '' || data.lead == null ? null : Number(data.lead);
    const { quantity: moqQuantity, unit: moqUnit } = parseMoq(data.moq);
    const supplier = {
      id, name: data.name.trim(), country: data.country || '', contact: data.contact || '', email: data.email || '', phone: data.phone || '',
      makes: data.makes || '', moq: data.moq || '', lead, terms: data.terms || '',
      rating: null, active: false, isPotential: true, serialNo: null, // real value comes from the DB sequence, filled in once the insert below confirms
    };
    qc.setQueryData(['suppliers'], (old = []) => [...old, supplier]);
    (async () => {
      const { error } = await supabase.from('suppliers').insert({
        id, organization_id: orgId, name: supplier.name, country: supplier.country || null, makes: supplier.makes || null,
        moq_quantity: moqQuantity, moq_unit: moqUnit, lead_time_days: lead, terms: supplier.terms || null,
        rating: null, is_active: false, is_potential: true,
      });
      if (error) { toast('Failed to add supplier.', 'error'); qc.invalidateQueries({ queryKey: ['suppliers'] }); return; }
      if (supplier.contact || supplier.email || supplier.phone) {
        const { error: contactError } = await supabase.from('supplier_contacts').insert({
          supplier_id: id, name: supplier.contact || null, email: supplier.email || null, phone: supplier.phone || null, is_primary: true,
        });
        if (contactError) toast('Supplier added, but contact details failed to save.', 'error');
      }
      qc.invalidateQueries({ queryKey: ['suppliers'] });
    })();
    logActivity('added potential supplier', supplier.name);
    toast(`Supplier "${supplier.name}" added as potential.`);
    return id;
  }, [orgId, qc, logActivity, toast]);

  const updateSupplier = useCallback((supplierId, patch) => {
    const current = suppliers.find(s => s.id === supplierId);
    qc.setQueryData(['suppliers'], (old = []) => old.map(s => s.id === supplierId ? { ...s, ...patch } : s));
    const fieldMap = { name: 'name', country: 'country', makes: 'makes', lead: 'lead_time_days', terms: 'terms', active: 'is_active', isPotential: 'is_potential' };
    const dbPatch = {};
    Object.entries(fieldMap).forEach(([jsKey, dbKey]) => { if (jsKey in patch) dbPatch[dbKey] = patch[jsKey]; });
    if ('moq' in patch) {
      const { quantity, unit } = parseMoq(patch.moq);
      dbPatch.moq_quantity = quantity; dbPatch.moq_unit = unit;
    }
    const hasContactPatch = ['contact', 'email', 'phone'].some(k => k in patch);
    (async () => {
      if (Object.keys(dbPatch).length) {
        const { data, error } = await supabase.from('suppliers').update(dbPatch).eq('id', supplierId).eq('row_version', current?.rowVersion).select('id');
        if (error) { toast('Failed to save supplier changes.', 'error'); }
        else if (!data?.length) {
          reportConflict({ queryKeys: ['suppliers'] });
          qc.invalidateQueries({ queryKey: ['suppliers'] });
          return;
        }
      }
      if (hasContactPatch) {
        const contactPatch = {};
        if ('contact' in patch) contactPatch.name = patch.contact || null;
        if ('email' in patch) contactPatch.email = patch.email || null;
        if ('phone' in patch) contactPatch.phone = patch.phone || null;
        const { data: existing } = await supabase.from('supplier_contacts').select('id').eq('supplier_id', supplierId).eq('is_primary', true).maybeSingle();
        const { error } = existing
          ? await supabase.from('supplier_contacts').update(contactPatch).eq('id', existing.id)
          : await supabase.from('supplier_contacts').insert({ supplier_id: supplierId, is_primary: true, ...contactPatch });
        if (error) toast('Failed to save contact details.', 'error');
      }
      qc.invalidateQueries({ queryKey: ['suppliers'] });
    })();
  }, [suppliers, qc, toast, reportConflict]);

  /* ---- QUOTATIONS ---- */
  // Returns a promise resolving to the new id once the quotation row is
  // actually confirmed in the DB (or null on failure) - callers that need to
  // reference this quotation right away (e.g. attaching a file to it at
  // creation time) must await this, not just take the id and run with it.
  // Reported live: attaching a file while creating a quotation failed every
  // time, while attaching to an already-loaded sampling project worked fine
  // - the difference was this function handing back a client-generated id
  // before its own insert had landed, so the attachment insert's
  // quotation_id FK pointed at a row that didn't exist yet.
  const addQuotation = useCallback(async (projectId, data) => {
    const project = projects.find(p => p.id === projectId);
    if (!canEditProject(currentUser, project)) { toast('Only the project owner or an admin can add quotations.', 'error'); return null; }
    const supplier = suppliers.find(s => s.id === data.supplierId);
    const id = crypto.randomUUID();
    const lineItems = (data.lineItems || []).map(li => ({
      id: crypto.randomUUID(), itemName: li.itemName || '', qty: Number(li.qty) || 0, unitPrice: Number(li.unitPrice) || 0,
      moq: li.moq || '', moldCost: Number(li.moldCost) || 0, samplingCost: Number(li.samplingCost) || 0,
      prodLeadTime: Number(li.prodLeadTime) || 0, samplingLeadTime: Number(li.samplingLeadTime) || 0,
      status: 'Pending', approvedReason: '', rejectedReason: '', decidedAt: null, samplingProjectId: null, locked: false,
    }));
    const q = {
      id, projectId, supplierId: data.supplierId, supplierName: supplier?.name || 'Unknown', referenceNo: data.referenceNo?.trim() || '',
      currency: data.currency || 'EUR', validUntil: data.validUntil || addDays(todayStr(), 30), createdAt: new Date().toISOString(),
      createdBy: currentUserId, lineItems,
    };
    qc.setQueryData(['quotations'], (old = []) => [...old, q]);
    logActivity('logged quote from', `${supplier?.name} — ${project?.name}`);
    toast('Quotation added.');
    const { error } = await supabase.from('quotations').insert({
      id, project_id: projectId, supplier_id: data.supplierId, supplier_name_snapshot: q.supplierName, reference_no: q.referenceNo || null,
      currency: q.currency, valid_until: q.validUntil, created_by: currentUserId,
    });
    if (error) { toast('Failed to add quotation.', 'error'); qc.invalidateQueries({ queryKey: ['quotations'] }); return null; }
    if (lineItems.length) {
      const { error: liError } = await supabase.from('quotation_line_items').insert(lineItems.map(li => {
        const { quantity: moqQuantity, unit: moqUnit } = parseMoq(li.moq);
        return {
          id: li.id, quotation_id: id, item_name: li.itemName, qty: li.qty, unit_price: li.unitPrice,
          moq_quantity: moqQuantity, moq_unit: moqUnit, mold_cost: li.moldCost, sampling_cost: li.samplingCost,
          prod_lead_time_days: li.prodLeadTime, sampling_lead_time_days: li.samplingLeadTime, status: 'Pending',
        };
      }));
      if (liError) toast('Quotation added, but line items failed to save.', 'error');
    }
    qc.invalidateQueries({ queryKey: ['quotations'] });
    return id;
  }, [suppliers, projects, currentUser, currentUserId, qc, logActivity, toast]);

  const updateQuotation = useCallback((qId, patch) => {
    const q = quotations.find(x => x.id === qId);
    const project = projects.find(p => p.id === q?.projectId);
    if (!canEditQuotation(currentUser, q, project)) { toast('Only the project owner or an admin can edit quotations.', 'error'); return; }
    qc.setQueryData(['quotations'], (old = []) => old.map(quote => quote.id === qId ? { ...quote, ...patch } : quote));
    // Always includes updated_at so the versioned write below happens even
    // when only lineItems changed (line items have no row_version of their
    // own yet) - this is the conflict-detection gate for the whole quotation.
    const dbPatch = { updated_at: new Date().toISOString() };
    if ('currency' in patch) dbPatch.currency = patch.currency;
    if ('validUntil' in patch) dbPatch.valid_until = patch.validUntil;
    if ('supplierId' in patch) dbPatch.supplier_id = patch.supplierId;
    if ('referenceNo' in patch) dbPatch.reference_no = patch.referenceNo?.trim() || null;
    (async () => {
      const { data: written, error: gateError } = await supabase.from('quotations').update(dbPatch).eq('id', qId).eq('row_version', q?.rowVersion).select('id');
      if (gateError) { toast('Failed to save quotation changes.', 'error'); return; }
      if (!written?.length) {
        reportConflict({ queryKeys: ['quotations'] });
        qc.invalidateQueries({ queryKey: ['quotations'] });
        return;
      }
      if ('lineItems' in patch) {
        const existingIds = new Set((q?.lineItems || []).map(li => li.id));
        const nextIds = new Set(patch.lineItems.map(li => li.id));
        const toDelete = [...existingIds].filter(lid => !nextIds.has(lid));
        if (toDelete.length) {
          const { error: delError } = await supabase.from('quotation_line_items').delete().in('id', toDelete);
          if (delError) toast('Failed to remove some line items.', 'error');
        }
        // Locked items are passed through unchanged by the edit form - never
        // write their decision/status columns here (status can be the
        // display-only 'In Sampling' value, which isn't valid in the DB enum).
        const toUpsert = patch.lineItems.map(li => {
          const { quantity: moqQuantity, unit: moqUnit } = parseMoq(li.moq);
          const row = {
            id: li.id, quotation_id: qId, item_name: li.itemName, qty: li.qty, unit_price: li.unitPrice,
            moq_quantity: moqQuantity, moq_unit: moqUnit, mold_cost: li.moldCost, sampling_cost: li.samplingCost,
            prod_lead_time_days: li.prodLeadTime, sampling_lead_time_days: li.samplingLeadTime,
          };
          if (!li.locked) {
            row.status = li.status || 'Pending';
            row.approved_reason = li.approvedReason || null;
            row.rejected_reason = li.rejectedReason || null;
          }
          return row;
        });
        if (toUpsert.length) {
          const { error: upsertError } = await supabase.from('quotation_line_items').upsert(toUpsert);
          if (upsertError) toast('Failed to save line items.', 'error');
        }
      }
      qc.invalidateQueries({ queryKey: ['quotations'] });
    })();
  }, [quotations, projects, currentUser, qc, toast, reportConflict]);

  const deleteQuotation = useCallback((qId) => {
    const q = quotations.find(x => x.id === qId);
    const project = projects.find(p => p.id === q?.projectId);
    if (!canDeleteQuotation(currentUser, q, project)) { toast('This quote has items in sampling — only an admin can delete it now.', 'error'); return; }
    (async () => {
      // Same Storage-cleanup pattern as deleteProject: the row's cascade
      // delete only removes the attachments table row, not the file bytes.
      const paths = attachments.filter(a => a.entityType === 'quotation' && a.entityId === qId).map(a => a.fileUrl).filter(Boolean);
      if (paths.length) await supabase.storage.from('attachments').remove(paths);
      const { error } = await supabase.from('quotations').delete().eq('id', qId);
      if (error) { toast('Failed to delete quotation.', 'error'); return; }
      qc.setQueryData(['quotations'], (old = []) => old.filter(x => x.id !== qId));
      toast('Quotation removed.');
    })();
  }, [quotations, projects, attachments, currentUser, qc, toast]);

  const setLineItemDecision = useCallback((qId, lineItemId, decision, reason) => {
    const q = quotations.find(x => x.id === qId);
    const project = projects.find(p => p.id === q?.projectId);
    const li = q?.lineItems.find(l => l.id === lineItemId);
    if (!li) return;
    if (!canEditLineItem(currentUser, q, project, li)) { toast(li.locked ? 'This item is in sampling — only an admin can override its decision.' : 'Only the project owner or an admin can change this.', 'error'); return; }
    const decidedAt = new Date().toISOString();
    const patch = {
      status: decision,
      approvedReason: decision === 'Approved' ? (reason ?? li.approvedReason ?? '') : '',
      rejectedReason: decision === 'Rejected' ? (reason ?? li.rejectedReason ?? '') : '',
      decidedAt,
    };
    qc.setQueryData(['quotations'], (old = []) => old.map(quote => quote.id !== qId ? quote : {
      ...quote, lineItems: quote.lineItems.map(item => item.id !== lineItemId ? item : { ...item, ...patch }),
    }));
    (async () => {
      const { error } = await supabase.from('quotation_line_items').update({
        status: decision, approved_reason: patch.approvedReason || null, rejected_reason: patch.rejectedReason || null, decided_at: decidedAt,
      }).eq('id', lineItemId);
      if (error) { toast('Failed to save decision.', 'error'); qc.invalidateQueries({ queryKey: ['quotations'] }); }
    })();
    if (decision === 'Developing' && project && !project.isArchived && project.status === 'Quotation') {
      updateProject(project.id, { status: 'Development' });
    }
  }, [quotations, projects, currentUser, qc, updateProject, toast]);

  // The actual writes (sampling_projects + supplier_bids inserts,
  // quotation_line_items link, project status) all happen atomically inside
  // the send_quote_to_sampling() RPC (see 20260805130000_workflow_rpcs.sql) -
  // a real Postgres transaction rather than a sequence of separate client
  // calls that could partially fail. The client still generates ids and
  // applies an optimistic cache update up front so the UI feels instant.
  const sendApprovedLineItems = useCallback((qId) => {
    const quote = quotations.find(q => q.id === qId);
    if (!quote) return;
    const project = projects.find(p => p.id === quote.projectId);
    if (!canEditQuotation(currentUser, quote, project)) { toast('Only the project owner or an admin can send approved items to sampling.', 'error'); return; }
    const eligible = (quote.lineItems || []).filter(li => li.status === 'Approved' && !li.locked);
    if (!eligible.length) { toast('No approved items to send.', 'error'); return; }

    const newSPs = [];
    const newBids = [];
    const spByItemId = {};
    const rpcItems = eligible.map(item => {
      const spId = crypto.randomUUID();
      const bidId = crypto.randomUUID();
      const targetApprovalDate = addDays(todayStr(), 21);
      spByItemId[item.id] = spId;
      newSPs.push({
        id: spId, name: `${project?.name || 'Project'} — ${item.itemName}`, itemName: item.itemName, category: 'Component',
        linkedProjectId: quote.projectId, status: 'Sourcing', ownerId: currentUserId,
        targetApprovalDate, notes: '', createdAt: todayStr(),
      });
      newBids.push({
        id: bidId, samplingProjectId: spId, supplierId: quote.supplierId,
        bidStatus: 'Quoted', quotedPrice: item.unitPrice, currency: quote.currency || 'EUR', quotedMOQ: item.moq, quotedQty: item.qty || 0,
        quotedLeadTime: item.prodLeadTime, quotedSamplingLeadTime: item.samplingLeadTime || 0, rating: null, moldCost: item.moldCost || 0, samplingCost: item.samplingCost || 0,
      });
      const { quantity: moqQuantity, unit: moqUnit } = parseMoq(item.moq);
      return {
        line_item_id: item.id, sampling_project_id: spId, supplier_bid_id: bidId, item_name: item.itemName,
        target_approval_date: targetApprovalDate, supplier_id: quote.supplierId, quoted_price: item.unitPrice,
        currency: quote.currency || 'EUR', moq_quantity: moqQuantity, moq_unit: moqUnit, quoted_qty: item.qty || 0,
        quoted_lead_time_days: item.prodLeadTime, quoted_sampling_lead_time_days: item.samplingLeadTime || 0,
        mold_cost: item.moldCost || 0, sampling_cost: item.samplingCost || 0,
      };
    });

    // samplingProjects/supplierBids aren't read from Supabase yet (a later
    // slice), so keep the optimistic local cache updated too - the Sampling
    // Hub still reads from here in the meantime.
    qc.setQueryData(['samplingProjects'], (old = []) => [...old, ...newSPs]);
    qc.setQueryData(['supplierBids'], (old = []) => [...old, ...newBids]);
    const sentAt = new Date().toISOString();
    qc.setQueryData(['quotations'], (old = []) => old.map(q => q.id !== qId ? q : {
      ...q,
      lineItems: q.lineItems.map(item => spByItemId[item.id] ? { ...item, status: 'In Sampling', samplingProjectId: spByItemId[item.id], locked: true, decidedAt: sentAt } : item),
    }));
    // Project status update happens server-side inside the RPC too; mirrored
    // here only so the UI reflects it before the invalidate/refetch lands.
    if (project && !project.isArchived && project.status !== 'Complete' && project.status !== 'Sampling') {
      qc.setQueryData(['projects'], (old = []) => old.map(p => p.id === quote.projectId ? { ...p, status: 'Sampling' } : p));
    }

    (async () => {
      const { error } = await supabase.rpc('send_quote_to_sampling', { p_quotation_id: qId, p_items: rpcItems, p_expected_version: quote.rowVersion });
      if (error) {
        if (isVersionConflict(error)) reportConflict({ queryKeys: ['quotations', 'samplingProjects', 'supplierBids'] });
        else toast('Failed to send items to sampling.', 'error');
      }
      qc.invalidateQueries({ queryKey: ['quotations'] });
      qc.invalidateQueries({ queryKey: ['samplingProjects'] });
      qc.invalidateQueries({ queryKey: ['supplierBids'] });
      qc.invalidateQueries({ queryKey: ['projects'] });
      qc.invalidateQueries({ queryKey: ['activity'] });
    })();

    toast(`${eligible.length} item${eligible.length > 1 ? 's' : ''} sent to sampling.`);
  }, [quotations, projects, currentUser, qc, currentUserId, toast, reportConflict, isVersionConflict]);

  /* ---- SAMPLING ----
     Every mutation below calls a SECURITY DEFINER RPC (20260805130000_workflow_rpcs.sql)
     that does the status update + status-history insert + any cross-entity
     side effect (supplier_bids, supplier promotion, sampling/project status)
     as one atomic transaction, and logs its own activity entry server-side -
     this is the Phase 5 "make business cascades correct and atomic" work.
     The client still applies an optimistic cache update up front so the UI
     feels instant, then invalidates to reconcile with the real result. */
  const requestSampleRound = useCallback((samplingProjectId, data) => {
    const sp = samplingProjects.find(s => s.id === samplingProjectId);
    const project = projects.find(p => p.id === sp?.linkedProjectId);
    if (!canEditItem(currentUser, sp?.ownerId, project)) { toast('Only the owner or an admin can request sample rounds.', 'error'); return; }
    const priorRounds = sampleRounds.filter(r => r.samplingProjectId === samplingProjectId && r.supplierId === data.supplierId);
    const version = `V${priorRounds.length + 1}`;
    const id = crypto.randomUUID();
    const cost = Number(data.cost) || 0;
    const leadTimeDays = data.leadTimeDays != null && data.leadTimeDays !== '' ? Number(data.leadTimeDays) : null;
    const round = {
      id, samplingProjectId, supplierId: data.supplierId, version, status: 'Requested',
      statusHistory: [{ status: 'Requested', updatedAt: new Date().toISOString(), updatedBy: currentUserId }],
      dateRequested: todayStr(), dateReceived: null, dateReviewed: null, resultNotes: null,
      nextAction: 'Awaiting delivery', cost, leadTimeDays,
    };
    qc.setQueryData(['sampleRounds'], (old = []) => [...old, round]);
    qc.setQueryData(['samplingProjects'], (old = []) => old.map(s => s.id === samplingProjectId ? { ...s, status: 'Sampling In Progress' } : s));
    (async () => {
      const { error } = await supabase.rpc('request_sample_round', {
        p_round_id: id, p_sampling_project_id: samplingProjectId, p_supplier_id: data.supplierId,
        p_version: version, p_cost: cost, p_lead_time_days: leadTimeDays,
      });
      if (error) toast('Failed to request sample round.', 'error');
      qc.invalidateQueries({ queryKey: ['sampleRounds'] });
      qc.invalidateQueries({ queryKey: ['samplingProjects'] });
      qc.invalidateQueries({ queryKey: ['activity'] });
    })();
    toast(`Sample round ${version} requested.`);
  }, [sampleRounds, samplingProjects, projects, currentUser, currentUserId, qc, toast]);

  const advanceSampleRoundStatus = useCallback((roundId, newStatus, note) => {
    const round = sampleRounds.find(r => r.id === roundId);
    if (!round) return;
    const sp = samplingProjects.find(s => s.id === round.samplingProjectId);
    const project = projects.find(p => p.id === sp?.linkedProjectId);
    if (!canEditItem(currentUser, sp?.ownerId, project)) { toast('Only the owner or an admin can update sample rounds.', 'error'); return; }
    const allowed = SAMPLE_ROUND_TRANSITIONS[round.status] || [];
    if (!allowed.includes(newStatus)) { toast(`Can't move a round from ${round.status} to ${newStatus}.`, 'error'); return; }
    const entry = { status: newStatus, updatedAt: new Date().toISOString(), updatedBy: currentUserId, note: note || undefined };
    const dateReceived = newStatus === 'Arrived' ? (round.dateReceived || todayStr()) : round.dateReceived;
    qc.setQueryData(['sampleRounds'], (old = []) => old.map(r => r.id === roundId ? { ...r, status: newStatus, statusHistory: [...(r.statusHistory || []), entry], dateReceived } : r));
    (async () => {
      const { error } = await supabase.rpc('advance_sample_round_status', { p_round_id: roundId, p_new_status: newStatus, p_expected_version: round.rowVersion, p_note: note || null });
      if (error) {
        if (isVersionConflict(error)) reportConflict({ queryKeys: ['sampleRounds'] });
        else toast('Failed to update sample round.', 'error');
      }
      qc.invalidateQueries({ queryKey: ['sampleRounds'] });
      qc.invalidateQueries({ queryKey: ['activity'] });
    })();
    toast(`${round.version} moved to ${newStatus}.`);
  }, [sampleRounds, samplingProjects, projects, currentUser, currentUserId, qc, toast, reportConflict, isVersionConflict]);

  const holdSampleRound = useCallback((roundId) => {
    const round = sampleRounds.find(r => r.id === roundId);
    if (!round || round.status === 'On Hold' || SAMPLE_ROUND_TERMINAL_STATUSES.includes(round.status)) return;
    const sp = samplingProjects.find(s => s.id === round.samplingProjectId);
    const project = projects.find(p => p.id === sp?.linkedProjectId);
    if (!canEditItem(currentUser, sp?.ownerId, project)) { toast('Only the owner or an admin can update sample rounds.', 'error'); return; }
    const entry = { status: 'On Hold', updatedAt: new Date().toISOString(), updatedBy: currentUserId, note: `Paused from ${round.status}` };
    qc.setQueryData(['sampleRounds'], (old = []) => old.map(r => r.id === roundId ? { ...r, heldFromStatus: r.status, status: 'On Hold', statusHistory: [...(r.statusHistory || []), entry] } : r));
    (async () => {
      const { error } = await supabase.rpc('hold_sample_round', { p_round_id: roundId, p_expected_version: round.rowVersion });
      if (error) {
        if (isVersionConflict(error)) reportConflict({ queryKeys: ['sampleRounds'] });
        else toast('Failed to put sample round on hold.', 'error');
      }
      qc.invalidateQueries({ queryKey: ['sampleRounds'] });
      qc.invalidateQueries({ queryKey: ['activity'] });
    })();
    toast(`${round.version} put on hold.`);
  }, [sampleRounds, samplingProjects, projects, currentUser, currentUserId, qc, toast, reportConflict, isVersionConflict]);

  const resumeSampleRound = useCallback((roundId) => {
    const round = sampleRounds.find(r => r.id === roundId);
    if (!round || round.status !== 'On Hold') return;
    const sp = samplingProjects.find(s => s.id === round.samplingProjectId);
    const project = projects.find(p => p.id === sp?.linkedProjectId);
    if (!canEditItem(currentUser, sp?.ownerId, project)) { toast('Only the owner or an admin can update sample rounds.', 'error'); return; }
    const resumedStatus = round.heldFromStatus || 'Requested';
    const entry = { status: resumedStatus, updatedAt: new Date().toISOString(), updatedBy: currentUserId, note: 'Resumed' };
    qc.setQueryData(['sampleRounds'], (old = []) => old.map(r => r.id === roundId ? { ...r, status: resumedStatus, heldFromStatus: null, statusHistory: [...(r.statusHistory || []), entry] } : r));
    (async () => {
      const { error } = await supabase.rpc('resume_sample_round', { p_round_id: roundId, p_expected_version: round.rowVersion });
      if (error) {
        if (isVersionConflict(error)) reportConflict({ queryKeys: ['sampleRounds'] });
        else toast('Failed to resume sample round.', 'error');
      }
      qc.invalidateQueries({ queryKey: ['sampleRounds'] });
      qc.invalidateQueries({ queryKey: ['activity'] });
    })();
    toast(`${round.version} resumed.`);
  }, [sampleRounds, samplingProjects, projects, currentUser, currentUserId, qc, toast, reportConflict, isVersionConflict]);

  const reviewSampleRound = useCallback((roundId, decision, notes) => {
    const round = sampleRounds.find(r => r.id === roundId);
    if (!round) return;
    const sp = samplingProjects.find(s => s.id === round.samplingProjectId);
    const supplier = suppliers.find(s => s.id === round.supplierId);
    const project = projects.find(p => p.id === sp?.linkedProjectId);
    if (!canEditItem(currentUser, sp?.ownerId, project)) { toast('Only the owner or an admin can review sample rounds.', 'error'); return; }
    const dateReceived = round.dateReceived || todayStr();
    const dateReviewed = todayStr();
    const entry = { status: decision, updatedAt: new Date().toISOString(), updatedBy: currentUserId, note: notes || undefined };
    const appendHistory = r => [...(r.statusHistory || []), entry];

    if (decision === 'Approved') {
      qc.setQueryData(['sampleRounds'], (old = []) => old.map(r => r.id === roundId ? { ...r, status: 'Approved', dateReceived, dateReviewed, resultNotes: notes || r.resultNotes, nextAction: 'Cleared for production', statusHistory: appendHistory(r) } : r));
      qc.setQueryData(['supplierBids'], (old = []) => old.map(b => (b.samplingProjectId === round.samplingProjectId && b.supplierId === round.supplierId) ? { ...b, bidStatus: 'Sample Approved' } : b));
      if (sp && sp.status !== 'Moved to Production') {
        qc.setQueryData(['samplingProjects'], (old = []) => old.map(s => s.id === round.samplingProjectId ? { ...s, status: 'Sample Approved' } : s));
      }
      if (supplier?.isPotential) {
        qc.setQueryData(['suppliers'], (old = []) => old.map(s => s.id === round.supplierId ? { ...s, isPotential: false, active: true } : s));
      }
      toast('Sample approved. Click "Move to Production" when ready.');
    } else if (decision === 'Rejected') {
      qc.setQueryData(['sampleRounds'], (old = []) => old.map(r => r.id === roundId ? { ...r, status: 'Rejected', dateReceived, dateReviewed, resultNotes: notes || r.resultNotes, nextAction: 'Rejected', statusHistory: appendHistory(r) } : r));
      qc.setQueryData(['supplierBids'], (old = []) => old.map(b => (b.samplingProjectId === round.samplingProjectId && b.supplierId === round.supplierId) ? { ...b, bidStatus: 'Rejected' } : b));
      toast('Sample rejected.', 'error');
    } else {
      qc.setQueryData(['sampleRounds'], (old = []) => old.map(r => r.id === roundId ? { ...r, status: 'Needs Revision', dateReceived, dateReviewed, resultNotes: notes || r.resultNotes, nextAction: 'Revision requested', statusHistory: appendHistory(r) } : r));
      toast('Revision requested.');
    }

    (async () => {
      const { error } = await supabase.rpc('review_sample_round', { p_round_id: roundId, p_decision: decision, p_expected_version: round.rowVersion, p_notes: notes || null });
      if (error) {
        if (isVersionConflict(error)) reportConflict({ queryKeys: ['sampleRounds', 'supplierBids', 'samplingProjects', 'suppliers'] });
        else toast('Failed to save review decision.', 'error');
      }
      qc.invalidateQueries({ queryKey: ['sampleRounds'] });
      qc.invalidateQueries({ queryKey: ['supplierBids'] });
      qc.invalidateQueries({ queryKey: ['samplingProjects'] });
      qc.invalidateQueries({ queryKey: ['suppliers'] });
      qc.invalidateQueries({ queryKey: ['activity'] });
    })();
  }, [sampleRounds, samplingProjects, suppliers, currentUser, currentUserId, qc, toast, reportConflict, isVersionConflict]);

  const requestRevisionRound = useCallback((roundId) => {
    const round = sampleRounds.find(r => r.id === roundId);
    if (!round) return;
    requestSampleRound(round.samplingProjectId, { supplierId: round.supplierId, cost: round.cost, leadTimeDays: round.leadTimeDays });
  }, [sampleRounds, requestSampleRound]);

  const moveSamplingToProduction = useCallback((samplingProjectId, confirmed) => {
    const sp = samplingProjects.find(s => s.id === samplingProjectId);
    if (!sp) return;
    const project = projects.find(p => p.id === sp.linkedProjectId);
    if (!canEditItem(currentUser, sp.ownerId, project)) { toast('Only the owner or an admin can move this to production.', 'error'); return; }
    const approvedRound = sampleRounds.find(r => r.samplingProjectId === samplingProjectId && r.status === 'Approved');
    if (!approvedRound) { toast('Approve a sample round first.', 'error'); return; }
    const qty = Number(confirmed?.qty);
    const price = Number(confirmed?.price);
    if (!(qty >= 0.0001) || !(price >= 0.0001)) { toast('Confirm a qty and price of at least 0.0001 before moving to production.', 'error'); return; }
    const bid = supplierBids.find(b => b.samplingProjectId === samplingProjectId && b.supplierId === approvedRound.supplierId);
    const supplier = suppliers.find(s => s.id === approvedRound.supplierId);
    const now = todayStr();
    const componentId = crypto.randomUUID();

    const newComponent = {
      id: componentId, name: sp.itemName || sp.name, projectId: sp.linkedProjectId, projectName: project?.name || '', samplingProjectId: sp.id,
      supplier: approvedRound.supplierId, country: supplier?.country || '', status: 'Under Production',
      price, currency: bid?.currency || 'EUR', orderQty: qty, duration: 0,
      nextAction: 'Will arrive soon', nextActionUpdatedAt: now, owner: sp.ownerId || currentUserId, startDate: now, completedDate: null,
    };
    qc.setQueryData(['components'], (old = []) => [...old, newComponent]);
    qc.setQueryData(['samplingProjects'], (old = []) => {
      const updated = old.map(s => s.id === samplingProjectId ? { ...s, status: 'Moved to Production' } : s);
      const siblingsDone = updated.filter(s => s.linkedProjectId === sp.linkedProjectId).every(s => ['Moved to Production', 'Cancelled'].includes(s.status));
      if (siblingsDone && project) qc.setQueryData(['projects'], (ps = []) => ps.map(p => p.id === project.id ? { ...p, status: 'Production' } : p));
      return updated;
    });
    (async () => {
      const { error } = await supabase.rpc('move_sampling_to_production', {
        p_sampling_project_id: samplingProjectId, p_component_id: componentId, p_qty: qty, p_price: price, p_expected_version: sp.rowVersion,
      });
      if (error) {
        if (isVersionConflict(error)) reportConflict({ queryKeys: ['samplingProjects', 'components', 'projects'] });
        else toast('Failed to move to production.', 'error');
      }
      qc.invalidateQueries({ queryKey: ['samplingProjects'] });
      qc.invalidateQueries({ queryKey: ['components'] });
      qc.invalidateQueries({ queryKey: ['projects'] });
      qc.invalidateQueries({ queryKey: ['activity'] });
    })();
    toast('Moved to production. Component created in tracker.');
  }, [samplingProjects, sampleRounds, supplierBids, suppliers, projects, currentUser, qc, currentUserId, toast, reportConflict, isVersionConflict]);

  const updateSamplingProject = useCallback((spId, patch) => {
    const sp = samplingProjects.find(s => s.id === spId);
    const project = projects.find(p => p.id === sp?.linkedProjectId);
    if (!canEditItem(currentUser, sp?.ownerId, project)) { toast('Only the owner or an admin can edit this sampling project.', 'error'); return; }
    qc.setQueryData(['samplingProjects'], (old = []) => old.map(s => s.id === spId ? { ...s, ...patch } : s));
    const fieldMap = { status: 'status', targetApprovalDate: 'target_approval_date', notes: 'notes' };
    const dbPatch = {};
    Object.entries(fieldMap).forEach(([jsKey, dbKey]) => { if (jsKey in patch) dbPatch[dbKey] = patch[jsKey]; });
    (async () => {
      if (Object.keys(dbPatch).length) {
        const { data, error } = await supabase.from('sampling_projects').update(dbPatch).eq('id', spId).eq('row_version', sp?.rowVersion).select('id');
        if (error) { toast('Failed to save changes.', 'error'); }
        else if (!data?.length) {
          reportConflict({ queryKeys: ['samplingProjects'] });
          qc.invalidateQueries({ queryKey: ['samplingProjects'] });
          return;
        }
      }
      qc.invalidateQueries({ queryKey: ['samplingProjects'] });
    })();
  }, [samplingProjects, projects, currentUser, qc, toast, reportConflict]);

  /* ---- COMPONENTS ---- */
  const updateComponent = useCallback((cId, patch) => {
    const comp = components.find(c => c.id === cId);
    if (!comp) return;
    const project = projects.find(p => p.id === comp.projectId);
    if (!canEditItem(currentUser, comp.owner, project)) { toast('Only the owner or an admin can edit this component.', 'error'); return; }

    if (patch.nextAction && patch.nextAction !== comp.nextAction) {
      let becameShipping = false;
      const updated = { ...comp, nextAction: patch.nextAction, nextActionUpdatedAt: todayStr() };
      if (patch.nextAction === 'Complete') {
        updated.status = 'Complete';
        if (comp.status !== 'Complete') updated.completedDate = todayStr();
      } else {
        updated.status = 'Under Production';
        updated.completedDate = null;
        if (patch.nextAction === 'Under Shipping') becameShipping = true;
      }

      qc.setQueryData(['components'], (old = []) => {
        const next = old.map(c => c.id === cId ? updated : c);
        if (project && !project.isArchived) {
          if (becameShipping && project.status !== 'Shipping') {
            qc.setQueryData(['projects'], (ps = []) => ps.map(p => p.id === project.id ? { ...p, status: 'Shipping' } : p));
          }
          const projComps = next.filter(c => c.projectId === project.id);
          if (projComps.length > 0 && projComps.every(c => c.status === 'Complete') && project.status !== 'Complete') {
            qc.setQueryData(['projects'], (ps = []) => ps.map(p => p.id === project.id ? { ...p, status: 'Complete', progress: 100 } : p));
          }
        }
        return next;
      });
      (async () => {
        const { error } = await supabase.rpc('update_component_next_action', { p_component_id: cId, p_next_action: patch.nextAction, p_expected_version: comp.rowVersion });
        if (error) {
          if (isVersionConflict(error)) reportConflict({ queryKeys: ['components', 'projects'] });
          else toast('Failed to save component changes.', 'error');
        }
        qc.invalidateQueries({ queryKey: ['components'] });
        qc.invalidateQueries({ queryKey: ['projects'] });
        qc.invalidateQueries({ queryKey: ['activity'] });
      })();
      return;
    }

    if ('owner' in patch) {
      qc.setQueryData(['components'], (old = []) => old.map(c => c.id === cId ? { ...c, owner: patch.owner } : c));
      (async () => {
        const { data, error } = await supabase.from('components').update({ owner_id: patch.owner }).eq('id', cId).eq('row_version', comp.rowVersion).select('id');
        if (error) { toast('Failed to save component changes.', 'error'); qc.invalidateQueries({ queryKey: ['components'] }); return; }
        if (!data?.length) { reportConflict({ queryKeys: ['components'] }); }
        qc.invalidateQueries({ queryKey: ['components'] });
      })();
    }
  }, [components, projects, currentUser, qc, toast, reportConflict, isVersionConflict]);

  const allComponentsComplete = useCallback((projectId) => {
    const list = components.filter(c => c.projectId === projectId);
    return list.length > 0 && list.every(c => c.status === 'Complete');
  }, [components]);

  /* ---- COMMENTS / ATTACHMENTS / TASKS ---- */
  // Posting goes through post_comment() (20260810150000_comment_mentions_and_notifications.sql)
  // rather than a direct insert, since it also handles @mention and
  // "someone commented on your project" notifications - notifications has
  // no client insert policy at all, only SECURITY DEFINER functions can
  // write it.
  const addComment = useCallback((entityType, entityId, text) => {
    const column = PARENT_COLUMN_BY_ENTITY_TYPE[entityType];
    const optimisticId = crypto.randomUUID();
    qc.setQueryData(['comments'], (old = []) => [...old, { id: optimisticId, entityType, entityId, userId: currentUserId, text, createdAt: fmtDateTimeShort(new Date().toISOString()) }]);
    (async () => {
      const params = { p_project_id: null, p_quotation_id: null, p_sampling_project_id: null, p_sample_round_id: null, p_component_id: null, p_supplier_id: null, p_body: text };
      params[`p_${column}`] = entityId;
      const { error } = await supabase.rpc('post_comment', params);
      if (error) toast(error.message || 'Failed to post comment.', 'error');
      qc.invalidateQueries({ queryKey: ['comments'] });
      qc.invalidateQueries({ queryKey: ['notifications'] });
    })();
  }, [currentUserId, qc, toast]);

  const addAttachment = useCallback((entityType, entityId, file) => {
    const column = PARENT_COLUMN_BY_ENTITY_TYPE[entityType];
    const id = crypto.randomUUID();
    const fileType = file.name.split('.').pop().toLowerCase();
    // Storage object keys reject some characters that are perfectly valid in
    // a filename (e.g. a fullwidth "＆" copy-pasted from a product name) with
    // a plain 400 - sanitize only the key, file.name itself is still what's
    // stored/shown as fileName below.
    const safeFileName = file.name.replace(/[^a-zA-Z0-9.\-]+/g, '_');
    const storagePath = `${entityType}/${entityId}/${id}-${safeFileName}`;
    qc.setQueryData(['attachments'], (old = []) => [...old, {
      id, entityType, entityId, fileName: file.name, fileUrl: storagePath, uploadedBy: currentUserId, uploadedAt: todayStr(),
      fileSize: `${(file.size / 1024 / 1024).toFixed(1)} MB`, fileType,
    }]);
    (async () => {
      const { error: uploadError } = await supabase.storage.from('attachments').upload(storagePath, file);
      if (uploadError) { toast('Failed to upload file.', 'error'); qc.invalidateQueries({ queryKey: ['attachments'] }); return; }
      const { error } = await supabase.from('attachments').insert({
        id, [column]: entityId, storage_path: storagePath, file_name: file.name, file_size_bytes: file.size, file_type: fileType, uploaded_by: currentUserId,
      });
      if (error) {
        toast('Upload saved, but the record failed to save.', 'error');
        await supabase.storage.from('attachments').remove([storagePath]);
      }
      qc.invalidateQueries({ queryKey: ['attachments'] });
    })();
    toast('File attached.');
  }, [currentUserId, qc, toast]);

  const deleteAttachment = useCallback((id) => {
    const att = attachments.find(a => a.id === id);
    qc.setQueryData(['attachments'], (old = []) => old.filter(a => a.id !== id));
    (async () => {
      if (att?.fileUrl) await supabase.storage.from('attachments').remove([att.fileUrl]);
      const { error } = await supabase.from('attachments').delete().eq('id', id);
      if (error) toast('Failed to delete attachment.', 'error');
      qc.invalidateQueries({ queryKey: ['attachments'] });
    })();
  }, [attachments, qc, toast]);

  // Signed, short-lived download links (Phase 6 hardening) instead of a
  // permanent public URL - the bucket is private, so this is also the only
  // way to actually fetch the bytes back at all. storage.objects' own select
  // policy still gates who can even request a signed URL for a given path.
  const getAttachmentUrl = useCallback(async (storagePath) => {
    const { data, error } = await supabase.storage.from('attachments').createSignedUrl(storagePath, 60);
    if (error || !data?.signedUrl) { toast('Could not open file.', 'error'); return null; }
    return data.signedUrl;
  }, [toast]);

  const addTask = useCallback((data) => {
    const id = crypto.randomUUID();
    const task = {
      id, title: data.title, description: data.description || '', assignedTo: data.assignedTo, createdBy: currentUserId,
      dueDate: data.dueDate, status: 'Todo', linkedEntityType: data.linkedEntityType || 'project', linkedEntityId: data.linkedEntityId || null,
    };
    qc.setQueryData(['tasks'], (old = []) => [...old, task]);
    (async () => {
      const { error } = await supabase.from('tasks').insert({
        id, title: task.title, description: task.description, assigned_to: task.assignedTo, created_by: currentUserId,
        due_date: task.dueDate, status: 'Todo', project_id: task.linkedEntityType === 'project' ? task.linkedEntityId : null,
      });
      if (error) { toast('Failed to create task.', 'error'); qc.invalidateQueries({ queryKey: ['tasks'] }); return; }
      qc.invalidateQueries({ queryKey: ['tasks'] });
    })();
    toast('Task assigned.');
  }, [currentUserId, qc, toast]);

  const updateTask = useCallback((id, patch) => {
    // Stamping completed_at here (not left null forever) is what makes a
    // real On-Time Rate possible on the Team page - status alone can't tell
    // you whether a Done task was finished before or after its due date.
    // Reopening a task (status moving away from Done) clears it back to
    // null rather than leaving a stale timestamp from a previous completion.
    const finalPatch = { ...patch };
    if ('status' in patch) finalPatch.completedAt = patch.status === 'Done' ? new Date().toISOString() : null;
    qc.setQueryData(['tasks'], (old = []) => old.map(t => t.id === id ? { ...t, ...finalPatch } : t));
    const fieldMap = { title: 'title', description: 'description', assignedTo: 'assigned_to', dueDate: 'due_date', status: 'status' };
    const dbPatch = {};
    Object.entries(fieldMap).forEach(([jsKey, dbKey]) => { if (jsKey in patch) dbPatch[dbKey] = patch[jsKey]; });
    if ('status' in patch) dbPatch.completed_at = finalPatch.completedAt;
    (async () => {
      if (Object.keys(dbPatch).length) {
        const { error } = await supabase.from('tasks').update(dbPatch).eq('id', id);
        if (error) toast('Failed to save task changes.', 'error');
      }
      qc.invalidateQueries({ queryKey: ['tasks'] });
    })();
  }, [qc, toast]);

  const updateProjectVisibility = useCallback((projectId, visibleTo) => {
    updateProject(projectId, { visibleTo });
    microToast('Access updated');
  }, [updateProject, microToast]);

  const updateProjectEditableBy = useCallback((projectId, editableBy) => {
    updateProject(projectId, { editableBy });
    microToast('Edit access updated');
  }, [updateProject, microToast]);

  const openNotification = useCallback((n) => {
    if (!n.read) {
      qc.setQueryData(['notifications'], (old = []) => old.map(x => x.id === n.id ? { ...x, read: true } : x));
      (async () => {
        const { error } = await supabase.from('notifications').update({ read_at: new Date().toISOString() }).eq('id', n.id);
        if (error) qc.invalidateQueries({ queryKey: ['notifications'] });
      })();
    }
    if (n.linkPage && n.linkId) navigate(n.linkPage, n.linkId);
  }, [qc, navigate]);

  const markAllNotificationsRead = useCallback(() => {
    const unreadIds = notifications.filter(n => !n.read).map(n => n.id);
    if (!unreadIds.length) return;
    qc.setQueryData(['notifications'], (old = []) => old.map(n => ({ ...n, read: true })));
    (async () => {
      const { error } = await supabase.from('notifications').update({ read_at: new Date().toISOString() }).in('id', unreadIds);
      if (error) toast('Failed to mark notifications read.', 'error');
      qc.invalidateQueries({ queryKey: ['notifications'] });
    })();
  }, [notifications, qc, toast]);

  // The default queries staleTime (30s, see the QueryClient constructor) is
  // fine for normal browsing, but the Project Overview report needs to promise
  // "generated from what's true right now" - a report built off up-to-30s-
  // stale cache could silently miss an update from two seconds ago. Report
  // generation force-refetches every entity the report engine reads before
  // rendering, bypassing staleTime entirely for that one action.
  const REPORT_QUERY_KEYS = ['projects', 'quotations', 'samplingProjects', 'sampleRounds', 'supplierBids', 'components', 'tasks', 'activity', 'users'];
  const refetchReportData = useCallback(() => (
    Promise.all(REPORT_QUERY_KEYS.map(key => qc.refetchQueries({ queryKey: [key] })))
  ), [qc]);

  const value = {
    users, currentUser, currentUserId,
    refetchReportData,
    projects, suppliers, quotations, samplingProjects, sampleRounds, supplierBids, components,
    attachments, comments, activity, tasks, notifications,
    route, navigate, toast, microToast, confirm, searchOpen, setSearchOpen, aiOpen, setAiOpen,
    fxRates, fxUpdatedAt, fxSource, refreshFxRates, updateFxRate,
    createProject, deleteProject, canDeleteProject, updateProject, markProjectComplete, addSupplier, updateSupplier,
    addQuotation, updateQuotation, deleteQuotation, setLineItemDecision, sendApprovedLineItems,
    requestSampleRound, reviewSampleRound, requestRevisionRound, updateSamplingProject, moveSamplingToProduction,
    advanceSampleRoundStatus, holdSampleRound, resumeSampleRound,
    updateComponent, allComponentsComplete,
    addComment, addAttachment, deleteAttachment, getAttachmentUrl, addTask, updateTask,
    updateProjectVisibility, updateProjectEditableBy, markAllNotificationsRead, openNotification, logActivity,
    inviteMember, updateMemberRole, updateMemberScope, removeMember,
  };

  // Guard against mounting the whole app before we know who's signed in - the
  // `profiles` fetch depends on the session already existing, but it's still
  // a real network round trip (Phase 4: no more instant local `USERS` const).
  if (usersLoading || projectsLoading || suppliersLoading || quotationsLoading || samplingProjectsLoading || sampleRoundsLoading || supplierBidsLoading || componentsLoading || attachmentsLoading || commentsLoading || tasksLoading || notificationsLoading || activityLoading || !currentUser) {
    return (
      <div className="min-h-screen flex items-center justify-center bg-base">
        <Loader2 className="w-6 h-6 text-gold animate-spin" />
      </div>
    );
  }

  return (
    <AppContext.Provider value={value}>
      {children}
      <ToastStack toasts={toasts} dismiss={dismissToast} />
      <ConfirmDialog state={confirmState} onCancel={handleConfirmCancel} onConfirm={handleConfirmOk} />
      <ConflictDialog state={conflictState} onDismiss={handleConflictDismiss} onReload={handleConflictReload} />
    </AppContext.Provider>
  );
}

/* ======================================================================
   6. AI ASSISTANT PANEL
   ====================================================================== */

/* ---- AI: fuzzy matching + shared helpers ---- */
const AI_STOP_WORDS = new Set(['the', 'is', 'are', 'was', 'were', 'what', "what's", 'whats', 'last', 'update', 'updates', 'on', 'for', 'of', 'how', 'much', 'cost', 'costs', 'price', 'status', 'show', 'me', 'tell', 'about', 'with', 'and', 'a', 'an', 'to', 'in', 'did', 'do', 'does', 'doing', 'new', 'latest', 'current', 'this', 'that', 'it', 'has', 'have', 'who', 'which', 'sample', 'samples', 'sampling', 'project', 'projects', 'component', 'components', 'quotation', 'quotations', 'quote', 'quotes', 'supplier', 'suppliers', 'draft', 'email', 'message', 'follow', 'up', 'send', 'please', 'can', 'you', 'total', 'happened', 'right', 'now']);

function aiLevenshtein(a, b) {
  if (a === b) return 0;
  if (!a.length) return b.length;
  if (!b.length) return a.length;
  let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
  for (let i = 1; i <= a.length; i++) {
    const row = [i];
    for (let j = 1; j <= b.length; j++) {
      row[j] = a[i - 1] === b[j - 1]
        ? prev[j - 1]
        : 1 + Math.min(prev[j - 1], prev[j], row[j - 1]);
    }
    prev = row;
  }
  return prev[b.length];
}
function aiFuzzyScore(name, query) {
  if (!name) return 0;
  const n = name.toLowerCase();
  const ql = query.toLowerCase().trim();
  if (!ql) return 0;
  if (n === ql) return 1000;
  if (ql.length > 2 && n.includes(ql)) return 500 + ql.length;
  const qWords = ql.split(/[^a-z0-9]+/).filter(w => w.length > 1 && !AI_STOP_WORDS.has(w));
  const nWords = n.split(/[^a-z0-9]+/).filter(w => w.length > 2);
  let score = 0;
  qWords.forEach(qw => {
    if (n.includes(qw)) { score += qw.length * 2; return; }
    // Typo tolerance: this query word has no exact substring match — try edit distance
    // against each significant word in the name so a misspelling still contributes.
    // Scored per word and summed (not a whole-name short-circuit), so a name that's an
    // exact match on one word AND a close typo match on another still outranks a name
    // that only coincidentally shares the one exact word.
    let bestTypo = 0;
    nWords.forEach(nw => {
      const dist = aiLevenshtein(qw, nw);
      const tolerance = qw.length <= 6 ? 1 : qw.length <= 10 ? 2 : 3;
      if (dist > 0 && dist <= tolerance) {
        const closeness = (nw.length - dist) * 1.5;
        if (closeness > bestTypo) bestTypo = closeness;
      }
    });
    score += bestTypo;
  });
  return score;
}
function aiFindBest(items, nameFn, query) {
  let best = null, bestScore = 0;
  items.forEach(item => {
    const s = aiFuzzyScore(nameFn(item), query);
    if (s > bestScore) { bestScore = s; best = item; }
  });
  return bestScore >= 3 ? best : null;
}
function aiStatusEmoji(status) {
  if (!status) return '';
  const s = status.toLowerCase();
  if (s.includes('approved') || s.includes('complete') || s.includes('selected') || s.includes('cleared')) return '✅';
  if (s.includes('reject') || s.includes('blocked')) return '🔴';
  if (s.includes('revision') || s.includes('progress') || s.includes('pending') || s.includes('review')) return '🟡';
  if (s.includes('production') || s.includes('sourcing') || s.includes('requested')) return '🟢';
  return '';
}
function aiVisibleProjects(ctx) { return ctx.projects.filter(p => canSeeProject(ctx.currentUser, p)); }
function aiCanSeeComponent(ctx, c) { const p = ctx.projects.find(p => p.id === c.projectId); return p ? canSeeProject(ctx.currentUser, p) : true; }
function aiCanSeeSamplingProject(ctx, sp) { const p = ctx.projects.find(p => p.id === sp.linkedProjectId); return p ? canSeeProject(ctx.currentUser, p) : true; }
function aiCanSeeQuotation(ctx, q) { const p = ctx.projects.find(p => p.id === q.projectId); return p ? canSeeProject(ctx.currentUser, p) : true; }
function aiFindLinkedSamplingProject(ctx, comp) {
  return comp.samplingProjectId ? ctx.samplingProjects.find(sp => sp.id === comp.samplingProjectId) : null;
}
// Ranks components, sampling tracks, and projects together against one query and returns
// the single best-scoring entity across all three — never lets a weak match in one domain
// (e.g. components, checked first) shadow a much stronger match in another (e.g. projects).
function aiBestEntityMatch(ctx, query) {
  const candidates = [];
  ctx.components.filter(c => aiCanSeeComponent(ctx, c)).forEach(c => candidates.push({ type: 'component', item: c, score: aiFuzzyScore(c.name, query) }));
  ctx.samplingProjects.filter(sp => aiCanSeeSamplingProject(ctx, sp)).forEach(sp => candidates.push({ type: 'sampling', item: sp, score: aiFuzzyScore(sp.itemName || sp.name, query) }));
  aiVisibleProjects(ctx).forEach(p => candidates.push({ type: 'project', item: p, score: aiFuzzyScore(p.name, query) }));
  let best = null;
  candidates.forEach(c => { if (c.score >= 3 && (!best || c.score > best.score)) best = c; });
  return best;
}
function aiAccessDenied(query, ctx) {
  if (aiFindBest(ctx.projects.filter(p => !canSeeProject(ctx.currentUser, p)), p => p.name, query)) return true;
  if (aiFindBest(ctx.components.filter(c => !aiCanSeeComponent(ctx, c)), c => c.name, query)) return true;
  if (aiFindBest(ctx.samplingProjects.filter(s => !aiCanSeeSamplingProject(ctx, s)), s => s.name, query)) return true;
  return false;
}
// Last resort before giving up: scan free-text fields (blockers, result notes, next
// actions, comments, task titles) for a literal word match, so a query that doesn't hit
// any named entity still surfaces something relevant instead of a flat dead end.
function aiDeepSearch(ctx, query) {
  const qWords = (query || '').toLowerCase().split(/[^a-z0-9]+/).filter(w => w.length > 2 && !AI_STOP_WORDS.has(w));
  if (!qWords.length) return null;
  const hits = [];
  const addHit = (label, field, val, link) => {
    if (val && qWords.some(w => val.toLowerCase().includes(w))) hits.push({ label, field, val, link });
  };
  aiVisibleProjects(ctx).forEach(p => {
    addHit(p.name, 'blocked on', p.blocking, { label: p.name, page: 'projectDetail', id: p.id });
    addHit(p.name, 'waiting on', p.waiting, { label: p.name, page: 'projectDetail', id: p.id });
  });
  ctx.sampleRounds.forEach(r => {
    const sp = ctx.samplingProjects.find(s => s.id === r.samplingProjectId);
    if (sp && aiCanSeeSamplingProject(ctx, sp)) addHit(sp.itemName || sp.name, `${r.version} result`, r.resultNotes, { label: sp.name, page: 'samplingDetail', id: sp.id });
  });
  ctx.components.filter(c => aiCanSeeComponent(ctx, c)).forEach(c => {
    addHit(c.name, 'next action', c.nextAction, { label: 'Components', page: 'components' });
  });
  ctx.comments.forEach(c => addHit(`a comment on ${c.entityType}`, 'comment', c.text, null));
  ctx.tasks.forEach(t => addHit(t.title, 'task', t.title, null));
  return hits.length ? hits.slice(0, 3) : null;
}
const AI_NOT_FOUND_TEXT = "I don't see any record of that in the system.";
const AI_NO_ACCESS_TEXT = "That project isn't in your workspace.";
function aiFinalize(result, query, ctx) {
  if (result) return result;
  if (aiAccessDenied(query, ctx)) return { text: AI_NO_ACCESS_TEXT };
  const hits = aiDeepSearch(ctx, query);
  if (hits) {
    const lines = ["I don't have an exact match for that, but a deeper search turned this up:", ''];
    hits.forEach(h => lines.push(`On **${h.label}** (${h.field}): "${h.val}"`));
    lines.push('', "Is one of these what you meant? Ask about it by name and I'll pull the full detail.");
    return { text: lines.join('\n'), links: hits.map(h => h.link).filter(Boolean) };
  }
  return { text: AI_NOT_FOUND_TEXT };
}
function aiSumByDominantCurrency(items) {
  if (!items.length) return { total: 0, currency: 'EUR', excluded: [] };
  const counts = {};
  items.forEach(i => { counts[i.currency] = (counts[i.currency] || 0) + 1; });
  const dominant = Object.keys(counts).sort((a, b) => counts[b] - counts[a])[0];
  const excluded = items.filter(i => i.currency !== dominant);
  const total = items.filter(i => i.currency === dominant).reduce((s, i) => s + i.cost, 0);
  return { total, currency: dominant, excluded };
}

/* ---- AI: freshness — real activity-log timestamps down to the second, not just a date field ---- */
function aiActivityMatch(ctx, needles) {
  const lc = (needles || []).filter(Boolean).map(n => String(n).toLowerCase());
  if (!lc.length) return null;
  // ctx.activity is newest-first (logActivity unshifts), so the first match is the latest one.
  return ctx.activity.find(a => {
    const text = `${a.action || ''} ${a.detail || ''}`.toLowerCase();
    return lc.every(n => text.includes(n));
  }) || null;
}
function aiFreshness(ctx, fallbackDateOrISO, needles) {
  const match = aiActivityMatch(ctx, needles);
  if (match) return { ts: match.createdAt, label: `${fmtDate(match.createdAt)} (${fmtRelativeFromISO(match.createdAt)})`, byActivity: true };
  if (!fallbackDateOrISO) return { ts: '', label: 'no update on record', byActivity: false };
  const iso = fallbackDateOrISO.length > 10 ? fallbackDateOrISO : `${fallbackDateOrISO}T00:00:00.000Z`;
  return { ts: iso, label: `${fmtDate(fallbackDateOrISO)} (${fmtRelativeFromISO(iso)})`, byActivity: false };
}

/* ---- AI: timeline variance — flag a sampling round running past its quoted lead time ---- */
function aiRoundTimelineNote(sp, ctx) {
  const rounds = ctx.sampleRounds.filter(r => r.samplingProjectId === sp.id)
    .sort((a, b) => (b.dateRequested || '').localeCompare(a.dateRequested || ''));
  const latest = rounds[0];
  if (!latest || !latest.dateRequested) return null;
  const inFlight = !SAMPLE_ROUND_TERMINAL_STATUSES.includes(latest.status) && latest.status !== 'Cancelled';
  if (!inFlight) return null;
  const bid = ctx.supplierBids.find(b => b.samplingProjectId === sp.id && b.supplierId === latest.supplierId);
  const expectedDays = bid?.quotedSamplingLeadTime;
  if (!expectedDays) return null;
  const elapsed = daysBetween(latest.dateRequested, todayStr());
  const overdueDays = elapsed - expectedDays;
  if (overdueDays <= 0) return null;
  return `Sampling was expected to wrap in ~${expectedDays}d (by ${fmtDate(addDays(latest.dateRequested, expectedDays))}) — it's ${elapsed}d in with no resolution yet, ${overdueDays}d overdue.`;
}

/* ---- AI: Pattern A — Last Update ---- */
function aiHandleLastUpdate(query, ctx) {
  const { samplingProjects, projects, sampleRounds, users } = ctx;
  const bestMatch = aiBestEntityMatch(ctx, query);
  const comp = bestMatch?.type === 'component' ? bestMatch.item : null;
  if (comp) {
    const owner = userById(users, comp.owner);
    const proj = projects.find(p => p.id === comp.projectId);
    const lines = [`**${comp.name}**`, `Status: ${comp.status} ${aiStatusEmoji(comp.status)}`.trim()];
    const compFresh = aiFreshness(ctx, comp.nextActionUpdatedAt || comp.startDate, [comp.name, proj?.name]);
    lines.push(`Last update: **${compFresh.label} by ${owner?.name || 'unknown'}**`);
    lines.push(`Action: "${comp.nextAction}"`);
    lines.push(comp.status === 'Complete' && comp.completedDate
      ? `Duration in production: ${daysBetween(comp.startDate, comp.completedDate)} days (started ${fmtDate(comp.startDate)})`
      : `Duration in production: ${durationSince(comp.startDate)} days (started ${fmtDate(comp.startDate)})`);
    const sp = aiFindLinkedSamplingProject(ctx, comp);
    if (sp) {
      const approvedRound = sampleRounds.filter(r => r.samplingProjectId === sp.id && r.status === 'Approved' && r.supplierId === comp.supplier)
        .sort((a, b) => (b.dateReviewed || '').localeCompare(a.dateReviewed || ''))[0];
      if (approvedRound) {
        lines.push('', `Previous update: ${fmtDate(approvedRound.dateReviewed)} by ${owner?.name || 'unknown'}`, `Action: Moved to production from sampling (${approvedRound.version} approved)`);
      }
    }
    if (proj?.isArchived) lines.push('', `Component is archived under ${proj.name}.`);
    return { text: lines.join('\n'), links: proj ? [{ label: proj.name, page: 'projectDetail', id: proj.id }] : [] };
  }

  const sp = bestMatch?.type === 'sampling' ? bestMatch.item : null;
  if (sp) {
    const proj = projects.find(p => p.id === sp.linkedProjectId);
    const rounds = sampleRounds.filter(r => r.samplingProjectId === sp.id).sort((a, b) => (b.dateReviewed || b.dateRequested || '').localeCompare(a.dateReviewed || a.dateRequested || ''));
    const latest = rounds[0];
    const owner = userById(users, sp.ownerId);
    const lines = [`**${sp.name}** (Sampling)`, `Status: ${sp.status} ${aiStatusEmoji(sp.status)}`.trim()];
    if (latest) {
      const spFresh = aiFreshness(ctx, latest.dateReviewed || latest.dateRequested, [sp.itemName || sp.name, proj?.name]);
      lines.push(`Last update: **${spFresh.label} by ${owner?.name || 'unknown'}**`);
      lines.push(`Action: ${latest.version} ${latest.status}${latest.resultNotes ? ` — "${latest.resultNotes}"` : ''}`);
      const overdueNote = aiRoundTimelineNote(sp, ctx);
      if (overdueNote) lines.push('', `⚠️ ${overdueNote}`);
    } else {
      lines.push('No sample rounds logged yet.');
    }
    return { text: lines.join('\n'), links: proj ? [{ label: proj.name, page: 'projectDetail', id: proj.id }] : [] };
  }

  const proj = bestMatch?.type === 'project' ? bestMatch.item : null;
  if (proj) {
    const related = ctx.activity.filter(a => (a.detail || '').toLowerCase().includes(proj.name.toLowerCase()) || (a.action || '').toLowerCase().includes(proj.name.toLowerCase()));
    const latestActivity = related[0];
    const lines = [`**${proj.name}** (Project)`, `Status: ${proj.status}`];
    if (latestActivity) {
      const u = userById(users, latestActivity.userId);
      lines.push(`Last update: **${fmtDate(latestActivity.createdAt)} (${fmtRelativeFromISO(latestActivity.createdAt)}) by ${u?.name || 'unknown'}**`, `Action: ${latestActivity.action} ${latestActivity.detail}`);
    }
    if (proj.blocking) lines.push(`Waiting on: ${proj.waiting || 'update'}`);
    const relatedSp = samplingProjects.find(s => s.linkedProjectId === proj.id);
    if (relatedSp) {
      const rounds = sampleRounds.filter(r => r.samplingProjectId === relatedSp.id).sort((a, b) => (b.dateReviewed || '').localeCompare(a.dateReviewed || ''));
      if (rounds[0]) lines.push('', `Sampling status: ${rounds[0].version} reviewed ${fmtDate(rounds[0].dateReviewed)} — ${rounds[0].status}`);
    }
    return { text: lines.join('\n'), links: [{ label: proj.name, page: 'projectDetail', id: proj.id }] };
  }
  return null;
}

/* ---- AI: Pattern B — Cost Query ---- */
function aiCostForComponent(comp, ctx) {
  const { suppliers, projects, sampleRounds, supplierBids } = ctx;
  const qty = parseNumeric(comp.orderQty);
  const productionCost = (comp.price || 0) * qty;
  const supplier = supplierById(suppliers, comp.supplier);
  const sp = aiFindLinkedSamplingProject(ctx, comp);
  const bid = sp ? supplierBids.find(b => b.samplingProjectId === sp.id && b.supplierId === comp.supplier) : null;
  const moldCost = bid?.moldCost || 0;
  const rounds = sp ? sampleRounds.filter(r => r.samplingProjectId === sp.id && r.supplierId === comp.supplier) : [];
  const samplingTotal = rounds.reduce((s, r) => s + (r.cost || 0), 0);
  const total = productionCost + moldCost + samplingTotal;
  const lines = [`**${comp.name}**`];
  lines.push(`Unit price: ${fmtCurrency(comp.price, comp.currency)}`);
  lines.push(`Order quantity: ${comp.orderQty} units`);
  lines.push(`Production cost: ${fmtCurrency(productionCost, comp.currency, { decimals: 0 })}`, '');
  lines.push(`Tooling / Mold: ${fmtCurrency(moldCost, comp.currency, { decimals: 0 })}${bid ? ` (from ${supplier?.name || 'supplier'} quotation)` : ''}`);
  lines.push(rounds.length
    ? `Sampling cost: ${fmtCurrency(samplingTotal, comp.currency, { decimals: 0 })} (${rounds.length} round${rounds.length > 1 ? 's' : ''} with ${supplier?.name || 'supplier'})`
    : `Sampling cost: ${fmtCurrency(0, comp.currency, { decimals: 0 })} (no rounds on record)`);
  lines.push('', `Total committed: **${fmtCurrency(total, comp.currency, { decimals: 0 })}**`);
  if (supplier) lines.push(`Supplier: ${supplier.name}`);
  const proj = projects.find(p => p.id === comp.projectId);
  return { text: lines.join('\n'), links: proj ? [{ label: proj.name, page: 'projectDetail', id: proj.id }] : [] };
}
function aiCostForProject(proj, ctx) {
  const { components, samplingProjects, sampleRounds, quotations, suppliers, supplierBids } = ctx;
  const compItems = components.filter(c => c.projectId === proj.id).map(c => {
    const qty = parseNumeric(c.orderQty);
    const cost = (c.price || 0) * qty;
    return { label: `${c.name}: ${fmtCurrency(c.price, c.currency)} × ${c.orderQty} = ${fmtCurrency(cost, c.currency, { decimals: 0 })}`, cost, currency: c.currency || 'EUR' };
  });
  const sps = samplingProjects.filter(s => s.linkedProjectId === proj.id);
  const samplingItems = sps.map(sp => {
    const rounds = sampleRounds.filter(r => r.samplingProjectId === sp.id);
    if (!rounds.length) return null;
    const total = rounds.reduce((s, r) => s + (r.cost || 0), 0);
    const supplierNames = [...new Set(rounds.map(r => supplierById(suppliers, r.supplierId)?.name).filter(Boolean))];
    const cur = supplierBids.find(b => b.samplingProjectId === sp.id)?.currency || 'EUR';
    return { label: `${sp.name} (${supplierNames.join(', ')}): ${fmtCurrency(total, cur, { decimals: 0 })} (${rounds.map(r => r.version).join('+')})`, cost: total, currency: cur };
  }).filter(Boolean);
  const toolingItems = quotations.filter(q => q.projectId === proj.id)
    .flatMap(q => (q.lineItems || []).filter(li => li.locked && li.moldCost).map(li => ({
      label: `${q.supplierName} — ${li.itemName} mold: ${fmtCurrency(li.moldCost, q.currency, { decimals: 0 })}`,
      cost: li.moldCost, currency: q.currency || 'EUR',
    })));

  const allItems = [...compItems, ...samplingItems, ...toolingItems];
  if (!allItems.length) return { text: `${proj.name} has no cost data on record yet (no quotations, sampling, or components logged).`, links: [{ label: proj.name, page: 'projectDetail', id: proj.id }] };

  const { total, currency: dominant, excluded } = aiSumByDominantCurrency(allItems);
  const lines = [`**${proj.name} — Total Project Cost**`, ''];
  if (compItems.length) { lines.push('Components in production:'); compItems.forEach((l, i) => lines.push(`${i === compItems.length - 1 ? '└─' : '├─'} ${l.label}`)); lines.push(''); }
  if (samplingItems.length) { lines.push('Sampling costs:'); samplingItems.forEach((l, i) => lines.push(`${i === samplingItems.length - 1 ? '└─' : '├─'} ${l.label}`)); lines.push(''); }
  if (toolingItems.length) { lines.push('Tooling:'); toolingItems.forEach((l, i) => lines.push(`${i === toolingItems.length - 1 ? '└─' : '├─'} ${l.label}`)); lines.push(''); }
  lines.push(`Total project exposure: **${fmtCurrency(total, dominant, { decimals: 0 })}**`);
  if (excluded.length) lines.push(`(${excluded.length} item${excluded.length > 1 ? 's' : ''} in other currencies not included in this total — see amounts above)`);
  return { text: lines.join('\n'), links: [{ label: proj.name, page: 'projectDetail', id: proj.id }] };
}
function aiHandleCost(query, ctx) {
  const { quotations, suppliers, projects } = ctx;
  const wantsProjectTotal = /\btotal\b/.test(query) || /\bproject\b/.test(query);
  const visibleComponents = ctx.components.filter(c => aiCanSeeComponent(ctx, c));
  const visibleProjects = aiVisibleProjects(ctx);
  const comp = aiFindBest(visibleComponents, c => c.name, query);
  const proj = aiFindBest(visibleProjects, p => p.name, query);

  if (wantsProjectTotal && proj) return aiCostForProject(proj, ctx);
  if (comp) return aiCostForComponent(comp, ctx);
  if (proj) return aiCostForProject(proj, ctx);

  const supplier = aiFindBest(suppliers, s => s.name, query);
  if (supplier) {
    const qs = quotations.filter(q => q.supplierId === supplier.id && aiCanSeeQuotation(ctx, q));
    if (qs.length) {
      const lines = [`**${supplier.name} — Quotations**`, ''];
      qs.forEach(q => {
        const projName = projects.find(p => p.id === q.projectId)?.name || 'Unknown project';
        (q.lineItems || []).forEach(li => lines.push(`${projName} — ${li.itemName}: ${fmtCurrency(li.unitPrice, q.currency)}/unit, MOQ ${li.moq} — ${li.status || 'Pending'}`));
      });
      return { text: lines.join('\n') };
    }
  }
  return null;
}

/* ---- AI: Pattern C — Sampling Status ---- */
function aiHandleSamplingStatus(query, ctx) {
  const { samplingProjects, sampleRounds, supplierBids, suppliers, projects, users } = ctx;
  const visibleSps = samplingProjects.filter(sp => aiCanSeeSamplingProject(ctx, sp));
  const proj = aiFindBest(aiVisibleProjects(ctx), p => p.name, query);
  const tracksForProject = proj ? visibleSps.filter(sp => sp.linkedProjectId === proj.id) : [];

  if (proj && tracksForProject.length > 1) {
    const lines = [`**${proj.name} — Sampling Status**`, ''];
    tracksForProject.forEach((sp, idx) => {
      const rounds = sampleRounds.filter(r => r.samplingProjectId === sp.id).sort((a, b) => (b.dateReviewed || b.dateRequested || '').localeCompare(a.dateReviewed || a.dateRequested || ''));
      const latest = rounds[0];
      lines.push(`${sp.name}:`);
      if (latest) {
        const supplier = supplierById(suppliers, latest.supplierId);
        lines.push(`├─ Latest: ${latest.version} ${latest.status}${latest.dateReviewed ? ` (${fmtDate(latest.dateReviewed)})` : ''}`);
        lines.push(`├─ Supplier: ${supplier?.name || 'unknown'}`);
        if (latest.resultNotes) lines.push(`├─ Result: "${latest.resultNotes}"`);
        lines.push(`└─ Next: ${latest.nextAction || '—'}`);
        const trackOverdue = aiRoundTimelineNote(sp, ctx);
        if (trackOverdue) lines.push(`   ⚠️ ${trackOverdue}`);
      } else {
        lines.push('└─ No rounds requested yet');
      }
      if (idx < tracksForProject.length - 1) lines.push('');
    });
    const owner = userById(users, proj.owner);
    lines.push('', `Target approval: ${fmtDate(proj.target)}`, `Owner: ${owner?.name || 'unknown'}`);
    return { text: lines.join('\n'), links: [{ label: proj.name, page: 'projectDetail', id: proj.id }] };
  }

  const sp = aiFindBest(visibleSps, s => s.name, query) || tracksForProject[0];
  if (sp) {
    const rounds = sampleRounds.filter(r => r.samplingProjectId === sp.id).sort((a, b) => (b.dateReviewed || b.dateRequested || '').localeCompare(a.dateReviewed || a.dateRequested || ''));
    const [latest, prev] = rounds;
    const bids = supplierBids.filter(b => b.samplingProjectId === sp.id);
    const lines = [`**${sp.name}** (Sampling)`, ''];
    if (latest) {
      const supplier = supplierById(suppliers, latest.supplierId);
      const owner = userById(users, sp.ownerId);
      lines.push(`Latest round: ${latest.version} with ${supplier?.name || 'unknown'}`);
      lines.push(`Status: ${latest.status} ${aiStatusEmoji(latest.status)}`.trim());
      if (latest.dateReceived) lines.push(`Received: ${fmtDate(latest.dateReceived)}`);
      if (latest.dateReviewed) lines.push(`Reviewed: ${fmtDate(latest.dateReviewed)} by ${owner?.name || 'unknown'}`);
      if (latest.resultNotes) lines.push(`Result: "${latest.resultNotes}"`);
      const overdueNote = aiRoundTimelineNote(sp, ctx);
      if (overdueNote) lines.push('', `⚠️ ${overdueNote}`);
    } else {
      lines.push('No sample rounds requested yet.');
    }
    if (prev) {
      const prevSupplier = supplierById(suppliers, prev.supplierId);
      lines.push('', `Previous round: ${prev.version}${prevSupplier && prevSupplier.id !== latest.supplierId ? ` (${prevSupplier.name})` : ''}`, `Status: ${prev.status}`);
      if (prev.dateReviewed) lines.push(`Reviewed: ${fmtDate(prev.dateReviewed)}`);
      if (prev.resultNotes) lines.push(`Issue: "${prev.resultNotes}"`);
    }
    lines.push('', `Next step: ${latest?.nextAction || 'Awaiting first round'}`);
    if (bids.length > 1) {
      lines.push('', 'Supplier comparison:');
      bids.forEach((b, i) => {
        const supplier = supplierById(suppliers, b.supplierId);
        lines.push(`${i === bids.length - 1 ? '└─' : '├─'} ${supplier?.name || 'unknown'}: ${b.bidStatus} (${fmtCurrency(b.quotedPrice, b.currency)}, ${b.quotedMOQ} MOQ, ${b.quotedLeadTime}d lead)`);
      });
    }
    const proj2 = projects.find(p => p.id === sp.linkedProjectId);
    return { text: lines.join('\n'), links: proj2 ? [{ label: proj2.name, page: 'projectDetail', id: proj2.id }] : [] };
  }
  return null;
}

/* ---- AI: Pattern D — Blockers ---- */
function aiHandleBlockers(ctx) {
  const { users, activity } = ctx;
  const blocked = aiVisibleProjects(ctx).filter(p => p.blocking && !p.isArchived);
  if (!blocked.length) return { text: 'No active blockers right now — everything is moving.' };
  const withAge = blocked.map(p => {
    const blockActivity = activity.find(a => (a.action || '').toLowerCase().includes('blocker') && (a.detail || '').toLowerCase().includes(p.name.toLowerCase()));
    const sinceDate = blockActivity ? blockActivity.createdAt : p.startDate;
    return { p, sinceDate, stuckDays: Math.max(0, daysBetween(sinceDate, todayStr())) };
  }).sort((a, b) => b.stuckDays - a.stuckDays);

  const lines = [`**${blocked.length} active blocker${blocked.length > 1 ? 's' : ''}:**`, ''];
  withAge.forEach((b, i) => {
    const owner = userById(users, b.p.owner);
    lines.push(`${i + 1}. ${b.p.name} — ${b.p.blocking}`);
    lines.push(`   Waiting on: ${b.p.waiting || 'update'}`);
    lines.push(`   Owner: ${owner?.name || 'unknown'}`);
    lines.push(`   Stuck for: ${b.stuckDays} day${b.stuckDays !== 1 ? 's' : ''} (since ${fmtDate(b.sinceDate)})`);
    lines.push(`   Impact: ${b.p.priority} priority, target ${fmtDate(b.p.target)}`);
    if (i < withAge.length - 1) lines.push('');
  });
  return { text: lines.join('\n'), links: withAge.map(b => ({ label: b.p.name, page: 'projectDetail', id: b.p.id })) };
}

/* ---- AI: Pattern K — "My day" personal digest ---- */
function aiHandleMyAttention(ctx) {
  const me = ctx.currentUser;
  if (!me) return { text: "I can't tell who's asking — please sign in." };
  const today = todayStr();
  const myProjects = ctx.projects.filter(p => p.owner === me.id && !p.isArchived);
  const blockers = myProjects.filter(p => p.blocking);
  const myTasks = ctx.tasks.filter(t => t.assignedTo === me.id && t.status !== 'Done').sort((a, b) => (a.dueDate || '').localeCompare(b.dueDate || ''));
  const dueSoonTasks = myTasks.filter(t => t.dueDate && daysBetween(today, t.dueDate) <= 3);
  const mySampling = ctx.samplingProjects.filter(sp => sp.ownerId === me.id && !['Moved to Production', 'Cancelled'].includes(sp.status));
  const overdueTracks = mySampling.map(sp => ({ sp, note: aiRoundTimelineNote(sp, ctx) })).filter(x => x.note);

  const lines = [`**Your day, ${me.name}**`, ''];
  const links = [];
  if (blockers.length) {
    lines.push(`Blocked (${blockers.length}):`);
    blockers.forEach(p => { lines.push(`├─ ${p.name} — ${p.blocking} (waiting on ${p.waiting || 'update'})`); links.push({ label: p.name, page: 'projectDetail', id: p.id }); });
    lines.push('');
  }
  if (overdueTracks.length) {
    lines.push(`Sampling running late (${overdueTracks.length}):`);
    overdueTracks.forEach(({ sp, note }) => { lines.push(`├─ ${sp.itemName || sp.name}: ${note}`); links.push({ label: sp.name, page: 'samplingDetail', id: sp.id }); });
    lines.push('');
  }
  if (dueSoonTasks.length) {
    lines.push(`Tasks due soon (${dueSoonTasks.length}):`);
    dueSoonTasks.forEach(t => lines.push(`├─ ${t.title} — due ${fmtDate(t.dueDate)}`));
    lines.push('');
  }
  if (!blockers.length && !overdueTracks.length && !dueSoonTasks.length) {
    lines.push("Nothing urgent on your plate — everything you own is on track.");
  }
  return { text: lines.join('\n').trim(), links: links.slice(0, 6) };
}

/* ---- AI: Pattern L — Overdue across the board ---- */
function aiHandleOverdue(ctx) {
  const today = todayStr();
  const overdueProjects = aiVisibleProjects(ctx).filter(p => !p.isArchived && p.status !== 'Complete' && daysBetween(p.target, today) > 0);
  const overdueTracks = ctx.samplingProjects.filter(sp => aiCanSeeSamplingProject(ctx, sp) && aiRoundTimelineNote(sp, ctx));
  const overdueTasks = ctx.tasks.filter(t => t.status !== 'Done' && t.dueDate && daysBetween(t.dueDate, today) > 0);
  if (!overdueProjects.length && !overdueTracks.length && !overdueTasks.length) {
    return { text: 'Nothing overdue right now — everything active is within its target window.' };
  }
  const lines = ['**Overdue right now**', ''];
  const links = [];
  if (overdueProjects.length) {
    lines.push(`Projects past target (${overdueProjects.length}):`);
    overdueProjects.forEach(p => { lines.push(`├─ ${p.name} — target was ${fmtDate(p.target)}, ${daysBetween(p.target, today)}d ago`); links.push({ label: p.name, page: 'projectDetail', id: p.id }); });
    lines.push('');
  }
  if (overdueTracks.length) {
    lines.push(`Sampling running late (${overdueTracks.length}):`);
    overdueTracks.forEach(sp => { lines.push(`├─ ${sp.itemName || sp.name}: ${aiRoundTimelineNote(sp, ctx)}`); links.push({ label: sp.name, page: 'samplingDetail', id: sp.id }); });
    lines.push('');
  }
  if (overdueTasks.length) {
    lines.push(`Tasks past due (${overdueTasks.length}):`);
    overdueTasks.forEach(t => lines.push(`├─ ${t.title} — was due ${fmtDate(t.dueDate)}`));
  }
  return { text: lines.join('\n').trim(), links: links.slice(0, 6) };
}

/* ---- AI: Pattern E — Project Briefing ---- */
function aiHandleProjectBriefing(query, ctx) {
  const { quotations, samplingProjects, sampleRounds, components, users } = ctx;
  const proj = aiFindBest(aiVisibleProjects(ctx), p => p.name, query);
  if (!proj) return null;
  const stageOrder = ['Development', 'Quotation', 'Sampling', 'Production', 'Shipping', 'Complete'];
  const lines = [`**${proj.name} — Operational Briefing**`, ''];
  lines.push(`Stage: ${proj.status} (${stageOrder.indexOf(proj.status) + 1} of ${stageOrder.length})`);
  lines.push(`Progress: ${computeProgress(selectProjectSlice(ctx, proj.id))}%`, '');

  const quotes = quotations.filter(q => q.projectId === proj.id);
  if (quotes.length) {
    const approvedItems = quotes.flatMap(q => (q.lineItems || []).filter(li => li.status === 'Approved' || li.status === 'In Sampling').map(li => ({ q, li })));
    lines.push(`Quotations: ${quotes.length} received. ${approvedItems.length ? approvedItems.map(({ q, li }) => `${q.supplierName} — ${li.itemName} approved (${fmtCurrency(li.unitPrice, q.currency)}, ${li.moq} MOQ)`).join('; ') : 'No items approved yet.'}`, '');
  }
  const sps = samplingProjects.filter(s => s.linkedProjectId === proj.id);
  if (sps.length) {
    lines.push('Sampling:');
    sps.forEach((sp, i) => {
      const rounds = sampleRounds.filter(r => r.samplingProjectId === sp.id).sort((a, b) => (b.dateReviewed || b.dateRequested || '').localeCompare(a.dateReviewed || a.dateRequested || ''));
      const latest = rounds[0];
      lines.push(`${i === sps.length - 1 ? '└─' : '├─'} ${sp.name}: ${latest ? `${latest.version} ${latest.status}${latest.dateReviewed ? ` ${fmtDate(latest.dateReviewed)}` : ''}` : 'no rounds yet'}`);
      const overdueNote = aiRoundTimelineNote(sp, ctx);
      if (overdueNote) lines.push(`   ⚠️ ${overdueNote}`);
    });
    const daysToTarget = daysBetween(todayStr(), proj.target);
    lines.push(`Target approval: ${fmtDate(proj.target)} (${daysToTarget >= 0 ? `${daysToTarget} days remaining` : `${-daysToTarget} days overdue`})`, '');
  }
  const comps = components.filter(c => c.projectId === proj.id);
  if (comps.length) {
    const inProd = comps.filter(c => c.status === 'Under Production');
    lines.push(`Components: ${inProd.length} in production${inProd.length ? ` (${inProd.map(c => `${c.name}, started ${fmtDate(c.startDate)}`).join('; ')})` : ''}`, '');
  }
  if (proj.blocking) {
    const owner = userById(users, proj.owner);
    lines.push(`Blocker: ${proj.blocking} (${proj.waiting || 'update pending'})`);
    lines.push(`Owner: ${owner?.name || 'unknown'} | Target: ${fmtDate(proj.target)}`);
  } else {
    lines.push('No active blockers.');
  }
  return { text: lines.join('\n'), links: [{ label: proj.name, page: 'projectDetail', id: proj.id }] };
}

/* ---- AI: Pattern F — Team Workload ---- */
function aiHandleTeamWorkload(query, ctx) {
  const { users, projects, components, samplingProjects, tasks } = ctx;
  const mentionedUser = users.find(u => query.includes(u.name.toLowerCase()) || aiFuzzyScore(u.name, query) >= 6);
  const rows = users.map(u => {
    const activeProjects = projects.filter(p => p.owner === u.id && !p.isArchived);
    const inProdComps = components.filter(c => c.owner === u.id && c.status === 'Under Production');
    const openSampling = samplingProjects.filter(sp => sp.ownerId === u.id && sp.status !== 'Moved to Production' && sp.status !== 'Cancelled');
    const pendingTasks = tasks.filter(t => t.assignedTo === u.id && t.status !== 'Done');
    const loadScore = activeProjects.length * 3 + inProdComps.length * 2 + openSampling.length * 2 + pendingTasks.length;
    return { u, activeProjects, inProdComps, openSampling, pendingTasks, loadScore };
  });
  const avgLoad = rows.reduce((s, r) => s + r.loadScore, 0) / (rows.length || 1);
  const renderRow = r => {
    const status = r.loadScore === 0 ? '🟢 Available' : r.loadScore > avgLoad * 1.3 ? '🔴 High load' : '🟢 On track';
    return [
      `**${r.u.name}** (${r.u.role})`,
      `├─ Projects: ${r.activeProjects.length} active${r.activeProjects.length ? ` (${r.activeProjects.map(p => p.name).join(', ')})` : ''}`,
      `├─ Components: ${r.inProdComps.length} in production`,
      `├─ Sampling: ${r.openSampling.length} track${r.openSampling.length !== 1 ? 's' : ''}`,
      `├─ Tasks: ${r.pendingTasks.length} pending`,
      `└─ Status: ${status}`,
    ].join('\n');
  };
  if (mentionedUser) {
    const r = rows.find(r => r.u.id === mentionedUser.id);
    return { text: renderRow(r) };
  }
  const lines = [`**Team Workload — ${fmtDate(todayStr())}**`, ''];
  rows.forEach((r, i) => { lines.push(renderRow(r)); if (i < rows.length - 1) lines.push(''); });
  return { text: lines.join('\n') };
}

/* ---- AI: Pattern G — Email Draft ---- */
function aiHandleEmailDraft(query, ctx) {
  const { suppliers, sampleRounds, quotations, samplingProjects, projects, currentUser } = ctx;
  const supplier = aiFindBest(suppliers, s => s.name, query);
  if (!supplier) return { text: 'Tell me which supplier — e.g. "Draft a follow-up to China Glass".' };
  const rounds = sampleRounds.filter(r => r.supplierId === supplier.id).sort((a, b) => (b.dateReviewed || b.dateRequested || '').localeCompare(a.dateReviewed || a.dateRequested || ''));
  const latestRound = rounds[0];
  const openQuote = quotations.filter(q => q.supplierId === supplier.id && aiCanSeeQuotation(ctx, q)).find(q => (q.lineItems || []).some(li => (li.status || 'Pending') === 'Pending'));

  const bodyParts = [];
  let idx = 1;
  if (latestRound) {
    const sp = samplingProjects.find(s => s.id === latestRound.samplingProjectId);
    if (latestRound.status === 'Approved') {
      bodyParts.push(`${idx}. ${sp?.name || 'Sample'} ${latestRound.version} — We approved the sample on ${fmtDate(latestRound.dateReviewed)} (${latestRound.resultNotes || 'no notes'}).\n   Could you confirm the production timeline?`);
    } else if (latestRound.status === 'Needs Revision') {
      bodyParts.push(`${idx}. ${sp?.name || 'Sample'} ${latestRound.version} — Following up on the revision requested ${fmtDate(latestRound.dateReviewed)}: "${latestRound.resultNotes}".\n   Could you share an updated timeline for the next round?`);
    } else {
      bodyParts.push(`${idx}. ${sp?.name || 'Sample'} ${latestRound.version} — Checking in on status since it was requested ${fmtDate(latestRound.dateRequested)}.`);
    }
    idx++;
  }
  if (openQuote) {
    const proj = projects.find(p => p.id === openQuote.projectId);
    const itemsSummary = (openQuote.lineItems || []).filter(li => (li.status || 'Pending') === 'Pending').map(li => `${li.itemName} (${fmtCurrency(li.unitPrice, openQuote.currency)}, MOQ ${li.moq})`).join(', ');
    bodyParts.push(`${idx}. ${proj?.name || 'Quote'} — Following up on your quote${itemsSummary ? ` for ${itemsSummary}` : ''}, still pending on our side. Wanted to check the terms are still valid through ${fmtDate(openQuote.validUntil)}.`);
    idx++;
  }
  if (!bodyParts.length) bodyParts.push(`${idx}. Checking in — no open items on record, just confirming everything is on track.`);

  const subjectItem = latestRound ? samplingProjects.find(s => s.id === latestRound.samplingProjectId)?.name : null;
  const subject = latestRound ? `Follow-up — ${subjectItem || supplier.name} ${latestRound.version}` : `Follow-up — ${supplier.name}`;
  const text = [
    `Subject: ${subject}`, '',
    `Hi ${supplier.contact?.split(' ')[0] || 'there'},`, '',
    "Hope you're well. Following up on:", '',
    ...bodyParts, '',
    'Please let me know if anything is blocking on your side.', '',
    'Best,', currentUser.name, 'SAKAN Product Development',
  ].join('\n');
  return { text };
}

/* ---- AI: person activity feed ("what did X do yesterday") ---- */
function aiHandlePersonActivity(query, ctx) {
  const { users, activity } = ctx;
  const targetUser = users.find(u => query.includes(u.name.toLowerCase()) || aiFuzzyScore(u.name, query) >= 6);
  if (!targetUser) return null;
  const wantsYesterday = query.includes('yesterday');
  const wantsToday = query.includes('today');
  const userActivity = activity.filter(a => a.userId === targetUser.id);
  const scoped = wantsYesterday ? userActivity.filter(a => relativeAgoBucket(a.createdAt) === 'Yesterday')
    : wantsToday ? userActivity.filter(a => relativeAgoBucket(a.createdAt) === 'Today')
    : userActivity;
  const dayLabel = wantsYesterday ? 'Yesterday' : wantsToday ? 'Today' : 'Recent';
  const refDate = wantsYesterday ? addDays(todayStr(), -1) : todayStr();
  const lines = [`**${targetUser.name}'s Activity — ${fmtDate(refDate)}**`, ''];
  if (!scoped.length) {
    lines.push(`No activity recorded ${dayLabel.toLowerCase()}.`);
    const recent = userActivity.slice(0, 2);
    if (recent.length) {
      lines.push('', 'Most recent:');
      recent.forEach(a => lines.push(`${fmtDate(a.createdAt)} — ${a.action} ${a.detail}`));
    }
  } else {
    scoped.forEach(a => lines.push(`${fmtDate(a.createdAt)} — ${a.action} ${a.detail}`));
  }
  return { text: lines.join('\n') };
}

/* ---- AI: Pattern H — Supplier Comparison ---- */
function aiHandleCompare(query, ctx) {
  const { suppliers, sampleRounds } = ctx;
  const mentioned = suppliers.filter(s => query.includes(s.name.toLowerCase()) || aiFuzzyScore(s.name, query) >= 6);
  if (mentioned.length < 2) return null;
  const [a, b] = mentioned;
  const avgLeadTime = (supplierId) => {
    const rounds = sampleRounds.filter(r => r.supplierId === supplierId && r.status === 'Approved' && r.leadTimeDays != null);
    if (!rounds.length) return null;
    return { avg: rounds.reduce((s, r) => s + r.leadTimeDays, 0) / rounds.length, count: rounds.length };
  };
  const statsA = avgLeadTime(a.id);
  const statsB = avgLeadTime(b.id);
  const lines = [`**${a.name} vs ${b.name} — Sample Lead Time**`, ''];
  lines.push(`${a.name}: ${statsA ? `${statsA.avg.toFixed(1)} days avg (${statsA.count} approved round${statsA.count !== 1 ? 's' : ''})` : 'no approved sample rounds on record'}`);
  lines.push(`${b.name}: ${statsB ? `${statsB.avg.toFixed(1)} days avg (${statsB.count} approved round${statsB.count !== 1 ? 's' : ''})` : 'no approved sample rounds on record'}`);
  lines.push('');
  if (statsA && statsB) {
    if (statsA.avg < statsB.avg) lines.push(`**${a.name} is faster** by ${(statsB.avg - statsA.avg).toFixed(1)} days on average.`);
    else if (statsB.avg < statsA.avg) lines.push(`**${b.name} is faster** by ${(statsA.avg - statsB.avg).toFixed(1)} days on average.`);
    else lines.push('Both suppliers average the same lead time.');
  } else {
    lines.push('Not enough approved-sample data on record to compare reliably.');
  }
  return { text: lines.join('\n') };
}

/* ---- AI: Pattern I — Completion Prediction ---- */
function aiPredictTrackCompletion(sp, ctx) {
  const { components, sampleRounds, supplierBids } = ctx;
  const component = components.find(c => c.samplingProjectId === sp.id);
  const rounds = sampleRounds.filter(r => r.samplingProjectId === sp.id)
    .sort((a, b) => (b.dateReviewed || b.dateRequested || '').localeCompare(a.dateReviewed || a.dateRequested || ''));
  const getProdLeadTime = (supplierId) => {
    const bid = supplierBids.find(b => b.samplingProjectId === sp.id && b.supplierId === supplierId);
    return bid?.quotedLeadTime || null;
  };

  if (component) {
    const leadTime = getProdLeadTime(component.supplier);
    if (leadTime) {
      const predicted = addDays(component.startDate, leadTime);
      return { sp, predicted, detail: `${sp.name}: in production since ${fmtDate(component.startDate)}, supplier lead time ${leadTime}d → expected ${fmtDate(predicted)}` };
    }
    return { sp, predicted: null, detail: `${sp.name}: in production since ${fmtDate(component.startDate)}, but no supplier lead time on record.` };
  }
  const approved = rounds.find(r => r.status === 'Approved');
  if (approved) {
    const leadTime = getProdLeadTime(approved.supplierId);
    const baseDate = approved.dateReviewed || approved.dateReceived || todayStr();
    if (leadTime) {
      const predicted = addDays(baseDate, leadTime);
      return { sp, predicted, detail: `${sp.name}: sample approved ${fmtDate(baseDate)}, supplier lead time ${leadTime}d → expected ${fmtDate(predicted)} once production starts` };
    }
    return { sp, predicted: null, detail: `${sp.name}: sample approved ${fmtDate(baseDate)}, but no supplier lead time on record.` };
  }
  const latest = rounds[0];
  return {
    sp, predicted: null,
    detail: latest
      ? `${sp.name}: still sampling — ${latest.version} ${latest.status}${latest.dateReviewed ? ` (${fmtDate(latest.dateReviewed)})` : ''}, not yet approved`
      : `${sp.name}: no sample rounds requested yet`,
  };
}
function aiHandlePredict(query, ctx) {
  const proj = aiFindBest(aiVisibleProjects(ctx), p => p.name, query);
  if (!proj) return null;
  const sps = ctx.samplingProjects.filter(sp => sp.linkedProjectId === proj.id);
  const lines = [`**${proj.name} — Completion Prediction**`, ''];

  if (!sps.length) {
    lines.push(`No sampling tracks on record yet — target ${fmtDate(proj.target)} can't be assessed.`);
    return { text: lines.join('\n'), links: [{ label: proj.name, page: 'projectDetail', id: proj.id }] };
  }

  const tracks = sps.map(sp => aiPredictTrackCompletion(sp, ctx));
  tracks.forEach(t => lines.push(t.detail));
  lines.push('');

  const known = tracks.filter(t => t.predicted);
  if (!known.length) {
    lines.push('Insufficient data to predict a completion date — no track has an approved sample with a known supplier lead time yet.');
    lines.push(`Target: ${fmtDate(proj.target)}`);
    lines.push('Risk level: 🟡 At Risk (sampling not yet cleared)');
    return { text: lines.join('\n'), links: [{ label: proj.name, page: 'projectDetail', id: proj.id }] };
  }

  const bottleneck = known.reduce((a, b) => (a.predicted > b.predicted ? a : b));
  const variance = daysBetween(proj.target, bottleneck.predicted);
  const risk = variance <= 0 ? 'On Track ✅' : variance <= 14 ? 'At Risk 🟡' : 'Will Miss 🔴';

  lines.push(`Predicted completion: **${fmtDate(bottleneck.predicted)}** (bottleneck: ${bottleneck.sp.name})`);
  lines.push(`Target: ${fmtDate(proj.target)}`);
  lines.push(`Variance: ${variance === 0 ? 'on target' : variance > 0 ? `${variance} days late` : `${-variance} days ahead`}`);
  lines.push(`Risk level: ${risk}`);
  const unknownCount = tracks.length - known.length;
  if (unknownCount > 0) lines.push('', `Note: ${unknownCount} track${unknownCount > 1 ? 's' : ''} still lack${unknownCount === 1 ? 's' : ''} an approved sample, so actual completion could be later than shown.`);

  return { text: lines.join('\n'), links: [{ label: proj.name, page: 'projectDetail', id: proj.id }] };
}

/* ---- AI: Pattern J — Cross-Stage Overview (broad "what are we working on" / "list everything" queries) ---- */
function aiListQuotationItems(ctx) {
  const rows = [];
  ctx.quotations.filter(q => aiCanSeeQuotation(ctx, q)).forEach(q => {
    const proj = ctx.projects.find(p => p.id === q.projectId);
    if (proj?.isArchived) return;
    (q.lineItems || []).forEach(li => {
      const fresh = aiFreshness(ctx, li.decidedAt || q.createdAt, [li.itemName, q.supplierName]);
      rows.push({
        item: li.itemName, project: proj?.name || 'Unknown project', status: li.status || 'Pending',
        owner: userById(ctx.users, proj?.owner)?.name || 'unknown',
        updatedLabel: fresh.label, sortTs: fresh.ts,
        link: proj ? { label: proj.name, page: 'quotationsByProject', id: proj.id } : null,
      });
    });
  });
  return rows.sort((a, b) => (b.sortTs || '').localeCompare(a.sortTs || ''));
}
function aiListSamplingTracks(ctx) {
  const rows = [];
  ctx.samplingProjects.filter(sp => aiCanSeeSamplingProject(ctx, sp)).forEach(sp => {
    const proj = ctx.projects.find(p => p.id === sp.linkedProjectId);
    if (proj?.isArchived) return;
    const rounds = ctx.sampleRounds.filter(r => r.samplingProjectId === sp.id).sort((a, b) => (b.dateRequested || '').localeCompare(a.dateRequested || ''));
    const latest = rounds[0];
    const fresh = aiFreshness(ctx, latest?.dateReviewed || latest?.dateRequested, [sp.itemName || sp.name, proj?.name]);
    rows.push({
      item: sp.itemName || sp.name, project: proj?.name || 'Unknown project', status: sp.status,
      owner: userById(ctx.users, sp.ownerId)?.name || 'unknown',
      updatedLabel: fresh.label, sortTs: fresh.ts, overdueNote: aiRoundTimelineNote(sp, ctx),
      link: { label: sp.name, page: 'samplingDetail', id: sp.id },
    });
  });
  return rows.sort((a, b) => (b.sortTs || '').localeCompare(a.sortTs || ''));
}
function aiListComponents(ctx) {
  const rows = [];
  ctx.components.filter(c => aiCanSeeComponent(ctx, c)).forEach(c => {
    const proj = ctx.projects.find(p => p.id === c.projectId);
    if (proj?.isArchived) return;
    const fresh = aiFreshness(ctx, c.nextActionUpdatedAt || c.startDate, [c.name, proj?.name]);
    rows.push({
      item: c.name, project: c.projectName || proj?.name || 'Unknown project', status: c.status,
      owner: userById(ctx.users, c.owner)?.name || 'unknown',
      updatedLabel: fresh.label, sortTs: fresh.ts,
      link: { label: 'Components', page: 'components' },
    });
  });
  return rows.sort((a, b) => (b.sortTs || '').localeCompare(a.sortTs || ''));
}
function aiRowLine(row) {
  const base = `**${row.item}** — ${row.project} · ${row.status} · owner ${row.owner} · updated ${row.updatedLabel}`;
  return row.overdueNote ? `${base}\n   ⚠️ ${row.overdueNote}` : base;
}
function aiHandleOverview(query, ctx, domain) {
  const sections = [];
  if (domain === 'quotations' || domain === 'all') {
    const rows = aiListQuotationItems(ctx);
    if (rows.length) sections.push({ title: 'Quotations', rows });
  }
  if (domain === 'sampling' || domain === 'all') {
    const rows = aiListSamplingTracks(ctx);
    if (rows.length) sections.push({ title: 'Sampling', rows });
  }
  if (domain === 'components' || domain === 'all') {
    const rows = aiListComponents(ctx);
    if (rows.length) sections.push({ title: 'Components', rows });
  }
  if (!sections.length) return { text: 'Nothing on record for that yet.' };

  const lines = [];
  const links = [];
  sections.forEach((sec, i) => {
    lines.push(`**${sec.title}** (${sec.rows.length})`);
    sec.rows.slice(0, 12).forEach(r => { lines.push(aiRowLine(r)); if (r.link) links.push(r.link); });
    if (sec.rows.length > 12) lines.push(`…and ${sec.rows.length - 12} more — open the ${sec.title} page for the full list.`);
    if (i < sections.length - 1) lines.push('');
  });
  return { text: lines.join('\n'), links: links.slice(0, 6) };
}

/* ---- AI: dispatcher ---- */
const AI_FALLBACK_TEXT = "I can help with: last updates, costs, sampling status, blockers, your day, what's overdue, team workload, supplier comparisons, completion predictions, drafting supplier emails, or a full list of what's happening in quotations, sampling, or components. What would you like to know?";
const AI_BROAD_RE = /\b(all|everything|overview|summar\w*|list|working on|what'?s (up|going on|happening)|whats (up|going on|happening))\b/;
const AI_GREETING_RE = /^(hi|hello|hey|yo|good morning|good afternoon|good evening|morning|afternoon|evening)[\s!.,]*$/;
const AI_THANKS_RE = /^(thanks|thank you|thx|ty|cheers|appreciate it)[\s!.,]*$/;
const AI_BYE_RE = /^(bye|goodbye|see you|see ya|later|cya)[\s!.,]*$/;
const AI_HOWAREYOU_RE = /how are you|how'?s it going|hows it going/;
const AI_HELP_RE = /^(help|what can you do|what do you do|who are you|what are your commands)[\s!.?]*$/;

function aiRespondDispatch(query, ctx) {
  const q = (query || '').toLowerCase().trim();
  if (!q) return { text: AI_FALLBACK_TEXT };
  if (AI_HELP_RE.test(q)) return { text: AI_FALLBACK_TEXT };
  if (AI_GREETING_RE.test(q)) {
    return { text: `Hey${ctx.currentUser?.name ? `, ${ctx.currentUser.name}` : ''} — what can I pull up for you? A status check, a cost, or a list of what's active right now?` };
  }
  if (AI_BYE_RE.test(q)) return { text: 'Talk soon.' };
  if (AI_HOWAREYOU_RE.test(q)) return { text: 'Running fine and keeping an eye on the pipeline. What do you need?' };
  if (AI_THANKS_RE.test(q)) return { text: 'Anytime — let me know if you need anything else.' };

  const mentionedUser = ctx.users.find(u => q.includes(u.name.toLowerCase()) || aiFuzzyScore(u.name, q) >= 6);
  const mentionedSuppliers = ctx.suppliers.filter(s => q.includes(s.name.toLowerCase()) || aiFuzzyScore(s.name, q) >= 6);
  const wantsList = AI_BROAD_RE.test(q);
  const hasSpecificMatch = !mentionedUser && !!aiBestEntityMatch(ctx, q);

  if (mentionedUser && /\bdid\b/.test(q)) {
    const r = aiHandlePersonActivity(q, ctx);
    if (r) return r;
  }
  if (mentionedSuppliers.length >= 2 && (q.includes('faster') || q.includes('slower') || q.includes('quicker') || q.includes(' vs ') || q.includes('compare'))) {
    const r = aiHandleCompare(q, ctx);
    if (r) return r;
  }
  if (q.includes('predict') || q.includes('hit target') || q.includes('hit the target') || q.includes('on track') || q.includes('will miss') || q.includes('eta') || (q.includes('will ') && q.includes('target'))) {
    return aiFinalize(aiHandlePredict(q, ctx), q, ctx);
  }
  if (q.includes('draft') || q.includes('follow up') || q.includes('follow-up') || (q.includes('email') && !q.includes('address'))) {
    return aiHandleEmailDraft(q, ctx);
  }
  if (q.includes('cost') || q.includes('price') || q.includes('how much')) {
    return aiFinalize(aiHandleCost(q, ctx), q, ctx);
  }
  if (/\bmy (attention|day|plate)\b|what should i (do|focus on)|what do i need to do|am i behind/.test(q)) {
    return aiHandleMyAttention(ctx);
  }
  if (/\boverdue\b|\bpast due\b|\bbehind schedule\b|what'?s late\b/.test(q)) {
    return aiHandleOverdue(ctx);
  }
  if (q.includes('blocking') || q.includes('blocker') || q.includes('stuck') || q.includes('worry')) {
    return aiHandleBlockers(ctx);
  }
  if (q.includes('team') || q.includes('workload') || q.includes('who is working') || (mentionedUser && (q.includes('doing') || q.includes('how is')))) {
    return aiHandleTeamWorkload(q, ctx);
  }

  // Cross-stage overview: broad "what are we working on" style queries, or a stage
  // keyword (quotations/sampling/components) with no specific item named — list instead
  // of failing with "I don't see any record of that."
  const domainHint = (q.includes('quotation') || q.includes('quote')) ? 'quotations'
    : (q.includes('sampling') || q.includes('sample')) ? 'sampling'
    : q.includes('component') ? 'components'
    : null;
  if (wantsList && !mentionedUser && !domainHint) {
    const r = aiHandleOverview(q, ctx, 'all');
    if (r) return r;
  }
  if (domainHint === 'quotations' && !hasSpecificMatch) {
    return aiHandleOverview(q, ctx, 'quotations');
  }
  if (q.includes('sampling') || (q.includes('sample') && (q.includes('status') || q.includes('latest') || q.includes('send')))) {
    const specific = aiHandleSamplingStatus(q, ctx);
    if (specific) return specific;
    if (wantsList || !hasSpecificMatch) return aiHandleOverview(q, ctx, 'sampling');
    return aiFinalize(null, q, ctx);
  }
  if (domainHint === 'components' && !hasSpecificMatch) {
    return aiHandleOverview(q, ctx, 'components');
  }
  if (q.includes('last update') || q.includes('latest') || q.includes('what happened') || q.includes('status of')) {
    return aiFinalize(aiHandleLastUpdate(q, ctx), q, ctx);
  }
  if (q.includes('show me') || q.includes('tell me about') || q.startsWith('show ')) {
    return aiFinalize(aiHandleProjectBriefing(q, ctx), q, ctx);
  }
  if (q.includes('accomplish') || q.includes('complete')) {
    const done = ctx.projects.filter(p => p.isArchived);
    if (!done.length) return { text: 'Nothing in Accomplished yet — first project completion is coming up.' };
    const text = `${done.length} project${done.length > 1 ? 's' : ''} in Accomplished: ` + done.map(p => {
      const dur = p.completedDate ? daysBetween(p.startDate, p.completedDate) : null;
      return `${p.name} (completed ${fmtDateShort(p.completedDate)}${dur ? `, took ${dur} days` : ''})`;
    }).join(', ') + '.';
    return { text };
  }
  if (q.includes('summar')) {
    return aiHandleOverview(q, ctx, 'all');
  }

  const fallback = aiHandleProjectBriefing(q, ctx) || aiHandleLastUpdate(q, ctx);
  return aiFinalize(fallback, q, ctx);
}

// Adds short-term conversational memory: if the raw query comes up completely empty,
// retry once with the last-discussed entity's name appended, so a natural follow-up
// ("what about it", "and the cap instead", "same for that one") resolves against
// whatever was just being talked about instead of dead-ending.
function aiRespond(query, ctx, contextName) {
  const first = aiRespondDispatch(query, ctx);
  const isDeadEnd = first && (first.text === AI_NOT_FOUND_TEXT || first.text === AI_NO_ACCESS_TEXT);
  if (isDeadEnd && contextName) {
    const retried = aiRespondDispatch(`${query} ${contextName}`, ctx);
    if (retried && retried.text !== first.text) return retried;
  }
  return first || { text: AI_FALLBACK_TEXT };
}

/* ---- AI: Claude-backed assistant (tool use) ----
   The same aiHandle* functions above still produce every answer — what
   changed is who picks which one(s) to call. Instead of aiRespondDispatch's
   regex guesses at the single best-matching pattern, Claude reads the
   question and chooses from CLAUDE_TOOLS itself, chaining multiple calls
   when a question spans more than one (e.g. "compare cost and sampling
   status of X and Y", which no single old pattern covered).

   Tool execution stays entirely client-side against `ctx` — the app state
   already scoped by Supabase RLS to what this signed-in user can see — so
   none of the existing visibility rules (aiCanSeeComponent, aiVisibleProjects,
   etc.) had to be re-implemented in SQL. The only server round-trip is
   call_claude_assistant(), a dumb authenticated relay to the real Claude API
   (see supabase/migrations/20260810230000_call_claude_assistant.sql) that
   holds the API key in Vault and never exposes it to the browser. */

const CLAUDE_TOOLS = [
  {
    name: 'get_last_update',
    description: 'Get the most recent status update, action, and owner for a specific project, sampling track, or component by name. Use for "what\'s the latest on X" / "status of X" questions.',
    input_schema: { type: 'object', properties: { entity: { type: 'string', description: 'Name (or partial name) of the project, sampling track, or component.' } }, required: ['entity'] },
  },
  {
    name: 'get_cost',
    description: 'Get a cost breakdown (unit price, production cost, tooling, sampling cost, total) for a project, a component, or a supplier\'s quotations. Use for "how much", "cost of", "price of" questions.',
    input_schema: { type: 'object', properties: { subject: { type: 'string', description: 'Name of the project, component, or supplier. Include the word "total" if the user wants a whole-project total rather than one component.' } }, required: ['subject'] },
  },
  {
    name: 'get_sampling_status',
    description: 'Get sample round status and history for a sampling track or project.',
    input_schema: { type: 'object', properties: { subject: { type: 'string', description: 'Name of the project or sampling track.' } }, required: ['subject'] },
  },
  {
    name: 'get_blockers',
    description: 'List every active blocker across all visible projects, with who owns each and how long it has been stuck.',
    input_schema: { type: 'object', properties: {}, additionalProperties: false },
  },
  {
    name: 'get_my_day',
    description: 'Get the signed-in user\'s personal daily digest: their blocked projects, late sampling, and tasks due soon. Use for "what\'s my day look like" / "what should I focus on".',
    input_schema: { type: 'object', properties: {}, additionalProperties: false },
  },
  {
    name: 'get_overdue',
    description: 'List everything overdue right now across projects, sampling, and tasks.',
    input_schema: { type: 'object', properties: {}, additionalProperties: false },
  },
  {
    name: 'get_project_briefing',
    description: 'Get a full operational briefing on one project: stage, quotations, sampling, components in production, and blockers. Use for "tell me about X" / broad project questions.',
    input_schema: { type: 'object', properties: { project: { type: 'string', description: 'Project name.' } }, required: ['project'] },
  },
  {
    name: 'get_team_workload',
    description: 'Get workload (active projects, components in production, open sampling, pending tasks) for one named team member, or the whole team if no name is given.',
    input_schema: { type: 'object', properties: { person: { type: 'string', description: 'Team member name. Omit for the whole team.' } } },
  },
  {
    name: 'draft_supplier_email',
    description: 'Draft a follow-up email to a named supplier based on their latest sample round and any open quotation.',
    input_schema: { type: 'object', properties: { supplier: { type: 'string', description: 'Supplier name.' } }, required: ['supplier'] },
  },
  {
    name: 'get_person_activity',
    description: 'Get what a named team member has done recently, from the activity log.',
    input_schema: {
      type: 'object',
      properties: {
        person: { type: 'string', description: 'Team member name.' },
        when: { type: 'string', enum: ['today', 'yesterday', 'recent'], description: 'Time window. Defaults to recent.' },
      },
      required: ['person'],
    },
  },
  {
    name: 'compare_suppliers',
    description: 'Compare two named suppliers on average approved-sample lead time.',
    input_schema: { type: 'object', properties: { supplier_a: { type: 'string' }, supplier_b: { type: 'string' } }, required: ['supplier_a', 'supplier_b'] },
  },
  {
    name: 'predict_completion',
    description: 'Predict whether a project will hit its target date, based on sampling progress and supplier lead times.',
    input_schema: { type: 'object', properties: { project: { type: 'string' } }, required: ['project'] },
  },
  {
    name: 'list_overview',
    description: 'List everything currently active in one or all of: quotations, sampling, components. Use for broad "what\'s going on" questions, or when the user names a whole category rather than one specific item.',
    input_schema: { type: 'object', properties: { domain: { type: 'string', enum: ['all', 'quotations', 'sampling', 'components'], description: 'Which category to list. Defaults to all.' } } },
  },
  {
    name: 'list_accomplished',
    description: 'List completed (archived) projects with how long each took.',
    input_schema: { type: 'object', properties: {}, additionalProperties: false },
  },
];

function aiExecuteClaudeTool(name, input, ctx) {
  const inp = input || {};
  const lc = (v) => (v || '').toLowerCase();
  switch (name) {
    case 'get_last_update':
      return aiFinalize(aiHandleLastUpdate(lc(inp.entity), ctx), lc(inp.entity), ctx);
    case 'get_cost':
      return aiFinalize(aiHandleCost(lc(inp.subject), ctx), lc(inp.subject), ctx);
    case 'get_sampling_status':
      return aiHandleSamplingStatus(lc(inp.subject), ctx) || { text: AI_NOT_FOUND_TEXT };
    case 'get_blockers':
      return aiHandleBlockers(ctx);
    case 'get_my_day':
      return aiHandleMyAttention(ctx);
    case 'get_overdue':
      return aiHandleOverdue(ctx);
    case 'get_project_briefing':
      return aiFinalize(aiHandleProjectBriefing(lc(inp.project), ctx), lc(inp.project), ctx);
    case 'get_team_workload':
      return aiHandleTeamWorkload(lc(inp.person), ctx);
    case 'draft_supplier_email':
      return aiHandleEmailDraft(`draft ${lc(inp.supplier)}`, ctx);
    case 'get_person_activity': {
      const q = `${lc(inp.person)} ${inp.when === 'today' ? 'today' : inp.when === 'yesterday' ? 'yesterday' : ''}`.trim();
      return aiHandlePersonActivity(q, ctx) || { text: AI_NOT_FOUND_TEXT };
    }
    case 'compare_suppliers':
      return aiHandleCompare(`${lc(inp.supplier_a)} ${lc(inp.supplier_b)}`, ctx) || { text: AI_NOT_FOUND_TEXT };
    case 'predict_completion':
      return aiFinalize(aiHandlePredict(lc(inp.project), ctx), lc(inp.project), ctx);
    case 'list_overview':
      return aiHandleOverview('', ctx, inp.domain || 'all');
    case 'list_accomplished': {
      const done = ctx.projects.filter(p => p.isArchived);
      if (!done.length) return { text: 'Nothing in Accomplished yet — first project completion is coming up.' };
      const text = `${done.length} project${done.length > 1 ? 's' : ''} in Accomplished: ` + done.map(p => {
        const dur = p.completedDate ? daysBetween(p.startDate, p.completedDate) : null;
        return `${p.name} (completed ${fmtDateShort(p.completedDate)}${dur ? `, took ${dur} days` : ''})`;
      }).join(', ') + '.';
      return { text };
    }
    default:
      return { text: `Unknown tool: ${name}` };
  }
}

function aiClaudeSystemPrompt(ctx, contextName) {
  const who = ctx.currentUser?.name ? `The signed-in user is ${ctx.currentUser.name}${ctx.currentUser.role ? ` (${ctx.currentUser.role})` : ''}.` : '';
  const where = contextName ? ` They were last discussing "${contextName}".` : '';
  return [
    'You are the SAKAN Assistant, an internal operations assistant for a product development / sourcing team.',
    ' Answer using ONLY the tools provided — never invent projects, suppliers, dates, costs, or statuses.',
    ' If a tool comes back empty or says something is not found, say so plainly rather than guessing.',
    ` ${who}${where}`,
    ' Call as many tools as the question needs before answering — e.g. a comparison needs two lookups, a "what happened with X" needs both a status and a briefing if one alone is thin.',
    ' Keep the final answer tight and factual, markdown **bold** for key figures, short lines, no filler like "I\'d be happy to help" or restating the question.',
  ].join('');
}

// Runs the actual tool-use loop against the Claude API via the
// call_claude_assistant() Postgres relay (Vault-held key, never sent to the
// browser). Tool calls are executed locally against `ctx` in aiExecuteClaudeTool
// and fed back until Claude stops asking for more and gives a final answer.
async function aiRespondWithClaude(query, ctx, contextName, priorTurns) {
  const messages = [
    ...(priorTurns || []).map(t => ({ role: t.role === 'ai' ? 'assistant' : 'user', content: t.text })),
    { role: 'user', content: query },
  ];
  const links = [];
  for (let i = 0; i < 5; i++) {
    const { data, error } = await supabase.rpc('call_claude_assistant', {
      p_system: aiClaudeSystemPrompt(ctx, contextName),
      p_messages: messages,
      p_tools: CLAUDE_TOOLS,
    });
    if (error) throw error;
    const content = data?.content || [];
    messages.push({ role: 'assistant', content });
    const toolUses = content.filter(b => b.type === 'tool_use');
    if (!toolUses.length) {
      const text = content.filter(b => b.type === 'text').map(b => b.text).join('\n').trim();
      return { text: text || AI_FALLBACK_TEXT, links: links.slice(0, 6) };
    }
    const toolResults = toolUses.map(tu => {
      const result = aiExecuteClaudeTool(tu.name, tu.input, ctx);
      if (result?.links) links.push(...result.links);
      return { type: 'tool_result', tool_use_id: tu.id, content: result?.text || 'No data.' };
    });
    messages.push({ role: 'user', content: toolResults });
  }
  return { text: 'That took more back-and-forth than expected — try asking more specifically.', links: links.slice(0, 6) };
}

// Trivial small-talk shortcut, checked before ever calling the Claude API — no
// reason to spend a request (or the latency) on "hi" or "thanks".
function aiQuickReply(query, ctx) {
  const q = (query || '').toLowerCase().trim();
  if (!q) return { text: AI_FALLBACK_TEXT };
  if (AI_HELP_RE.test(q)) return { text: AI_FALLBACK_TEXT };
  if (AI_GREETING_RE.test(q)) return { text: `Hey${ctx.currentUser?.name ? `, ${ctx.currentUser.name}` : ''} — what can I pull up for you? A status check, a cost, or a list of what's active right now?` };
  if (AI_BYE_RE.test(q)) return { text: 'Talk soon.' };
  if (AI_HOWAREYOU_RE.test(q)) return { text: 'Running fine and keeping an eye on the pipeline. What do you need?' };
  if (AI_THANKS_RE.test(q)) return { text: 'Anytime — let me know if you need anything else.' };
  return null;
}

/* ---- AI: message rendering ---- */
function AiMessageText({ text }) {
  return (
    <div className="whitespace-pre-wrap font-mono text-[11px] leading-relaxed">
      {text.split('\n').map((line, i) => (
        <div key={i}>
          {line === '' ? ' ' : line.split(/(\*\*[^*]+\*\*)/g).filter(Boolean).map((seg, j) => (
            seg.startsWith('**') && seg.endsWith('**')
              ? <span key={j} className="text-gold font-semibold">{seg.slice(2, -2)}</span>
              : <span key={j}>{seg}</span>
          ))}
        </div>
      ))}
    </div>
  );
}

function AiPanel() {
  const app = useApp();
  const [messages, setMessages] = useState([{ role: 'ai', text: "Hi, I'm your SAKAN operations assistant. Ask me for a last update, a cost, sampling status, your day, what's overdue, blockers, team workload, a supplier follow-up email, or a full list of what's happening in quotations, sampling, or components right now." }]);
  const [input, setInput] = useState('');
  const [typing, setTyping] = useState(false);
  const [lastEntityName, setLastEntityName] = useState(null);
  const scrollRef = useRef(null);

  useEffect(() => { scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' }); }, [messages, typing]);

  const send = (text) => {
    const t = (text ?? input).trim();
    if (!t) return;
    const priorTurns = messages.slice(1).slice(-6); // skip the static greeting, cap history sent to Claude
    setMessages(m => [...m, { role: 'user', text: t }]);
    setInput('');
    setTyping(true);
    (async () => {
      let normalized;
      try {
        normalized = aiQuickReply(t, app) || await aiRespondWithClaude(t, app, lastEntityName, priorTurns);
      } catch {
        // Claude not configured yet (Vault secret missing) or the API call failed —
        // fall back to the original local dispatcher so the assistant still answers.
        const reply = aiRespond(t, app, lastEntityName);
        normalized = typeof reply === 'string' ? { text: reply } : reply;
      }
      if (normalized.links?.[0]?.label) setLastEntityName(normalized.links[0].label);
      setMessages(m => [...m, { role: 'ai', text: normalized.text, links: normalized.links }]);
      setTyping(false);
    })();
  };

  const chips = ['What\'s my day look like?', 'What\'s overdue?', 'What are we working on?', 'Will EDP Bottles hit target?'];

  if (!app.aiOpen) {
    return (
      <button
        onClick={() => app.setAiOpen(true)}
        className="fixed bottom-20 md:bottom-6 right-4 md:right-6 z-40 w-14 h-14 rounded-full bg-gold hover:bg-goldbright text-white shadow-2xl flex items-center justify-center transition-all hover:scale-105"
      >
        <Sparkles className="w-6 h-6" />
      </button>
    );
  }

  return (
    <div className="fixed bottom-4 md:bottom-6 right-4 md:right-6 z-40 w-[calc(100vw-2rem)] max-w-sm h-[70vh] max-h-[560px] bg-surface border border-border rounded-2xl shadow-2xl flex flex-col animate-dialogIn overflow-hidden">
      <div className="flex items-center justify-between px-4 py-3 border-b border-border bg-elevated">
        <div className="flex items-center gap-2">
          <div className="w-7 h-7 rounded-full bg-gold/20 flex items-center justify-center"><Sparkles className="w-4 h-4 text-gold" /></div>
          <span className="font-display text-primary">SAKAN Assistant</span>
        </div>
        <button onClick={() => app.setAiOpen(false)} className="text-muted hover:text-primary"><X className="w-4 h-4" /></button>
      </div>
      <div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-3 space-y-3">
        {messages.map((m, i) => (
          <div key={i} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
            <div className={`max-w-[90%] rounded-xl px-3 py-2 text-sm ${m.role === 'user' ? 'bg-gold text-white' : 'bg-elevated text-primary border border-border'}`}>
              {m.role === 'ai' ? <AiMessageText text={m.text} /> : m.text}
              {m.role === 'ai' && m.links?.length > 0 && (
                <div className="mt-2 pt-2 border-t border-border/60 flex flex-wrap gap-1.5">
                  {m.links.map((l, j) => (
                    <button key={j} onClick={() => app.navigate(l.page, l.id)} className="text-[10px] font-mono px-2 py-1 rounded-full border border-gold/30 text-gold hover:bg-gold/10 transition-colors">
                      → {l.label}
                    </button>
                  ))}
                </div>
              )}
            </div>
          </div>
        ))}
        {typing && (
          <div className="flex justify-start">
            <div className="bg-elevated border border-border rounded-xl px-3 py-2.5 flex gap-1">
              {[0, 1, 2].map(i => <span key={i} className="w-1.5 h-1.5 rounded-full bg-muted animate-pulseDot" style={{ animationDelay: `${i * 0.2}s` }} />)}
            </div>
          </div>
        )}
      </div>
      <div className="px-4 pb-2 flex gap-1.5 flex-wrap">
        {chips.map(c => (
          <button key={c} onClick={() => send(c)} className="text-xs px-2.5 py-1 rounded-full border border-border text-muted hover:text-gold hover:border-gold/40 transition-colors">{c}</button>
        ))}
      </div>
      <div className="p-3 border-t border-border flex gap-2">
        <Input value={input} onChange={e => setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && send()} placeholder="Ask anything..." />
        <Button size="sm" onClick={() => send()} icon={Send} />
      </div>
    </div>
  );
}

/* ======================================================================
   7. SEARCH MODAL
   ====================================================================== */

function SearchModal() {
  const app = useApp();
  const [q, setQ] = useState('');
  const inputRef = useRef(null);
  useEffect(() => { if (app.searchOpen) setTimeout(() => inputRef.current?.focus(), 50); }, [app.searchOpen]);
  if (!app.searchOpen) return null;

  const query = q.toLowerCase();
  const results = query.length === 0 ? [] : [
    ...app.projects.filter(p => p.name.toLowerCase().includes(query)).map(p => ({ type: 'Project', label: p.name, sub: p.status, go: () => app.navigate('projectDetail', p.id) })),
    ...app.suppliers.filter(s => s.name.toLowerCase().includes(query) || (s.serialNo != null && formatSupplierSerial(s.serialNo).toLowerCase().includes(query))).map(s => ({ type: 'Supplier', label: s.name, sub: s.serialNo != null ? `${s.country} · ${formatSupplierSerial(s.serialNo)}` : s.country, go: () => app.navigate('supplierDetail', s.id) })),
    ...app.samplingProjects.filter(s => s.name.toLowerCase().includes(query)).map(s => ({ type: 'Sampling', label: s.name, sub: s.status, go: () => app.navigate('samplingDetail', s.id) })),
    ...app.components.filter(c => c.name.toLowerCase().includes(query)).map(c => ({ type: 'Component', label: c.name, sub: c.projectName, go: () => app.navigate('components') })),
    ...app.quotations.filter(qt => qt.supplierName.toLowerCase().includes(query) || (qt.referenceNo || '').toLowerCase().includes(query)).map(qt => ({ type: 'Quotation', label: qt.referenceNo ? `${qt.supplierName} — Ref: ${qt.referenceNo}` : qt.supplierName, sub: qt.lineItems?.length ? `${qt.lineItems.length} item${qt.lineItems.length > 1 ? 's' : ''} · ${fmtCurrency(qt.lineItems[0].unitPrice, qt.currency)}` : '', go: () => app.navigate('quotationsByProject', qt.projectId) })),
  ].slice(0, 12);

  return createPortal(
    <div className="fixed inset-0 z-[150] flex items-start justify-center pt-24 px-4" onKeyDown={e => e.key === 'Escape' && app.setSearchOpen(false)}>
      <div className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={() => app.setSearchOpen(false)} />
      <div className="relative w-full max-w-xl bg-surface border border-border rounded-xl shadow-2xl animate-dialogIn overflow-hidden">
        <div className="flex items-center gap-3 px-4 py-3 border-b border-border">
          <Search className="w-4 h-4 text-muted" />
          <input ref={inputRef} value={q} onChange={e => setQ(e.target.value)} placeholder="Search projects, suppliers, components..." className="flex-1 bg-transparent text-sm text-primary placeholder:text-stale focus:outline-none" />
          <kbd className="text-[10px] font-mono text-muted border border-border rounded px-1.5 py-0.5">ESC</kbd>
        </div>
        <div className="max-h-96 overflow-y-auto">
          {q.length === 0 && <div className="px-4 py-8 text-center text-sm text-muted">Type to search across the platform.</div>}
          {q.length > 0 && results.length === 0 && <div className="px-4 py-8 text-center text-sm text-muted">No results for "{q}".</div>}
          {results.map((r, i) => (
            <button key={i} onClick={() => { r.go(); app.setSearchOpen(false); setQ(''); }} className="w-full flex items-center justify-between px-4 py-2.5 hover:bg-elevated transition-colors text-left">
              <div>
                <div className="text-sm text-primary">{r.label}</div>
                <div className="text-xs text-muted">{r.sub}</div>
              </div>
              <span className="text-[10px] font-mono uppercase tracking-wide text-gold bg-gold/10 rounded px-1.5 py-0.5">{r.type}</span>
            </button>
          ))}
        </div>
      </div>
    </div>,
    document.body
  );
}

/* ======================================================================
   8. LAYOUT: SIDEBAR / HEADER / MOBILE NAV
   ====================================================================== */

const NAV_ITEMS = [
  { key: 'dashboard', label: 'Dashboard', icon: LayoutDashboard, group: 'Workflow' },
  { key: 'pipeline', label: 'Pipeline', icon: GitBranch, group: 'Workflow' },
  { key: 'quotations', label: 'Quotations', icon: FileText, group: 'Workflow' },
  { key: 'sampling', label: 'Sampling', icon: FlaskConical, group: 'Workflow' },
  { key: 'components', label: 'Components', icon: Package, group: 'Workflow' },
  { key: 'suppliers', label: 'Suppliers', icon: Building2, group: 'Network' },
  { key: 'team', label: 'Team', icon: Users, group: 'Network' },
  { key: 'accomplished', label: 'Completed', icon: Archive, group: 'Archive' },
];
const NAV_GROUPS = ['Workflow', 'Network', 'Archive'];
const MOBILE_NAV = ['dashboard', 'pipeline', 'quotations', 'components'];

function useNavCounts() {
  const app = useApp();
  const nonArchived = app.projects.filter(p => !p.isArchived);
  const pipelineCount = nonArchived.length;
  const quotationsCount = app.quotations.reduce((n, q) => n + (q.lineItems || []).filter(li => (li.status || 'Pending') === 'Pending').length, 0);
  const samplingCount = app.samplingProjects.filter(sp => !['Moved to Production', 'Cancelled'].includes(sp.status)).length;
  return { pipeline: pipelineCount, quotations: quotationsCount, sampling: samplingCount };
}

function Sidebar({ collapsed, setCollapsed }) {
  const app = useApp();
  const counts = useNavCounts();
  return (
    <aside className={`hidden md:flex flex-col shrink-0 transition-[width] duration-[250ms] ease-in-out ${collapsed ? 'w-[72px]' : 'w-64'}`} style={{ background: '#121110' }}>
      <div className="flex items-center gap-2.5 px-5 py-5 border-b border-white/10">
        <div className="w-8 h-8 rounded-lg bg-[#C9A66B]/15 flex items-center justify-center shrink-0">
          <span className="font-display text-lg text-[#E4C989]">S</span>
        </div>
        {!collapsed && <span className="font-display text-xl text-white tracking-wide">SAKAN</span>}
      </div>
      <nav className="flex-1 py-4 px-3 space-y-4 overflow-y-auto">
        {NAV_GROUPS.map(group => (
          <div key={group}>
            {!collapsed && <div className="px-3 mb-1 text-[10px] uppercase tracking-widest text-white/40">{group}</div>}
            <div className="space-y-1">
              {NAV_ITEMS.filter(item => item.group === group).map(item => {
                const active = app.route.page === item.key;
                const Icon = item.icon;
                const count = counts[item.key];
                return (
                  <button
                    key={item.key}
                    onClick={() => app.navigate(item.key)}
                    className={`relative w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors ${active ? 'bg-[#C9A66B]/15 text-[#E4C989]' : 'text-white/60 hover:text-white hover:bg-white/5'}`}
                    title={item.label}
                  >
                    {active && <span className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-4 rounded-full bg-[#C9A66B] animate-navIndicatorIn" />}
                    <Icon className="w-4.5 h-4.5 shrink-0" />
                    {!collapsed && <span className="font-medium flex-1 text-left">{item.label}</span>}
                    {!collapsed && !!count && (
                      <span className={`text-[10px] font-mono rounded-full px-1.5 py-0.5 ${active ? 'bg-[#C9A66B]/20 text-[#E4C989]' : 'bg-white/10 text-white/50'}`}>{count}</span>
                    )}
                  </button>
                );
              })}
            </div>
          </div>
        ))}
      </nav>
      <div className="p-3 border-t border-white/10">
        <button onClick={() => setCollapsed(c => !c)} className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-white/60 hover:text-white hover:bg-white/5 text-sm">
          <ChevronsUpDown className="w-4 h-4 rotate-90" />
          {!collapsed && <span>Collapse</span>}
        </button>
      </div>
    </aside>
  );
}

function Header({ scrolled = false }) {
  const app = useApp();
  const unread = app.notifications.filter(n => !n.read).length;
  const prevUnread = usePrevious(unread);
  const [shake, setShake] = useState(false);
  useEffect(() => {
    if (prevUnread !== undefined && unread > prevUnread) {
      setShake(true);
      const t = setTimeout(() => setShake(false), 400);
      return () => clearTimeout(t);
    }
  }, [unread, prevUnread]);
  return (
    <header className={`sticky top-0 z-30 flex items-center gap-3 px-4 md:px-6 py-3.5 border-b border-border bg-[#FBF9F5]/85 backdrop-blur-md transition-shadow duration-200 ${scrolled ? 'shadow-[0_2px_8px_rgba(0,0,0,0.06)]' : ''}`}>
      <button onClick={() => app.setSearchOpen(true)} className="flex-1 md:flex-none md:w-80 flex items-center gap-2 px-3 py-2 rounded-lg border border-border bg-[#FBF9F5]/85 backdrop-blur-md text-muted hover:border-gold/40 transition-colors text-sm">
        <Search className="w-4 h-4" />
        <span className="flex-1 text-left">Search...</span>
        <kbd className="hidden md:inline text-[10px] font-mono border border-border rounded px-1.5 py-0.5">⌘K</kbd>
      </button>
      <div className="flex-1 hidden md:block" />
      <Dropdown
        align="right"
        trigger={
          <button className={`relative p-2 rounded-lg hover:bg-elevated text-muted hover:text-primary transition-colors ${shake ? 'animate-bellShake' : ''}`}>
            <Bell className="w-4.5 h-4.5" />
            {unread > 0 && <span className="absolute top-1 right-1 w-2 h-2 rounded-full bg-oud animate-ringPulse" />}
          </button>
        }
      >
        <div className="px-3 py-2 border-b border-border flex items-center justify-between">
          <span className="text-sm font-medium text-primary">Notifications</span>
          <button onClick={app.markAllNotificationsRead} className="text-xs text-gold hover:underline">Mark all read</button>
        </div>
        <div className="max-h-72 overflow-y-auto">
          {app.notifications.map(n => (
            <div
              key={n.id}
              onClick={() => app.openNotification(n)}
              className={`px-3 py-2.5 text-xs border-b border-border last:border-0 ${!n.read ? 'bg-gold/5' : ''} ${n.linkPage ? 'cursor-pointer hover:bg-elevated' : ''}`}
            >
              <p className="text-primary leading-snug">{n.text}</p>
              <span className="text-muted font-mono">{n.time}</span>
            </div>
          ))}
        </div>
      </Dropdown>
      <Dropdown
        align="right"
        trigger={
          <button className="flex items-center gap-2 pl-1 pr-2.5 py-1 rounded-full border border-border hover:border-gold/40 transition-colors">
            <Avatar user={app.currentUser} size="sm" />
            <span className="hidden sm:inline text-sm text-primary">{app.currentUser.name}</span>
            <ChevronDown className="w-3.5 h-3.5 text-muted" />
          </button>
        }
      >
        <div className="px-3 py-2.5 border-b border-border">
          <div className="text-sm text-primary font-medium">{app.currentUser?.name}</div>
          <div className="text-[11px] text-muted">{app.currentUser?.role} · {app.currentUser?.accessRole}</div>
        </div>
        <div className="border-t border-border mt-1 pt-1">
          <button onClick={() => supabase.auth.signOut()} className="w-full flex items-center gap-2 px-3 py-2 text-xs text-muted hover:text-oud"><LogOut className="w-3.5 h-3.5" /> Sign out</button>
        </div>
      </Dropdown>
    </header>
  );
}

function MobileBottomNav() {
  const app = useApp();
  const [menuOpen, setMenuOpen] = useState(false);
  const totalSlots = MOBILE_NAV.length + 1; // + the trailing "Menu" button, all evenly flex-1
  const activeIndex = MOBILE_NAV.indexOf(app.route.page);
  return (
    <>
      <nav className="md:hidden fixed bottom-0 inset-x-0 z-40 bg-surface border-t border-border flex items-stretch">
        {activeIndex >= 0 && (
          <span
            className="absolute top-0 h-0.5 bg-gold rounded-full transition-[left] duration-200 ease-out"
            style={{ left: `${(activeIndex / totalSlots) * 100}%`, width: `${100 / totalSlots}%` }}
          />
        )}
        {MOBILE_NAV.map(key => {
          const item = NAV_ITEMS.find(n => n.key === key);
          const Icon = item.icon;
          const active = app.route.page === key;
          return (
            <button key={key} onClick={() => app.navigate(key)} className={`flex-1 flex flex-col items-center gap-1 py-2.5 text-[10px] transition-colors active:scale-90 duration-150 ${active ? 'text-gold' : 'text-muted'}`}>
              <Icon className="w-5 h-5" /> {item.label}
            </button>
          );
        })}
        <button onClick={() => setMenuOpen(true)} className="flex-1 flex flex-col items-center gap-1 py-2.5 text-[10px] text-muted active:scale-90 transition-transform duration-150">
          <Menu className="w-5 h-5" /> Menu
        </button>
      </nav>
      {menuOpen && (
        <div className="md:hidden fixed inset-0 z-50 flex items-end">
          <div className="absolute inset-0 bg-black/70" onClick={() => setMenuOpen(false)} />
          <div className="relative w-full bg-surface border-t border-border rounded-t-2xl p-4 animate-dialogIn">
            <div className="grid grid-cols-3 gap-3">
              {NAV_ITEMS.map(item => {
                const Icon = item.icon;
                return (
                  <button key={item.key} onClick={() => { app.navigate(item.key); setMenuOpen(false); }} className="flex flex-col items-center gap-2 py-4 rounded-xl border border-border text-muted hover:text-gold">
                    <Icon className="w-5 h-5" /><span className="text-xs">{item.label}</span>
                  </button>
                );
              })}
            </div>
          </div>
        </div>
      )}
    </>
  );
}

/* ======================================================================
   9. DASHBOARD
   ====================================================================== */

function Donut({ data, onSelect }) {
  const total = data.reduce((s, d) => s + d.value, 0) || 1;
  let acc = 0;
  const stops = data.map(d => {
    const start = (acc / total) * 360;
    acc += d.value;
    const end = (acc / total) * 360;
    return `${d.color} ${start}deg ${end}deg`;
  }).join(', ');
  return (
    <div className="flex items-center gap-4">
      <div className="w-28 h-28 rounded-full shrink-0 relative shadow-[inset_0_1px_3px_rgba(0,0,0,0.08)]" style={{ background: `conic-gradient(${stops})` }}>
        <div className="absolute inset-[10px] rounded-full bg-surface flex items-center justify-center flex-col">
          <span className="font-mono text-lg text-primary tabular-nums">{total}</span>
          <span className="text-[10px] text-muted">projects</span>
        </div>
      </div>
      <div className="space-y-1.5">
        {data.map(d => (
          <button
            key={d.label}
            onClick={() => onSelect && onSelect(d)}
            className={`flex items-center gap-2 text-xs w-full text-left rounded px-1 -mx-1 transition-colors ${onSelect ? 'hover:bg-elevated hover:text-gold cursor-pointer' : ''}`}
          >
            <span className="w-2 h-2 rounded-full shrink-0" style={{ background: d.color }} />
            <span className="text-muted">{d.label}</span>
            <span className="font-mono text-primary">{d.value}</span>
          </button>
        ))}
      </div>
    </div>
  );
}

function KpiCard({ label, value, icon: Icon, context, onClick }) {
  const prevValue = usePrevious(value);
  const [flash, setFlash] = useState(false);
  useEffect(() => {
    if (prevValue !== undefined && prevValue !== value) {
      setFlash(true);
      const t = setTimeout(() => setFlash(false), 400);
      return () => clearTimeout(t);
    }
  }, [value, prevValue]);
  return (
    <Card hover={!!onClick} onClick={onClick} className="p-4">
      <div className="flex items-center justify-between mb-2">
        <span className="text-[10px] uppercase tracking-widest text-muted">{label}</span>
        <div className="w-7 h-7 rounded-lg bg-elevated flex items-center justify-center shrink-0">
          <Icon className="w-3.5 h-3.5 text-gold" />
        </div>
      </div>
      <div className={`inline-block font-mono font-medium text-2xl tabular-nums transition-all duration-300 ${flash ? 'scale-105 text-[#E4C989]' : 'scale-100 text-[#B88E4B]'}`}>{value}</div>
      {context && <div className="text-xs text-muted mt-1 truncate">{context}</div>}
    </Card>
  );
}


function ExchangeRatesModal({ open, onClose }) {
  const app = useApp();
  const [drafts, setDrafts] = useState({});
  const [refreshing, setRefreshing] = useState(false);

  const handleRefresh = async () => {
    setRefreshing(true);
    await app.refreshFxRates();
    setRefreshing(false);
  };

  const commit = (code) => {
    const val = drafts[code];
    if (val === undefined || val === '') return;
    const num = Number(val);
    if (!isNaN(num) && num > 0) app.updateFxRate(code, num);
  };

  return (
    <Modal open={open} onClose={onClose} title="Exchange Rates" width="max-w-lg">
      <div className="space-y-4">
        <div className="flex items-center justify-between rounded-lg border border-border bg-base px-3 py-2.5">
          <div className="text-xs text-muted">
            Source: <span className="text-primary">{app.fxSource === 'live' ? 'Live market data' : app.fxSource === 'manual' ? 'Manually set' : 'Default estimate'}</span>
            <span className="mx-1.5">·</span>Updated {fmtRelativeFromISO(app.fxUpdatedAt)}
          </div>
          <Button size="sm" variant="secondary" icon={RefreshCw} onClick={handleRefresh} loading={refreshing}>{refreshing ? 'Refreshing...' : 'Refresh Now'}</Button>
        </div>
        <p className="text-xs text-muted">Rate shown is how many US Dollars 1 unit of that currency is worth. Edit any value and click away to save it manually.</p>
        <div className="space-y-2 max-h-80 overflow-y-auto pr-1">
          {CURRENCIES.filter(c => c.code !== 'USD').map(c => (
            <div key={c.code} className="flex items-center justify-between gap-3">
              <div className="flex items-center gap-2 text-sm text-primary w-28 shrink-0">
                <span className="font-mono text-muted w-10">{c.code}</span>
                <span className="text-muted">{c.symbol}</span>
              </div>
              <div className="flex items-center gap-1.5 flex-1">
                <span className="text-xs text-muted">1 {c.code} =</span>
                <Input
                  type="number" step="0.0001"
                  value={drafts[c.code] !== undefined ? drafts[c.code] : (app.fxRates[c.code] ?? EXCHANGE_RATES_TO_USD[c.code] ?? 1)}
                  onChange={e => setDrafts(d => ({ ...d, [c.code]: e.target.value }))}
                  onBlur={() => commit(c.code)}
                  onKeyDown={e => { if (e.key === 'Enter') e.target.blur(); }}
                  className="!w-28"
                />
                <span className="text-xs text-muted">USD</span>
              </div>
            </div>
          ))}
        </div>
        <div className="flex justify-end pt-2"><Button variant="secondary" onClick={onClose}>Close</Button></div>
      </div>
    </Modal>
  );
}

/* ---- Dashboard helpers ---- */
// Timestamp of the last real activity on a sampling track — the newest statusHistory
// entry across all its rounds, or its own creation date if no round has been touched
// yet. Distinct from getOverdueSamplingInfo below: that one flags running past the
// *quoted* lead time; this one flags plain silence, regardless of how much budget is
// left — a track can go quiet well within its quoted window.
function getSamplingLastTouchedTs(sp, ctx) {
  let latest = sp.createdAt ? new Date(sp.createdAt).getTime() : 0;
  ctx.sampleRounds.filter(r => r.samplingProjectId === sp.id).forEach(r => {
    (r.statusHistory || []).forEach(h => {
      const t = h.updatedAt ? new Date(h.updatedAt).getTime() : 0;
      if (t > latest) latest = t;
    });
  });
  return latest;
}
// Checkpoints along a project's own start→target timeline. Flags the highest checkpoint
// already crossed (by elapsed time) where real progress hasn't kept pace — e.g. 70% of
// the timeline has passed but progress is still at 40%. Returns null once a project is
// done, has no usable date range, or is progressing in line with (or ahead of) elapsed time.
// `progress` is computeProgress(selectProjectSlice(ctx, p.id)) - never the unmaintained
// projects.progress column (which only ever moves 5 -> 100, so this detector used to
// false-fire for nearly every active project past 30% of its timeline).
const TIMELINE_CHECKPOINTS = [90, 70, 50, 30];
function getTimelineCheckpointRisk(p, today, ctx) {
  if (p.isArchived || p.status === 'Complete' || !p.startDate || !p.target) return null;
  const totalDays = daysBetween(p.startDate, p.target);
  if (totalDays <= 0) return null;
  const elapsedPct = Math.min(100, Math.round((daysBetween(p.startDate, today) / totalDays) * 100));
  const progress = computeProgress(selectProjectSlice(ctx, p.id));
  for (const checkpoint of TIMELINE_CHECKPOINTS) {
    if (elapsedPct >= checkpoint && progress < checkpoint) return { checkpoint, elapsedPct, progress };
  }
  return null;
}
// Structured counterpart to aiRoundTimelineNote (SakanPlatform.jsx, AI section) — same
// "is the latest in-flight round running past its quoted sampling lead time" logic, but
// returns the raw numbers instead of a sentence so the Dashboard can render/sort/aggregate
// them (Risk Radar rows, Supplier Scorecard overdue counts, Financial Exposure at-risk).
function getOverdueSamplingInfo(sp, ctx) {
  const rounds = ctx.sampleRounds.filter(r => r.samplingProjectId === sp.id)
    .sort((a, b) => (b.dateRequested || '').localeCompare(a.dateRequested || ''));
  const latest = rounds[0];
  if (!latest || !latest.dateRequested) return null;
  const inFlight = !SAMPLE_ROUND_TERMINAL_STATUSES.includes(latest.status) && latest.status !== 'Cancelled';
  if (!inFlight) return null;
  const bid = ctx.supplierBids.find(b => b.samplingProjectId === sp.id && b.supplierId === latest.supplierId);
  const expectedDays = bid?.quotedSamplingLeadTime;
  if (!expectedDays) return null;
  const elapsed = daysBetween(latest.dateRequested, todayStr());
  const overdueDays = elapsed - expectedDays;
  if (overdueDays <= 0) return null;
  return { round: latest, supplierId: latest.supplierId, expectedDays, elapsed, overdueDays };
}
// Classifies one quotation line item into a Decision Funnel stage. A line item's own
// `status` only distinguishes Pending/Approved/Rejected/Developing/In Sampling — it's
// never updated again once sent, so "Moved to Production" has to be derived by
// following the item's linked SamplingProject (and, past that, its Component). The
// funnel only tracks stages that still need attention — Rejected items have exited the
// pipeline, and Complete items are done — so both return null (excluded) rather than a
// bucket, keeping the funnel a clean view of exactly what's still active.
function getLineItemStage(li, ctx) {
  const status = li.status || 'Pending';
  if (status === 'Pending') return 'Pending';
  if (status === 'Rejected') return null;
  if (status === 'Developing') return 'Developing';
  if (status === 'Approved' && !li.locked) return 'Approved (unsent)';
  const sp = ctx.samplingProjects.find(s => s.id === li.samplingProjectId);
  if (!sp || sp.status !== 'Moved to Production') return 'In Sampling';
  const comp = ctx.components.find(c => c.samplingProjectId === sp.id);
  return comp?.status === 'Complete' ? null : 'Moved to Production';
}
const PROJECT_STAGE_ORDER = ['Sampling', 'Approved', 'Production', 'Shipping', 'Complete'];
const PROJECT_STAGE_TONE = { Sampling: 'gold', Approved: 'gold', Production: 'gold', Shipping: 'gold', Complete: 'sage' };

function Dashboard() {
  const app = useApp();
  const today = todayStr();
  const me = app.currentUser;
  const [spendOpen, setSpendOpen] = useState(false);
  const [ratesModalOpen, setRatesModalOpen] = useState(false);
  const [actionExpanded, setActionExpanded] = useState(false);
  const visibleProjects = app.projects.filter(p => canSeeProject(me, p));
  const nonArchived = visibleProjects.filter(p => !p.isArchived);

  /* ---- Action Center: personal inbox, in priority order ---- */
  const myBlockedProjects = nonArchived.filter(p => p.owner === me.id && p.blocking);
  const myTasksDueSoon = app.tasks.filter(t => t.assignedTo === me.id && t.status !== 'Done' && t.dueDate && daysBetween(today, t.dueDate) <= 7)
    .sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate));
  const myPendingDecisions = app.quotations.flatMap(q => {
    const proj = app.projects.find(p => p.id === q.projectId);
    if (!proj || !canEditProject(me, proj)) return [];
    return (q.lineItems || []).filter(li => (li.status || 'Pending') === 'Pending').map(li => ({ li, q, proj }));
  });
  const myReviewRounds = app.samplingProjects.filter(sp => sp.ownerId === me.id).flatMap(sp =>
    app.sampleRounds.filter(r => r.samplingProjectId === sp.id && ['Arrived', 'Under Review'].includes(r.status)).map(r => ({ r, sp }))
  );
  const myStaleComponents = app.components.filter(c => c.owner === me.id && c.status !== 'Complete'
    && daysBetween(c.nextActionUpdatedAt || c.startDate, today) > 5);

  const actionItems = [
    ...myBlockedProjects.map(p => ({
      key: `blocker-${p.id}`, statusLabel: 'Blocked', title: `Resolve blocker: ${p.blocking}`, details: p.name,
      ownerId: p.owner, dueDate: p.target, est: '10 min', actionLabel: 'Review',
      go: () => app.navigate('projectDetail', p.id),
    })),
    ...myPendingDecisions.map(({ li, q, proj }) => ({
      key: `qli-${li.id}`, statusLabel: 'Decision', title: `Decide: ${li.itemName}`, details: `${q.supplierName} — ${proj.name}`,
      ownerId: quotationOwnerId(q, proj), dueDate: q.validUntil || null, est: '3 min', actionLabel: 'Decide',
      go: () => app.navigate('quotationsByProject', proj.id),
    })),
    ...myReviewRounds.map(({ r, sp }) => ({
      key: `round-${r.id}`, statusLabel: 'Decision', title: `Review sample ${r.version} — ${sp.itemName || sp.name}`, details: r.status,
      ownerId: sp.ownerId, dueDate: null, est: '5 min', actionLabel: 'Review',
      go: () => app.navigate('samplingDetail', sp.id),
    })),
    ...myTasksDueSoon.map(t => {
      const days = daysBetween(today, t.dueDate);
      return {
        key: `task-${t.id}`, statusLabel: days < 0 ? 'Blocked' : days <= 2 ? 'Decision' : 'Stale',
        title: t.title, details: 'Task', ownerId: me.id, dueDate: t.dueDate, est: '15 min', actionLabel: 'Open',
        go: (t.linkedEntityType === 'project' && t.linkedEntityId) ? () => app.navigate('projectDetail', t.linkedEntityId) : null,
      };
    }),
    ...myStaleComponents.map(c => ({
      key: `comp-${c.id}`, statusLabel: 'Stale', title: `Stale: ${c.name}`, details: c.projectName,
      ownerId: c.owner, dueDate: null, est: '2 min', actionLabel: 'Open',
      go: () => app.navigate('components'),
    })),
  ];

  /* ---- Quotation Decision Funnel ---- */
  const FUNNEL_STAGES = [
    { key: 'Pending', pillStatus: 'Pending', suffix: null, tone: 'muted' },
    { key: 'Approved (unsent)', pillStatus: 'Approved', suffix: '(unsent)', tone: 'sage' },
    { key: 'Developing', pillStatus: 'Developing', suffix: null, tone: 'purple' },
    { key: 'In Sampling', pillStatus: 'In Sampling', suffix: null, tone: 'gold' },
    { key: 'Moved to Production', pillStatus: 'Moved to Production', suffix: null, tone: 'gold' },
  ];
  const funnelItems = { 'Pending': [], 'Approved (unsent)': [], 'Developing': [], 'In Sampling': [], 'Moved to Production': [] };
  app.quotations.forEach(q => {
    const proj = app.projects.find(p => p.id === q.projectId);
    if (!proj || !canSeeProject(me, proj)) return;
    (q.lineItems || []).forEach(li => {
      const stage = getLineItemStage(li, app);
      if (stage) funnelItems[stage].push({ li, q });
    });
  });
  const funnelCounts = Object.fromEntries(Object.entries(funnelItems).map(([k, v]) => [k, v.length]));
  const funnelTotal = Object.values(funnelCounts).reduce((s, n) => s + n, 0) || 1;
  const funnelMax = Math.max(...Object.values(funnelCounts), 1);
  const decisionBacklog = funnelCounts['Pending'] + funnelCounts['Approved (unsent)'];
  const funnelAvgDays = (key) => {
    const items = funnelItems[key];
    if (!items.length) return null;
    const total = items.reduce((s, { li, q }) => s + Math.max(0, daysBetween(li.decidedAt || q.createdAt, today)), 0);
    return Math.round(total / items.length);
  };

  /* ---- Risk Radar ----
     Scope is deliberately tighter than the rest of the Dashboard: Admins get the full
     picture, everyone else only sees risk on projects they own or are a collaborator
     (editableBy) on — not just anything visibleTo them. Every check below shares this
     same scope, and every result carries a `severity` (larger = worse) so the whole
     list can be sorted into one unified feed by how urgent it actually is, tone first. */
  const myRiskScope = (p) => !!p && (me.accessRole === 'Admin' || p.owner === me.id || (p.editableBy || []).includes(me.id));

  const overdueRoundsList = app.samplingProjects
    .filter(sp => myRiskScope(app.projects.find(p => p.id === sp.linkedProjectId)))
    .map(sp => ({ sp, info: getOverdueSamplingInfo(sp, app) }))
    .filter(x => x.info);
  const overdueProjects = nonArchived.filter(p => p.status !== 'Complete' && myRiskScope(p) && daysBetween(p.target, today) > 0);
  const expiringQuotes = app.quotations.filter(q => {
    const proj = app.projects.find(p => p.id === q.projectId);
    return myRiskScope(proj) && ['Pending', 'Partial'].includes(deriveQuotationStatus(q)) && daysBetween(today, q.validUntil) <= 7;
  });
  const staleProductionComponents = app.components.filter(c => {
    const proj = app.projects.find(p => p.id === c.projectId);
    return myRiskScope(proj) && c.status === 'Under Production' && hoursSince(c.nextActionUpdatedAt || c.startDate) > 72;
  });
  // Quotation line items sitting in a given status for 72h+ without a decision moving
  // them forward — Pending (never decided) and Developing (decided, but that track has
  // gone quiet) are the two statuses that can silently stall with no other signal.
  const staleLineItemsByStatus = (status) => {
    const out = [];
    app.quotations.forEach(q => {
      const proj = app.projects.find(p => p.id === q.projectId);
      if (!myRiskScope(proj)) return;
      (q.lineItems || []).forEach(li => {
        if ((li.status || 'Pending') !== status) return;
        const hours = hoursSince(li.decidedAt || q.createdAt);
        if (hours > 72) out.push({ li, q, proj, hours });
      });
    });
    return out;
  };
  const stalePendingItems = staleLineItemsByStatus('Pending');
  const staleDevelopingItems = staleLineItemsByStatus('Developing');
  // Sampling tracks that have gone quiet for 72h+, independent of whether they're still
  // within their quoted lead time (overdueRoundsList above covers that separate case).
  const staleSamplingTracks = app.samplingProjects.filter(sp => {
    const proj = app.projects.find(p => p.id === sp.linkedProjectId);
    if (!myRiskScope(proj) || ['Moved to Production', 'Cancelled'].includes(sp.status)) return false;
    return hoursSince(getSamplingLastTouchedTs(sp, app)) > 72;
  });
  const timelineRiskProjects = nonArchived
    .filter(p => p.status !== 'Complete' && myRiskScope(p) && daysBetween(p.target, today) <= 0)
    .map(p => ({ p, risk: getTimelineCheckpointRisk(p, today, app) }))
    .filter(x => x.risk);

  const riskItems = [
    ...overdueRoundsList.map(({ sp, info }) => {
      const proj = app.projects.find(p => p.id === sp.linkedProjectId);
      const supplier = supplierById(app.suppliers, info.supplierId);
      return {
        key: `risk-round-${sp.id}`, icon: FlaskConical, tone: 'oud', severity: info.overdueDays * 24,
        text: `${supplier?.name || 'Supplier'} — ${sp.itemName || sp.name} is ${info.overdueDays}d overdue`,
        sub: proj?.name || '', ownerId: sp.ownerId, go: () => app.navigate('samplingDetail', sp.id),
      };
    }),
    ...overdueProjects.map(p => ({
      key: `risk-proj-${p.id}`, icon: CalendarClock, tone: 'oud', severity: daysBetween(p.target, today) * 24,
      text: `${p.name} is ${daysBetween(p.target, today)}d past target`, sub: `Target was ${fmtDateShort(p.target)}`,
      ownerId: p.owner, go: () => app.navigate('projectDetail', p.id),
    })),
    ...expiringQuotes.map(q => {
      const proj = app.projects.find(p => p.id === q.projectId);
      const daysLeft = daysBetween(today, q.validUntil);
      return {
        key: `risk-quote-${q.id}`, icon: FileText, tone: 'amber', severity: (7 - daysLeft) * 24,
        text: `${q.supplierName} quote ${daysLeft < 0 ? 'has expired' : `expires in ${daysLeft}d`}`, sub: proj?.name || '',
        ownerId: quotationOwnerId(q, proj), go: () => app.navigate('quotationsByProject', q.projectId),
      };
    }),
    ...staleProductionComponents.map(c => ({
      key: `risk-comp-${c.id}`, icon: Package, tone: 'amber', severity: hoursSince(c.nextActionUpdatedAt || c.startDate),
      text: `${c.name} — no update in ${Math.floor(hoursSince(c.nextActionUpdatedAt || c.startDate) / 24)}d`, sub: c.projectName,
      ownerId: c.owner, go: () => app.navigate('components'),
    })),
    ...stalePendingItems.map(({ li, q, proj, hours }) => ({
      key: `risk-pending-${li.id}`, icon: Clock, tone: 'amber', severity: hours,
      text: `${li.itemName} still Pending after ${Math.floor(hours / 24)}d`, sub: `${q.supplierName} — ${proj?.name || ''}`,
      ownerId: quotationOwnerId(q, proj), go: () => app.navigate('quotationsByProject', q.projectId),
    })),
    ...staleDevelopingItems.map(({ li, q, proj, hours }) => ({
      key: `risk-developing-${li.id}`, icon: Clock, tone: 'purple', severity: hours,
      text: `${li.itemName} has been Developing for ${Math.floor(hours / 24)}d`, sub: `${q.supplierName} — ${proj?.name || ''}`,
      ownerId: quotationOwnerId(q, proj), go: () => app.navigate('quotationsByProject', q.projectId),
    })),
    ...staleSamplingTracks.map(sp => {
      const proj = app.projects.find(p => p.id === sp.linkedProjectId);
      const hours = hoursSince(getSamplingLastTouchedTs(sp, app));
      return {
        key: `risk-sampling-quiet-${sp.id}`, icon: FlaskConical, tone: 'amber', severity: hours,
        text: `${sp.itemName || sp.name} — no update in ${Math.floor(hours / 24)}d`, sub: proj?.name || '',
        ownerId: sp.ownerId, go: () => app.navigate('samplingDetail', sp.id),
      };
    }),
    ...timelineRiskProjects.map(({ p, risk }) => ({
      key: `risk-timeline-${p.id}`, icon: TrendingUp, tone: 'amber', severity: risk.checkpoint * 10,
      text: `${p.name} is ${risk.elapsedPct}% through its timeline, only ${risk.progress}% complete`,
      sub: `${risk.checkpoint}% checkpoint passed`, ownerId: p.owner, go: () => app.navigate('projectDetail', p.id),
    })),
  ];
  const TONE_RANK = { oud: 3, amber: 2, purple: 1, gold: 1, sage: 1 };
  riskItems.sort((a, b) => (TONE_RANK[b.tone] - TONE_RANK[a.tone]) || (b.severity - a.severity));

  /* ---- Supplier Scorecard ---- */
  const supplierScorecard = app.suppliers.map(s => {
    const rounds = app.sampleRounds.filter(r => r.supplierId === s.id);
    const activeRounds = rounds.filter(r => !SAMPLE_ROUND_TERMINAL_STATUSES.includes(r.status) && r.status !== 'Cancelled');
    const hasOpenQuote = app.quotations.some(q => q.supplierId === s.id && (q.lineItems || []).some(li => (li.status || 'Pending') === 'Pending'));
    if (!activeRounds.length && !hasOpenQuote) return null;
    const received = rounds.filter(r => r.dateReceived);
    const avgResponse = received.length ? Math.round(received.reduce((sum, r) => sum + daysBetween(r.dateRequested, r.dateReceived), 0) / received.length) : null;
    const decided = rounds.filter(r => SAMPLE_ROUND_TERMINAL_STATUSES.includes(r.status));
    const approvalRate = decided.length ? Math.round((decided.filter(r => r.status === 'Approved').length / decided.length) * 100) : null;
    const overdueCount = overdueRoundsList.filter(x => x.info.supplierId === s.id).length;
    return { supplier: s, activeCount: activeRounds.length, avgResponse, approvalRate, overdueCount };
  }).filter(Boolean)
    .sort((a, b) => (b.overdueCount - a.overdueCount) || ((b.avgResponse || 0) - (a.avgResponse || 0)))
    .slice(0, 5);

  /* ---- Project Health Tracker ----
     One row per active project that has reached sampling — i.e. has at least one
     linked samplingProject or component. Pre-sampling projects (Quotation/Development)
     are intentionally excluded; the Pipeline board already covers those stages. */
  const projectHealthRows = nonArchived
    .filter(p => app.samplingProjects.some(sp => sp.linkedProjectId === p.id) || app.components.some(c => c.projectId === p.id))
    .map(p => {
      const projSampling = app.samplingProjects.filter(sp => sp.linkedProjectId === p.id);
      const projComponents = app.components.filter(c => c.projectId === p.id);

      const isBehind = p.status !== 'Complete' && daysBetween(p.target, today) > 0;
      const hasOverdueSampling = projSampling.some(sp => getOverdueSamplingInfo(sp, app));
      const hasStaleComponent = projComponents.some(c => c.status !== 'Complete' && daysBetween(c.nextActionUpdatedAt || c.startDate, today) > 7);
      const health = isBehind ? 'Behind' : (p.blocking || hasOverdueSampling || hasStaleComponent) ? 'At Risk' : 'On Track';

      // Component.status only ever holds 'Under Production' or 'Complete' — the
      // Under Shipping state actually lives on nextAction (see updateComponent), so
      // Production/Shipping are split on that instead of a status value that doesn't exist.
      const stageCounts = {
        Sampling: projSampling.filter(sp => ['Sourcing', 'Sampling In Progress', 'On Hold'].includes(sp.status)).length,
        Approved: projSampling.filter(sp => sp.status === 'Sample Approved').length,
        Production: projComponents.filter(c => c.status === 'Under Production' && c.nextAction !== 'Under Shipping').length,
        Shipping: projComponents.filter(c => c.status === 'Under Shipping' || (c.status === 'Under Production' && c.nextAction === 'Under Shipping')).length,
        Complete: projComponents.filter(c => c.status === 'Complete').length,
      };
      return { project: p, health, stageCounts };
    });

  /* ---- Financial Exposure by Stage ---- */
  let approvedExposure = 0, inSamplingExposure = 0, inProductionExposure = 0, atRiskExposure = 0;
  app.quotations.forEach(q => {
    const proj = app.projects.find(p => p.id === q.projectId);
    if (!proj || !canSeeProject(me, proj)) return;
    (q.lineItems || []).forEach(li => {
      if ((li.status || 'Pending') === 'Approved' && !li.locked) {
        const amt = toUSD(lineItemAmount(li), q.currency || 'EUR', app.fxRates);
        approvedExposure += amt;
        if (proj.blocking) atRiskExposure += amt;
      }
    });
  });
  app.samplingProjects.forEach(sp => {
    const proj = app.projects.find(p => p.id === sp.linkedProjectId);
    if (!proj || !canSeeProject(me, proj) || sp.status === 'Moved to Production' || sp.status === 'Cancelled') return;
    let li = null, q = null;
    for (const qq of app.quotations) {
      const found = (qq.lineItems || []).find(x => x.samplingProjectId === sp.id);
      if (found) { li = found; q = qq; break; }
    }
    const currency = q?.currency || 'EUR';
    const roundsCost = app.sampleRounds.filter(r => r.samplingProjectId === sp.id).reduce((s, r) => s + (r.cost || 0), 0);
    const amt = toUSD((li?.moldCost || 0) + roundsCost, currency, app.fxRates);
    inSamplingExposure += amt;
    const overdueInfo = getOverdueSamplingInfo(sp, app);
    if (overdueInfo || proj.blocking) atRiskExposure += amt;
  });
  app.components.forEach(c => {
    if (c.status !== 'Under Production') return;
    const proj = app.projects.find(p => p.id === c.projectId);
    if (proj && !canSeeProject(me, proj)) return;
    const qty = parseNumeric(c.orderQty);
    const amt = toUSD((c.price || 0) * qty, c.currency || 'EUR', app.fxRates);
    inProductionExposure += amt;
    const stale = daysBetween(c.nextActionUpdatedAt || c.startDate, today) > 7;
    if (stale || proj?.blocking) atRiskExposure += amt;
  });
  const totalExposure = approvedExposure + inSamplingExposure + inProductionExposure;

  /* ---- Projects by Stage (incl. Blocked / Overdue) ---- */
  // Chart palette restricted to the approved data-viz colors only (primary/accent blue,
  // the four status colors, neutral grey) - no purple/gold, per the design system's
  // Charts & Data Visualization rules. A couple of stages necessarily share a color
  // since there are more pipeline stages than approved chart colors.
  const stageColors = { Blocked: '#D94A3A', Overdue: '#E5A72E', Development: '#66C2FB', Quotation: '#1827F5', Sampling: '#E5A72E', Production: '#66C2FB', Shipping: '#3E9B68' };
  const bucketOf = (p) => {
    if (p.blocking) return 'Blocked';
    if (p.status !== 'Complete' && daysBetween(p.target, today) > 0) return 'Overdue';
    return p.status;
  };
  const bucketCounts = {};
  nonArchived.forEach(p => { const b = bucketOf(p); bucketCounts[b] = (bucketCounts[b] || 0) + 1; });
  const donutData = Object.entries(bucketCounts).map(([label, value]) => ({ label, value, color: stageColors[label] || '#98A2B3' }));

  /* ---- Recent Activity ---- */
  const recentActivity = app.activity.slice(0, 10);
  const activityGroups = { Today: [], Yesterday: [], Earlier: [] };
  recentActivity.forEach(a => activityGroups[relativeAgoBucket(a.createdAt)].push(a));

  /* ---- Upcoming Deadlines (next 7 days) ---- */
  const deadlineItems = [
    ...nonArchived.filter(p => p.status !== 'Complete').map(p => ({ date: p.target, label: p.name, type: 'Project', ownerId: p.owner, go: () => app.navigate('projectDetail', p.id) })),
    ...app.samplingProjects.filter(sp => !['Moved to Production', 'Cancelled'].includes(sp.status)).map(sp => ({ date: sp.targetApprovalDate, label: sp.name, type: 'Sampling', ownerId: sp.ownerId, go: () => app.navigate('samplingDetail', sp.id) })),
    ...app.tasks.filter(t => t.status !== 'Done').map(t => ({ date: t.dueDate, label: t.title, type: 'Task', ownerId: t.assignedTo, go: (t.linkedEntityType === 'project' && t.linkedEntityId) ? () => app.navigate('projectDetail', t.linkedEntityId) : null })),
  ].filter(x => x.date && daysBetween(today, x.date) >= 0 && daysBetween(today, x.date) <= 7)
    .sort((a, b) => new Date(a.date) - new Date(b.date));

  /* ---- Compact KPI strip ---- */
  const dueThisWeek = nonArchived.filter(p => p.status !== 'Complete' && daysBetween(today, p.target) >= 0 && daysBetween(today, p.target) <= 7)
    .sort((a, b) => new Date(a.target) - new Date(b.target));
  const waitingProjects = nonArchived.filter(p => p.waiting);
  const waitingBreakdown = { Supplier: 0, Internal: 0, External: 0 };
  waitingProjects.forEach(p => { waitingBreakdown[classifyWaiting(p.waiting, app.suppliers, app.users)]++; });
  const inProductionCount = app.components.filter(c => c.status === 'Under Production').length;
  const inProductionDelayedCount = app.components.filter(c => c.status === 'Under Production' && hoursSince(c.nextActionUpdatedAt || c.startDate) > 72).length;
  const inTransitComponents = app.components.filter(c => c.status === 'Under Shipping');
  const inTransitCount = inTransitComponents.length;
  const inTransitExpected = inTransitComponents
    .map(c => app.projects.find(p => p.id === c.projectId)?.target).filter(Boolean).sort()[0] || null;
  const completedThisWeekCount = app.projects.filter(p => p.isArchived && p.completedDate && daysBetween(p.completedDate, today) >= 0 && daysBetween(p.completedDate, today) <= 7).length;

  const actionShown = actionExpanded ? actionItems : actionItems.slice(0, 5);

  return (
    <div className="p-4 md:p-6 max-w-[1400px] mx-auto">
      <div className="mb-4">
        <h1 className="font-display italic text-3xl text-primary tracking-tight">Welcome back, {me.name.split(' ')[0]}</h1>
        <p className="text-muted text-sm mt-1">
          {actionItems.length > 0 ? `${actionItems.length} item${actionItems.length !== 1 ? 's' : ''} need your attention.` : "You're all caught up."}
        </p>
      </div>

      {/* ---- 1. Action Center ---- */}
      <Card className="mb-6 animate-fadeUp overflow-hidden" style={{ animationDelay: '0ms' }}>
        <div className="px-4 pt-4 pb-1">
          <h3 className="text-[10px] uppercase tracking-widest text-muted">Needs your attention</h3>
        </div>
        {actionItems.length === 0 ? (
          <div className="px-4 py-8 text-center text-sm text-muted">You're all caught up.</div>
        ) : (
          <>
            <div className="overflow-x-auto">
              <table className="w-full text-sm">
                <thead>
                  <tr className="text-left text-[10px] uppercase tracking-widest text-muted border-b border-border">
                    <th className="py-2 px-4">Status</th>
                    <th className="py-2 px-4">Item</th>
                    <th className="py-2 px-4 hidden md:table-cell">Details</th>
                    <th className="py-2 px-4 hidden lg:table-cell">Owner</th>
                    <th className="py-2 px-4 hidden sm:table-cell">Due</th>
                    <th className="py-2 px-4 hidden lg:table-cell">Est. time</th>
                    <th className="py-2 px-4 text-right">Action</th>
                  </tr>
                </thead>
                <tbody>
                  {actionShown.map(item => (
                    <tr key={item.key} className="border-b border-border last:border-0 hover:bg-elevated transition-colors">
                      <td className="py-2.5 px-4"><StatusPill status={item.statusLabel} size="sm" /></td>
                      <td className="py-2.5 px-4 font-medium text-primary">{item.title}</td>
                      <td className="py-2.5 px-4 hidden md:table-cell text-muted truncate max-w-[240px]">{item.details}</td>
                      <td className="py-2.5 px-4 hidden lg:table-cell"><Avatar user={userById(app.users, item.ownerId)} size="sm" /></td>
                      <td className="py-2.5 px-4 hidden sm:table-cell text-muted whitespace-nowrap">
                        <span className="inline-flex items-center gap-1.5"><CalendarClock className="w-3 h-3" />{fmtDueRelative(item.dueDate)}</span>
                      </td>
                      <td className="py-2.5 px-4 hidden lg:table-cell font-mono text-muted">{item.est}</td>
                      <td className="py-2.5 px-4 text-right">
                        {item.go ? <Button size="sm" variant="ghost" onClick={item.go}>{item.actionLabel}</Button> : <span className="text-xs text-muted">—</span>}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
            {actionItems.length > 5 && (
              <div className="px-4 py-2.5 border-t border-border">
                <button onClick={() => setActionExpanded(e => !e)} className="text-xs font-medium text-gold hover:underline">
                  {actionExpanded ? 'Show less' : `View all (${actionItems.length})`}
                </button>
              </div>
            )}
          </>
        )}
      </Card>

      {/* ---- 2. KPI Cards ---- */}
      <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-6 animate-fadeUp" style={{ animationDelay: '60ms' }}>
        <KpiCard
          label="Due This Week" value={dueThisWeek.length} icon={CalendarClock}
          context={dueThisWeek.length > 0 ? `Next: ${dueThisWeek[0].name}` : null}
          onClick={() => app.navigate('pipeline')}
        />
        <Card hover onClick={() => app.navigate('pipeline')} className="p-4">
          <div className="flex items-center justify-between mb-2">
            <span className="text-[10px] uppercase tracking-widest text-muted">Waiting On</span>
            <div className="w-7 h-7 rounded-lg flex items-center justify-center bg-elevated"><Handshake className="w-3.5 h-3.5 text-gold" /></div>
          </div>
          <div className="flex items-center gap-3">
            {['Supplier', 'Internal', 'External'].map(k => (
              <div key={k} className="text-center">
                <div className="font-display text-lg text-primary">{waitingBreakdown[k]}</div>
                <div className="text-[10px] text-muted">{k}</div>
              </div>
            ))}
          </div>
        </Card>
        <KpiCard
          label="In Production" value={inProductionCount} icon={Package}
          context={inProductionDelayedCount > 0 ? `${inProductionDelayedCount} delayed` : null}
          onClick={() => app.navigate('components', null, { statusFilter: 'Under Production' })}
        />
        <KpiCard
          label="In Transit" value={inTransitCount} icon={Truck}
          context={inTransitExpected ? `Expected ${fmtDateShort(inTransitExpected)}` : null}
          onClick={() => app.navigate('components', null, { statusFilter: 'Under Shipping' })}
        />
        <KpiCard label="Completed This Week" value={completedThisWeekCount} icon={CheckCircle2} onClick={() => app.navigate('accomplished')} />
      </div>

      {/* ---- 3. Quotation Decision Funnel ---- */}
      <Card className="p-5 mb-6 animate-fadeUp" style={{ animationDelay: '120ms' }}>
        <div className="flex items-center justify-between mb-4 flex-wrap gap-2">
          <h3 className="text-[10px] uppercase tracking-widest text-muted">Quotation Decision Funnel</h3>
          <button onClick={() => app.navigate('quotations')} className="inline-flex items-center gap-1 text-[10px] uppercase tracking-widest font-medium text-gold bg-elevated rounded-full px-3 py-1.5 hover:bg-gold/15 transition-colors animate-chipBounce">
            {decisionBacklog} Decision Backlog <ChevronRight className="w-3 h-3" />
          </button>
        </div>
        <div className="overflow-x-auto">
          <table className="w-full text-sm">
            <thead>
              <tr className="text-left text-[10px] uppercase tracking-widest text-muted border-b border-border">
                <th className="py-2 pr-3">Stage</th>
                <th className="py-2 pr-3 w-1/3">Progress</th>
                <th className="py-2 pr-3 text-right">Count</th>
                <th className="py-2 pr-3 text-right">% of Total</th>
                <th className="py-2 text-right">Avg. Days</th>
              </tr>
            </thead>
            <tbody>
              {FUNNEL_STAGES.map(stage => {
                const count = funnelCounts[stage.key];
                const avgDays = funnelAvgDays(stage.key);
                return (
                  <tr key={stage.key} onClick={() => app.navigate('quotations')} className="border-b border-border last:border-0 cursor-pointer hover:bg-elevated transition-colors">
                    <td className="py-2.5 pr-3">
                      <div className="flex items-center gap-2 whitespace-nowrap">
                        <span className={`w-2 h-2 rounded-full shrink-0 ${TONE_DOT[stage.tone]}`} />
                        <span className="text-primary font-medium">{stage.key.replace(' (unsent)', '')}</span>
                        {stage.suffix && <span className="text-xs text-muted">{stage.suffix}</span>}
                      </div>
                    </td>
                    <td className="py-2.5 pr-3">
                      <div className="h-2 rounded-full bg-border overflow-hidden">
                        <div className="h-full rounded-full bg-gold transition-all duration-500" style={{ width: count ? `${Math.max(4, (count / funnelMax) * 100)}%` : '0%' }} />
                      </div>
                    </td>
                    <td className="py-2.5 pr-3 text-right font-mono text-primary">{count}</td>
                    <td className="py-2.5 pr-3 text-right font-mono text-muted">{Math.round((count / funnelTotal) * 100)}%</td>
                    <td className="py-2.5 text-right font-mono text-muted">{avgDays != null ? `${avgDays}d` : '—'}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      </Card>

      {/* ---- 4. Risk Radar + 5. Supplier Scorecard ---- */}
      <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6 animate-fadeUp" style={{ animationDelay: '180ms' }}>
        <Card className="p-4">
          <h3 className="text-[10px] uppercase tracking-widest text-muted mb-1 px-1">Risk Radar</h3>
          <p className="text-xs text-muted px-1 mb-3">{riskItems.length} active risk{riskItems.length !== 1 ? 's' : ''} across sampling, targets, quotes, and production.</p>
          <div className="max-h-[360px] overflow-y-auto space-y-1">
            {riskItems.length === 0 ? (
              <div className="px-3 py-6 text-center text-sm text-sage">No active risks — the pipeline is clear.</div>
            ) : riskItems.map(r => (
              <button key={r.key} onClick={r.go} className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-elevated transition-colors text-left">
                <div className={`w-7 h-7 rounded-lg flex items-center justify-center bg-${r.tone}/10 shrink-0`}><r.icon className={`w-3.5 h-3.5 text-${r.tone}`} /></div>
                <div className="flex-1 min-w-0">
                  <div className="text-sm text-primary truncate">{r.text}</div>
                  {r.sub && <div className="text-xs text-muted truncate">{r.sub}</div>}
                </div>
                <Avatar user={userById(app.users, r.ownerId)} size="sm" />
              </button>
            ))}
          </div>
        </Card>

        <Card className="p-4">
          <h3 className="text-[10px] uppercase tracking-widest text-muted mb-3 px-1">Supplier Scorecard</h3>
          {supplierScorecard.length === 0 ? (
            <div className="px-3 py-6 text-center text-sm text-muted">No suppliers with active work right now.</div>
          ) : (
            <div className="overflow-x-auto">
              <table className="w-full text-sm">
                <thead><tr className="text-left text-[10px] uppercase tracking-widest text-muted border-b border-border">
                  <th className="py-2 pr-3">Supplier</th><th className="py-2 pr-3">Active</th><th className="py-2 pr-3">Avg Resp.</th><th className="py-2 pr-3">Approval</th><th className="py-2">Overdue</th>
                </tr></thead>
                <tbody>
                  {supplierScorecard.map(row => (
                    <tr key={row.supplier.id} onClick={() => app.navigate('supplierDetail', row.supplier.id)} className="group border-b border-border last:border-0 cursor-pointer hover:bg-elevated/40 transition-colors">
                      <td className="py-2.5 pr-3">
                        <div className="flex items-center gap-2 min-w-0">
                          <Avatar user={{ name: row.supplier.name, color: '#667085', avatar: initials(row.supplier.name) }} size="sm" />
                          <span className="text-primary truncate border-b border-transparent group-hover:border-gold/60 group-hover:text-gold transition-colors">{row.supplier.name}</span>
                        </div>
                      </td>
                      <td className="py-2.5 pr-3 font-mono text-muted">{row.activeCount}</td>
                      <td className="py-2.5 pr-3 font-mono text-muted">{row.avgResponse != null ? `${row.avgResponse}d` : '—'}</td>
                      <td className="py-2.5 pr-3 font-mono text-muted">{row.approvalRate != null ? `${row.approvalRate}%` : '—'}</td>
                      <td className={`py-2.5 font-mono ${row.overdueCount > 0 ? 'text-oud' : 'text-sage'}`}>{row.overdueCount}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          )}
        </Card>
      </div>

      {/* ---- 6. Project Health ---- */}
      <Card className="p-5 mb-6 animate-fadeUp" style={{ animationDelay: '240ms' }}>
        <h3 className="text-[10px] uppercase tracking-widest text-muted mb-4">Project Health</h3>
        {projectHealthRows.length === 0 ? <p className="text-sm text-muted">No projects in sampling or production yet.</p> : (
          <div className="space-y-5">
            {projectHealthRows.map(({ project: p, health, stageCounts }) => {
              const totalItems = PROJECT_STAGE_ORDER.reduce((s, k) => s + stageCounts[k], 0);
              return (
                <div key={p.id}>
                  <div className="flex items-center justify-between mb-2 flex-wrap gap-2">
                    <div className="flex items-center gap-3 flex-wrap">
                      <button onClick={() => app.navigate('projectDetail', p.id)} className="font-medium text-primary hover:text-gold transition-colors">{p.name}</button>
                      <span className="text-xs text-muted font-mono">target {fmtDateShort(p.target)}</span>
                      <span className="pl-2 border-l-4 rounded-sm" style={{ borderLeftColor: TONE_HEX[STATUS_TONE[health] || 'muted'] }}>
                        <StatusPill status={health} size="sm" glow={health === 'Behind'} />
                      </span>
                    </div>
                  </div>
                  {totalItems === 0 ? (
                    <p className="text-xs text-muted">Nothing in sampling yet.</p>
                  ) : (
                    <>
                      <div className="h-2 rounded-full bg-elevated overflow-hidden flex mb-2">
                        {PROJECT_STAGE_ORDER.filter(s => stageCounts[s] > 0).map(s => (
                          <div key={s} title={`${s}: ${stageCounts[s]}`} className={TONE_DOT[PROJECT_STAGE_TONE[s]]} style={{ width: `${(stageCounts[s] / totalItems) * 100}%` }} />
                        ))}
                      </div>
                      <div className="flex flex-wrap gap-x-3 gap-y-1 text-xs font-mono">
                        {PROJECT_STAGE_ORDER.map(s => (
                          <span key={s} className={stageCounts[s] > 0 ? 'text-primary' : 'text-muted/40'}>{s} {stageCounts[s]}</span>
                        ))}
                      </div>
                    </>
                  )}
                </div>
              );
            })}
          </div>
        )}
      </Card>

      {/* ---- 7. Committed Spend ---- */}
      <Card className="p-4 mb-6 animate-fadeUp" style={{ animationDelay: '300ms' }}>
        <div className="w-full flex items-center justify-between">
          <button onClick={() => setSpendOpen(o => !o)} className="flex-1 flex items-center justify-between">
            <div className="flex items-center gap-2.5">
              <div className="w-7 h-7 rounded-lg flex items-center justify-center bg-elevated shrink-0"><Wallet className="w-3.5 h-3.5 text-gold" /></div>
              <span className="text-[10px] uppercase tracking-widest text-muted">Committed Spend</span>
              <span className="text-xs text-muted">(FX {app.fxSource === 'live' ? 'live' : app.fxSource === 'manual' ? 'manual' : 'default'}, updated {fmtRelativeFromISO(app.fxUpdatedAt)})</span>
            </div>
            <div className="flex items-center gap-3">
              <span className="font-mono text-sm text-primary">{fmtCurrency(totalExposure, 'USD', { decimals: 0 })}</span>
              {spendOpen ? <ChevronDown className="w-4 h-4 text-muted" /> : <ChevronRight className="w-4 h-4 text-muted" />}
            </div>
          </button>
          {app.currentUser.accessRole === 'Admin' && (
            <button onClick={() => setRatesModalOpen(true)} title="Manage exchange rates" className="ml-3 text-muted hover:text-gold shrink-0"><Settings2 className="w-4 h-4" /></button>
          )}
        </div>
        {spendOpen && (
          <div className="mt-4 pt-4 border-t border-border space-y-2.5">
            {[
              { label: 'Approved (not yet sent to sampling)', value: approvedExposure },
              { label: 'In Sampling (mold + sample costs)', value: inSamplingExposure },
              { label: 'In Production (order value)', value: inProductionExposure },
            ].map(row => (
              <div key={row.label} className="flex items-center justify-between text-xs">
                <span className="text-muted">{row.label}</span>
                <span className="font-mono text-primary">{fmtCurrency(row.value, 'USD', { decimals: 0 })}</span>
              </div>
            ))}
            <div className="flex items-center justify-between text-xs pt-2.5 mt-1 border-t border-border">
              <span className="text-oud">At Risk (blocked / overdue / stale)</span>
              <span className="font-mono text-oud font-medium">{fmtCurrency(atRiskExposure, 'USD', { decimals: 0 })}</span>
            </div>
            <div className="flex items-center justify-between text-xs pt-2.5 mt-1 border-t border-border">
              <span className="text-muted">Total exposure</span>
              <span className="font-mono text-primary font-medium">{fmtCurrency(totalExposure, 'USD', { decimals: 0 })}</span>
            </div>
          </div>
        )}
      </Card>

      {/* ---- Projects by Stage + Recent Activity ---- */}
      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6 animate-fadeUp" style={{ animationDelay: '360ms' }}>
        <Card className="p-5 lg:col-span-2">
          <h3 className="text-[10px] uppercase tracking-widest text-muted mb-4">Projects by Stage</h3>
          <Donut data={donutData.length ? donutData : [{ label: 'No active projects', value: 1, color: '#E4E7EC' }]} onSelect={() => app.navigate('pipeline')} />
        </Card>
        <Card className="p-4">
          <h3 className="text-[10px] uppercase tracking-widest text-muted mb-3 px-1">Recent Activity</h3>
          <div className="max-h-[340px] overflow-y-auto">
            {['Today', 'Yesterday', 'Earlier'].map(group => activityGroups[group].length > 0 && (
              <div key={group} className="mb-1">
                <div className="text-[10px] uppercase tracking-wide text-muted px-3 py-1">{group}</div>
                {activityGroups[group].map(a => {
                  const proj = app.projects.find(p => a.detail.includes(p.name));
                  return (
                    <button key={a.id} onClick={proj ? () => app.navigate('projectDetail', proj.id) : undefined} disabled={!proj} className="w-full text-left disabled:cursor-default hover:bg-elevated rounded-lg transition-colors">
                      <ActivityRow item={a} user={userById(app.users, a.userId)} />
                    </button>
                  );
                })}
              </div>
            ))}
            {recentActivity.length === 0 && <p className="text-xs text-muted px-3">No recent activity.</p>}
          </div>
        </Card>
      </div>

      {/* ---- Upcoming Deadlines ---- */}
      <Card className="p-5 overflow-x-auto animate-fadeUp" style={{ animationDelay: '420ms' }}>
        <h3 className="text-[10px] uppercase tracking-widest text-muted mb-4">Upcoming Deadlines</h3>
        {deadlineItems.length === 0 ? <p className="text-sm text-muted">Nothing due in the next 7 days.</p> : (
          <table className="w-full text-sm">
            <thead><tr className="text-left text-[10px] uppercase tracking-widest text-muted border-b border-border">
              <th className="py-2 pr-3">Date</th><th className="py-2 pr-3">Item</th><th className="py-2 pr-3">Type</th><th className="py-2">Owner</th>
            </tr></thead>
            <tbody>
              {deadlineItems.map((d, i) => (
                <tr key={i} onClick={d.go || undefined} className={`border-b border-border last:border-0 ${d.go ? 'cursor-pointer hover:bg-elevated' : ''}`}>
                  <td className="py-2.5 pr-3 font-mono text-muted">{fmtDateShort(d.date)}</td>
                  <td className="py-2.5 pr-3 text-primary">{d.label}</td>
                  <td className="py-2.5 pr-3"><span className="text-[10px] uppercase font-mono text-muted bg-elevated rounded px-1.5 py-0.5">{d.type}</span></td>
                  <td className="py-2.5"><Avatar user={userById(app.users, d.ownerId)} size="sm" /></td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </Card>

      <ExchangeRatesModal open={ratesModalOpen} onClose={() => setRatesModalOpen(false)} />
    </div>
  );
}

/* ======================================================================
   10. PIPELINE / KANBAN
   ====================================================================== */

// Wraps window.SakanIntelligenceReport (registered by IntelligenceReport.jsx,
// loaded as its own Babel-transformed script tag before this file - see the
// comment at the top of that file for why it's not a plain `import`).
// `ctx` is the live useApp() object; the report engine reads straight off
// it, so there is no separate reporting dataset to fall out of sync.
function IntelligenceReportView({ ctx, scope, projectIds, onBack }) {
  const Comp = window.SakanIntelligenceReport;
  if (!Comp) return <div className="p-8 text-center text-sm text-muted">Loading report engine…</div>;
  return <Comp ctx={ctx} scope={scope} projectIds={projectIds} onBack={onBack} />;
}

// Registry of report types shown as sub-tabs under Pipeline -> Report, one
// level below Board | Timeline | Report. Project Overview is the only
// entry today; a future report type is a new {key, label, Component} row
// here, not a rewrite of this tab strip.
const REPORT_TYPES = [
  { key: 'intelligence', label: 'Project Overview', Component: ProjectIntelligencePanel },
];

function ReportTab() {
  const app = useApp();
  const [reportType, setReportType] = useState(REPORT_TYPES[0].key);
  const Active = REPORT_TYPES.find(r => r.key === reportType)?.Component || REPORT_TYPES[0].Component;
  return (
    <div>
      {REPORT_TYPES.length > 1 && (
        <div className="flex rounded-lg border border-border overflow-hidden mb-4 w-fit">
          {REPORT_TYPES.map(r => (
            <button key={r.key} onClick={() => setReportType(r.key)} className={`px-4 py-1.5 text-sm ${reportType === r.key ? 'bg-gold/15 text-gold' : 'text-muted hover:bg-elevated'}`}>{r.label}</button>
          ))}
        </div>
      )}
      <Active />
    </div>
  );
}

// One unified picker - search by name, check any number of projects (one,
// several, or all), then Generate. Replaces the old three-button All/
// Select/Single mode toggle, which forced picking a mode before you could
// even see the project list. Reads an initial selection from route.params
// so Project Detail's "Project Overview" link lands here pre-filtered
// to one project and generates immediately, no extra click required.
function ProjectIntelligencePanel() {
  const app = useApp();
  const initial = app.route.params || {};
  const [selected, setSelected] = useState(initial.reportProjectIds || []);
  const [search, setSearch] = useState('');
  const [generating, setGenerating] = useState(false);
  const [generated, setGenerated] = useState(false);
  const autoGenerated = useRef(false);
  const visible = app.projects.filter(p => canSeeProject(app.currentUser, p) && !p.isArchived);
  const filtered = search.trim() ? visible.filter(p => p.name.toLowerCase().includes(search.trim().toLowerCase())) : visible;
  const allFilteredSelected = filtered.length > 0 && filtered.every(p => selected.includes(p.id));

  // Report generation always re-fetches every entity the engine reads
  // first (see refetchReportData), so "Generated <date>" on the report
  // itself is never more than a few seconds stale, not up-to-30s-stale
  // like ordinary cached browsing.
  const generate = useCallback(async () => {
    setGenerating(true);
    await app.refetchReportData();
    setGenerating(false);
    setGenerated(true);
  }, [app]);

  useEffect(() => {
    if (initial.reportProjectIds?.length && !autoGenerated.current) {
      autoGenerated.current = true;
      generate();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  if (generated && selected.length > 0) {
    const scope = selected.length === 1 ? 'single' : (selected.length === visible.length ? 'all' : 'combined');
    return (
      <div>
        <button onClick={() => setGenerated(false)} className="text-xs text-muted hover:text-primary mb-3 inline-flex items-center gap-1">
          <ChevronLeft className="w-3 h-3" /> Change selection ({selected.length} project{selected.length > 1 ? 's' : ''})
        </button>
        <IntelligenceReportView ctx={app} scope={scope} projectIds={selected} onBack={() => { setGenerated(false); setSelected([]); }} />
      </div>
    );
  }

  return (
    <Card className="p-6 max-w-2xl">
      <h3 className="font-display text-lg text-primary mb-1">Project Overview</h3>
      <p className="text-sm text-muted mb-4">Search or check projects below, then generate — one project, a few, or all of them.</p>
      <Input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search projects by name…" className="mb-3" />
      {visible.length === 0 ? (
        <p className="text-sm text-muted">No active projects to report on yet.</p>
      ) : (
        <>
          <label className="flex items-center gap-2 text-sm font-medium text-primary cursor-pointer pb-2 mb-2 border-b border-border">
            <input
              type="checkbox"
              checked={allFilteredSelected}
              onChange={e => {
                const filteredIds = filtered.map(p => p.id);
                setSelected(s => e.target.checked ? Array.from(new Set([...s, ...filteredIds])) : s.filter(id => !filteredIds.includes(id)));
              }}
              className="accent-gold"
            />
            Select all{search.trim() ? ' (matching search)' : ''}
          </label>
          <div className="space-y-2 max-h-64 overflow-y-auto">
            {filtered.length === 0 ? (
              <p className="text-sm text-muted">No projects match "{search}".</p>
            ) : filtered.map(p => (
              <label key={p.id} className="flex items-center gap-2 text-sm text-primary cursor-pointer">
                <input
                  type="checkbox"
                  checked={selected.includes(p.id)}
                  onChange={e => setSelected(s => e.target.checked ? [...s, p.id] : s.filter(x => x !== p.id))}
                  className="accent-gold"
                />
                {p.name}
              </label>
            ))}
          </div>
        </>
      )}
      <div className="flex items-center justify-between pt-4 mt-4 border-t border-border">
        <span className="text-xs text-muted">{selected.length} project{selected.length === 1 ? '' : 's'} selected</span>
        <Button size="sm" disabled={!selected.length} loading={generating} onClick={generate}>Generate</Button>
      </div>
    </Card>
  );
}

// Pipeline -> Timeline tab. Each bar spans a project's own startDate->target
// on one shared calendar axis, same as before, but now:
// 1. the bar is split into a real-progress fill (computeProgress) inside a
//    lighter full-range envelope, instead of one flat block regardless of
//    how much has actually happened;
// 2. a "today" line crosses every row, so a project's filled portion vs.
//    where today falls inside its own bar shows pace at a glance;
// 3. the envelope/fill is tinted by computeHealth (On Track/At Risk/Behind)
//    - the same signal the Dashboard and Project Overview report already use;
// 4. a small triangle marks computeForecast's projected completion date,
//    separate from the target-date tick, so slippage is visible directly
//    on the axis instead of only as a number elsewhere;
// 5. projects with a missing/invalid start or target date are excluded from
//    the shared axis instead of poisoning every row's position with NaN.
function TimelineTab({ visible, app }) {
  const today = todayStr();
  const historical = useMemo(() => computeHistoricalBaseline(app), [app]);

  const rows = visible
    .filter(p => p.startDate && p.target && !isNaN(new Date(p.startDate)) && !isNaN(new Date(p.target)))
    .map(p => {
      const slice = selectProjectSlice(app, p.id);
      const progress = computeProgress(slice);
      const health = computeHealth(slice);
      const forecastDate = p.status === 'Complete' ? null : computeForecast(slice, historical).onTimeDate;
      return { p, progress, health, forecastDate };
    });

  if (!rows.length) {
    return <EmptyState icon={Calendar} title="Nothing to schedule yet" message="Projects need a start and target date to appear here." />;
  }

  const allDates = [
    ...rows.flatMap(r => [new Date(r.p.startDate), new Date(r.p.target)]),
    new Date(today),
    ...rows.filter(r => r.forecastDate).map(r => new Date(r.forecastDate)),
  ];
  const min = new Date(Math.min(...allDates)), max = new Date(Math.max(...allDates));
  const span = Math.max(1, max - min);
  const pctOf = (d) => Math.max(0, Math.min(100, ((new Date(d) - min) / span) * 100));
  const todayPct = pctOf(today);

  return (
    <Card className="p-5 overflow-x-auto">
      <div className="min-w-[760px]">
        <div className="flex items-center gap-4 mb-2 relative h-4">
          <div className="w-40 shrink-0" />
          <div className="flex-1 relative h-full">
            <span className="absolute -translate-x-1/2 text-[10px] font-mono text-muted whitespace-nowrap" style={{ left: `${todayPct}%` }}>Today</span>
          </div>
          <div className="w-40 shrink-0" />
        </div>
        <div className="space-y-4">
          {rows.map(({ p, progress, health, forecastDate }) => {
            const start = new Date(p.startDate), end = new Date(p.target);
            const left = pctOf(p.startDate);
            const width = Math.max(3, ((end - start) / span) * 100);
            const tone = HEALTH_TONE[health] || 'info';
            const forecastPct = forecastDate ? pctOf(forecastDate) : null;
            return (
              <div key={p.id} className="flex items-center gap-4">
                <div className="w-40 shrink-0 text-sm text-primary truncate">{p.name}</div>
                <div className="flex-1 h-6 relative bg-elevated rounded-full border border-border overflow-visible">
                  <div className="absolute top-0 bottom-0 w-px bg-primary/25" style={{ left: `${todayPct}%` }} />
                  <div className={`absolute h-full rounded-full border overflow-hidden ${TONE_CLASSES[tone]}`} style={{ left: `${left}%`, width: `${width}%` }}>
                    <div className="h-full pyramid-progress" style={{ width: `${progress}%`, backgroundColor: TONE_HEX[tone] }} />
                  </div>
                  {forecastPct != null && (
                    <div
                      className="absolute z-10"
                      style={{ left: `calc(${forecastPct}% - 4px)`, top: -5, width: 0, height: 0, borderLeft: '4px solid transparent', borderRight: '4px solid transparent', borderTop: `6px solid ${TONE_HEX[tone]}` }}
                      title={`Forecast completion: ${fmtDateShort(forecastDate)}`}
                    />
                  )}
                </div>
                <div className="w-40 shrink-0 text-xs font-mono text-muted text-right leading-tight">
                  <div>{p.status} · {progress}%</div>
                  <div>{fmtDateShort(p.target)}</div>
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </Card>
  );
}

function Pipeline() {
  const app = useApp();
  const [tab, setTab] = useState(app.route.params?.tab || 'board');
  const visible = app.projects.filter(p => canSeeProject(app.currentUser, p) && !p.isArchived);

  const moveProject = (project, status) => {
    if (!canEditProject(app.currentUser, project)) { app.toast('Only the owner or an admin can move this project.', 'error'); return; }
    if (status === 'Complete' && !app.allComponentsComplete(project.id)) { app.toast('Complete all components first before moving to Complete.', 'error'); return; }
    app.updateProject(project.id, { status });
    app.microToast('Saved');
  };

  return (
    <div className="p-4 md:p-6 max-w-[1500px] mx-auto animate-fadeUp">
      <div className="flex items-center justify-between mb-5 flex-wrap gap-3">
        <h1 className="font-display italic text-2xl text-primary tracking-tight">Pipeline</h1>
        <div className="flex rounded-lg border border-border overflow-hidden">
          {['board', 'timeline', 'report'].map(t => (
            <button key={t} onClick={() => setTab(t)} className={`px-4 py-1.5 text-sm capitalize ${tab === t ? 'bg-gold/15 text-gold' : 'text-muted hover:bg-elevated'}`}>{t}</button>
          ))}
        </div>
      </div>

      {tab === 'report' ? (
        <ReportTab />
      ) : tab === 'board' ? (
        <div className="flex gap-4 overflow-x-auto pb-4">
          {PROJECT_STATUSES.map(status => {
            const items = visible.filter(p => p.status === status);
            return (
              <div key={status} className="w-72 shrink-0">
                <div className="flex items-center justify-between mb-3 px-1">
                  <span className="text-sm font-medium text-primary">{status}</span>
                  <span className="text-xs font-mono text-muted bg-elevated rounded-full px-2 py-0.5">{items.length}</span>
                </div>
                <div className="space-y-3 min-h-[100px]">
                  {items.map(p => (
                    <Card key={p.id} hover className="p-3" onClick={() => app.navigate('projectDetail', p.id)}>
                      <div className="flex items-start justify-between mb-2">
                        <h5 className="font-display text-primary text-sm leading-tight pr-2">{p.name}</h5>
                        <PriorityTag priority={p.priority} />
                      </div>
                      <PyramidProgress progress={computeProgress(selectProjectSlice(app, p.id))} height="h-1.5" />
                      <div className="flex items-center justify-between mt-2.5">
                        <Avatar user={userById(app.users, p.owner)} size="sm" />
                        {p.blocking ? <span className="w-1.5 h-1.5 rounded-full bg-oud animate-pulseDot" /> : <span />}
                        {canEditProject(app.currentUser, p) && (
                          <Dropdown
                            align="right"
                            trigger={<button onClick={e => e.stopPropagation()} className="text-muted hover:text-primary"><MoreHorizontal className="w-4 h-4" /></button>}
                          >
                            {PROJECT_STATUSES.filter(s => s !== status).map(s => (
                              <button key={s} onClick={(e) => { e.stopPropagation(); moveProject(p, s); }} className="w-full text-left px-3 py-1.5 text-xs text-primary hover:bg-base">Move to {s}</button>
                            ))}
                          </Dropdown>
                        )}
                      </div>
                    </Card>
                  ))}
                  {items.length === 0 && (
                    <div className="flex flex-col items-center justify-center text-center py-8 border border-dashed border-border rounded-lg">
                      <Inbox className="w-8 h-8 text-stale/40 mb-2" strokeWidth={1.5} />
                      <span className="text-xs text-muted">Nothing here yet</span>
                    </div>
                  )}
                </div>
              </div>
            );
          })}
        </div>
      ) : (
        <TimelineTab visible={visible} app={app} />
      )}
    </div>
  );
}

/* ======================================================================
   11. PROJECT DETAIL
   ====================================================================== */

function ComponentCostTooltip({ lines }) {
  return (
    <div className="pointer-events-none absolute z-50 hidden group-hover:block bottom-full right-0 mb-2 w-72 max-h-56 overflow-y-auto rounded-lg border border-border bg-elevated px-3 py-2 shadow-2xl">
      {lines.map((l, i) => (
        <div key={i} className="text-[11px] font-mono text-primary leading-snug py-0.5 border-b border-border/50 last:border-0">{l}</div>
      ))}
    </div>
  );
}

function ProjectDetail({ id }) {
  const app = useApp();
  const project = app.projects.find(p => p.id === id);
  const [commentText, setCommentText] = useState('');
  const fileInputRef = useRef(null);

  if (!project) return <EmptyState title="Project not found" message="It may have been deleted." actionLabel="Back to Dashboard" onAction={() => app.navigate('dashboard')} />;
  if (!canSeeProject(app.currentUser, project)) return <EmptyState icon={AlertTriangle} title="No access" message="You don't have visibility into this project." actionLabel="Back to Dashboard" onAction={() => app.navigate('dashboard')} />;

  const owner = userById(app.users, project.owner);
  const projQuotes = app.quotations.filter(q => q.projectId === id);
  const projSampling = app.samplingProjects.filter(sp => sp.linkedProjectId === id);
  const projRounds = app.sampleRounds.filter(r => projSampling.some(sp => sp.id === r.samplingProjectId));
  const projComponents = app.components.filter(c => c.projectId === id);
  const projComments = app.comments.filter(c => c.entityType === 'project' && c.entityId === id);
  const relatedActivity = app.activity.filter(a => a.detail.includes(project.name)).slice(0, project.isArchived ? 200 : 8);
  const allDone = app.allComponentsComplete(id);
  const editable = canEditProject(app.currentUser, project);
  const canDelete = editable && app.canDeleteProject(id);

  const compCostItems = projComponents.map(c => {
    const currency = c.currency || 'EUR';
    const qty = parseFloat(String(c.orderQty ?? '').replace(/[^0-9.]/g, '')) || 0;
    const priceUsd = toUSD(c.price || 0, currency, app.fxRates);
    return { c, currency, qty, priceUsd, totalUsd: priceUsd * qty };
  });
  const totalProjectCostUsd = compCostItems.reduce((s, i) => s + i.totalUsd, 0);
  const totalProjectQty = compCostItems.reduce((s, i) => s + i.qty, 0);
  const aveCostPerPcsUsd = totalProjectQty > 0 ? totalProjectCostUsd / totalProjectQty : 0;
  const allComponentPricesUnset = projComponents.length > 0 && projComponents.every(c => !c.price);
  const compCostTooltipLines = compCostItems.map(i => {
    const priceLabel = i.c.price ? fmtCurrency(i.c.price, i.currency) : `${fmtCurrency(0, i.currency)} (price not set)`;
    const qtyLabel = i.qty > 0 ? i.qty.toLocaleString('en-US') : '0 (qty not set)';
    const originalTotal = (i.c.price || 0) * i.qty;
    return `${i.c.name}: ${priceLabel} × ${qtyLabel} = ${fmtCurrency(originalTotal, i.currency, { decimals: 0 })} (≈ ${fmtCurrency(i.totalUsd, 'USD', { decimals: 0 })})`;
  });

  const hasActiveSampling = projSampling.some(sp => !['Moved to Production', 'Cancelled'].includes(sp.status));
  const showQuotations = project.isArchived ? projQuotes.length > 0 : (project.status === 'Development' || project.status === 'Quotation');
  const showSampling = project.isArchived ? projSampling.length > 0 : (project.status === 'Sampling' || hasActiveSampling);
  const showComponents = project.isArchived ? projComponents.length > 0 : (project.status === 'Production' || project.status === 'Shipping' || projComponents.length > 0);

  const projAttachments = project.isArchived
    ? app.attachments.filter(a =>
        (a.entityType === 'project' && a.entityId === id) ||
        (a.entityType === 'quotation' && projQuotes.some(q => q.id === a.entityId)) ||
        (a.entityType === 'samplingProject' && projSampling.some(sp => sp.id === a.entityId)) ||
        (a.entityType === 'sampleRound' && projRounds.some(r => r.id === a.entityId)) ||
        (a.entityType === 'component' && projComponents.some(c => c.id === a.entityId)))
    : app.attachments.filter(a => a.entityType === 'project' && a.entityId === id);

  const attachmentSourceLabel = (a) => {
    if (a.entityType === 'project') return 'Project';
    if (a.entityType === 'quotation') return `Quotation — ${projQuotes.find(q => q.id === a.entityId)?.supplierName || ''}`;
    if (a.entityType === 'samplingProject') return `Sampling — ${projSampling.find(sp => sp.id === a.entityId)?.name || ''}`;
    if (a.entityType === 'sampleRound') { const r = projRounds.find(x => x.id === a.entityId); const sp = projSampling.find(s => s.id === r?.samplingProjectId); return `Sample ${r?.version || ''} — ${sp?.name || ''}`; }
    if (a.entityType === 'component') return `Component — ${projComponents.find(c => c.id === a.entityId)?.name || ''}`;
    return '';
  };

  const handleDelete = async () => {
    const ok = await app.confirm({ title: `Delete ${project.name}?`, message: 'This cannot be undone.', confirmLabel: 'Delete' });
    if (ok) { app.deleteProject(id); app.navigate('quotations'); }
  };

  const handleMarkComplete = async () => {
    const ok = await app.confirm({ title: 'Mark project complete?', message: `${project.name} will move to Accomplished Projects and become read-only.`, confirmLabel: 'Mark Complete' });
    if (ok) app.markProjectComplete(id);
  };

  return (
    <div className="p-4 md:p-6 max-w-[1300px] mx-auto animate-fadeUp">
      <button onClick={() => app.navigate('dashboard')} className="flex items-center gap-1.5 text-sm text-muted hover:text-primary mb-4">
        <ArrowLeft className="w-4 h-4" /> Back
      </button>

      {project.isArchived && (
        <div className="mb-5 rounded-xl border border-sage/40 bg-sage/10 px-5 py-4 flex items-center gap-3">
          <CheckCircle2 className="w-5 h-5 text-sage shrink-0" />
          <span className="text-sage font-medium">Accomplished on {fmtDate(project.completedDate)}</span>
        </div>
      )}

      <div className="flex flex-wrap items-center justify-between gap-3 mb-4">
        <div className="flex items-center gap-3 flex-wrap">
          <h1 className="font-display italic text-4xl text-primary tracking-tight">{project.name}</h1>
          <PriorityTag priority={project.priority} />
          <StatusPill status={project.status} />
        </div>
        <div className="flex items-center gap-2">
          <Button
            variant="secondary"
            size="sm"
            icon={BarChart3}
            onClick={() => app.navigate('pipeline', null, { tab: 'report', reportProjectIds: [project.id] })}
          >
            Project Overview
          </Button>
          {!project.isArchived && canDelete && (
            <Button variant="ghost" size="sm" icon={Trash2} onClick={handleDelete}>Delete</Button>
          )}
          {!project.isArchived && (
            <Button variant="primary" size="sm" icon={CheckCircle2} disabled={!allDone || !editable} onClick={handleMarkComplete}>Mark Complete</Button>
          )}
        </div>
      </div>

      {project.blocking && !project.isArchived && (
        <div className="mb-5 rounded-xl border border-oud/40 bg-oud/10 px-5 py-3 flex items-center gap-3">
          <AlertTriangle className="w-4 h-4 text-oud shrink-0" />
          <span className="text-sm text-primary"><span className="text-oud font-medium">Blocked:</span> {project.blocking} — waiting on {project.waiting}</span>
        </div>
      )}

      {allDone && !project.isArchived && (
        <div className="mb-5 rounded-xl border border-gold/40 bg-gold/10 px-5 py-3 flex items-center justify-between gap-3">
          <span className="text-sm text-primary">All components complete. Ready to mark this project as done.</span>
          {editable && <Button size="sm" onClick={handleMarkComplete}>Mark Complete</Button>}
        </div>
      )}

      <Card className="p-5 mb-5">
        <LifecycleStepper status={project.status} />
      </Card>

      <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
        <Card className="p-4 md:col-span-2">
          <div className="text-xs text-muted mb-2">Progress</div>
          <PyramidProgress progress={computeProgress(selectProjectSlice(app, id))} height="h-3" showLabel />
        </Card>
        <Card className="p-4">
          <div className="text-xs text-muted mb-1">Duration</div>
          <div className="font-display text-2xl text-primary">{project.isArchived ? daysBetween(project.startDate, project.completedDate) : durationSince(project.startDate)} <span className="text-sm text-muted">days</span></div>
        </Card>
      </div>

      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
        <div className="lg:col-span-2 space-y-6">
          {showQuotations && (
            <Card className="p-5">
              <h3 className="font-display text-lg text-primary mb-3">Linked Quotations</h3>
              {projQuotes.length === 0 ? (
                <EmptyState icon={FileText} title="No quotations yet" message="Add supplier quotes from the Quotations page." actionLabel="Go to Quotations" onAction={() => app.navigate('quotationsByProject', id)} />
              ) : (
                <div className="space-y-2">
                  {projQuotes.map(q => (
                    <div key={q.id} className="flex items-center justify-between px-3 py-2.5 rounded-lg border border-border bg-base">
                      <div>
                        <span className="text-sm text-primary">{q.supplierName}</span>
                        {(q.lineItems || []).map(li => (
                          <div key={li.id} className="text-xs text-muted font-mono">{li.itemName} — {fmtCurrency(li.unitPrice, q.currency)}/unit · MOQ {li.moq} · {li.status || 'Pending'}</div>
                        ))}
                      </div>
                      <StatusPill status={deriveQuotationStatus(q)} size="sm" />
                    </div>
                  ))}
                </div>
              )}
            </Card>
          )}
          {showSampling && (
            <Card className="p-5">
              <h3 className="font-display text-lg text-primary mb-3">Linked Sampling Projects</h3>
              {projSampling.length === 0 ? <EmptyState icon={FlaskConical} title="No sampling projects" message="Select a quote to auto-create one." /> : (
                <div className="grid sm:grid-cols-2 gap-3">
                  {projSampling.map(sp => (
                    <Card key={sp.id} hover className="p-3" onClick={() => app.navigate('samplingDetail', sp.id)}>
                      <div className="flex items-center justify-between mb-1.5">
                        <h5 className="font-display text-sm text-primary">{sp.name}</h5>
                        <StatusPill status={sp.status} size="sm" />
                      </div>
                      <SamplingStageTracker status={sp.status} />
                    </Card>
                  ))}
                </div>
              )}
            </Card>
          )}
          {showComponents && (
            <Card className="p-5">
              <h3 className="font-display text-lg text-primary mb-3">Linked Components</h3>
              {projComponents.length === 0 ? <EmptyState icon={Package} title="No components yet" message="Approve a sample to auto-create one." /> : (
                <div className="overflow-x-auto">
                  <table className="w-full text-sm">
                    <thead><tr className="text-left text-[10px] uppercase tracking-widest text-muted border-b border-border">
                      <th className="py-2 pr-3">Component</th><th className="py-2 pr-3">Supplier</th><th className="py-2 pr-3">Status</th><th className="py-2 pr-3">Price</th><th className="py-2">Duration</th>
                    </tr></thead>
                    <tbody>
                      {projComponents.map(c => (
                        <tr key={c.id} className="border-b border-border last:border-0">
                          <td className="py-2.5 pr-3 text-primary">{c.name}</td>
                          <td className="py-2.5 pr-3 text-muted">{supplierById(app.suppliers, c.supplier)?.name}</td>
                          <td className="py-2.5 pr-3"><StatusPill status={c.status} size="sm" /></td>
                          <td className="py-2.5 pr-3 font-mono text-primary">{fmtCurrency(c.price, c.currency)}</td>
                          <td className="py-2.5 font-mono text-muted">{c.completedDate ? daysBetween(c.startDate, c.completedDate) : durationSince(c.startDate)}d</td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </div>
              )}
            </Card>
          )}

          <Card className="p-5">
            <div className="flex items-center justify-between mb-3">
              <h3 className="font-display text-lg text-primary">{project.isArchived ? 'All Attachments' : 'Attachments'}</h3>
              {!project.isArchived && editable && <Button size="sm" variant="secondary" icon={Upload} onClick={() => fileInputRef.current?.click()}>Upload</Button>}
              <input ref={fileInputRef} type="file" className="hidden" onChange={e => { if (e.target.files[0]) app.addAttachment('project', id, e.target.files[0]); }} />
            </div>
            {project.isArchived && <p className="text-xs text-muted mb-3">Every file uploaded across this project's quotations, sampling rounds, and components — kept for the record.</p>}
            <div className="space-y-2">
              {projAttachments.map(a => (
                <AttachmentRow
                  key={a.id}
                  attachment={a}
                  user={userById(app.users, a.uploadedBy)}
                  onDelete={project.isArchived ? null : app.deleteAttachment}
                  onOpen={app.getAttachmentUrl}
                  sourceLabel={project.isArchived ? attachmentSourceLabel(a) : null}
                />
              ))}
              {projAttachments.length === 0 && <p className="text-xs text-muted">No attachments yet.</p>}
            </div>
          </Card>

          <Card className="p-5">
            <h3 className="font-display text-lg text-primary mb-3">Comments</h3>
            <div className="space-y-4 mb-4">
              {projComments.map(c => <CommentBubble key={c.id} comment={c} user={userById(app.users, c.userId)} />)}
              {projComments.length === 0 && <p className="text-xs text-muted">No comments yet.</p>}
            </div>
            {!project.isArchived && (
              <div className="flex gap-2">
                <CommentComposer value={commentText} onChange={setCommentText} users={app.users} onSubmit={() => { if (commentText.trim()) { app.addComment('project', id, commentText); setCommentText(''); } }} />
                <Button size="sm" onClick={() => { if (commentText.trim()) { app.addComment('project', id, commentText); setCommentText(''); } }}>Post</Button>
              </div>
            )}
          </Card>
        </div>

        <div className="space-y-6">
          <Card className="p-5">
            <h4 className="text-xs uppercase tracking-wide text-muted mb-3">Details</h4>
            <dl className="space-y-2.5 text-sm">
              <div className="flex justify-between"><dt className="text-muted">Owner</dt><dd className="text-primary flex items-center gap-2"><Avatar user={owner} size="sm" />{owner?.name}</dd></div>
              <div className="flex justify-between"><dt className="text-muted">Category</dt><dd className="text-primary">{project.category}</dd></div>
              <div className="flex justify-between"><dt className="text-muted">Launch Wave</dt><dd className="text-primary font-mono">Wave {project.launchWave}</dd></div>
              <div className="flex justify-between"><dt className="text-muted">Start Date</dt><dd className="text-primary font-mono">{fmtDateShort(project.startDate)}</dd></div>
              <div className="flex justify-between"><dt className="text-muted">Target Date</dt><dd className="text-primary font-mono">{fmtDateShort(project.target)}</dd></div>
              {project.completedDate && <div className="flex justify-between"><dt className="text-muted">Completed</dt><dd className="text-sage font-mono">{fmtDateShort(project.completedDate)}</dd></div>}
              {projComponents.length > 0 && (
                <>
                  <div className="relative group flex justify-between items-start pt-1">
                    <dt className="text-xs uppercase tracking-wide text-muted pt-1">Ave. Cost / Pcs</dt>
                    <dd className="text-right">
                      <span className="font-mono text-primary text-lg">${aveCostPerPcsUsd.toFixed(2)} / pcs</span>
                      {allComponentPricesUnset && <div className="text-[10px] text-muted">(prices not set)</div>}
                    </dd>
                    <ComponentCostTooltip lines={compCostTooltipLines} />
                  </div>
                  <div className="relative group flex justify-between items-start">
                    <dt className="text-xs uppercase tracking-wide text-muted pt-1">Total Project Cost</dt>
                    <dd className="text-right">
                      <span className="font-mono text-primary text-lg">{fmtCurrency(totalProjectCostUsd, 'USD', { decimals: 0 })}</span>
                      {allComponentPricesUnset && <div className="text-[10px] text-muted">(prices not set)</div>}
                    </dd>
                    <ComponentCostTooltip lines={compCostTooltipLines} />
                  </div>
                </>
              )}
            </dl>
          </Card>

          {app.currentUser.accessRole === 'Admin' && !project.isArchived && (
            <Card className="p-5">
              <h4 className="text-xs uppercase tracking-wide text-muted mb-3">Visible To</h4>
              <div className="space-y-2">
                <label className="flex items-center gap-2 text-sm text-primary">
                  <input type="checkbox" checked={project.visibleTo.includes('all')} onChange={e => app.updateProjectVisibility(id, e.target.checked ? ['all'] : [])} className="accent-gold" />
                  Everyone
                </label>
                {!project.visibleTo.includes('all') && app.users.map(u => (
                  <label key={u.id} className="flex items-center gap-2 text-sm text-primary">
                    <input type="checkbox" checked={project.visibleTo.includes(u.id)} onChange={e => {
                      const next = e.target.checked ? [...project.visibleTo, u.id] : project.visibleTo.filter(v => v !== u.id);
                      app.updateProjectVisibility(id, next);
                    }} className="accent-gold" />
                    {u.name}
                  </label>
                ))}
              </div>
            </Card>
          )}

          {app.currentUser.accessRole === 'Admin' && !project.isArchived && (
            <Card className="p-5">
              <h4 className="text-xs uppercase tracking-wide text-muted mb-1">Editable By</h4>
              <p className="text-xs text-muted mb-3">Only {owner?.name || 'the owner'} and admins can amend this project by default. Grant edit access to others here.</p>
              <div className="space-y-2">
                {app.users.filter(u => u.id !== project.owner).map(u => (
                  <label key={u.id} className="flex items-center gap-2 text-sm text-primary">
                    <input type="checkbox" checked={project.editableBy?.includes(u.id) || false} onChange={e => {
                      const current = project.editableBy || [];
                      const next = e.target.checked ? [...current, u.id] : current.filter(v => v !== u.id);
                      app.updateProjectEditableBy(id, next);
                    }} className="accent-gold" />
                    {u.name}
                  </label>
                ))}
              </div>
            </Card>
          )}

          <Card className="p-5">
            <h4 className="text-xs uppercase tracking-wide text-muted mb-3">Audit Log</h4>
            <div className="max-h-64 overflow-y-auto divide-y divide-border">
              {relatedActivity.map(a => <ActivityRow key={a.id} item={a} user={userById(app.users, a.userId)} />)}
              {relatedActivity.length === 0 && <p className="text-xs text-muted">No history yet.</p>}
            </div>
          </Card>
        </div>
      </div>
    </div>
  );
}

/* ======================================================================
   12. QUOTATION LIST
   ====================================================================== */

const emptyProjectForm = (app) => ({
  name: '', category: 'Perfume', target: addDays(todayStr(), 45), owner: app.currentUserId, priority: 'Normal',
  estimatedBudget: '', budgetCurrency: 'EUR',
});

function NewProjectModal({ open, onClose }) {
  const app = useApp();
  const [form, setForm] = useState(() => emptyProjectForm(app));
  const submit = () => {
    if (!form.name.trim()) { app.toast('Project name is required.', 'error'); return; }
    const budget = Number(form.estimatedBudget);
    if (!form.estimatedBudget || !(budget > 0)) { app.toast('Estimated budget is required.', 'error'); return; }
    if (!form.budgetCurrency) { app.toast('Currency is required.', 'error'); return; }
    const id = app.createProject({ ...form, estimatedBudget: budget });
    onClose();
    setForm(emptyProjectForm(app));
    app.navigate('projectDetail', id);
  };
  return (
    <Modal open={open} onClose={onClose} title="New Project">
      <div className="space-y-4">
        <div><Label>Project Name</Label><Input autoFocus value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} placeholder="e.g. Amber EDP 50ml" /></div>
        <div className="grid grid-cols-2 gap-3">
          <div>
            <Label>Category</Label>
            <Input list="category-options" value={form.category} onChange={e => setForm(f => ({ ...f, category: e.target.value }))} placeholder="e.g. Perfume" />
            <datalist id="category-options">{CATEGORY_OPTIONS.map(c => <option key={c} value={c} />)}</datalist>
          </div>
          <div><Label>Priority</Label><Select value={form.priority} onChange={e => setForm(f => ({ ...f, priority: e.target.value }))}>{PRIORITY_OPTIONS.map(p => <option key={p} value={p}>{p}</option>)}</Select></div>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <div><Label>Target Date</Label><Input type="date" value={form.target} onChange={e => setForm(f => ({ ...f, target: e.target.value }))} /></div>
          <div><Label>Owner</Label><Select value={form.owner} onChange={e => setForm(f => ({ ...f, owner: e.target.value }))}>{app.users.map(u => <option key={u.id} value={u.id}>{u.name}</option>)}</Select></div>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <div>
            <Label>Estimated Budget</Label>
            <Input type="number" min="0" step="0.01" value={form.estimatedBudget} onChange={e => setForm(f => ({ ...f, estimatedBudget: e.target.value }))} placeholder="e.g. 30000" />
          </div>
          <div>
            <Label>Currency</Label>
            <Select value={form.budgetCurrency} onChange={e => setForm(f => ({ ...f, budgetCurrency: e.target.value }))}>
              {CURRENCIES.map(c => <option key={c.code} value={c.code}>{c.code} ({c.symbol})</option>)}
            </Select>
          </div>
        </div>
        <div className="flex justify-end gap-2 pt-2">
          <Button variant="secondary" onClick={onClose}>Cancel</Button>
          <Button onClick={submit}>Create Project</Button>
        </div>
      </div>
    </Modal>
  );
}

function emptyLineItem() {
  return { itemName: '', qty: '', unitPrice: '', moq: '', moldCost: '', samplingCost: '', prodLeadTime: '', samplingLeadTime: '' };
}

function LineItemCard({ item, canRemove, onChange, onRemove }) {
  const set = (field, val) => onChange({ ...item, [field]: val });
  const locked = !!item.locked;
  return (
    <div className={`rounded-lg border border-border p-3 space-y-3 ${locked ? 'bg-elevated/40' : 'bg-base'}`}>
      <div className="flex items-start gap-2">
        <div className="flex-1">
          <Label>Item Name</Label>
          <Input disabled={locked} value={item.itemName} onChange={e => set('itemName', e.target.value)} placeholder="e.g. Bottle EDP — Gold Brushed" />
        </div>
        {locked ? (
          <span className="mt-6 text-[10px] text-muted shrink-0 whitespace-nowrap" title="In sampling — locked">🔒 In Sampling</span>
        ) : canRemove && (
          <button type="button" onClick={onRemove} title="Remove item" className="mt-6 text-muted hover:text-oud shrink-0"><X className="w-4 h-4" /></button>
        )}
      </div>
      <div className="grid grid-cols-3 gap-3">
        <div><Label>Qty</Label><Input disabled={locked} value={item.qty} onChange={e => set('qty', e.target.value)} placeholder="30,000" /></div>
        <div><Label>Unit Price</Label><Input disabled={locked} type="number" step="0.01" value={item.unitPrice} onChange={e => set('unitPrice', e.target.value)} /></div>
        <div><Label>MOQ</Label><Input disabled={locked} value={item.moq} onChange={e => set('moq', e.target.value)} placeholder="30,000" /></div>
      </div>
      <div className="grid grid-cols-2 gap-3">
        <div><Label>Mold Cost</Label><Input disabled={locked} type="number" value={item.moldCost} onChange={e => set('moldCost', e.target.value)} /></div>
        <div><Label>Sampling Cost</Label><Input disabled={locked} type="number" value={item.samplingCost} onChange={e => set('samplingCost', e.target.value)} /></div>
      </div>
      <div className="grid grid-cols-2 gap-3">
        <div><Label>Prod. Lead (days)</Label><Input disabled={locked} type="number" value={item.prodLeadTime} onChange={e => set('prodLeadTime', e.target.value)} /></div>
        <div><Label>Sampling Lead (days)</Label><Input disabled={locked} type="number" value={item.samplingLeadTime} onChange={e => set('samplingLeadTime', e.target.value)} /></div>
      </div>
    </div>
  );
}

function LineItemsField({ items, onChange }) {
  const list = items?.length ? items : [emptyLineItem()];
  const update = (i, item) => onChange(list.map((it, idx) => idx === i ? item : it));
  const add = () => onChange([...list, emptyLineItem()]);
  const remove = i => onChange(list.filter((_, idx) => idx !== i));
  const removableCount = list.filter(it => !it.locked).length;
  return (
    <div>
      <Label>Line Items</Label>
      <div className="space-y-3">
        {list.map((item, i) => (
          <LineItemCard key={item.id || i} item={item} canRemove={!item.locked && removableCount > 1} onChange={val => update(i, val)} onRemove={() => remove(i)} />
        ))}
      </div>
      <button type="button" onClick={add} className="mt-2 flex items-center gap-1.5 text-xs font-medium text-gold hover:text-goldbright">
        <Plus className="w-3.5 h-3.5" /> Add Item
      </button>
    </div>
  );
}

function AddQuoteModal({ open, onClose, projectId }) {
  const app = useApp();
  const allSuppliers = [...app.suppliers];
  const emptyForm = { supplierId: allSuppliers[0]?.id || '', currency: 'EUR', validUntil: addDays(todayStr(), 30), referenceNo: '', lineItems: [emptyLineItem()] };
  const [form, setForm] = useState(emptyForm);
  const [file, setFile] = useState(null);
  const fileInputRef = useRef(null);

  const submit = () => {
    if (!form.supplierId) { app.toast('Supplier is required.', 'error'); return; }
    const cleanItems = form.lineItems.filter(li => li.itemName.trim() || li.unitPrice !== '');
    if (!cleanItems.length) { app.toast('At least one item is required.', 'error'); return; }
    if (cleanItems.some(li => !li.itemName.trim() || !li.unitPrice)) { app.toast('Each item needs a name and unit price.', 'error'); return; }
    (async () => {
      const id = await app.addQuotation(projectId, { ...form, lineItems: cleanItems });
      if (id && file) app.addAttachment('quotation', id, file);
    })();
    onClose();
    setForm(emptyForm);
    setFile(null);
  };
  return (
    <Modal open={open} onClose={onClose} title="Add Quotation" width="max-w-2xl">
      <div className="space-y-4">
        <div className="grid grid-cols-2 gap-3">
          <div><Label>Supplier</Label><Select value={form.supplierId} onChange={e => setForm(f => ({ ...f, supplierId: e.target.value }))}>{allSuppliers.map(s => <option key={s.id} value={s.id}>{s.name} ({s.country})</option>)}</Select></div>
          <div><Label>Currency</Label><Select value={form.currency} onChange={e => setForm(f => ({ ...f, currency: e.target.value }))}>{CURRENCIES.map(c => <option key={c.code} value={c.code}>{c.code} ({c.symbol})</option>)}</Select></div>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <div><Label>Valid Until</Label><Input type="date" value={form.validUntil} onChange={e => setForm(f => ({ ...f, validUntil: e.target.value }))} /></div>
          <div><Label>Quot. Ref No.</Label><Input value={form.referenceNo} onChange={e => setForm(f => ({ ...f, referenceNo: e.target.value }))} placeholder="e.g. supplier's own quote number" /></div>
        </div>
        <LineItemsField items={form.lineItems} onChange={lineItems => setForm(f => ({ ...f, lineItems }))} />
        <div>
          <Label>Attachment (optional)</Label>
          <div className="flex items-center gap-2">
            <Button type="button" variant="secondary" size="sm" icon={Paperclip} onClick={() => fileInputRef.current?.click()}>{file ? 'Change File' : 'Choose File'}</Button>
            {file && <span className="text-xs text-muted truncate max-w-[200px]">{file.name}</span>}
            {file && <button type="button" onClick={() => setFile(null)} className="text-muted hover:text-oud"><X className="w-3.5 h-3.5" /></button>}
          </div>
          <input ref={fileInputRef} type="file" className="hidden" onChange={e => { if (e.target.files[0]) setFile(e.target.files[0]); }} />
        </div>
        <div className="flex justify-end gap-2 pt-2"><Button variant="secondary" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Quote</Button></div>
      </div>
    </Modal>
  );
}

function EditQuoteModal({ open, onClose, quotation }) {
  const app = useApp();
  const [form, setForm] = useState(null);

  useEffect(() => {
    if (quotation) {
      setForm({
        currency: quotation.currency || 'EUR', validUntil: quotation.validUntil, referenceNo: quotation.referenceNo || '',
        lineItems: quotation.lineItems?.length ? quotation.lineItems.map(li => ({ ...li })) : [emptyLineItem()],
      });
    }
  }, [quotation]);

  if (!form) return null;

  const submit = () => {
    const lockedItems = form.lineItems.filter(li => li.locked);
    const editableItems = form.lineItems.filter(li => !li.locked);
    const cleanEditable = editableItems.filter(li => (li.itemName || '').toString().trim() || li.unitPrice !== '');
    if (!cleanEditable.length && !lockedItems.length) { app.toast('At least one item is required.', 'error'); return; }
    if (cleanEditable.some(li => !(li.itemName || '').toString().trim() || !li.unitPrice)) { app.toast('Each item needs a name and unit price.', 'error'); return; }
    const mergedLineItems = [
      ...lockedItems,
      ...cleanEditable.map(li => ({
        id: li.id || crypto.randomUUID(), itemName: li.itemName.trim(), qty: Number(li.qty) || 0, unitPrice: Number(li.unitPrice) || 0,
        moq: li.moq || '', moldCost: Number(li.moldCost) || 0, samplingCost: Number(li.samplingCost) || 0,
        prodLeadTime: Number(li.prodLeadTime) || 0, samplingLeadTime: Number(li.samplingLeadTime) || 0,
        status: li.status || 'Pending', approvedReason: li.approvedReason, rejectedReason: li.rejectedReason,
        samplingProjectId: li.samplingProjectId || null, locked: false,
      })),
    ];
    app.updateQuotation(quotation.id, { currency: form.currency, validUntil: form.validUntil, referenceNo: form.referenceNo, lineItems: mergedLineItems });
    onClose();
  };

  return (
    <Modal open={open} onClose={onClose} title={`Edit Quote — ${quotation?.supplierName || ''}`} width="max-w-2xl">
      <div className="space-y-4">
        <div className="grid grid-cols-2 gap-3">
          <div><Label>Currency</Label><Select value={form.currency} onChange={e => setForm(f => ({ ...f, currency: e.target.value }))}>{CURRENCIES.map(c => <option key={c.code} value={c.code}>{c.code} ({c.symbol})</option>)}</Select></div>
          <div><Label>Valid Until</Label><Input type="date" value={form.validUntil} onChange={e => setForm(f => ({ ...f, validUntil: e.target.value }))} /></div>
          <div className="col-span-2"><Label>Quot. Ref No.</Label><Input value={form.referenceNo} onChange={e => setForm(f => ({ ...f, referenceNo: e.target.value }))} placeholder="e.g. supplier's own quote number" /></div>
        </div>
        <LineItemsField items={form.lineItems} onChange={lineItems => setForm(f => ({ ...f, lineItems }))} />
        <div className="flex justify-end gap-2 pt-2"><Button variant="secondary" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save Changes</Button></div>
      </div>
    </Modal>
  );
}

function EditableCell({ value, onSave, type = 'text', mono = false, currency = null }) {
  const [editing, setEditing] = useState(false);
  const [val, setVal] = useState(value);
  useEffect(() => setVal(value), [value]);
  if (editing) {
    return (
      <input
        autoFocus type={type} value={val}
        onChange={e => setVal(e.target.value)}
        onBlur={() => { setEditing(false); if (val !== value) onSave(val); }}
        onKeyDown={e => { if (e.key === 'Enter') e.target.blur(); }}
        className={`w-full bg-elevated border border-gold/40 rounded px-1.5 py-0.5 text-sm ${mono ? 'font-mono' : ''}`}
      />
    );
  }
  return (
    <span onClick={() => setEditing(true)} className={`cursor-text hover:bg-elevated rounded px-1 -mx-1 ${mono ? 'font-mono' : ''}`}>
      {type === 'number' ? (currency ? fmtCurrency(value, currency) : fmtEUR(value)) : value}
    </span>
  );
}

function QuotationFilesCell({ quotationId, editable }) {
  const app = useApp();
  const fileInputRef = useRef(null);
  const files = app.attachments.filter(a => a.entityType === 'quotation' && a.entityId === quotationId);
  return (
    <Dropdown
      align="right"
      trigger={
        <button className="flex items-center gap-1 text-muted hover:text-gold">
          <Paperclip className="w-3.5 h-3.5" />
          {files.length > 0 && <span className="text-xs font-mono">{files.length}</span>}
        </button>
      }
    >
      <div className="w-64 p-2">
        <div className="text-xs uppercase tracking-wide text-muted px-1 mb-1.5">Attachments</div>
        <div className="space-y-1.5 max-h-48 overflow-y-auto">
          {files.map(a => <AttachmentRow key={a.id} attachment={a} user={userById(app.users, a.uploadedBy)} onDelete={editable ? app.deleteAttachment : () => {}} onOpen={app.getAttachmentUrl} />)}
          {files.length === 0 && <p className="text-xs text-muted px-1">No files yet.</p>}
        </div>
        {editable && (
          <>
            <button onClick={() => fileInputRef.current?.click()} className="mt-2 w-full text-xs text-gold hover:underline flex items-center justify-center gap-1 py-1">
              <Upload className="w-3 h-3" /> Add file
            </button>
            <input ref={fileInputRef} type="file" className="hidden" onChange={e => { if (e.target.files[0]) app.addAttachment('quotation', quotationId, e.target.files[0]); }} />
          </>
        )}
      </div>
    </Dropdown>
  );
}

function QuotationList() {
  const app = useApp();
  const [modalOpen, setModalOpen] = useState(false);
  const [search, setSearch] = useState('');
  const [tab, setTab] = useState('byProject');

  const visibleProjects = app.projects.filter(p => canSeeProject(app.currentUser, p) && !p.isArchived);
  const filtered = visibleProjects.filter(p => {
    if (!search) return true;
    const s = search.toLowerCase();
    return p.name.toLowerCase().includes(s) || app.quotations.some(q => q.projectId === p.id && q.supplierName.toLowerCase().includes(s));
  });

  return (
    <div className="p-4 md:p-6 max-w-[1400px] mx-auto animate-fadeUp">
      <div className="flex items-center justify-between mb-4 flex-wrap gap-3">
        <h1 className="font-display italic text-2xl text-primary tracking-tight">Quotations</h1>
        <Button icon={Plus} onClick={() => setModalOpen(true)}>New Project</Button>
      </div>

      <div className="flex items-center gap-3 mb-5 flex-wrap">
        <div className="flex rounded-lg border border-border overflow-hidden">
          {[['byProject', 'By Project'], ['allQuotes', 'All Quotes']].map(([key, label]) => (
            <button key={key} onClick={() => setTab(key)} className={`px-4 py-1.5 text-sm ${tab === key ? 'bg-gold/15 text-gold' : 'text-muted hover:bg-elevated'}`}>{label}</button>
          ))}
        </div>
        <div className="relative flex-1 min-w-[200px] max-w-sm">
          <Search className="w-3.5 h-3.5 text-muted absolute left-3 top-1/2 -translate-y-1/2" />
          <Input className="pl-8" placeholder="Filter by project or supplier..." value={search} onChange={e => setSearch(e.target.value)} />
        </div>
      </div>

      {tab === 'byProject' ? (
        <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
          {filtered.map(project => {
            const quotes = app.quotations.filter(q => q.projectId === project.id);
            const approvedItemCount = quotes.reduce((n, q) => n + (q.lineItems || []).filter(li => li.status === 'Approved' || li.status === 'In Sampling').length, 0);
            return (
              <Card key={project.id} hover className="p-4" onClick={() => app.navigate('quotationsByProject', project.id)}>
                <div className="flex items-center justify-between mb-2">
                  <h4 className="font-display text-lg text-primary">{project.name}</h4>
                  <StatusPill status={project.status} size="sm" />
                </div>
                <p className="text-xs text-muted mb-3">
                  {quotes.length} quotation{quotes.length !== 1 ? 's' : ''}
                  {approvedItemCount > 0 ? ` · ${approvedItemCount} item${approvedItemCount !== 1 ? 's' : ''} approved` : ''}
                </p>
                <div className="flex items-center justify-between">
                  <span className="text-xs text-muted font-mono">Target {fmtDateShort(project.target)}</span>
                  <Avatar user={userById(app.users, project.owner)} size="sm" />
                </div>
              </Card>
            );
          })}
          {filtered.length === 0 && (
            <div className="col-span-full">
              <EmptyState icon={FileText} title="No projects match" message="Try a different search or create a new project." actionLabel="New Project" onAction={() => setModalOpen(true)} />
            </div>
          )}
        </div>
      ) : (
        <Card className="overflow-x-auto">
          <table className="w-full text-sm min-w-[900px]">
            <thead><tr className="text-left text-[10px] uppercase tracking-widest text-muted border-b border-border">
              <th className="py-3 px-4">Project</th><th className="py-3 px-4">Supplier</th><th className="py-3 px-4">Added By</th><th className="py-3 px-4">Item</th><th className="py-3 px-4">Qty</th><th className="py-3 px-4">Unit Price</th><th className="py-3 px-4">MOQ</th><th className="py-3 px-4">Valid Until</th><th className="py-3 px-4">Status</th><th className="py-3 px-4">Files</th>
            </tr></thead>
            <tbody>
              {app.quotations.filter(q => visibleProjects.some(p => p.id === q.projectId)).flatMap(q => {
                const proj = app.projects.find(p => p.id === q.projectId);
                const creator = userById(app.users, quotationOwnerId(q, proj));
                return (q.lineItems || []).map((li, i) => (
                  <tr key={li.id || `${q.id}-${i}`} className="border-b border-border last:border-0 hover:bg-elevated/40">
                    <td className="py-2.5 px-4 text-primary">{proj?.name}</td>
                    <td className="py-2.5 px-4 text-muted">{q.supplierName}</td>
                    <td className="py-2.5 px-4"><Avatar user={creator} size="sm" ring={false} /></td>
                    <td className="py-2.5 px-4 text-primary">{li.itemName}</td>
                    <td className="py-2.5 px-4 font-mono text-muted">{li.qty ? li.qty.toLocaleString('en-US') : '—'}</td>
                    <td className="py-2.5 px-4 font-mono text-primary">{fmtCurrency(li.unitPrice, q.currency)}</td>
                    <td className="py-2.5 px-4 text-muted">{li.moq}</td>
                    <td className="py-2.5 px-4 font-mono text-muted">{fmtDateShort(q.validUntil)}</td>
                    <td className="py-2.5 px-4"><StatusPill status={li.status || 'Pending'} size="sm" /></td>
                    <td className="py-2.5 px-4"><QuotationFilesCell quotationId={q.id} editable={canEditQuotation(app.currentUser, q, proj)} /></td>
                  </tr>
                ));
              })}
            </tbody>
          </table>
        </Card>
      )}

      <NewProjectModal open={modalOpen} onClose={() => setModalOpen(false)} />
    </div>
  );
}

function LineItemDecisionRow({ quotation, project, lineItem }) {
  const app = useApp();
  const editable = canEditLineItem(app.currentUser, quotation, project, lineItem);
  const [reason, setReason] = useState(lineItem.approvedReason || lineItem.rejectedReason || '');

  useEffect(() => {
    setReason(lineItem.approvedReason || lineItem.rejectedReason || '');
  }, [lineItem.id, lineItem.status, lineItem.approvedReason, lineItem.rejectedReason]);

  const decide = (decision) => {
    if (!editable) return;
    app.setLineItemDecision(quotation.id, lineItem.id, decision, reason);
  };
  const saveReason = () => {
    if (!editable || (lineItem.status || 'Pending') === 'Pending') return;
    app.setLineItemDecision(quotation.id, lineItem.id, lineItem.status, reason);
  };

  const segmentCls = (active, tone) => {
    if (!active) return 'px-2.5 py-1 text-xs font-medium text-muted hover:bg-elevated disabled:hover:bg-transparent';
    const toneCls = tone === 'sage' ? 'bg-sage/20 text-sage' : tone === 'oud' ? 'bg-oud/20 text-oud' : tone === 'purple' ? 'bg-purple/20 text-purple' : 'bg-elevated text-primary';
    return `px-2.5 py-1 text-xs font-medium ${toneCls}`;
  };

  return (
    <div className="rounded-lg border border-border bg-base p-3">
      <div className="grid grid-cols-2 sm:grid-cols-7 gap-x-3 gap-y-2 text-xs mb-2.5">
        <div className="col-span-2"><div className="text-muted">Item</div><div className="text-primary font-medium mt-0.5">{lineItem.itemName}</div></div>
        <div><div className="text-muted">Qty</div><div className="font-mono text-primary mt-0.5">{lineItem.qty ? lineItem.qty.toLocaleString('en-US') : '—'}</div></div>
        <div><div className="text-muted">Unit</div><div className="font-mono text-primary mt-0.5">{fmtCurrency(lineItem.unitPrice, quotation.currency)}</div></div>
        <div><div className="text-muted">MOQ</div><div className="font-mono text-muted mt-0.5">{lineItem.moq || '—'}</div></div>
        <div><div className="text-muted">Mold / Sampling</div><div className="font-mono text-muted mt-0.5">{fmtCurrency(lineItem.moldCost, quotation.currency, { decimals: 0 })} / {fmtCurrency(lineItem.samplingCost, quotation.currency, { decimals: 0 })}</div></div>
        <div><div className="text-muted">Lead</div><div className="font-mono text-muted mt-0.5">{lineItem.prodLeadTime || 0}d / {lineItem.samplingLeadTime || 0}d</div></div>
      </div>
      <div className="flex items-center gap-2 flex-wrap pt-2 border-t border-border">
        {lineItem.locked ? (
          <span className="inline-flex items-center gap-1.5 text-xs text-muted">🔒 In Sampling</span>
        ) : (
          <div className="inline-flex rounded-lg border border-border overflow-hidden shrink-0">
            <button type="button" disabled={!editable} onClick={() => decide('Pending')} className={segmentCls((lineItem.status || 'Pending') === 'Pending', 'muted')}>Pending</button>
            <button type="button" disabled={!editable} onClick={() => decide('Approved')} className={segmentCls(lineItem.status === 'Approved', 'sage')}>Approve</button>
            <button type="button" disabled={!editable} onClick={() => decide('Rejected')} className={segmentCls(lineItem.status === 'Rejected', 'oud')}>Reject</button>
            <button type="button" disabled={!editable} onClick={() => decide('Developing')} className={segmentCls(lineItem.status === 'Developing', 'purple')}>Developing</button>
          </div>
        )}
        {!lineItem.locked && lineItem.status !== 'Pending' && lineItem.status !== 'Developing' && editable && (
          <input
            value={reason} onChange={e => setReason(e.target.value)} onBlur={saveReason}
            onKeyDown={e => { if (e.key === 'Enter') e.target.blur(); }}
            placeholder="Reason (optional)"
            className="flex-1 min-w-[160px] text-xs bg-surface border border-border rounded-lg px-2.5 py-1.5 text-primary placeholder:text-stale focus:outline-none focus:ring-2 focus:ring-gold/40"
          />
        )}
        {(!editable || lineItem.locked) && (lineItem.approvedReason || lineItem.rejectedReason) && (
          <span className="text-xs text-muted">Reason: "{lineItem.approvedReason || lineItem.rejectedReason}"</span>
        )}
      </div>
    </div>
  );
}

function QuotationCard({ quotation, project, onEdit }) {
  const app = useApp();
  const [expanded, setExpanded] = useState(false);
  const editable = canEditQuotation(app.currentUser, quotation, project);
  const deletable = canDeleteQuotation(app.currentUser, quotation, project);
  const items = quotation.lineItems || [];
  const derivedStatus = deriveQuotationStatus(quotation);
  const acceptedAmt = quotationAcceptedAmount(quotation);
  const originalAmt = quotationTotalAmount(quotation);
  const rejectedAmt = quotationRejectedAmount(quotation);
  const acceptedUSD = toUSD(acceptedAmt, quotation.currency || 'EUR', app.fxRates);
  const originalUSD = toUSD(originalAmt, quotation.currency || 'EUR', app.fxRates);
  const summary = lineItemDecisionSummary(items);
  const hasApprovedToSend = items.some(li => li.status === 'Approved' && !li.locked);

  const handleDelete = async () => {
    const ok = await app.confirm({ title: `Delete quote from ${quotation.supplierName}?`, message: 'This cannot be undone.', confirmLabel: 'Delete' });
    if (ok) app.deleteQuotation(quotation.id);
  };

  const handleSend = async () => {
    const count = items.filter(li => li.status === 'Approved' && !li.locked).length;
    const ok = await app.confirm({ title: 'Send approved items to sampling?', message: `${count} approved item${count > 1 ? 's' : ''} will each get its own Sampling Project.`, confirmLabel: 'Send' });
    if (ok) app.sendApprovedLineItems(quotation.id);
  };

  const creator = userById(app.users, quotationOwnerId(quotation, project));

  return (
    <Card className="p-4">
      <div className="grid grid-cols-1 sm:grid-cols-[1.1fr_0.8fr_1fr_1fr_1.4fr_0.8fr_auto] items-center gap-3">
        <div className="flex items-center gap-2 min-w-0">
          <div className="min-w-0">
            <span className="text-sm text-primary font-medium truncate block">{quotation.supplierName}</span>
            {quotation.referenceNo && <span className="text-[10px] text-muted font-mono truncate block">Ref: {quotation.referenceNo}</span>}
          </div>
        </div>
        <div>
          {items.length > 1 ? (
            <span className="text-[10px] font-mono uppercase tracking-wide text-muted bg-elevated rounded px-1.5 py-0.5">{items.length} items</span>
          ) : (
            <span className="text-xs text-muted truncate block">{items[0]?.itemName || '—'}</span>
          )}
        </div>
        <div className="text-xs">
          <div className="text-muted">Accepted</div>
          <div className="font-mono text-sage">{fmtCurrency(acceptedUSD, 'USD', { decimals: 0 })}</div>
        </div>
        <div className="text-xs">
          <div className="text-muted">Original</div>
          <div className="font-mono text-primary">{fmtCurrency(originalUSD, 'USD', { decimals: 0 })}</div>
        </div>
        <div className="flex items-center gap-2 flex-wrap">
          <StatusPill status={derivedStatus} size="sm" />
          <span className="text-xs text-muted">{summary}</span>
        </div>
        <div className="flex items-center gap-1.5 min-w-0">
          <Avatar user={creator} size="sm" ring={false} />
          <span className="text-xs text-muted truncate">{creator?.name || '—'}</span>
        </div>
        <div className="flex items-center gap-1.5 justify-end">
          <QuotationFilesCell quotationId={quotation.id} editable={editable} />
          {editable && <button onClick={() => onEdit(quotation)} title="Edit quote" className="text-muted hover:text-gold"><Pencil className="w-3.5 h-3.5" /></button>}
          {deletable && <button onClick={handleDelete} title="Delete quote" className="text-muted hover:text-oud"><Trash2 className="w-3.5 h-3.5" /></button>}
          <button onClick={() => setExpanded(e => !e)} className="flex items-center gap-1 text-xs text-muted hover:text-gold">
            {expanded ? <ChevronDown className="w-3.5 h-3.5" /> : <ChevronRight className="w-3.5 h-3.5" />} Expand
          </button>
        </div>
      </div>

      {expanded && (
        <div className="mt-4 pt-4 border-t border-border">
          <div className="flex items-center justify-between flex-wrap gap-2 mb-3 rounded-lg border border-border bg-elevated px-3 py-2.5">
            <div>
              <div className="text-xs text-muted">{summary}</div>
              <div className="text-xs font-mono text-primary mt-0.5">
                Accepted: {fmtCurrency(acceptedAmt, quotation.currency, { decimals: 0 })}
                <span className="text-muted"> | Original: {fmtCurrency(originalAmt, quotation.currency, { decimals: 0 })}</span>
                {rejectedAmt > 0 && <span className="text-muted"> | Rejected: {fmtCurrency(rejectedAmt, quotation.currency, { decimals: 0 })}</span>}
              </div>
            </div>
            {hasApprovedToSend && editable && (
              <Button size="sm" onClick={handleSend}>Send Approved →</Button>
            )}
          </div>
          <div className="space-y-2">
            {items.map(li => <LineItemDecisionRow key={li.id} quotation={quotation} project={project} lineItem={li} />)}
          </div>
        </div>
      )}
    </Card>
  );
}

function QuotationsForProject({ id }) {
  const app = useApp();
  const project = app.projects.find(p => p.id === id);
  const [addQuoteOpen, setAddQuoteOpen] = useState(false);
  const [editQuote, setEditQuote] = useState(null);

  if (!project) return <EmptyState title="Project not found" message="It may have been removed." actionLabel="Back to Quotations" onAction={() => app.navigate('quotations')} />;

  const quotes = app.quotations.filter(q => q.projectId === id);
  const editable = canEditProject(app.currentUser, project);
  const canDelete = editable && app.canDeleteProject(id);
  const sentItems = quotes.flatMap(q => (q.lineItems || []).filter(li => li.locked).map(li => ({ q, li })));

  const handleDelete = async () => {
    const ok = await app.confirm({ title: `Delete ${project.name}?`, message: 'This cannot be undone.', confirmLabel: 'Delete' });
    if (ok) { app.deleteProject(id); app.navigate('quotations'); }
  };

  return (
    <div className="p-4 md:p-6 max-w-[1400px] mx-auto animate-fadeUp">
      <button onClick={() => app.navigate('quotations')} className="flex items-center gap-1.5 text-sm text-muted hover:text-primary mb-4"><ArrowLeft className="w-4 h-4" /> Back to Quotations</button>

      <Card className="p-5">
        <div className="flex items-center justify-between mb-4 flex-wrap gap-2">
          <div>
            <div className="flex items-center gap-2">
              <h3 className="font-display text-xl text-primary">{project.name}</h3>
              {canDelete && (
                <button onClick={handleDelete} className="text-muted hover:text-oud"><Trash2 className="w-3.5 h-3.5" /></button>
              )}
            </div>
            <div className="flex items-center gap-2 mt-1 flex-wrap">
              <StatusPill status={project.status} size="sm" />
              <span className="text-xs text-muted font-mono">Target {fmtDateShort(project.target)}</span>
              <Avatar user={userById(app.users, project.owner)} size="sm" />
            </div>
          </div>
          {editable ? (
            <Button variant="secondary" size="sm" icon={Plus} onClick={() => setAddQuoteOpen(true)}>Add Quote</Button>
          ) : (
            <span className="text-xs text-muted">View only</span>
          )}
        </div>

        {sentItems.length > 0 && (
          <div className="mb-3 rounded-lg border border-sage/40 bg-sage/10 px-4 py-2.5 flex items-center justify-between text-sm">
            <span className="text-sage">{sentItems.length} item{sentItems.length > 1 ? 's' : ''} sent to sampling.</span>
            <button onClick={() => app.navigate('samplingByProject', project.id)} className="text-gold flex items-center gap-1 hover:underline">View Sampling <ExternalLink className="w-3 h-3" /></button>
          </div>
        )}

        {quotes.length === 0 ? (
          <EmptyState icon={FileText} title="No quotations yet" message="Add first quote to start comparing suppliers." actionLabel="Add Quote" onAction={() => setAddQuoteOpen(true)} />
        ) : (
          <div className="overflow-x-auto">
            <div className="min-w-[900px]">
              <div className="hidden sm:grid grid-cols-[1.1fr_0.8fr_1fr_1fr_1.4fr_0.8fr_auto] gap-3 px-4 pb-2 text-[11px] uppercase tracking-wide text-muted">
                <span>Supplier</span><span>Items</span><span>Accepted Value</span><span>Original Total</span><span>Decision Summary</span><span>Added By</span><span className="text-right pr-1">Actions</span>
              </div>
              <div className="space-y-3">
                {quotes.map(q => (
                  <QuotationCard key={q.id} quotation={q} project={project} onEdit={setEditQuote} />
                ))}
              </div>
            </div>
          </div>
        )}
      </Card>

      <AddQuoteModal open={addQuoteOpen} onClose={() => setAddQuoteOpen(false)} projectId={id} />
      <EditQuoteModal open={!!editQuote} onClose={() => setEditQuote(null)} quotation={editQuote} />
    </div>
  );
}

/* ======================================================================
   13. SAMPLING HUB + DETAIL
   ====================================================================== */

function SamplingHub() {
  const app = useApp();
  const [statusFilter, setStatusFilter] = useState('all');
  const visibleProjects = app.projects.filter(p => canSeeProject(app.currentUser, p) && !p.isArchived);
  const projectIdsWithSampling = new Set(app.samplingProjects.map(sp => sp.linkedProjectId));
  const projectsWithSampling = visibleProjects.filter(p => projectIdsWithSampling.has(p.id) &&
    app.samplingProjects.some(sp => sp.linkedProjectId === p.id && !['Moved to Production', 'Cancelled'].includes(sp.status)));
  const filteredProjects = projectsWithSampling.filter(p => {
    if (statusFilter === 'all') return true;
    return app.samplingProjects.some(sp => sp.linkedProjectId === p.id && sp.status === statusFilter);
  });

  return (
    <div className="p-4 md:p-6 max-w-[1400px] mx-auto animate-fadeUp">
      <div className="flex items-center justify-between mb-5 flex-wrap gap-3">
        <h1 className="font-display italic text-2xl text-primary tracking-tight">Sampling</h1>
        <Select className="w-48" value={statusFilter} onChange={e => setStatusFilter(e.target.value)}>
          <option value="all">All statuses</option>
          {['Sourcing', 'Sampling In Progress', 'Sample Approved', 'Moved to Production', 'On Hold', 'Cancelled'].map(s => <option key={s} value={s}>{s}</option>)}
        </Select>
      </div>
      {filteredProjects.length === 0 && <EmptyState icon={FlaskConical} title="No sampling projects" message="Select a quotation to auto-create a sampling project." actionLabel="Go to Quotations" onAction={() => app.navigate('quotations')} />}
      <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
        {filteredProjects.map(project => {
          const tracks = app.samplingProjects.filter(sp => sp.linkedProjectId === project.id);
          const rounds = app.sampleRounds.filter(r => tracks.some(t => t.id === r.samplingProjectId));
          const movedToProduction = tracks.filter(t => t.status === 'Moved to Production').length;
          const latestRound = [...rounds].sort((a, b) => new Date(b.dateRequested) - new Date(a.dateRequested))[0];
          return (
            <Card key={project.id} hover className="p-4" onClick={() => app.navigate('samplingByProject', project.id)}>
              <div className="flex items-center justify-between mb-2">
                <h4 className="font-display text-lg text-primary">{project.name}</h4>
                <StatusPill status={project.status} size="sm" />
              </div>
              <p className="text-xs text-muted mb-2">{tracks.length} sampling track{tracks.length !== 1 ? 's' : ''} · {rounds.length} round{rounds.length !== 1 ? 's' : ''} total</p>
              {latestRound ? (
                <p className="text-xs text-primary mb-3">Latest: {latestRound.version} {latestRound.status.toLowerCase()} with {supplierById(app.suppliers, latestRound.supplierId)?.name}</p>
              ) : (
                <p className="text-xs text-muted mb-3">No rounds requested yet</p>
              )}
              <div className="flex items-center justify-between">
                <span className="text-xs text-muted font-mono">{movedToProduction}/{tracks.length} moved to production</span>
                <Avatar user={userById(app.users, project.owner)} size="sm" />
              </div>
            </Card>
          );
        })}
      </div>
    </div>
  );
}

function SamplingByProject({ id }) {
  const app = useApp();
  const project = app.projects.find(p => p.id === id);
  if (!project) return <EmptyState title="Project not found" message="It may have been removed." actionLabel="Back to Sampling" onAction={() => app.navigate('sampling')} />;

  const tracks = app.samplingProjects.filter(sp => sp.linkedProjectId === id);

  return (
    <div className="p-4 md:p-6 max-w-[1300px] mx-auto animate-fadeUp">
      <button onClick={() => app.navigate('sampling')} className="flex items-center gap-1.5 text-sm text-muted hover:text-primary mb-4"><ArrowLeft className="w-4 h-4" /> Back to Sampling</button>
      <div className="flex items-center gap-3 mb-1 flex-wrap">
        <h1 className="font-display italic text-2xl text-primary tracking-tight">{project.name}</h1>
        <StatusPill status={project.status} />
        <Avatar user={userById(app.users, project.owner)} size="sm" />
      </div>
      <button onClick={() => app.navigate('projectDetail', project.id)} className="text-sm text-gold hover:underline flex items-center gap-1 mb-5">Open project <ExternalLink className="w-3 h-3" /></button>

      {tracks.length === 0 ? (
        <EmptyState icon={FlaskConical} title="No sampling tracks yet" message="Select a quotation for this project to auto-create one." actionLabel="Go to Quotations" onAction={() => app.navigate('quotationsByProject', id)} />
      ) : (
        <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
          {tracks.map(sp => {
            const rounds = app.sampleRounds.filter(r => r.samplingProjectId === sp.id);
            const bids = app.supplierBids.filter(b => b.samplingProjectId === sp.id);
            const latest = rounds[rounds.length - 1];
            return (
              <Card key={sp.id} hover className="p-4" onClick={() => app.navigate('samplingDetail', sp.id)}>
                <div className="flex items-center justify-between mb-2">
                  <h4 className="font-display text-primary">{sp.name}</h4>
                  <StatusPill status={sp.status} size="sm" />
                </div>
                <p className="text-xs text-muted mb-2">{bids.length} supplier{bids.length !== 1 ? 's' : ''} bidding · {rounds.length} round{rounds.length !== 1 ? 's' : ''} total</p>
                {latest && <p className="text-xs text-primary mb-3">{latest.version} {latest.status.toLowerCase()} with {supplierById(app.suppliers, latest.supplierId)?.name}</p>}
                <div className="flex items-center justify-between">
                  <SamplingStageTracker status={sp.status} />
                  <Avatar user={userById(app.users, sp.ownerId)} size="sm" />
                </div>
              </Card>
            );
          })}
        </div>
      )}
    </div>
  );
}

function RequestRoundModal({ open, onClose, samplingProjectId }) {
  const app = useApp();
  const [form, setForm] = useState({ supplierId: '', cost: '', leadTimeDays: '' });
  const bids = app.supplierBids.filter(b => b.samplingProjectId === samplingProjectId);
  const currentBid = bids.find(b => b.supplierId === form.supplierId);
  useEffect(() => { if (open && bids[0]) setForm(f => ({ ...f, supplierId: bids[0].supplierId })); }, [open]);
  const submit = () => {
    if (!form.supplierId) { app.toast('Choose a supplier.', 'error'); return; }
    app.requestSampleRound(samplingProjectId, form);
    onClose();
  };
  return (
    <Modal open={open} onClose={onClose} title="Request New Sample Round">
      <div className="space-y-4">
        <div><Label>Supplier</Label><Select value={form.supplierId} onChange={e => setForm(f => ({ ...f, supplierId: e.target.value }))}>{bids.map(b => <option key={b.id} value={b.supplierId}>{supplierById(app.suppliers, b.supplierId)?.name}</option>)}</Select></div>
        <div className="grid grid-cols-2 gap-3">
          <div><Label>Sample Cost ({currencySymbol(currentBid?.currency || 'EUR')})</Label><Input type="number" value={form.cost} onChange={e => setForm(f => ({ ...f, cost: e.target.value }))} /></div>
          <div><Label>Lead Time (days)</Label><Input type="number" value={form.leadTimeDays} onChange={e => setForm(f => ({ ...f, leadTimeDays: e.target.value }))} /></div>
        </div>
        <div className="flex justify-end gap-2 pt-2"><Button variant="secondary" onClick={onClose}>Cancel</Button><Button onClick={submit}>Request Round</Button></div>
      </div>
    </Modal>
  );
}

function MoveToProductionModal({ open, onClose, samplingProjectId }) {
  const app = useApp();
  const [form, setForm] = useState({ qty: '', price: '' });

  const approvedRound = app.sampleRounds.find(r => r.samplingProjectId === samplingProjectId && r.status === 'Approved');
  const bid = approvedRound ? app.supplierBids.find(b => b.samplingProjectId === samplingProjectId && b.supplierId === approvedRound.supplierId) : null;
  const supplier = approvedRound ? supplierById(app.suppliers, approvedRound.supplierId) : null;
  const currency = bid?.currency || 'EUR';

  useEffect(() => {
    if (open) {
      const defaultQty = bid?.quotedQty || parseNumeric(bid?.quotedMOQ) || '';
      setForm({ qty: defaultQty ? String(defaultQty) : '', price: bid?.quotedPrice != null ? String(bid.quotedPrice) : '' });
    }
  }, [open, samplingProjectId]);

  const qtyNum = Number(form.qty);
  const priceNum = Number(form.price);
  const valid = qtyNum >= 0.0001 && priceNum >= 0.0001;

  const submit = () => {
    if (!valid) { app.toast('Enter a qty and price of at least 0.0001.', 'error'); return; }
    app.moveSamplingToProduction(samplingProjectId, { qty: qtyNum, price: priceNum });
    onClose();
  };

  return (
    <Modal open={open} onClose={onClose} title="Confirm Move to Production" width="max-w-md">
      <div className="space-y-4">
        <p className="text-sm text-muted">
          Confirm the order quantity and unit price with {supplier?.name || 'the supplier'} before creating the component. These are pre-filled from the quotation — edit them if anything changed.
        </p>
        <div className="grid grid-cols-2 gap-3">
          <div>
            <Label>Order Qty</Label>
            <Input type="number" step="1" min="0" value={form.qty} onChange={e => setForm(f => ({ ...f, qty: e.target.value }))} />
          </div>
          <div>
            <Label>Unit Price ({currencySymbol(currency)})</Label>
            <Input type="number" step="0.01" min="0" value={form.price} onChange={e => setForm(f => ({ ...f, price: e.target.value }))} />
          </div>
        </div>
        {!valid && (form.qty !== '' || form.price !== '') && (
          <p className="text-xs text-oud">Both qty and price must be at least 0.0001.</p>
        )}
        <div className="flex justify-end gap-2 pt-2">
          <Button variant="secondary" onClick={onClose}>Cancel</Button>
          <Button onClick={submit} disabled={!valid}>Confirm & Move to Production</Button>
        </div>
      </div>
    </Modal>
  );
}

function ReviewRoundPanel({ round, onSubmit }) {
  const [notes, setNotes] = useState('');
  return (
    <div className="space-y-2">
      <Input value={notes} onChange={e => setNotes(e.target.value)} placeholder="Result notes (optional)" />
      <div className="flex flex-wrap gap-2">
        <Button size="sm" variant="success" icon={ThumbsUp} onClick={() => onSubmit('Approved', notes)}>Approve</Button>
        <Button size="sm" variant="danger" icon={ThumbsDown} onClick={() => onSubmit('Rejected', notes)}>Reject</Button>
        <Button size="sm" variant="warning" icon={RotateCcw} onClick={() => onSubmit('Needs Revision', notes)}>Request Revision</Button>
      </div>
    </div>
  );
}

function SampleRoundTracker({ round }) {
  const effectiveStatus = round.status === 'On Hold' ? (round.heldFromStatus || 'Requested') : round.status;
  const isTerminal = SAMPLE_ROUND_TERMINAL_STATUSES.includes(effectiveStatus);
  const currentIdx = isTerminal ? SAMPLE_ROUND_FLOW_STEPS.length : SAMPLE_ROUND_FLOW_STEPS.indexOf(effectiveStatus);
  return (
    <div className="flex items-center flex-wrap gap-y-2 py-1">
      {SAMPLE_ROUND_FLOW_STEPS.map((step, i) => (
        <React.Fragment key={step}>
          {i > 0 && <div className={`h-px w-4 md:w-6 shrink-0 ${i <= currentIdx ? 'bg-gold' : 'bg-border'}`} />}
          <div className="flex items-center gap-1.5 shrink-0" title={step}>
            <span className={
              i < currentIdx ? 'w-2 h-2 rounded-full bg-gold shrink-0'
                : i === currentIdx ? 'w-2 h-2 rounded-full bg-gold animate-pulseDot shrink-0'
                  : 'w-2 h-2 rounded-full border border-border shrink-0'
            } />
            <span className={`text-[10px] font-mono whitespace-nowrap ${i <= currentIdx ? 'text-primary' : 'text-muted'}`}>{step}</span>
          </div>
        </React.Fragment>
      ))}
      {isTerminal && (
        <>
          <div className="h-px w-4 md:w-6 shrink-0 bg-gold" />
          <div className="flex items-center gap-1.5 shrink-0">
            <span className={`w-2 h-2 rounded-full shrink-0 ${TONE_DOT[SAMPLE_ROUND_TERMINAL_TONE[effectiveStatus]]}`} />
            <span className="text-[10px] font-mono whitespace-nowrap text-primary">{effectiveStatus}</span>
          </div>
        </>
      )}
      {round.status === 'On Hold' && <span className="ml-2 text-[10px] font-mono text-amber">(On Hold — was {round.heldFromStatus || 'Requested'})</span>}
    </div>
  );
}

function SampleRoundHistory({ history, users }) {
  if (!history?.length) return null;
  const sorted = [...history].sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
  return (
    <div>
      <div className="text-[10px] uppercase tracking-wide text-muted mb-1">Status History</div>
      <div className="space-y-0.5">
        {sorted.map((h, i) => {
          const u = userById(users, h.updatedBy);
          return (
            <div key={i} className="font-mono text-xs text-muted flex gap-1.5">
              <span className="text-border shrink-0">{i === sorted.length - 1 ? '└─' : '├─'}</span>
              <span>{fmtDateTimeShort(h.updatedAt)} — {u ? `${u.name} ` : ''}{h.note || `marked as ${h.status}`}</span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

function SampleRoundStatusDropdown({ round, nextOptions, onUpdate }) {
  return (
    <select
      value=""
      onChange={e => { if (e.target.value) onUpdate(e.target.value); }}
      onClick={e => e.stopPropagation()}
      className={`text-xs font-medium rounded-full border px-2.5 py-1 bg-transparent cursor-pointer focus:outline-none focus:ring-2 focus:ring-gold/40 ${TONE_CLASSES[STATUS_TONE[round.status] || 'muted']}`}
    >
      <option value="" disabled>Update Status ▼</option>
      {nextOptions.map(o => <option key={o} value={o} className="bg-elevated text-primary">{o}</option>)}
    </select>
  );
}

function SampleRoundActionBar({ round, editable, app }) {
  if (!editable) return null;
  if (round.status === 'On Hold') {
    return (
      <div className="flex flex-wrap items-center gap-2">
        <Button size="sm" variant="secondary" icon={RefreshCw} onClick={() => app.resumeSampleRound(round.id)}>Resume</Button>
      </div>
    );
  }
  const nextOptions = SAMPLE_ROUND_TRANSITIONS[round.status] || [];
  const canHold = SAMPLE_ROUND_FLOW_STEPS.includes(round.status);
  return (
    <div className="flex flex-wrap items-center gap-2">
      {round.status === 'Arrived' && (
        <Button size="sm" onClick={() => app.advanceSampleRoundStatus(round.id, 'Under Review')}>Mark as Under Review</Button>
      )}
      {round.status === 'Under Review' && (
        <ReviewRoundPanel round={round} onSubmit={(decision, notes) => app.reviewSampleRound(round.id, decision, notes)} />
      )}
      {nextOptions.length > 0 && round.status !== 'Arrived' && round.status !== 'Under Review' && (
        <SampleRoundStatusDropdown round={round} nextOptions={nextOptions} onUpdate={newStatus => app.advanceSampleRoundStatus(round.id, newStatus)} />
      )}
      {round.status === 'Needs Revision' && (
        <Button size="sm" variant="secondary" icon={RotateCcw} onClick={() => app.requestRevisionRound(round.id)}>Request Next Version</Button>
      )}
      {canHold && (
        <Button size="sm" variant="ghost" onClick={() => app.holdSampleRound(round.id)}>Put On Hold</Button>
      )}
    </div>
  );
}

function SamplingProjectDetail({ id }) {
  const app = useApp();
  const sp = app.samplingProjects.find(s => s.id === id);
  const [requestOpen, setRequestOpen] = useState(false);
  const [moveToProductionOpen, setMoveToProductionOpen] = useState(false);
  const [expanded, setExpanded] = useState({});
  const [commentText, setCommentText] = useState('');
  const fileInputRef = useRef(null);

  if (!sp) return <EmptyState title="Not found" message="This sampling project may have been removed." actionLabel="Back to Sampling" onAction={() => app.navigate('sampling')} />;

  const project = app.projects.find(p => p.id === sp.linkedProjectId);
  const bids = app.supplierBids.filter(b => b.samplingProjectId === id);
  const rounds = app.sampleRounds.filter(r => r.samplingProjectId === id);
  const bySupplier = {};
  rounds.forEach(r => { bySupplier[r.supplierId] = bySupplier[r.supplierId] || []; bySupplier[r.supplierId].push(r); });
  const attachments = app.attachments.filter(a => a.entityType === 'samplingProject' && a.entityId === id);
  const projComments = app.comments.filter(c => c.entityType === 'samplingProject' && c.entityId === id);
  const daysRemaining = daysBetween(todayStr(), sp.targetApprovalDate);
  const anyApproved = rounds.some(r => r.status === 'Approved');
  const readOnly = !!project?.isArchived;
  const editable = !readOnly && canEditItem(app.currentUser, sp.ownerId, project);

  return (
    <div className="p-4 md:p-6 max-w-[1300px] mx-auto animate-fadeUp">
      <button onClick={() => app.navigate('samplingByProject', sp.linkedProjectId)} className="flex items-center gap-1.5 text-sm text-muted hover:text-primary mb-4"><ArrowLeft className="w-4 h-4" /> Back</button>

      <div className="flex flex-wrap items-center justify-between gap-3 mb-4">
        <div className="flex items-center gap-3 flex-wrap">
          <h1 className="font-display italic text-2xl text-primary tracking-tight">{sp.name}</h1>
          <StatusPill status={sp.status} />
          <button onClick={() => app.navigate('projectDetail', project?.id)} className="text-sm text-gold hover:underline flex items-center gap-1">{project?.name} <ExternalLink className="w-3 h-3" /></button>
        </div>
        <Avatar user={userById(app.users, sp.ownerId)} />
      </div>

      <div className="grid grid-cols-3 gap-4 mb-5">
        <Card className="p-4"><div className="text-xs text-muted mb-1">Target Date</div><div className="font-mono text-primary">{fmtDateShort(sp.targetApprovalDate)}</div></Card>
        <Card className="p-4"><div className="text-xs text-muted mb-1">Days Remaining</div><div className={`font-mono ${daysRemaining < 0 ? 'text-oud' : 'text-primary'}`}>{daysRemaining}</div></Card>
        <Card className="p-4"><div className="text-xs text-muted mb-1">Created</div><div className="font-mono text-primary">{fmtDateShort(sp.createdAt)}</div></Card>
      </div>

      {readOnly ? (
        <div className="mb-6 rounded-lg border border-sage/40 bg-sage/10 px-4 py-2.5 text-xs text-sage">
          This project is complete — this sampling history is read-only.
        </div>
      ) : (
        <>
          {!editable && (
            <div className="mb-4 rounded-lg border border-border bg-elevated px-4 py-2.5 text-xs text-muted">
              View only — only {userById(app.users, sp.ownerId)?.name || 'the owner'} or an admin can make changes here.
            </div>
          )}
          <div className="flex flex-wrap gap-2 mb-6">
            <Button size="sm" icon={Plus} onClick={() => setRequestOpen(true)} disabled={!editable || sp.status === 'Moved to Production' || sp.status === 'Cancelled'}>Request New Round</Button>
            <Button size="sm" variant="secondary" disabled={!editable || !anyApproved || sp.status === 'Moved to Production' || sp.status === 'Cancelled'} onClick={() => setMoveToProductionOpen(true)}>Move to Production</Button>
            <Button size="sm" variant="secondary" disabled={!editable} onClick={() => app.updateSamplingProject(id, { status: 'On Hold' })}>Put On Hold</Button>
            <Button size="sm" variant="danger" disabled={!editable} onClick={() => app.updateSamplingProject(id, { status: 'Cancelled' })}>Cancel</Button>
          </div>
        </>
      )}

      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
        <div className="lg:col-span-2 space-y-6">
          <Card className="p-5">
            <h3 className="font-display text-lg text-primary mb-3">Supplier Comparison</h3>
            <div className="overflow-x-auto">
              <table className="w-full text-sm min-w-[600px]">
                <thead><tr className="text-left text-[10px] uppercase tracking-widest text-muted border-b border-border">
                  <th className="py-2 pr-3">Supplier</th><th className="py-2 pr-3">Bid Status</th><th className="py-2 pr-3">Quoted Price</th><th className="py-2 pr-3">MOQ</th><th className="py-2 pr-3">Lead Time</th><th className="py-2 pr-3">Latest Round</th>
                </tr></thead>
                <tbody>
                  {bids.map(b => {
                    const sRounds = bySupplier[b.supplierId] || [];
                    const latest = sRounds[sRounds.length - 1];
                    return (
                      <tr key={b.id} className="border-b border-border last:border-0">
                        <td className="py-2.5 pr-3 text-primary">{supplierById(app.suppliers, b.supplierId)?.name}</td>
                        <td className="py-2.5 pr-3"><StatusPill status={b.bidStatus} size="sm" /></td>
                        <td className="py-2.5 pr-3 font-mono text-primary">{fmtCurrency(b.quotedPrice, b.currency)}</td>
                        <td className="py-2.5 pr-3 text-muted">{b.quotedMOQ}</td>
                        <td className="py-2.5 pr-3 font-mono text-muted">{b.quotedLeadTime}d</td>
                        <td className="py-2.5 pr-3 text-muted">{latest ? `${latest.version} — ${latest.status}` : '—'}</td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          </Card>

          <Card className="p-5">
            <h3 className="font-display text-lg text-primary mb-4">Sample Rounds Timeline</h3>
            <div className="space-y-6">
              {Object.entries(bySupplier).map(([supplierId, sRounds]) => (
                <div key={supplierId}>
                  <h4 className="text-sm font-medium text-gold mb-2">{supplierById(app.suppliers, supplierId)?.name}</h4>
                  <div className="pl-4 border-l-2 border-border space-y-3">
                    {sRounds.map(r => {
                      const isOpen = expanded[r.id];
                      const roundAttachments = app.attachments.filter(a => a.entityType === 'sampleRound' && a.entityId === r.id);
                      return (
                        <div key={r.id} className="relative">
                          <div className="absolute -left-[21px] top-1 w-2.5 h-2.5 rounded-full bg-gold" />
                          <div className="cursor-pointer" onClick={() => setExpanded(e => ({ ...e, [r.id]: !e[r.id] }))}>
                            <div className="flex items-center gap-2 flex-wrap">
                              <span className="font-mono text-sm text-primary">{r.version}</span>
                              <span className="text-xs text-muted">({fmtDateShort(r.dateRequested)}{r.dateReceived ? ` → ${fmtDateShort(r.dateReceived)}` : ''})</span>
                              <StatusPill status={r.status} size="sm" />
                            </div>
                            <p className="text-xs text-muted mt-0.5">{r.resultNotes || r.nextAction || 'Awaiting update'}</p>
                          </div>
                          {isOpen && (
                            <div className="mt-2 pl-1 space-y-3">
                              <SampleRoundTracker round={r} />
                              <SampleRoundHistory history={r.statusHistory} users={app.users} />
                              <div className="text-xs text-muted font-mono">Cost: {fmtCurrency(r.cost, bids.find(b => b.supplierId === r.supplierId)?.currency)} {r.leadTimeDays ? `· Lead: ${r.leadTimeDays}d` : ''} {r.dateReviewed ? `· Reviewed ${fmtDateShort(r.dateReviewed)}` : ''}</div>
                              {roundAttachments.map(a => <AttachmentRow key={a.id} attachment={a} user={userById(app.users, a.uploadedBy)} onDelete={readOnly ? null : app.deleteAttachment} onOpen={app.getAttachmentUrl} />)}
                              <SampleRoundActionBar round={r} editable={editable} app={app} />
                            </div>
                          )}
                        </div>
                      );
                    })}
                    {sRounds.length === 0 && <p className="text-xs text-muted">No rounds yet.</p>}
                  </div>
                </div>
              ))}
              {rounds.length === 0 && <EmptyState icon={FlaskConical} title="No sample rounds" message="Request the first round to begin tracking supplier samples." actionLabel="Request Round" onAction={() => setRequestOpen(true)} />}
            </div>
          </Card>
        </div>

        <div className="space-y-6">
          <Card className="p-5">
            <div className="flex items-center justify-between mb-3">
              <h4 className="text-xs uppercase tracking-wide text-muted">Attachments</h4>
              {editable && <Button size="sm" variant="ghost" icon={Upload} onClick={() => fileInputRef.current?.click()} />}
              <input ref={fileInputRef} type="file" className="hidden" onChange={e => { if (e.target.files[0]) app.addAttachment('samplingProject', id, e.target.files[0]); }} />
            </div>
            <div className="space-y-2">
              {attachments.map(a => <AttachmentRow key={a.id} attachment={a} user={userById(app.users, a.uploadedBy)} onDelete={readOnly ? null : app.deleteAttachment} onOpen={app.getAttachmentUrl} />)}
              {attachments.length === 0 && <p className="text-xs text-muted">None yet.</p>}
            </div>
          </Card>
          <Card className="p-5">
            <h4 className="text-xs uppercase tracking-wide text-muted mb-3">Notes</h4>
            <p className="text-sm text-muted">{sp.notes || 'No notes recorded.'}</p>
          </Card>
          <Card className="p-5">
            <h4 className="text-xs uppercase tracking-wide text-muted mb-3">Comments</h4>
            <div className="space-y-3 mb-3">
              {projComments.map(c => <CommentBubble key={c.id} comment={c} user={userById(app.users, c.userId)} />)}
              {projComments.length === 0 && <p className="text-xs text-muted">No comments yet.</p>}
            </div>
            {!readOnly && (
              <div className="flex gap-2">
                <CommentComposer value={commentText} onChange={setCommentText} users={app.users} onSubmit={() => { if (commentText.trim()) { app.addComment('samplingProject', id, commentText); setCommentText(''); } }} />
                <Button size="sm" onClick={() => { if (commentText.trim()) { app.addComment('samplingProject', id, commentText); setCommentText(''); } }}>Post</Button>
              </div>
            )}
          </Card>
        </div>
      </div>

      {!readOnly && <RequestRoundModal open={requestOpen} onClose={() => setRequestOpen(false)} samplingProjectId={id} />}
      {!readOnly && <MoveToProductionModal open={moveToProductionOpen} onClose={() => setMoveToProductionOpen(false)} samplingProjectId={id} />}
    </div>
  );
}

/* ======================================================================
   14. COMPONENT TRACKER
   ====================================================================== */

function ComponentTracker() {
  const app = useApp();
  const [statusFilter, setStatusFilter] = useState(() => app.route.params?.statusFilter || 'all');
  const [ownerFilter, setOwnerFilter] = useState('all');
  const [projectFilter, setProjectFilter] = useState('all');
  const [selected, setSelected] = useState([]);

  const visibleProjectIds = app.projects.filter(p => canSeeProject(app.currentUser, p) && !p.isArchived).map(p => p.id);
  const list = app.components.filter(c => visibleProjectIds.includes(c.projectId))
    .filter(c => statusFilter === 'all' || c.status === statusFilter)
    .filter(c => ownerFilter === 'all' || c.owner === ownerFilter)
    .filter(c => projectFilter === 'all' || c.projectId === projectFilter);

  const isEditable = (c) => canEditItem(app.currentUser, c.owner, app.projects.find(p => p.id === c.projectId));
  const editableList = list.filter(isEditable);

  const toggleSelect = (id) => setSelected(s => s.includes(id) ? s.filter(x => x !== id) : [...s, id]);

  const handleNextActionChange = (comp, nextAction) => {
    app.updateComponent(comp.id, { nextAction });
    app.microToast('Saved');
    if (nextAction === 'Complete') {
      const done = app.allComponentsComplete(comp.projectId);
      if (done) app.toast(`All components complete for ${comp.projectName}. Visit the project to mark it complete.`);
    }
  };

  const exportCSV = () => {
    const header = ['Name', 'Project', 'Supplier', 'Status', 'Price', 'Currency', 'Order Qty', 'Start Prod. Date', 'Duration', 'Next Action', 'Updated', 'Owner'];
    const rows = list.map(c => [c.name, c.projectName, supplierById(app.suppliers, c.supplier)?.name, c.status, c.price, c.currency || 'EUR', c.orderQty, c.startDate, durationSince(c.startDate), c.nextAction, c.nextActionUpdatedAt || c.startDate, userById(app.users, c.owner)?.name]);
    const csv = [header, ...rows].map(r => r.join(',')).join('\n');
    const blob = new Blob([csv], { type: 'text/csv' });
    const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'components.csv'; a.click();
  };

  return (
    <div className="p-4 md:p-6 max-w-[1500px] mx-auto animate-fadeUp">
      <div className="flex items-center justify-between mb-5 flex-wrap gap-3">
        <h1 className="font-display italic text-2xl text-primary tracking-tight">Components</h1>
        <Button variant="secondary" size="sm" icon={Download} onClick={exportCSV}>Export CSV</Button>
      </div>

      <div className="flex flex-wrap gap-3 mb-4">
        <Select className="w-44" value={statusFilter} onChange={e => setStatusFilter(e.target.value)}>
          <option value="all">All statuses</option>{COMPONENT_STATUS_FILTERS.map(s => <option key={s} value={s}>{s}</option>)}
        </Select>
        <Select className="w-44" value={ownerFilter} onChange={e => setOwnerFilter(e.target.value)}>
          <option value="all">All owners</option>{app.users.map(u => <option key={u.id} value={u.id}>{u.name}</option>)}
        </Select>
        <Select className="w-52" value={projectFilter} onChange={e => setProjectFilter(e.target.value)}>
          <option value="all">All projects</option>{app.projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
        </Select>
      </div>

      {selected.length > 0 && (
        <div className="mb-3 flex items-center gap-3 bg-elevated border border-border rounded-lg px-4 py-2.5">
          <span className="text-sm text-primary">{selected.length} selected</span>
          <Select className="w-44" onChange={e => { selected.forEach(id => app.updateComponent(id, { nextAction: e.target.value })); app.toast('Bulk next action updated.'); setSelected([]); }}>
            <option value="">Update Next Action...</option>{NEXT_ACTIONS.map(s => <option key={s} value={s}>{s}</option>)}
          </Select>
          <Select className="w-40" onChange={e => { selected.forEach(id => app.updateComponent(id, { owner: e.target.value })); app.toast('Owner assigned.'); setSelected([]); }}>
            <option value="">Assign Owner...</option>{app.users.map(u => <option key={u.id} value={u.id}>{u.name}</option>)}
          </Select>
        </div>
      )}

      {list.length === 0 ? (
        <EmptyState icon={Package} title="No components yet" message="Components appear here once you move an approved sample to production." actionLabel="Go to Sampling" onAction={() => app.navigate('sampling')} />
      ) : (
        <Card className="overflow-x-auto">
          <table className="w-full text-sm min-w-[1300px]">
            <thead><tr className="text-left text-[10px] uppercase tracking-widest text-muted border-b border-border">
              <th className="py-3 px-4"><input type="checkbox" className="accent-gold" checked={editableList.length > 0 && selected.length === editableList.length} onChange={e => setSelected(e.target.checked ? editableList.map(c => c.id) : [])} /></th>
              <th className="py-3 px-3">Component</th><th className="py-3 px-3">Project</th><th className="py-3 px-3">Supplier</th>
              <th className="py-3 px-3">Status</th><th className="py-3 px-3">Price</th><th className="py-3 px-3">Order Qty</th>
              <th className="py-3 px-3">Start Prod. Date</th><th className="py-3 px-3">Duration</th>
              <th className="py-3 px-3">Next Action</th><th className="py-3 px-3">Updated</th><th className="py-3 px-3">Owner</th>
            </tr></thead>
            <tbody>
              {list.map(c => {
                const editable = isEditable(c);
                return (
                  <tr key={c.id} className="border-b border-border last:border-0 hover:bg-elevated/40">
                    <td className="py-2.5 px-4"><input type="checkbox" className="accent-gold" disabled={!editable} checked={selected.includes(c.id)} onChange={() => toggleSelect(c.id)} /></td>
                    <td className="py-2.5 px-3 text-primary">{c.name}</td>
                    <td className="py-2.5 px-3"><button onClick={() => app.navigate('projectDetail', c.projectId)} className="text-gold hover:underline">{c.projectName}</button></td>
                    <td className="py-2.5 px-3 text-muted">{supplierById(app.suppliers, c.supplier)?.name}</td>
                    <td className="py-2.5 px-3"><StatusPill status={c.status} size="sm" /></td>
                    <td className="py-2.5 px-3 font-mono text-primary">{fmtCurrency(c.price, c.currency)}</td>
                    <td className="py-2.5 px-3 text-muted">{typeof c.orderQty === 'number' ? c.orderQty.toLocaleString('en-US') : c.orderQty}</td>
                    <td className="py-2.5 px-3 font-mono text-muted">{fmtDateShort(c.startDate)}</td>
                    <td className="py-2.5 px-3 font-mono text-muted">{c.completedDate ? daysBetween(c.startDate, c.completedDate) : durationSince(c.startDate)} days</td>
                    <td className="py-2.5 px-3">
                      {editable ? (
                        <InlineSelect value={c.nextAction} options={NEXT_ACTIONS} tone="muted" onChange={v => handleNextActionChange(c, v)} />
                      ) : (
                        <span className="text-xs text-muted" title="Only the owner or an admin can change this">{c.nextAction}</span>
                      )}
                    </td>
                    <td className="py-2.5 px-3 font-mono text-muted">{fmtDateShort(c.nextActionUpdatedAt || c.startDate)}</td>
                    <td className="py-2.5 px-3"><div className="flex items-center gap-1.5"><Avatar user={userById(app.users, c.owner)} size="sm" /><span className="text-xs text-muted hidden xl:inline">{userById(app.users, c.owner)?.name}</span></div></td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </Card>
      )}
    </div>
  );
}

/* ======================================================================
   15. SUPPLIERS
   ====================================================================== */

// Star rating is systemized rather than hand-set (developer decision,
// 2026-08-10): of every line item ever quoted by this supplier (any
// status - a batch of items quoted and never converted is itself a real
// performance signal), what fraction actually reached production. "Reached
// production" means the item's linked sampling track hit 'Moved to
// Production' - deliberately including items whose resulting component has
// since finished ('Complete'), unlike getLineItemStage's funnel-stage
// classification (which nulls those out because Complete is a separate
// bucket in that specific widget, not because they don't count as
// produced). Returns null (render as "Unrated") until the supplier has at
// least one quoted line item - a fresh/potential supplier with nothing
// quoted yet shouldn't read as "0 stars, terrible."
// serialNo comes from a real DB sequence (supplier_serial_no_seq) - this is
// purely display formatting, "No. 00001", never the source of the number.
function formatSupplierSerial(serialNo) {
  return `No. ${String(serialNo).padStart(5, '0')}`;
}

function supplierPerformanceRating(supplierId, quotations, samplingProjects) {
  const items = quotations.filter(q => q.supplierId === supplierId).flatMap(q => q.lineItems || []);
  if (!items.length) return null;
  const produced = items.filter(li => {
    if (!li.samplingProjectId) return false;
    const sp = samplingProjects.find(s => s.id === li.samplingProjectId);
    return !!sp && sp.status === 'Moved to Production';
  }).length;
  return { stars: Math.round((produced / items.length) * 5), produced, quoted: items.length };
}

// Suppliers are shared org-wide by design (no per-project restriction), so
// the same real-world supplier can easily get re-entered by two different
// people who don't know the other already added it - especially once
// several people are creating suppliers. Since names/reps often get typed
// differently each time ("Zhejiang Top One" vs "Top One Packaging Co."),
// this flags a *possible* match rather than blocking on an exact-string
// check: any shared significant word in the name or representative name, or
// a matching phone number (digits only, tolerant of missing country-code
// prefixes), surfaces a "did you mean this one" prompt before creating a
// new row - not a hard constraint, since a genuine near-duplicate name for a
// different real company is possible and shouldn't be blocked outright.
const SUPPLIER_MATCH_STOPWORDS = new Set(['co', 'ltd', 'inc', 'corp', 'llc', 'gmbh', 'sa', 'the', 'and', 'company', 'group', 'technology', 'trading', 'industries']);
function significantWords(text) {
  return (text || '').toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, ' ').split(/\s+/).filter(w => w.length > 1 && !SUPPLIER_MATCH_STOPWORDS.has(w));
}
function normalizePhoneDigits(phone) {
  return (phone || '').replace(/\D/g, '');
}
function findPossibleDuplicateSupplier(form, suppliers) {
  const nameWords = significantWords(form.name);
  const contactWords = significantWords(form.contact);
  const phoneDigits = normalizePhoneDigits(form.phone);
  for (const s of suppliers) {
    const nameMatch = nameWords.some(w => significantWords(s.name).includes(w));
    const contactMatch = contactWords.length > 0 && contactWords.some(w => significantWords(s.contact).includes(w));
    const sPhoneDigits = normalizePhoneDigits(s.phone);
    const phoneMatch = phoneDigits.length >= 7 && sPhoneDigits.length >= 7 && (phoneDigits.endsWith(sPhoneDigits) || sPhoneDigits.endsWith(phoneDigits));
    if (nameMatch || contactMatch || phoneMatch) return s;
  }
  return null;
}

function AddSupplierModal({ open, onClose }) {
  const app = useApp();
  const emptyForm = { name: '', contact: '', email: '', phone: '', country: '', makes: '', moq: '', lead: '', terms: '' };
  const [form, setForm] = useState(emptyForm);

  const submit = async () => {
    if (!form.name.trim()) { app.toast('Supplier name is required.', 'error'); return; }
    const dup = findPossibleDuplicateSupplier(form, app.suppliers);
    if (dup) {
      const goToExisting = await app.confirm({
        title: 'Possible existing supplier',
        message: `This looks like it might already be "${dup.name}" — same name, representative, or phone number as a supplier already in the system. Want to open that one instead?`,
        confirmLabel: 'Yes, show me',
        cancelLabel: "No, it's different",
        confirmVariant: 'primary',
      });
      if (goToExisting) { onClose(); app.navigate('supplierDetail', dup.id); return; }
    }
    app.addSupplier(form);
    onClose();
    setForm(emptyForm);
  };

  return (
    <Modal open={open} onClose={onClose} title="Add Supplier" width="max-w-xl">
      <div className="space-y-4">
        <div><Label>Supplier Name</Label><Input autoFocus value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} placeholder="e.g. Nile Glassworks" /></div>
        <div className="grid grid-cols-2 gap-3">
          <div><Label>Representative Name</Label><Input value={form.contact} onChange={e => setForm(f => ({ ...f, contact: e.target.value }))} placeholder="e.g. Sara Youssef" /></div>
          <div><Label>Country</Label><Input value={form.country} onChange={e => setForm(f => ({ ...f, country: e.target.value }))} placeholder="e.g. Egypt" /></div>
          <div><Label>Email Address</Label><Input type="email" value={form.email} onChange={e => setForm(f => ({ ...f, email: e.target.value }))} placeholder="name@company.com" /></div>
          <div><Label>Phone Number</Label><Input type="tel" value={form.phone} onChange={e => setForm(f => ({ ...f, phone: e.target.value }))} placeholder="+20 10 1234 5678" /></div>
          <div><Label>Category</Label><Input value={form.makes} onChange={e => setForm(f => ({ ...f, makes: e.target.value }))} placeholder="e.g. Bottles & Jars" /></div>
          <div><Label>MOQ</Label><Input value={form.moq} onChange={e => setForm(f => ({ ...f, moq: e.target.value }))} placeholder="e.g. 30,000 units" /></div>
          <div><Label>Lead Time (days)</Label><Input type="number" value={form.lead} onChange={e => setForm(f => ({ ...f, lead: e.target.value }))} /></div>
          <div><Label>Terms</Label><Input value={form.terms} onChange={e => setForm(f => ({ ...f, terms: e.target.value }))} placeholder="e.g. 30% TT / 70% BL" /></div>
        </div>
        <div className="flex justify-end gap-2 pt-2"><Button variant="secondary" onClick={onClose}>Cancel</Button><Button onClick={submit}>Add Supplier</Button></div>
      </div>
    </Modal>
  );
}

function EditSupplierModal({ open, onClose, supplier }) {
  const app = useApp();
  const [form, setForm] = useState(null);

  useEffect(() => {
    if (supplier) {
      setForm({
        name: supplier.name || '', contact: supplier.contact || '', email: supplier.email || '', phone: supplier.phone || '',
        country: supplier.country || '', makes: supplier.makes || '', moq: supplier.moq || '', lead: supplier.lead ?? '',
        terms: supplier.terms || '',
      });
    }
  }, [supplier]);

  if (!form) return null;

  const submit = () => {
    if (!form.name.trim()) { app.toast('Supplier name is required.', 'error'); return; }
    app.updateSupplier(supplier.id, {
      name: form.name.trim(), contact: form.contact, email: form.email, phone: form.phone,
      country: form.country, makes: form.makes, moq: form.moq,
      lead: form.lead === '' ? null : Number(form.lead), terms: form.terms,
    });
    app.microToast('Saved');
    onClose();
  };

  return (
    <Modal open={open} onClose={onClose} title={`Edit Supplier — ${supplier?.name || ''}`} width="max-w-xl">
      <div className="space-y-4">
        <div><Label>Supplier Name</Label><Input value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} /></div>
        <div className="grid grid-cols-2 gap-3">
          <div><Label>Representative Name</Label><Input value={form.contact} onChange={e => setForm(f => ({ ...f, contact: e.target.value }))} /></div>
          <div><Label>Country</Label><Input value={form.country} onChange={e => setForm(f => ({ ...f, country: e.target.value }))} /></div>
          <div><Label>Email Address</Label><Input type="email" value={form.email} onChange={e => setForm(f => ({ ...f, email: e.target.value }))} /></div>
          <div><Label>Phone Number</Label><Input type="tel" value={form.phone} onChange={e => setForm(f => ({ ...f, phone: e.target.value }))} /></div>
          <div><Label>Category</Label><Input value={form.makes} onChange={e => setForm(f => ({ ...f, makes: e.target.value }))} /></div>
          <div><Label>MOQ</Label><Input value={form.moq} onChange={e => setForm(f => ({ ...f, moq: e.target.value }))} /></div>
          <div><Label>Lead Time (days)</Label><Input type="number" value={form.lead} onChange={e => setForm(f => ({ ...f, lead: e.target.value }))} /></div>
          <div><Label>Terms</Label><Input value={form.terms} onChange={e => setForm(f => ({ ...f, terms: e.target.value }))} /></div>
        </div>
        <p className="text-xs text-muted">Star rating is calculated automatically from production conversion rate — see the Performance tab.</p>
        <div className="flex justify-end gap-2 pt-2"><Button variant="secondary" onClick={onClose}>Cancel</Button><Button onClick={submit}>Save Changes</Button></div>
      </div>
    </Modal>
  );
}

function supplierStatusLabel(s) {
  if (s.isPotential) return 'Potential';
  if (s.active) return 'Active';
  return 'Inactive';
}
const SUPPLIER_STATUS_TONE = { Active: 'sage', Potential: 'purple', Inactive: 'muted' };

// "Last Activity" must reflect something that actually happened - suppliers.updated_at
// is never bumped by any trigger, so it's frozen at creation and would be dishonest to
// show here. Instead derive the most recent real touchpoint from quotations and sample
// rounds tied to this supplier, the same signals supplierPerformanceRating already uses.
function supplierLastActivityISO(supplierId, quotations, sampleRounds) {
  let latest = null;
  const consider = iso => { if (iso && (!latest || iso > latest)) latest = iso; };
  quotations.forEach(q => { if (q.supplierId === supplierId) consider(q.createdAt); });
  sampleRounds.forEach(r => {
    if (r.supplierId !== supplierId) return;
    consider(r.dateRequested); consider(r.dateReceived); consider(r.dateReviewed);
    (r.statusHistory || []).forEach(h => consider(h.updatedAt));
  });
  return latest;
}

function exportSuppliersCSV(list, quotations, samplingProjects, sampleRounds) {
  const headers = ['Supplier', 'Serial No', 'Category', 'Country', 'Representative', 'Email', 'Phone', 'MOQ', 'Lead Time (days)', 'Terms', 'Rating', 'Status', 'Last Activity'];
  const rows = list.map(s => {
    const perf = supplierPerformanceRating(s.id, quotations, samplingProjects);
    const lastActivity = supplierLastActivityISO(s.id, quotations, sampleRounds);
    return [
      s.name, s.serialNo != null ? formatSupplierSerial(s.serialNo) : '', s.makes || '', s.country || '',
      s.contact || '', s.email || '', s.phone || '', s.moq || '', s.lead ?? '', s.terms || '',
      perf ? `${perf.stars}/5` : '', supplierStatusLabel(s), lastActivity || '',
    ];
  });
  const csv = [headers, ...rows].map(r => r.map(v => `"${String(v ?? '').replace(/"/g, '""')}"`).join(',')).join('\n');
  const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = `suppliers-${todayStr()}.csv`;
  document.body.appendChild(a); a.click(); document.body.removeChild(a);
  URL.revokeObjectURL(url);
}

const SUPPLIERS_PAGE_SIZE = 25;

function SuppliersView() {
  const app = useApp();
  const [search, setSearch] = useState('');
  const [categoryFilter, setCategoryFilter] = useState('');
  const [countryFilter, setCountryFilter] = useState('');
  const [statusFilter, setStatusFilter] = useState('');
  const [minRating, setMinRating] = useState('');
  const [hasContactOnly, setHasContactOnly] = useState(false);
  const [sortKey, setSortKey] = useState('name');
  const [sortDir, setSortDir] = useState('asc');
  const [page, setPage] = useState(1);
  const [addOpen, setAddOpen] = useState(false);
  const [editSupplier, setEditSupplier] = useState(null);

  const categories = [...new Set(app.suppliers.map(s => s.makes).filter(Boolean))].sort();
  const countries = [...new Set(app.suppliers.map(s => s.country).filter(Boolean))].sort();

  const resetPage = fn => e => { fn(e); setPage(1); };

  const enriched = app.suppliers.map(s => ({
    ...s,
    perf: supplierPerformanceRating(s.id, app.quotations, app.samplingProjects),
    statusLabel: supplierStatusLabel(s),
    lastActivity: supplierLastActivityISO(s.id, app.quotations, app.sampleRounds),
  }));

  let filtered = enriched.filter(s => {
    const q = search.trim().toLowerCase();
    if (q && !(s.name.toLowerCase().includes(q) || (s.contact || '').toLowerCase().includes(q) || (s.makes || '').toLowerCase().includes(q))) return false;
    if (categoryFilter && s.makes !== categoryFilter) return false;
    if (countryFilter && s.country !== countryFilter) return false;
    if (statusFilter && s.statusLabel !== statusFilter) return false;
    if (minRating && (!s.perf || s.perf.stars < Number(minRating))) return false;
    if (hasContactOnly && !s.email && !s.phone) return false;
    return true;
  });

  filtered.sort((a, b) => {
    let av, bv;
    if (sortKey === 'name') { av = a.name.toLowerCase(); bv = b.name.toLowerCase(); }
    else if (sortKey === 'rating') { av = a.perf?.stars ?? -1; bv = b.perf?.stars ?? -1; }
    else if (sortKey === 'lastActivity') { av = a.lastActivity || ''; bv = b.lastActivity || ''; }
    if (av < bv) return sortDir === 'asc' ? -1 : 1;
    if (av > bv) return sortDir === 'asc' ? 1 : -1;
    return 0;
  });

  const total = filtered.length;
  const totalPages = Math.max(1, Math.ceil(total / SUPPLIERS_PAGE_SIZE));
  const clampedPage = Math.min(page, totalPages);
  const pageStart = (clampedPage - 1) * SUPPLIERS_PAGE_SIZE;
  const shown = filtered.slice(pageStart, pageStart + SUPPLIERS_PAGE_SIZE);

  const toggleSort = key => {
    if (sortKey === key) setSortDir(d => (d === 'asc' ? 'desc' : 'asc'));
    else { setSortKey(key); setSortDir('asc'); }
    setPage(1);
  };

  const SortHeader = ({ label, k, className = '' }) => (
    <th className={`py-2.5 px-4 select-none ${className}`}>
      <button onClick={() => toggleSort(k)} className={`inline-flex items-center gap-1 hover:text-primary transition-colors ${sortKey === k ? 'text-primary' : ''}`}>
        {label}<ChevronsUpDown className="w-3 h-3" />
      </button>
    </th>
  );

  const hasFilters = search || categoryFilter || countryFilter || statusFilter || minRating || hasContactOnly;
  const clearFilters = () => { setSearch(''); setCategoryFilter(''); setCountryFilter(''); setStatusFilter(''); setMinRating(''); setHasContactOnly(false); setPage(1); };

  return (
    <div className="p-4 md:p-6 max-w-[1400px] mx-auto animate-fadeUp">
      <div className="flex items-center justify-between mb-5 flex-wrap gap-3">
        <h1 className="font-display italic text-2xl text-primary tracking-tight">Suppliers</h1>
      </div>

      <div className="flex items-center justify-between mb-4 flex-wrap gap-3">
        <div className="relative w-72">
          <Search className="w-3.5 h-3.5 text-muted absolute left-3 top-1/2 -translate-y-1/2" />
          <Input className="pl-8" placeholder="Search name, contact, category..." value={search} onChange={resetPage(e => setSearch(e.target.value))} />
        </div>
        <div className="flex items-center gap-2 flex-wrap">
          <Select className="!w-auto" value={categoryFilter} onChange={resetPage(e => setCategoryFilter(e.target.value))}>
            <option value="">All Categories</option>
            {categories.map(c => <option key={c} value={c}>{c}</option>)}
          </Select>
          <Select className="!w-auto" value={countryFilter} onChange={resetPage(e => setCountryFilter(e.target.value))}>
            <option value="">All Countries</option>
            {countries.map(c => <option key={c} value={c}>{c}</option>)}
          </Select>
          <Select className="!w-auto" value={statusFilter} onChange={resetPage(e => setStatusFilter(e.target.value))}>
            <option value="">All Statuses</option>
            <option value="Active">Active</option>
            <option value="Potential">Potential</option>
            <option value="Inactive">Inactive</option>
          </Select>
          <Dropdown align="right" trigger={
            <Button variant="secondary" icon={Filter} size="md">More Filters{hasFilters && (minRating || hasContactOnly) ? ' •' : ''}</Button>
          }>
            <div className="px-3 py-2 w-56" onClick={e => e.stopPropagation()}>
              <Label>Minimum Rating</Label>
              <Select value={minRating} onChange={e => { setMinRating(e.target.value); setPage(1); }}>
                <option value="">Any</option>
                {[5, 4, 3, 2, 1].map(n => <option key={n} value={n}>{n}+ stars</option>)}
              </Select>
              <label className="flex items-center gap-2 mt-3 text-sm text-primary cursor-pointer">
                <input type="checkbox" checked={hasContactOnly} onChange={e => { setHasContactOnly(e.target.checked); setPage(1); }} />
                Has email or phone
              </label>
              {hasFilters && (
                <button onClick={clearFilters} className="text-xs text-gold hover:underline mt-3">Clear all filters</button>
              )}
            </div>
          </Dropdown>
          <Button variant="secondary" icon={Download} onClick={() => exportSuppliersCSV(filtered, app.quotations, app.samplingProjects, app.sampleRounds)}>Export</Button>
          <Button icon={Plus} onClick={() => setAddOpen(true)}>Add Supplier</Button>
        </div>
      </div>

      {total === 0 ? (
        <Card>
          <EmptyState
            icon={Building2} title={hasFilters ? 'No suppliers match' : 'Nothing here yet'}
            message={hasFilters ? 'Try different search or filter criteria.' : 'Add your first supplier to start tracking quotes and samples.'}
            actionLabel={hasFilters ? undefined : 'Add Supplier'} onAction={() => setAddOpen(true)}
          />
        </Card>
      ) : (
        <Card className="overflow-hidden">
          <div className="overflow-x-auto">
            <table className="w-full text-sm">
              <thead>
                <tr className="text-left text-[10px] uppercase tracking-widest text-muted border-b border-border">
                  <SortHeader label="Supplier" k="name" />
                  <th className="py-2.5 px-4 hidden md:table-cell">Category</th>
                  <th className="py-2.5 px-4 hidden lg:table-cell">Country</th>
                  <th className="py-2.5 px-4 hidden lg:table-cell">Contact</th>
                  <th className="py-2.5 px-4 hidden xl:table-cell">MOQ</th>
                  <th className="py-2.5 px-4 hidden xl:table-cell">Lead Time</th>
                  <th className="py-2.5 px-4 hidden xl:table-cell">Terms</th>
                  <SortHeader label="Rating" k="rating" />
                  <th className="py-2.5 px-4">Status</th>
                  <SortHeader label="Last Activity" k="lastActivity" className="hidden md:table-cell" />
                  <th className="py-2.5 px-4 text-right">Actions</th>
                </tr>
              </thead>
              <tbody>
                {shown.map(s => (
                  <tr key={s.id} className="border-b border-border last:border-0 hover:bg-elevated transition-colors cursor-pointer" onClick={() => app.navigate('supplierDetail', s.id)}>
                    <td className="py-3 px-4">
                      <div className="font-medium text-primary">{s.name}</div>
                      {s.serialNo != null && <div className="text-[10px] font-mono text-muted mt-0.5">{formatSupplierSerial(s.serialNo)}</div>}
                    </td>
                    <td className="py-3 px-4 hidden md:table-cell text-muted">{s.makes || '—'}</td>
                    <td className="py-3 px-4 hidden lg:table-cell text-muted">
                      {s.country ? <span className="flex items-center gap-1"><Globe2 className="w-3 h-3" />{s.country}</span> : '—'}
                    </td>
                    <td className="py-3 px-4 hidden lg:table-cell text-muted">
                      {s.contact && <div className="text-primary">{s.contact}</div>}
                      {s.email && <div className="text-xs">{s.email}</div>}
                      {s.phone && <div className="text-xs">{s.phone}</div>}
                      {!s.contact && !s.email && !s.phone && '—'}
                    </td>
                    <td className="py-3 px-4 hidden xl:table-cell font-mono text-muted">{s.moq || '—'}</td>
                    <td className="py-3 px-4 hidden xl:table-cell font-mono text-muted">{s.lead != null ? `${s.lead}d` : '—'}</td>
                    <td className="py-3 px-4 hidden xl:table-cell text-muted">{s.terms || '—'}</td>
                    <td className="py-3 px-4">
                      {s.perf ? (
                        <div className="flex items-center gap-0.5" title={`${s.perf.produced} of ${s.perf.quoted} quoted items reached production`}>
                          {[1, 2, 3, 4, 5].map(i => <Star key={i} className={`w-3 h-3 ${i <= s.perf.stars ? 'fill-gold text-gold' : 'text-border'}`} />)}
                        </div>
                      ) : <span className="text-xs text-muted">Unrated</span>}
                    </td>
                    <td className="py-3 px-4">
                      <span className={`text-[10px] uppercase font-mono rounded px-1.5 py-0.5 border ${TONE_CLASSES[SUPPLIER_STATUS_TONE[s.statusLabel]]}`}>{s.statusLabel}</span>
                    </td>
                    <td className="py-3 px-4 hidden md:table-cell text-muted whitespace-nowrap">{fmtRelativeFromISO(s.lastActivity)}</td>
                    <td className="py-3 px-4 text-right" onClick={e => e.stopPropagation()}>
                      <Dropdown align="right" trigger={
                        <button className="text-muted hover:text-primary p-1 rounded hover:bg-surface"><MoreHorizontal className="w-4 h-4" /></button>
                      }>
                        <button onClick={() => app.navigate('supplierDetail', s.id)} className="w-full text-left px-3 py-1.5 text-sm text-primary hover:bg-surface">View Details</button>
                        <button onClick={() => setEditSupplier(s)} className="w-full text-left px-3 py-1.5 text-sm text-primary hover:bg-surface">Edit Supplier</button>
                      </Dropdown>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
          <div className="flex items-center justify-between px-4 py-3 border-t border-border text-sm">
            <span className="text-muted">Showing {pageStart + 1}–{Math.min(pageStart + SUPPLIERS_PAGE_SIZE, total)} of {total} supplier{total !== 1 ? 's' : ''}</span>
            <div className="flex items-center gap-2">
              <Button variant="secondary" size="sm" icon={ChevronLeft} disabled={clampedPage <= 1} onClick={() => setPage(p => Math.max(1, p - 1))}>Previous</Button>
              <span className="text-muted text-xs">Page {clampedPage} of {totalPages}</span>
              <Button variant="secondary" size="sm" onClick={() => setPage(p => Math.min(totalPages, p + 1))} disabled={clampedPage >= totalPages}>Next<ChevronRight className="w-3.5 h-3.5" /></Button>
            </div>
          </div>
        </Card>
      )}
      <AddSupplierModal open={addOpen} onClose={() => setAddOpen(false)} />
      <EditSupplierModal open={!!editSupplier} onClose={() => setEditSupplier(null)} supplier={editSupplier} />
    </div>
  );
}

function SupplierDetail({ id }) {
  const app = useApp();
  const supplier = supplierById(app.suppliers, id);
  const [tab, setTab] = useState('components');
  const [editOpen, setEditOpen] = useState(false);
  if (!supplier) return <EmptyState title="Not found" message="Supplier missing." actionLabel="Back" onAction={() => app.navigate('suppliers')} />;

  const comps = app.components.filter(c => c.supplier === id);
  const quotes = app.quotations.filter(q => q.supplierId === id);
  const rounds = app.sampleRounds.filter(r => r.supplierId === id);
  const attachments = app.attachments.filter(a => a.entityType === 'supplier' && a.entityId === id);
  const approvedRounds = rounds.filter(r => r.status === 'Approved').length;
  const onTimeRate = rounds.length ? Math.round((approvedRounds / rounds.length) * 100) : 0;
  const perf = supplierPerformanceRating(id, app.quotations, app.samplingProjects);

  return (
    <div className="p-4 md:p-6 max-w-[1200px] mx-auto animate-fadeUp">
      <div className="flex items-center justify-between mb-4">
        <button onClick={() => app.navigate('suppliers')} className="flex items-center gap-1.5 text-sm text-muted hover:text-primary"><ArrowLeft className="w-4 h-4" /> Back</button>
        <Button variant="secondary" size="sm" icon={Pencil} onClick={() => setEditOpen(true)}>Edit Supplier</Button>
      </div>
      <div className="flex items-center gap-3 mb-1 flex-wrap">
        <h1 className="font-display italic text-4xl text-primary tracking-tight">{supplier.name}</h1>
        {supplier.serialNo != null && <span className="text-xs font-mono text-muted">{formatSupplierSerial(supplier.serialNo)}</span>}
        {supplier.isPotential && <span className="text-[10px] uppercase font-mono text-purple bg-purple/10 rounded px-1.5 py-0.5">Potential</span>}
        {perf ? (
          <div className="flex items-center gap-1.5" title={`${perf.produced} of ${perf.quoted} quoted items reached production`}>
            <div className="flex items-center gap-0.5">{[1, 2, 3, 4, 5].map(i => <Star key={i} className={`w-4 h-4 ${i <= perf.stars ? 'fill-gold text-gold' : 'text-border'}`} />)}</div>
            <span className="text-xs text-muted">({perf.produced}/{perf.quoted} produced)</span>
          </div>
        ) : (
          <span className="text-xs text-muted">Unrated — no quotes yet</span>
        )}
      </div>
      <p className="text-muted text-sm mb-5 flex items-center gap-3 flex-wrap">
        {supplier.country && <span className="flex items-center gap-1"><Globe2 className="w-3.5 h-3.5" />{supplier.country}</span>}
        {supplier.contact && <span className="flex items-center gap-1"><UserCog className="w-3.5 h-3.5" />{supplier.contact}</span>}
        {supplier.email && <span className="flex items-center gap-1"><Mail className="w-3.5 h-3.5" />{supplier.email}</span>}
        {supplier.phone && <span className="flex items-center gap-1"><Phone className="w-3.5 h-3.5" />{supplier.phone}</span>}
      </p>

      <div className="grid grid-cols-3 gap-4 mb-6">
        <Card className="p-4"><div className="text-xs text-muted mb-1">Lead Time</div><div className="font-display text-2xl text-primary">{supplier.lead != null ? <>{supplier.lead}<span className="text-sm text-muted"> days</span></> : '—'}</div></Card>
        <Card className="p-4"><div className="text-xs text-muted mb-1">MOQ</div><div className="font-display text-2xl text-primary">{supplier.moq || '—'}</div></Card>
        <Card className="p-4"><div className="text-xs text-muted mb-1">Terms</div><div className="font-display text-lg text-primary">{supplier.terms || '—'}</div></Card>
      </div>

      <div className="flex rounded-lg border border-border overflow-hidden mb-4 w-fit">
        {[['components', 'Components'], ['quotations', 'Quotations'], ['samples', 'Sample History'], ['performance', 'Performance'], ['attachments', 'Attachments']].map(([key, label]) => (
          <button key={key} onClick={() => setTab(key)} className={`px-4 py-1.5 text-sm ${tab === key ? 'bg-gold/15 text-gold' : 'text-muted hover:bg-elevated'}`}>{label}</button>
        ))}
      </div>

      <Card className="p-5">
        {tab === 'components' && (comps.length ? comps.map(c => (
          <div key={c.id} className="flex items-center justify-between py-2 border-b border-border last:border-0">
            <span className="text-primary text-sm">{c.name}</span><StatusPill status={c.status} size="sm" />
          </div>
        )) : <p className="text-sm text-muted">No components sourced yet.</p>)}
        {tab === 'quotations' && (quotes.length ? quotes.map(q => (
          <div key={q.id} className="flex items-center justify-between py-2 border-b border-border last:border-0">
            <div>
              <span className="text-primary text-sm">{app.projects.find(p => p.id === q.projectId)?.name}</span>
              <span className="text-xs text-muted ml-2">{q.lineItems?.length || 0} item{q.lineItems?.length !== 1 ? 's' : ''}</span>
              {q.referenceNo && <span className="text-[10px] text-muted font-mono ml-2">Ref: {q.referenceNo}</span>}
            </div>
            <span className="font-mono text-sm text-primary">{fmtCurrency(q.lineItems?.[0]?.unitPrice, q.currency)}{q.lineItems?.length > 1 ? '+' : ''}</span>
            <StatusPill status={deriveQuotationStatus(q)} size="sm" />
          </div>
        )) : <p className="text-sm text-muted">No quotations yet.</p>)}
        {tab === 'samples' && (rounds.length ? rounds.map(r => (
          <div key={r.id} className="flex items-center justify-between py-2 border-b border-border last:border-0">
            <span className="text-primary text-sm font-mono">{r.version}</span>
            <span className="text-xs text-muted">{r.resultNotes}</span>
            <StatusPill status={r.status} size="sm" />
          </div>
        )) : <p className="text-sm text-muted">No sample history yet.</p>)}
        {tab === 'performance' && (
          <div className="grid grid-cols-2 gap-4">
            <div><div className="text-xs text-muted mb-1">Sample Approval Rate</div><div className="font-display text-2xl text-sage">{onTimeRate}%</div></div>
            <div><div className="text-xs text-muted mb-1">Total Sample Rounds</div><div className="font-display text-2xl text-primary">{rounds.length}</div></div>
            <div>
              <div className="text-xs text-muted mb-1">Production Conversion Rate</div>
              <div className="font-display text-2xl text-primary">{perf ? `${Math.round((perf.produced / perf.quoted) * 100)}%` : '—'}</div>
              {perf && <div className="text-xs text-muted mt-0.5">{perf.produced} of {perf.quoted} quoted items reached production — drives the star rating</div>}
            </div>
          </div>
        )}
        {tab === 'attachments' && (attachments.length ? <div className="space-y-2">{attachments.map(a => <AttachmentRow key={a.id} attachment={a} user={userById(app.users, a.uploadedBy)} onDelete={app.deleteAttachment} onOpen={app.getAttachmentUrl} />)}</div> : <p className="text-sm text-muted">No attachments.</p>)}
      </Card>

      <EditSupplierModal open={editOpen} onClose={() => setEditOpen(false)} supplier={supplier} />
    </div>
  );
}

/* ======================================================================
   16. TEAM
   ====================================================================== */

function MultiUserSelect({ users, selected, onChange }) {
  const [open, setOpen] = useState(false);
  const [search, setSearch] = useState('');
  const ref = useRef(null);

  useEffect(() => {
    function onDoc(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, []);

  const toggle = (id) => onChange(selected.includes(id) ? selected.filter(x => x !== id) : [...selected, id]);
  const selectedUsers = users.filter(u => selected.includes(u.id));
  const filtered = users.filter(u => u.name.toLowerCase().includes(search.toLowerCase()));

  return (
    <div className="relative" ref={ref}>
      <button
        type="button"
        onClick={() => setOpen(o => !o)}
        className="w-full min-h-[38px] bg-surface border border-border rounded-lg px-3 py-1.5 text-sm text-left flex items-center justify-between gap-2 focus:outline-none focus:shadow-[0_0_0_3px_rgba(24,39,245,0.12)] focus:border-gold/50 transition-all"
      >
        <span className="flex-1 flex flex-wrap gap-1 min-w-0">
          {selectedUsers.length === 0 ? (
            <span className="text-muted/60">Select people...</span>
          ) : selectedUsers.map(u => (
            <span key={u.id} className="inline-flex items-center gap-1 bg-gold/15 text-gold text-xs rounded-full px-2 py-0.5">{u.name}</span>
          ))}
        </span>
        <ChevronDown className={`w-3.5 h-3.5 text-muted shrink-0 transition-transform ${open ? 'rotate-180' : ''}`} />
      </button>
      {open && (
        <div className="absolute z-50 mt-2 left-0 right-0 bg-elevated border border-border rounded-lg shadow-2xl animate-dialogIn overflow-hidden">
          <div className="p-2 border-b border-border">
            <div className="relative">
              <Search className="w-3.5 h-3.5 text-muted absolute left-2.5 top-1/2 -translate-y-1/2" />
              <input
                autoFocus
                value={search}
                onChange={e => setSearch(e.target.value)}
                placeholder="Search people..."
                className="w-full bg-surface border border-border rounded-lg pl-8 pr-2 py-1.5 text-sm text-primary placeholder:text-stale focus:outline-none focus:border-gold/50 transition-all"
              />
            </div>
          </div>
          <div className="max-h-48 overflow-y-auto py-1">
            {filtered.length === 0 ? (
              <div className="px-3 py-4 text-center text-xs text-muted">No matches.</div>
            ) : filtered.map(u => (
              <label key={u.id} className="flex items-center gap-2.5 px-3 py-2 text-sm text-primary hover:bg-base cursor-pointer transition-colors">
                <input type="checkbox" checked={selected.includes(u.id)} onChange={() => toggle(u.id)} className="accent-gold shrink-0" />
                <Avatar user={u} size="sm" ring={false} />
                <span className="truncate">{u.name}</span>
              </label>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

function AssignTaskModal({ open, onClose, assigneeId }) {
  const app = useApp();
  const [mode, setMode] = useState('existing');
  const [form, setForm] = useState({ assignedTo: assigneeId, dueDate: addDays(todayStr(), 7), project: '' });
  const [newProject, setNewProject] = useState({ name: '', category: 'Perfume', target: addDays(todayStr(), 45), owner: assigneeId, priority: 'Normal', assignees: assigneeId ? [assigneeId] : [] });

  useEffect(() => {
    if (!open) return;
    setMode('existing');
    setForm({ assignedTo: assigneeId || app.users[0]?.id, dueDate: addDays(todayStr(), 7), project: '' });
    setNewProject({ name: '', category: 'Perfume', target: addDays(todayStr(), 45), owner: assigneeId || app.currentUserId, priority: 'Normal', assignees: assigneeId ? [assigneeId] : [] });
  }, [open, assigneeId]);

  const selectProject = (projectId) => {
    const proj = app.projects.find(p => p.id === projectId);
    setForm(f => ({ ...f, project: projectId, dueDate: proj?.target || f.dueDate }));
  };

  const submit = () => {
    if (mode === 'existing') {
      const proj = app.projects.find(p => p.id === form.project);
      if (!proj) { app.toast('Select a project first.', 'error'); return; }
      app.addTask({ title: proj.name, description: '', assignedTo: form.assignedTo, dueDate: form.dueDate, linkedEntityType: 'project', linkedEntityId: proj.id });
      // Assigning someone a task on this project makes them a full collaborator, not just a
      // viewer — same edit rights as the owner (add/amend quotations, sampling, components, etc).
      const currentEditableBy = proj.editableBy || [];
      if (form.assignedTo !== proj.owner && !currentEditableBy.includes(form.assignedTo)) {
        app.updateProjectEditableBy(proj.id, [...currentEditableBy, form.assignedTo]);
      }
    } else {
      if (!newProject.name.trim()) { app.toast('Project name is required.', 'error'); return; }
      if (!newProject.assignees.length) { app.toast('Select at least one person to assign.', 'error'); return; }
      const editableBy = newProject.assignees.filter(userId => userId !== newProject.owner);
      const projectId = app.createProject({ ...newProject, editableBy });
      newProject.assignees.forEach(userId => {
        app.addTask({ title: newProject.name, description: '', assignedTo: userId, dueDate: newProject.target, linkedEntityType: 'project', linkedEntityId: projectId });
      });
    }
    onClose();
  };

  return (
    <Modal open={open} onClose={onClose} title="Assign Task">
      <div className="max-h-[70vh] overflow-y-auto pr-1 space-y-4">
        <div className="flex gap-2">
          <Button type="button" size="sm" variant={mode === 'existing' ? 'primary' : 'secondary'} className="flex-1" onClick={() => setMode('existing')}>Link to Existing Project</Button>
          <Button type="button" size="sm" variant={mode === 'new' ? 'primary' : 'secondary'} className="flex-1" onClick={() => setMode('new')}>Create New Project</Button>
        </div>

        {mode === 'existing' ? (
          <>
            <div>
              <Label>Project</Label>
              <Select autoFocus value={form.project} onChange={e => selectProject(e.target.value)}>
                <option value="">Select a project...</option>
                {app.projects.filter(p => !p.isArchived).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
              </Select>
            </div>
            <div className="grid grid-cols-2 gap-3">
              <div><Label>Assign To</Label><Select value={form.assignedTo} onChange={e => setForm(f => ({ ...f, assignedTo: e.target.value }))}>{app.users.map(u => <option key={u.id} value={u.id}>{u.name}</option>)}</Select></div>
              <div><Label>Target Date</Label><Input type="date" value={form.dueDate} onChange={e => setForm(f => ({ ...f, dueDate: e.target.value }))} /></div>
            </div>
          </>
        ) : (
          <>
            <div><Label>Project Name</Label><Input autoFocus value={newProject.name} onChange={e => setNewProject(f => ({ ...f, name: e.target.value }))} placeholder="e.g. Amber EDP 50ml" /></div>
            <div>
              <Label>Assign To</Label>
              <MultiUserSelect users={app.users} selected={newProject.assignees} onChange={next => setNewProject(f => ({ ...f, assignees: next }))} />
            </div>
            <div className="grid grid-cols-2 gap-3">
              <div>
                <Label>Category</Label>
                <Input list="assign-task-category-options" value={newProject.category} onChange={e => setNewProject(f => ({ ...f, category: e.target.value }))} placeholder="e.g. Perfume" />
                <datalist id="assign-task-category-options">{CATEGORY_OPTIONS.map(c => <option key={c} value={c} />)}</datalist>
              </div>
              <div><Label>Priority</Label><Select value={newProject.priority} onChange={e => setNewProject(f => ({ ...f, priority: e.target.value }))}>{PRIORITY_OPTIONS.map(p => <option key={p} value={p}>{p}</option>)}</Select></div>
            </div>
            <div className="grid grid-cols-2 gap-3">
              <div><Label>Target Date</Label><Input type="date" value={newProject.target} onChange={e => setNewProject(f => ({ ...f, target: e.target.value }))} /></div>
              <div><Label>Owner</Label><Select value={newProject.owner} onChange={e => setNewProject(f => ({ ...f, owner: e.target.value }))}>{app.users.map(u => <option key={u.id} value={u.id}>{u.name}</option>)}</Select></div>
            </div>
          </>
        )}

        <div className="flex justify-end gap-2 pt-2"><Button variant="secondary" onClick={onClose}>Cancel</Button><Button onClick={submit}>Assign Task</Button></div>
      </div>
    </Modal>
  );
}

function InviteMemberModal({ open, onClose }) {
  const app = useApp();
  const emptyForm = { email: '', name: '', title: '', accessRole: 'Team', seesAllProjects: false };
  const [form, setForm] = useState(emptyForm);
  const [sending, setSending] = useState(false);

  const submit = async () => {
    if (!form.email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) { app.toast('Enter a valid email address.', 'error'); return; }
    if (!form.name.trim()) { app.toast('Name is required.', 'error'); return; }
    setSending(true);
    await app.inviteMember(form);
    setSending(false);
    setForm(emptyForm);
    onClose();
  };

  return (
    <Modal open={open} onClose={onClose} title="Invite Team Member" width="max-w-md">
      <div className="space-y-4">
        <div><Label>Email Address</Label><Input autoFocus type="email" value={form.email} onChange={e => setForm(f => ({ ...f, email: e.target.value }))} placeholder="name@company.com" /></div>
        <div><Label>Full Name</Label><Input value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} placeholder="e.g. Sarah Ahmed" /></div>
        <div><Label>Job Title</Label><Input value={form.title} onChange={e => setForm(f => ({ ...f, title: e.target.value }))} placeholder="e.g. Supplier Relations" /></div>
        <div>
          <Label>Access Role</Label>
          <Select value={form.accessRole} onChange={e => setForm(f => ({ ...f, accessRole: e.target.value }))}>
            <option value="Team">Team</option>
            <option value="Admin">Admin</option>
          </Select>
        </div>
        {form.accessRole === 'Team' && (
          <div>
            <Label>Access Scope</Label>
            <Select value={form.seesAllProjects ? 'all' : 'related'} onChange={e => setForm(f => ({ ...f, seesAllProjects: e.target.value === 'all' }))}>
              <option value="related">Related Tasks — only projects they own or are added to</option>
              <option value="all">All Tasks — can see every project in the workspace</option>
            </Select>
          </div>
        )}
        <p className="text-xs text-muted">They'll get an email with a link to set their password and sign in.</p>
        <div className="flex justify-end gap-2 pt-2"><Button variant="secondary" onClick={onClose}>Cancel</Button><Button onClick={submit} loading={sending}>{sending ? 'Sending...' : 'Send Invite'}</Button></div>
      </div>
    </Modal>
  );
}

function TeamView() {
  const app = useApp();
  const [taskModalUser, setTaskModalUser] = useState(null);
  const [inviteOpen, setInviteOpen] = useState(false);
  const isAdmin = app.currentUser.accessRole === 'Admin';
  const adminCount = app.users.filter(u => u.accessRole === 'Admin').length;

  const handleRoleChange = (userId, newRole) => {
    if (userId === app.currentUser.id && newRole !== 'Admin' && adminCount <= 1) {
      app.toast('You are the only admin — promote someone else first.', 'error');
      return;
    }
    app.updateMemberRole(userId, newRole);
  };

  const handleRemove = async (u) => {
    if (u.id === app.currentUser.id) { app.toast("You can't remove yourself.", 'error'); return; }
    if (u.accessRole === 'Admin' && adminCount <= 1) { app.toast('Cannot remove the only remaining admin.', 'error'); return; }
    const ok = await app.confirm({ title: `Remove ${u.name}?`, message: 'They will immediately lose access to this workspace. Their past activity and records stay intact.', confirmLabel: 'Remove' });
    if (ok) app.removeMember(u.id);
  };

  const activeProjects = app.projects.filter(p => !p.isArchived);
  const workloads = app.users.map(u => ({
    user: u,
    projectCount: activeProjects.filter(p => p.owner === u.id).length,
    compCount: app.components.filter(c => c.owner === u.id && c.status !== 'Complete').length,
  }));
  const avgLoad = workloads.reduce((s, w) => s + w.projectCount + w.compCount, 0) / (workloads.length || 1);
  const maxLoad = Math.max(...workloads.map(w => w.projectCount + w.compCount), 1);
  const tasksCompletedCount = app.tasks.filter(t => t.status === 'Done').length;

  return (
    <div className="p-4 md:p-6 max-w-[1300px] mx-auto animate-fadeUp">
      <div className="flex items-center justify-between mb-5 flex-wrap gap-3">
        <h1 className="font-display italic text-2xl text-primary tracking-tight">Team</h1>
        {isAdmin && <Button size="sm" icon={UserPlus} onClick={() => setInviteOpen(true)}>Invite Member</Button>}
      </div>

      {isAdmin && (
        <Card className="p-5 mb-6 overflow-x-auto">
          <h3 className="font-display text-lg text-primary mb-4">Manage Team</h3>
          <table className="w-full text-sm min-w-[680px]">
            <thead><tr className="text-left text-[10px] uppercase tracking-widest text-muted border-b border-border">
              <th className="py-2 pr-3">Name</th><th className="py-2 pr-3">Email</th><th className="py-2 pr-3">Title</th><th className="py-2 pr-3">Role</th><th className="py-2 pr-3">Scope</th><th className="py-2"></th>
            </tr></thead>
            <tbody>
              {app.users.map(u => (
                <tr key={u.id} className="border-b border-border last:border-0">
                  <td className="py-2.5 pr-3"><div className="flex items-center gap-2"><Avatar user={u} size="sm" /><span className="text-primary">{u.name}</span></div></td>
                  <td className="py-2.5 pr-3 text-muted font-mono text-xs">{u.email}</td>
                  <td className="py-2.5 pr-3 text-muted">{u.role || '—'}</td>
                  <td className="py-2.5 pr-3">
                    <Select value={u.accessRole} onChange={e => handleRoleChange(u.id, e.target.value)} className="!py-1 !text-xs">
                      <option value="Team">Team</option>
                      <option value="Admin">Admin</option>
                    </Select>
                  </td>
                  <td className="py-2.5 pr-3">
                    {u.accessRole === 'Admin' ? (
                      <span className="text-xs text-muted">All (Admin)</span>
                    ) : (
                      <Select value={u.seesAllProjects ? 'all' : 'related'} onChange={e => app.updateMemberScope(u.id, e.target.value === 'all')} className="!py-1 !text-xs">
                        <option value="related">Related Tasks</option>
                        <option value="all">All Tasks</option>
                      </Select>
                    )}
                  </td>
                  <td className="py-2.5 text-right">
                    <button onClick={() => handleRemove(u)} className="text-muted hover:text-oud transition-colors" title="Remove from workspace"><Trash2 className="w-3.5 h-3.5" /></button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </Card>
      )}

      <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
        <Card className="p-5">
          <div className="flex items-center gap-2 mb-2"><CheckCircle2 className="w-4 h-4 text-sage" /><span className="text-xs text-muted">Tasks Completed</span></div>
          <div className="font-display text-3xl text-primary">{tasksCompletedCount}<span className="text-sm text-muted ml-2">all time</span></div>
        </Card>
        <Card className="p-5">
          <div className="flex items-center gap-2 mb-2"><Flag className="w-4 h-4 text-gold" /><span className="text-xs text-muted">Shared Milestones</span></div>
          <div className="font-display text-3xl text-primary">{app.projects.filter(p => p.isArchived).length}<span className="text-sm text-muted ml-2">completed</span></div>
        </Card>
      </div>

      <Card className="p-5 mb-6">
        <h3 className="font-display text-lg text-primary mb-4">Workload Balance</h3>
        <div className="space-y-3">
          {workloads.map(w => {
            const load = w.projectCount + w.compCount;
            const pct = Math.min(100, (load / maxLoad) * 100);
            const overloaded = load > avgLoad * 1.3;
            return (
              <div key={w.user.id} className="flex items-center gap-3">
                <Avatar user={w.user} size="sm" />
                <span className="w-20 text-sm text-primary shrink-0">{w.user.name}</span>
                <div className="flex-1 h-2 rounded-full bg-elevated border border-border overflow-hidden">
                  <div className="h-full pyramid-progress rounded-full" style={{ width: `${pct}%` }} />
                </div>
                <span className="text-xs font-mono text-muted w-24 shrink-0">{w.projectCount}p · {w.compCount}c</span>
                {overloaded && <span className="text-[10px] bg-oud/15 text-oud rounded-full px-2 py-0.5 shrink-0">Could use a hand</span>}
              </div>
            );
          })}
        </div>
      </Card>

      <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
        {workloads.map(w => {
          const overloaded = (w.projectCount + w.compCount) > avgLoad * 1.3;
          const myTasks = app.tasks.filter(t => t.assignedTo === w.user.id);
          const doneTasks = myTasks.filter(t => t.status === 'Done').length;
          // On-Time Rate is sourced from completed projects, not tasks -
          // "Mark Complete" on a project is a real, already-used action with
          // real target_date/completed_date data behind it; nothing in the
          // app ever transitions a task to Done, so a task-based rate could
          // never populate. A member counts as "part of" a project if they
          // own it or are an editableBy collaborator - both share the
          // outcome, on time or late. Dates are plain YYYY-MM-DD strings, so
          // direct comparison is chronological with no timezone ambiguity.
          const myCompletedProjects = app.projects.filter(p => p.isArchived && p.completedDate && (p.owner === w.user.id || (p.editableBy || []).includes(w.user.id)));
          const onTimeCount = myCompletedProjects.filter(p => p.completedDate <= p.target).length;
          const onTime = myCompletedProjects.length ? Math.round((onTimeCount / myCompletedProjects.length) * 100) : null;
          return (
            <Card key={w.user.id} className="p-5">
              <div className="flex items-center gap-3 mb-3">
                <Avatar user={w.user} size="lg" />
                <div>
                  <h4 className="font-display text-lg text-primary">{w.user.name}</h4>
                  <p className="text-xs text-muted">{w.user.role}</p>
                </div>
              </div>
              {overloaded && <span className="inline-block text-[10px] bg-oud/15 text-oud rounded-full px-2 py-0.5 mb-3">Could use a hand</span>}
              <dl className="space-y-2 text-sm mb-3">
                <div className="flex justify-between"><dt className="text-muted">Projects Owned</dt><dd className="text-primary font-mono">{w.projectCount}</dd></div>
                <div className="flex justify-between"><dt className="text-muted">Tasks Done</dt><dd className="text-primary font-mono">{doneTasks}</dd></div>
              </dl>
              <div className="mb-4">
                <div className="flex justify-between text-xs text-muted mb-1"><span>On-Time Rate</span><span className="font-mono">{onTime === null ? 'No data yet' : `${onTime}%`}</span></div>
                <PyramidProgress progress={onTime ?? 0} height="h-1.5" />
              </div>
              <Button size="sm" variant="secondary" className="w-full" onClick={() => setTaskModalUser(w.user.id)}>Assign Task</Button>
            </Card>
          );
        })}
      </div>

      <AssignTaskModal open={!!taskModalUser} onClose={() => setTaskModalUser(null)} assigneeId={taskModalUser} />
      <InviteMemberModal open={inviteOpen} onClose={() => setInviteOpen(false)} />
    </div>
  );
}

/* ======================================================================
   17. ACCOMPLISHED PROJECTS
   ====================================================================== */

function AccomplishedProjects() {
  const app = useApp();
  const [search, setSearch] = useState('');
  const done = app.projects.filter(p => p.isArchived && p.name.toLowerCase().includes(search.toLowerCase()));

  return (
    <div className="p-4 md:p-6 max-w-[1300px] mx-auto animate-fadeUp">
      <div className="flex items-center justify-between mb-5 flex-wrap gap-3">
        <h1 className="font-display italic text-2xl text-primary tracking-tight">Accomplished Projects</h1>
        <div className="relative w-64">
          <Search className="w-3.5 h-3.5 text-muted absolute left-3 top-1/2 -translate-y-1/2" />
          <Input className="pl-8" placeholder="Search..." value={search} onChange={e => setSearch(e.target.value)} />
        </div>
      </div>
      {done.length === 0 ? (
        <EmptyState icon={Award} title="Nothing to celebrate here yet" message="Complete a project to see it here." />
      ) : (
        <Card className="overflow-x-auto">
          <table className="w-full text-sm min-w-[800px]">
            <thead><tr className="text-left text-[10px] uppercase tracking-widest text-muted border-b border-border">
              <th className="py-3 px-4">Name</th><th className="py-3 px-4">Category</th><th className="py-3 px-4">Start Date</th><th className="py-3 px-4">Completed</th><th className="py-3 px-4">Duration</th><th className="py-3 px-4">Owner</th><th className="py-3 px-4">Status</th>
            </tr></thead>
            <tbody>
              {done.map(p => (
                <tr key={p.id} onClick={() => app.navigate('projectDetail', p.id)} className="border-b border-border last:border-0 hover:bg-elevated/40 cursor-pointer">
                  <td className="py-2.5 px-4 text-primary font-display">{p.name}</td>
                  <td className="py-2.5 px-4 text-muted">{p.category}</td>
                  <td className="py-2.5 px-4 font-mono text-muted">{fmtDateShort(p.startDate)}</td>
                  <td className="py-2.5 px-4 font-mono text-muted">{fmtDateShort(p.completedDate)}</td>
                  <td className="py-2.5 px-4 font-mono text-sage">{daysBetween(p.startDate, p.completedDate)}d</td>
                  <td className="py-2.5 px-4"><Avatar user={userById(app.users, p.owner)} size="sm" /></td>
                  <td className="py-2.5 px-4"><StatusPill status={p.status} size="sm" /></td>
                </tr>
              ))}
            </tbody>
          </table>
        </Card>
      )}
    </div>
  );
}

/* ======================================================================
   18. APP SHELL / ROOT
   ====================================================================== */

function PageRouter() {
  const app = useApp();
  switch (app.route.page) {
    case 'dashboard': return <Dashboard />;
    case 'pipeline': return <Pipeline />;
    case 'projectDetail': return <ProjectDetail id={app.route.id} />;
    case 'quotations': return <QuotationList />;
    case 'quotationsByProject': return <QuotationsForProject id={app.route.id} />;
    case 'sampling': return <SamplingHub />;
    case 'samplingByProject': return <SamplingByProject id={app.route.id} />;
    case 'samplingDetail': return <SamplingProjectDetail id={app.route.id} />;
    case 'components': return <ComponentTracker />;
    case 'suppliers': return <SuppliersView />;
    case 'supplierDetail': return <SupplierDetail id={app.route.id} />;
    case 'team': return <TeamView />;
    case 'accomplished': return <AccomplishedProjects />;
    default: return <Dashboard />;
  }
}

function Shell() {
  const app = useApp();
  const [collapsed, setCollapsed] = useState(false);

  useEffect(() => {
    function onKey(e) {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); app.setSearchOpen(true); }
      if (e.key === 'Escape') { app.setSearchOpen(false); }
    }
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [app]);

  const [scrolled, setScrolled] = useState(false);

  return (
    <div className="flex h-screen overflow-hidden bg-base">
      <Sidebar collapsed={collapsed} setCollapsed={setCollapsed} />
      <div className="flex-1 flex flex-col overflow-hidden">
        <Header scrolled={scrolled} />
        <main className="flex-1 overflow-y-auto pb-20 md:pb-0 atmosphere" onScroll={e => setScrolled(e.currentTarget.scrollTop > 10)}>
          <PageRouter />
        </main>
      </div>
      <MobileBottomNav />
      <SearchModal />
      <AiPanel />
    </div>
  );
}

function LoginScreen() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);

  const submit = async (e) => {
    e.preventDefault();
    setError('');
    setLoading(true);
    const { error: signInError } = await supabase.auth.signInWithPassword({ email, password });
    setLoading(false);
    if (signInError) setError(signInError.message);
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-base atmosphere px-4">
      <Card className="w-full max-w-sm p-8">
        <div className="flex flex-col items-center mb-6">
          <div className="w-10 h-10 rounded-lg bg-elevated flex items-center justify-center mb-3">
            <span className="font-display text-xl text-gold">S</span>
          </div>
          <h1 className="font-display italic text-2xl text-primary">SAKAN</h1>
          <p className="text-sm text-muted mt-1">Sign in to continue</p>
        </div>
        <form onSubmit={submit} className="space-y-4">
          <div>
            <Label>Email</Label>
            <Input type="email" autoComplete="username" value={email} onChange={e => setEmail(e.target.value)} required autoFocus />
          </div>
          <div>
            <Label>Password</Label>
            <Input type="password" autoComplete="current-password" value={password} onChange={e => setPassword(e.target.value)} required />
          </div>
          {error && <p className="text-xs text-oud">{error}</p>}
          <Button type="submit" className="w-full" loading={loading} icon={LogIn}>Sign In</Button>
        </form>
      </Card>
    </div>
  );
}

function SetPasswordScreen({ onDone }) {
  const [password, setPassword] = useState('');
  const [confirm, setConfirm] = useState('');
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);

  const submit = async (e) => {
    e.preventDefault();
    setError('');
    if (password.length < 8) { setError('Password must be at least 8 characters.'); return; }
    if (password !== confirm) { setError('Passwords do not match.'); return; }
    setLoading(true);
    const { error: updateError } = await supabase.auth.updateUser({ password });
    setLoading(false);
    if (updateError) { setError(updateError.message); return; }
    onDone();
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-base atmosphere px-4">
      <Card className="w-full max-w-sm p-8">
        <div className="flex flex-col items-center mb-6">
          <div className="w-10 h-10 rounded-lg bg-elevated flex items-center justify-center mb-3">
            <span className="font-display text-xl text-gold">S</span>
          </div>
          <h1 className="font-display italic text-2xl text-primary">Welcome to SAKAN</h1>
          <p className="text-sm text-muted mt-1 text-center">Set a password to finish setting up your account.</p>
        </div>
        <form onSubmit={submit} className="space-y-4">
          <div>
            <Label>New Password</Label>
            <Input type="password" autoComplete="new-password" value={password} onChange={e => setPassword(e.target.value)} required autoFocus />
          </div>
          <div>
            <Label>Confirm Password</Label>
            <Input type="password" autoComplete="new-password" value={confirm} onChange={e => setConfirm(e.target.value)} required />
          </div>
          {error && <p className="text-xs text-oud">{error}</p>}
          <Button type="submit" className="w-full" loading={loading}>Set Password &amp; Continue</Button>
        </form>
      </Card>
    </div>
  );
}

function App() {
  // undefined = still checking for an existing session, null = signed out,
  // a Session object = signed in. Real Supabase Auth (Phase 4) - replaces
  // the prototype's "switch user" dropdown entirely.
  const [session, setSession] = useState(undefined);
  // Supabase's invite/recovery links are magic sign-in links - clicking one
  // establishes a real session via the URL hash (detectSessionInUrl,
  // default true) without ever asking for a password. Read the raw hash
  // once, synchronously, before Supabase's own async processing has a
  // chance to touch it, so a just-invited person gets a set-password step
  // instead of landing in the app with no way to ever sign back in once
  // this session ends.
  const [needsPassword, setNeedsPassword] = useState(() => /type=invite|type=recovery/.test(window.location.hash));

  useEffect(() => {
    supabase.auth.getSession().then(({ data }) => setSession(data.session));
    const { data: subscription } = supabase.auth.onAuthStateChange((_event, nextSession) => setSession(nextSession));
    return () => subscription.subscription.unsubscribe();
  }, []);

  if (session === undefined) {
    return (
      <div className="min-h-screen flex items-center justify-center bg-base">
        <Loader2 className="w-6 h-6 text-gold animate-spin" />
      </div>
    );
  }

  if (!session) {
    return <LoginScreen />;
  }

  if (needsPassword) {
    return <SetPasswordScreen onDone={() => { window.history.replaceState(null, '', window.location.pathname); setNeedsPassword(false); }} />;
  }

  return (
    <QueryClientProvider client={queryClient}>
      <AppProvider authUserId={session.user.id}>
        <Shell />
      </AppProvider>
    </QueryClientProvider>
  );
}

const root = createRoot(document.getElementById('root'));
root.render(<App />);

export default App;
