// ============================================================================
// LEON Operations Hub — Centralized Reporting Center
// One report engine, config-driven report definitions, reading live from the
// same project/vendor/subcontractor/AP records everywhere else in the app —
// no parallel reporting database. See docs/production-audit for the
// prototype-wide caveats this inherits (client-only storage, etc).
// ============================================================================

const OPS_REPORT_ROLES = ['Admin', 'General Manager', 'Project Coordinator', 'Production Director', 'Accounting'];
const SUBMITTAL_SLA_DAYS = 5;
function canViewFinReport(role) { return canSeeFinancials(role); }
function canViewOpsReport(role) { return OPS_REPORT_ROLES.includes(role); }
// Not-Shipped..Delivered ladder used by the Project Logistics Report —
// mirrors scopeLogisticsStatus's return values exactly, in chain order.
const MATERIAL_STATUS_LADDER = ['Not Shipped', 'Ready to Ship', 'Booked', 'In Transit', 'In Customs', 'At Warehouse', 'Allocated', 'Released', 'Scheduled for Delivery', 'Delivered'];
// Estimated days between a container's ETA and material actually landing on
// site (customs clearance + inland transport + warehouse handling) — used
// only by the Required-on-Site vs ETA report, since those sub-durations
// aren't separately tracked per shipment. Documented as an estimate in that
// report's description, not presented as a measured value.
const LANDED_LOGISTICS_BUFFER_DAYS = 6;

const REPORT_CATEGORIES = [
  { key: 'financial', label: 'Financial Reports' },
  { key: 'sales', label: 'Sales Reports' },
  { key: 'operations', label: 'Operations Reports' },
  { key: 'logistics', label: 'Logistics Reports' },
  { key: 'tariff', label: 'Trade Compliance Reports' },
  { key: 'document', label: 'Document Reports' },
  { key: 'vendor', label: 'Vendor Reports' },
  { key: 'subcontractor', label: 'Subcontractor Reports' },
];
function canViewLogisticsReport(role) { return canSeeLogistics(role); }
function canViewTradeComplianceReport(role) { return canSeeTradeCompliance(role); }

// ---------------------------------------------------------------------------
// Small shared helpers
// ---------------------------------------------------------------------------
function fmtReportValue(col, value) {
  if (value === null || value === undefined || value === '') return '—';
  if (col.type === 'money') return fmtMoney(value);
  if (col.type === 'pct') return fmtPct(value);
  if (col.type === 'date') return fmtDate(value);
  return String(value);
}
// csvEscape/downloadCsv now live in components.jsx (relocated so any tab can
// reuse them for record/list export, not just the Report Builder) — same
// global scope, so no import needed.
// Estimate -> PO -> PI chain spans two parallel data stores (vendor and
// freight), unified here the same way AP invoices already unify Vendor/
// Freight/Subcontractor via partyType — so every procurement report below
// covers both without duplicating logic.
function allEstimates(projects) {
  return projects.flatMap(p => [
    ...(p.vendorEstimates || []).map(v => ({ ...v, partyType: 'Vendor', vendorName: v.vendorName, projectId: p.id, projectName: p.name })),
    ...(p.freightEstimates || []).map(f => ({ ...f, partyType: 'Freight', vendorName: f.carrier, projectId: p.id, projectName: p.name })),
  ]);
}
function allPOs(projects) {
  return projects.flatMap(p => [
    ...(p.purchaseOrders || []).map(po => ({ ...po, partyType: 'Vendor', projectId: p.id, projectName: p.name })),
    ...(p.freightPOs || []).map(po => ({ ...po, partyType: 'Freight', vendorName: po.carrier, projectId: p.id, projectName: p.name })),
  ]);
}
function allPIs(projects) {
  return projects.flatMap(p => (p.proformaInvoices || []).map(pi => ({ ...pi, projectName: p.name })));
}
function partyBalanceRows(invoices, partyType) {
  const byVendor = {};
  invoices.filter(i => i.partyType === partyType).forEach(i => {
    if (!byVendor[i.vendorId]) byVendor[i.vendorId] = { name: i.vendorName, invoices: [] };
    byVendor[i.vendorId].invoices.push(i);
  });
  return Object.values(byVendor).map(v => ({ name: v.name, ...partyFinancialSummary(v.invoices) })).filter(r => r.totalInvoiced > 0);
}
// Roll-up "current phase" / "next critical date" the same way ProjectCard
// derives it on the Dashboard — first incomplete scope's in-progress/delayed
// stage.
function projectCurrentStage(project) {
  const activeScope = project.scopes.find(s => !scopeIsComplete(s));
  if (!activeScope) return null;
  return activeScope.stages.find(s => s.status === 'In Progress' || s.status === 'Delayed') || null;
}
function projectCompletionPct(project) {
  const stages = project.scopes.flatMap(s => s.stages);
  if (!stages.length) return 0;
  return (stages.filter(s => s.status === 'Completed').length / stages.length) * 100;
}
function projectMajorIssue(project) {
  const open = project.issues.filter(i => i.status === 'Open');
  if (!open.length) return null;
  const order = { High: 0, Medium: 1, Low: 2 };
  return [...open].sort((a, b) => (order[a.severity] ?? 9) - (order[b.severity] ?? 9))[0].title;
}
function scopeSelectionStatus(scope) {
  const vals = Object.values(scope.selections || {});
  if (!vals.length) return 'N/A';
  if (vals.every(v => v)) return 'Complete';
  if (vals.some(v => v)) return 'In Progress';
  return 'Not Started';
}
function latestByDate(list) {
  if (!list || !list.length) return null;
  return [...list].sort((a, b) => (a.createdDate < b.createdDate ? 1 : -1))[0];
}

// ---------------------------------------------------------------------------
// Logistics module shared helpers — every logistics report below reads live
// from ctx.exportContainers (top-level, § multi-project containers) /
// project.deliveries / ctx.warehouseMaterials / ctx.materialAllocations /
// ctx.logisticsClaims, the exact same records the Export/Delivery/Warehouse
// tabs and Logistics Dashboard already use. No parallel logistics database.
// ---------------------------------------------------------------------------
function allContainers(ctx) {
  return containerShipmentRows(ctx.exportContainers, ctx.projects);
}
function allDeliveries(projects) {
  return projects.flatMap(p => p.deliveries.map(d => {
    const scope = p.scopes.find(s => s.id === d.scopeId);
    return { ...d, projectId: p.id, projectName: p.name, scopeName: scope ? scope.name : '—' };
  }));
}
// Chains a scope's material journey across Procurement → Export → Warehouse
// → Delivery into one plain-language status, the same underlying signals
// scopeStatus (operations report, above) already reads per stage, just
// re-summarized for a single Not-Shipped..Delivered ladder.
function scopeLogisticsStatus(p, s, allAllocations, exportContainers) {
  const hasDelivery = (p.deliveries || []).some(d => d.scopeId === s.id && d.deliveryStatus === 'Delivered');
  if (hasDelivery) return 'Delivered';
  const scheduled = (p.deliveries || []).some(d => d.scopeId === s.id && d.approvalStatus === 'Approved' && d.deliveryStatus !== 'Delivered');
  if (scheduled) return 'Scheduled for Delivery';
  const allocations = (allAllocations || []).filter(a => a.projectId === p.id && a.scopeId === s.id);
  if (allocations.some(a => a.quantityReleased > 0)) return 'Released';
  if (allocations.some(a => a.status !== 'Cancelled')) return 'Allocated';
  const containers = containersForProjectScope(exportContainers || [], p.id, s.id);
  if (containers.some(c => c.status === 'Arrived' || c.status === 'Delivered')) return 'At Warehouse';
  if (containers.some(c => c.customsStatus && !['Not Started', 'Cleared'].includes(c.customsStatus))) return 'In Customs';
  if (containers.some(c => c.status === 'In Transit')) return 'In Transit';
  if (containers.some(c => ['Booked', 'Loaded'].includes(c.status))) return 'Booked';
  if (containers.length > 0) return 'Ready to Ship';
  return 'Not Shipped';
}

// ---------------------------------------------------------------------------
// Report definitions
// ---------------------------------------------------------------------------
const REPORT_DEFINITIONS = [

  // ==== OPERATIONS =========================================================
  {
    id: 'execPortfolio', category: 'operations', name: 'Executive Project Portfolio',
    description: "Every project's status, phase, and headline financials in one view.",
    canView: canViewOpsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'client', label: 'Client', type: 'text' },
      { key: 'status', label: 'Status', type: 'text' },
      { key: 'phase', label: 'Current Phase', type: 'text' },
      { key: 'completionPct', label: 'Completion %', type: 'pct' },
      { key: 'contractValue', label: 'Contract Value', type: 'money', financial: true },
      { key: 'projectedProfit', label: 'Projected Profit', type: 'money', financial: true },
      { key: 'marginPct', label: 'Projected Margin %', type: 'pct', financial: true },
      { key: 'majorIssue', label: 'Major Open Issue', type: 'text' },
      { key: 'nextCriticalDate', label: 'Next Critical Date', type: 'date' },
      { key: 'lead', label: 'Responsible Project Lead', type: 'text' },
    ],
    filterKeys: ['status', 'client', 'lead'],
    groupByKeys: ['status', 'lead'],
    searchKeys: ['project', 'client'],
    getRows: (ctx) => ctx.projects.map(p => {
      const account = ctx.accounts.find(a => a.id === p.accountId);
      const stage = projectCurrentStage(p);
      const pl = projectPL(p);
      return {
        _id: p.id, _onOpen: () => ctx.goProject(p.id),
        project: p.name, client: account ? account.name : '—', status: p.pipelineStatus,
        phase: stage ? stage.name : (p.scopes.length ? 'Complete' : '—'),
        completionPct: projectCompletionPct(p),
        contractValue: revisedContractValue(p), projectedProfit: pl.projectedProfit, marginPct: pl.projectedMarginPct,
        majorIssue: projectMajorIssue(p) || '—', nextCriticalDate: stage ? stage.plannedDue : null,
        lead: personName(ctx.teamDirectory, teamMemberFor(p, 'Project Manager')) || '—',
      };
    }),
    summaryCards: (rows) => [
      { label: 'Total Contract Value', value: fmtMoney(rows.reduce((s, r) => s + (r.contractValue || 0), 0)), financial: true },
      { label: 'Total Projected Profit', value: fmtMoney(rows.reduce((s, r) => s + (r.projectedProfit || 0), 0)), financial: true },
      { label: 'Active Jobs', value: String(rows.filter(r => r.status === 'Active Job').length), filterKey: 'status', filterValue: 'Active Job' },
      { label: 'Projects with Open Issues', value: String(rows.filter(r => r.majorIssue !== '—').length) },
    ],
  },

  {
    id: 'scopeStatus', category: 'operations', name: 'Project Status by Scope',
    description: 'Where each scope sits across selections, drawings, procurement, production, and installation.',
    canView: canViewOpsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'selectionStatus', label: 'Selection Status', type: 'text' },
      { key: 'shopDrawingStatus', label: 'Shop Drawing / Submittal Status', type: 'text' },
      { key: 'clientApprovalStatus', label: 'Client Approval Status', type: 'text' },
      { key: 'procurementStatus', label: 'Procurement Status', type: 'text' },
      { key: 'productionStatus', label: 'Production Status', type: 'text' },
      { key: 'exportStatus', label: 'Export Status', type: 'text' },
      { key: 'deliveryStatus', label: 'Delivery Status', type: 'text' },
      { key: 'installationStatus', label: 'Installation Status', type: 'text' },
      { key: 'punchStatus', label: 'Punch List Status', type: 'text' },
      { key: 'responsible', label: 'Responsible Person', type: 'text' },
      { key: 'delayIssue', label: 'Current Delay / Issue', type: 'text' },
      { key: 'windowSystem', label: 'Window System', type: 'text' },
      { key: 'windowCriticalStatus', label: 'Window Schedule Status', type: 'text' },
      { key: 'windowNextApprovalDue', label: 'Next Window Approval Due', type: 'text' },
    ],
    filterKeys: ['project', 'installationStatus', 'productionStatus'],
    groupByKeys: ['project', 'installationStatus'],
    searchKeys: ['project', 'scope'],
    getRows: (ctx) => ctx.projects.flatMap(p => p.scopes.map(s => {
      const latestSubmittal = latestByDate(s.submittals);
      const latestResponse = latestByDate(s.clientResponses);
      const hasPO = (p.purchaseOrders || []).some(po => (p.vendorEstimates.find(v => v.id === po.vendorEstimateId) || {}).scopeId === s.id);
      const hasEstimate = (p.vendorEstimates || []).some(v => v.scopeId === s.id);
      const prodRecs = (p.productionRecords || []).filter(r => r.scopeId === s.id);
      const latestProd = prodRecs.length ? [...prodRecs].sort((a, b) => (a.createdDate < b.createdDate ? 1 : -1))[0] : null;
      const exportDocs = (p.exportDocuments || []).filter(d => d.scopeId === s.id);
      const hasDelivery = (p.deliveries || []).some(d => d.scopeId === s.id);
      const instRecs = (p.installationRecords || []).filter(r => r.scopeId === s.id);
      const latestInst = instRecs.length ? [...instRecs].sort((a, b) => (a.scheduledStart < b.scheduledStart ? 1 : -1))[0] : null;
      const openPunch = (p.punchItems || []).filter(pi => pi.status !== 'Closed').length;
      const stage = s.stages.find(st => st.status === 'In Progress' || st.status === 'Delayed');
      // Window Schedule columns (§ Window Schedule template integration) —
      // populated only for window-classified scopes; every other scope's
      // rows/columns above are completely untouched.
      let windowSystem = '', windowCriticalStatus = '', windowNextApprovalDue = '';
      if (s.windowSchedule) {
        windowSystem = s.windowSchedule.systemName || '—';
        const critIds = new Set(identifyCriticalPath(s.windowSchedule));
        const critNode = s.windowSchedule.nodes.filter(n => critIds.has(n.id)).sort((a, b) => (a.forecastEnd > b.forecastEnd ? -1 : 1))[0];
        windowCriticalStatus = critNode ? deriveWindowScheduleStatus(critNode, todayISO()) : 'Not Computed';
        const nextApproval = Object.values(s.windowSchedule.approvals)
          .map(a => s.windowSchedule.nodes.find(n => n.id === a.nodeId))
          .filter(n => n && n.status !== 'Completed' && n.forecastEnd)
          .sort((a, b) => (a.forecastEnd < b.forecastEnd ? -1 : 1))[0];
        windowNextApprovalDue = nextApproval ? `${nextApproval.name} — ${fmtDate(nextApproval.forecastEnd)}` : '—';
      }
      return {
        _id: s.id, _onOpen: () => ctx.goProjectTab(p.id, 'scopes'),
        project: p.name, scope: s.name,
        windowSystem, windowCriticalStatus, windowNextApprovalDue,
        selectionStatus: scopeSelectionStatus(s),
        shopDrawingStatus: latestSubmittal ? latestSubmittal.status : 'None',
        clientApprovalStatus: latestResponse ? latestResponse.status : 'None',
        procurementStatus: hasPO ? 'Ordered' : hasEstimate ? 'Estimated' : 'Not Started',
        productionStatus: latestProd ? latestProd.status : 'Not Started',
        exportStatus: exportDocs.length === 0 ? 'Not Started' : `${new Set(exportDocs.map(d => d.step)).size}/${EXPORT_WORKFLOW_STEPS.length} steps`,
        deliveryStatus: hasDelivery ? 'Delivered' : '—',
        installationStatus: latestInst ? latestInst.status : 'Not Started',
        punchStatus: openPunch > 0 ? `${openPunch} Open` : 'Clear',
        responsible: personName(ctx.teamDirectory, teamMemberFor(p, 'Project Manager')) || '—',
        delayIssue: scopeTotalDelayDays(s) > 0 ? `+${scopeTotalDelayDays(s)}d${stage && stage.delayReason ? ` — ${stage.delayReason}` : ''}` : '—',
      };
    })),
    summaryCards: (rows) => [
      { label: 'Scopes Tracked', value: String(rows.length) },
      { label: 'Behind Schedule', value: String(rows.filter(r => r.delayIssue !== '—').length) },
      { label: 'Open Punch Items', value: String(rows.filter(r => r.punchStatus !== 'Clear').length), filterKey: 'punchStatus', filterValue: null },
    ],
  },

  {
    id: 'tasksIssues', category: 'operations', name: 'Tasks & Issues',
    description: 'Open and overdue tasks alongside open and critical issues, across every project.',
    canView: canViewOpsReport,
    columns: [
      { key: 'kind', label: 'Type', type: 'text' },
      { key: 'title', label: 'Title', type: 'text' },
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'responsible', label: 'Responsible Person', type: 'text' },
      { key: 'dueDate', label: 'Due Date', type: 'date' },
      { key: 'daysOverdue', label: 'Days Overdue', type: 'number' },
      { key: 'priority', label: 'Priority', type: 'text' },
      { key: 'status', label: 'Status', type: 'text' },
    ],
    filterKeys: ['project', 'responsible', 'status', 'priority', 'kind'],
    groupByKeys: ['project', 'responsible', 'kind'],
    searchKeys: ['title', 'project'],
    getRows: (ctx) => {
      const today = todayISO();
      const out = [];
      ctx.projects.forEach(p => {
        p.tasks.forEach(t => {
          const scope = p.scopes.find(s => s.id === t.scopeId);
          const overdue = t.status !== 'Completed' && t.dueDate < today;
          out.push({
            _id: t.id, _onOpen: () => ctx.goProjectTab(p.id, 'tasks'),
            kind: 'Task', title: t.title, project: p.name, scope: scope ? scope.name : '—',
            responsible: personName(ctx.teamDirectory, t.assigneeId) || '—',
            dueDate: t.dueDate, daysOverdue: overdue ? daysBetween(t.dueDate, today) : 0,
            priority: t.priority, status: overdue ? 'Overdue' : t.status,
          });
        });
        p.issues.forEach(i => {
          const scope = p.scopes.find(s => s.id === i.scopeId);
          out.push({
            _id: i.id, _onOpen: () => ctx.goProjectTab(p.id, 'issues'),
            kind: 'Issue', title: i.title, project: p.name, scope: scope ? scope.name : '—',
            responsible: '—', dueDate: null, daysOverdue: 0,
            priority: i.severity, status: i.status,
          });
        });
      });
      return out;
    },
    summaryCards: (rows) => [
      { label: 'Open Tasks', value: String(rows.filter(r => r.kind === 'Task' && r.status !== 'Completed').length), filterKey: 'kind', filterValue: 'Task' },
      { label: 'Overdue Tasks', value: String(rows.filter(r => r.status === 'Overdue').length), filterKey: 'status', filterValue: 'Overdue' },
      { label: 'Open Issues', value: String(rows.filter(r => r.kind === 'Issue' && r.status === 'Open').length), filterKey: 'kind', filterValue: 'Issue' },
      { label: 'Critical Issues', value: String(rows.filter(r => r.kind === 'Issue' && r.status === 'Open' && r.priority === 'High').length) },
    ],
  },

  {
    id: 'productionExport', category: 'operations', name: 'Production & Export Status',
    description: 'Production and export progress by scope. ETD/ETA and computed delay are not tracked yet, so those are shown as such rather than estimated.',
    canView: canViewOpsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'vendor', label: 'Vendor', type: 'text' },
      { key: 'status', label: 'Production Status', type: 'text' },
      { key: 'qcStatus', label: 'QC Status', type: 'text' },
      { key: 'container', label: 'Container / Tracking #', type: 'text' },
      { key: 'etd', label: 'ETD', type: 'text' },
      { key: 'eta', label: 'ETA', type: 'text' },
      { key: 'responsible', label: 'Responsible Person', type: 'text' },
    ],
    filterKeys: ['project', 'vendor', 'status'],
    groupByKeys: ['project', 'vendor'],
    searchKeys: ['project', 'scope', 'vendor'],
    getRows: (ctx) => ctx.projects.flatMap(p => (p.productionRecords || []).map(r => {
      const scope = p.scopes.find(s => s.id === r.scopeId);
      const latestQc = latestByDate(r.qcReports);
      const exportDoc = (p.exportDocuments || []).find(d => d.scopeId === r.scopeId && (d.trackingNumber || d.containerNumber));
      return {
        _id: r.id, _onOpen: () => ctx.goProjectTab(p.id, 'production'),
        project: p.name, scope: scope ? scope.name : '—', vendor: r.vendorName || '—',
        status: r.status, qcStatus: latestQc ? latestQc.result : 'Not Started',
        container: exportDoc ? (exportDoc.containerNumber || exportDoc.trackingNumber) : 'Not tracked',
        etd: 'Not tracked', eta: 'Not tracked',
        responsible: personName(ctx.teamDirectory, teamMemberFor(p, 'Production Manager')) || '—',
      };
    })),
    summaryCards: (rows) => [
      { label: 'In Production', value: String(rows.filter(r => r.status === 'In Production').length), filterKey: 'status', filterValue: 'In Production' },
      { label: 'In Quality Check', value: String(rows.filter(r => r.status === 'Quality Check').length), filterKey: 'status', filterValue: 'Quality Check' },
      { label: 'Complete', value: String(rows.filter(r => r.status === 'Complete').length), filterKey: 'status', filterValue: 'Complete' },
    ],
  },

  {
    id: 'deliveryInstall', category: 'operations', name: 'Delivery & Installation',
    description: 'Delivery dates and installation progress, with open punch items, by scope. Deliveries have no stored status field today, so only the date is shown.',
    canView: canViewOpsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'deliveryDate', label: 'Delivery Date', type: 'date' },
      { key: 'installer', label: 'Installer / Subcontractor', type: 'text' },
      { key: 'installStart', label: 'Installation Start', type: 'date' },
      { key: 'installComplete', label: 'Installation Completion', type: 'date' },
      { key: 'installStatus', label: 'Installation Status', type: 'text' },
      { key: 'openIssues', label: 'Open Installation Issues', type: 'number' },
      { key: 'punchItems', label: 'Punch List Items', type: 'number' },
      { key: 'responsible', label: 'Responsible Person', type: 'text' },
    ],
    filterKeys: ['project', 'installStatus', 'installer'],
    groupByKeys: ['project', 'installer'],
    searchKeys: ['project', 'scope'],
    getRows: (ctx) => ctx.projects.flatMap(p => (p.installationRecords || []).map(r => {
      const scope = p.scopes.find(s => s.id === r.scopeId);
      const delivery = (p.deliveries || []).find(d => d.scopeId === r.scopeId);
      const sub = ctx.subcontractors.find(s => s.id === r.assignedSubcontractorId);
      const openIssues = (p.fieldIssues || []).filter(fi => fi.scopeId === r.scopeId && fi.status === 'Open').length;
      // Punch items aren't linked to a scope directly — match on building/floor instead.
      const punch = (p.punchItems || []).filter(pi => pi.building === r.building && pi.floor === r.floor).length;
      return {
        _id: r.id, _onOpen: () => ctx.goProjectTab(p.id, 'installation', 'records'),
        project: p.name, scope: scope ? scope.name : '—',
        deliveryDate: delivery ? delivery.date : null,
        installer: sub ? sub.companyName : (r.assignedCrew || '—'),
        installStart: r.actualStart || r.scheduledStart, installComplete: r.actualCompletion || null,
        installStatus: r.status, openIssues, punchItems: punch,
        responsible: personName(ctx.teamDirectory, teamMemberFor(p, 'Project Manager')) || '—',
      };
    })),
    summaryCards: (rows) => [
      { label: 'Records Tracked', value: String(rows.length) },
      { label: 'In Progress', value: String(rows.filter(r => r.installStatus === 'Installation Started').length), filterKey: 'installStatus', filterValue: 'Installation Started' },
      { label: 'Open Field Issues', value: String(rows.reduce((s, r) => s + r.openIssues, 0)) },
    ],
  },

  {
    id: 'takeoffVsInstalled', category: 'operations', name: 'Take-Off vs Purchased vs Delivered vs Installed',
    description: 'Only Take-Off Quantity is tracked as a stored number today — Ordered/Delivered/Installed quantities are not captured anywhere in the app yet, so those columns are shown as "Not tracked" rather than estimated.',
    canView: canViewOpsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'unit', label: 'Unit', type: 'text' },
      { key: 'takeOffQty', label: 'Take-Off Quantity', type: 'number' },
      { key: 'orderedQty', label: 'Ordered Quantity', type: 'text' },
      { key: 'deliveredQty', label: 'Delivered Quantity', type: 'text' },
      { key: 'installedQty', label: 'Installed Quantity', type: 'text' },
    ],
    filterKeys: ['project'],
    groupByKeys: ['project'],
    searchKeys: ['project', 'scope'],
    getRows: (ctx) => ctx.projects.flatMap(p => p.scopes.filter(s => s.quantity != null).map(s => ({
      _id: s.id, _onOpen: () => ctx.goProjectTab(p.id, 'scopes'),
      project: p.name, scope: s.name, unit: s.unit || '—',
      takeOffQty: s.quantity, orderedQty: 'Not tracked', deliveredQty: 'Not tracked', installedQty: 'Not tracked',
    }))),
    summaryCards: (rows) => [{ label: 'Scopes with a Take-Off Quantity', value: String(rows.length) }],
  },

  // ==== DOCUMENT ============================================================
  {
    id: 'submittalStatus', category: 'document', name: 'Submittal / Document Status',
    description: 'Shop drawing/submittal and client-response activity, with items awaiting response highlighted past a 5-business-day house standard.',
    canView: canViewOpsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'document', label: 'Document', type: 'text' },
      { key: 'revision', label: 'Revision', type: 'number' },
      { key: 'submissionDate', label: 'Submission Date', type: 'date' },
      { key: 'responseDate', label: 'Client Response Date', type: 'date' },
      { key: 'status', label: 'Current Status', type: 'text' },
      { key: 'daysAwaiting', label: 'Days Awaiting Response', type: 'number' },
      { key: 'responsible', label: 'Responsible Person', type: 'text' },
      { key: 'requiredByDate', label: 'Required By', type: 'date' },
      { key: 'lateFlag', label: 'Late', type: 'text' },
    ],
    filterKeys: ['project', 'status'],
    groupByKeys: ['project', 'status'],
    searchKeys: ['project', 'scope', 'document'],
    getRows: (ctx) => {
      const today = todayISO();
      const out = [];
      ctx.projects.forEach(p => p.scopes.forEach(s => {
        [...s.submittals, ...s.clientResponses].forEach(t => {
          const latestRev = t.revisions.length ? t.revisions[t.revisions.length - 1] : null;
          const awaiting = !['Approved', 'Approved as Noted', 'Rejected', 'Closed', 'Superseded'].includes(t.status);
          const days = latestRev ? daysBetween(latestRev.date, today) : 0;
          // requiredByDate is set only on threads created by the Window
          // Schedule's approval trackers — null (and this column blank) for
          // every other submittal/response, unaffected.
          const isLate = !!(t.requiredByDate && awaiting && latestRev && latestRev.date > t.requiredByDate);
          out.push({
            _id: t.id, _onOpen: () => ctx.goProjectTab(p.id, 'documents'),
            project: p.name, scope: s.name, document: t.name,
            revision: t.revisions.length, submissionDate: latestRev ? latestRev.date : t.createdDate,
            responseDate: awaiting ? null : (latestRev ? latestRev.date : null),
            status: t.status, daysAwaiting: awaiting ? days : 0,
            responsible: latestRev ? (latestRev.responsiblePerson || '—') : '—',
            requiredByDate: t.requiredByDate || null, lateFlag: isLate ? 'Late' : (t.requiredByDate ? 'On Time' : '—'),
          });
        });
      }));
      return out;
    },
    summaryCards: (rows) => [
      { label: 'Awaiting Response', value: String(rows.filter(r => r.daysAwaiting > 0).length) },
      { label: `Past ${SUBMITTAL_SLA_DAYS}-Day Standard`, value: String(rows.filter(r => r.daysAwaiting > SUBMITTAL_SLA_DAYS).length), tone: 'red' },
    ],
  },

  {
    id: 'materialStatus', category: 'document', name: 'Material / Selection Status',
    description: 'Selections and their material specs, with missing or incomplete selections easy to spot.',
    canView: canViewOpsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'material', label: 'Material', type: 'text' },
      { key: 'manufacturer', label: 'Manufacturer', type: 'text' },
      { key: 'productCode', label: 'Product Code', type: 'text' },
      { key: 'finish', label: 'Finish', type: 'text' },
      { key: 'size', label: 'Size', type: 'text' },
      { key: 'thickness', label: 'Thickness', type: 'text' },
      { key: 'selectionStatus', label: 'Selection Status', type: 'text' },
    ],
    filterKeys: ['project', 'selectionStatus'],
    groupByKeys: ['project'],
    searchKeys: ['project', 'scope', 'material', 'manufacturer'],
    getRows: (ctx) => {
      const out = [];
      ctx.projects.forEach(p => p.scopes.forEach(s => {
        Object.entries(s.materialLinks || {}).forEach(([categoryId, materialId]) => {
          const m = ctx.materialLibrary.find(mm => mm.id === materialId);
          out.push({
            _id: `${s.id}-${categoryId}`, _onOpen: () => ctx.goProjectTab(p.id, 'selections'),
            project: p.name, scope: s.name, material: m ? m.name : 'Not selected',
            manufacturer: m ? m.manufacturer || '—' : '—', productCode: m ? m.productCode || '—' : '—',
            finish: m ? (m.specs && m.specs.finish) || '—' : '—',
            size: m ? (m.specs && (m.specs.nominalSize || m.specs.slabWidth)) || '—' : '—',
            thickness: m ? (m.specs && (m.specs.thickness || m.specs.actualThickness)) || '—' : '—',
            selectionStatus: m ? 'Selected' : 'Missing',
          });
        });
      }));
      return out;
    },
    summaryCards: (rows) => [
      { label: 'Missing Selections', value: String(rows.filter(r => r.selectionStatus === 'Missing').length), tone: 'red', filterKey: 'selectionStatus', filterValue: 'Missing' },
    ],
  },

  // ==== VENDOR ==============================================================
  {
    id: 'openPO', category: 'vendor', name: 'Procurement / Open PO & PI',
    description: 'Every vendor estimate and its purchase order, with payment progress.',
    canView: canViewOpsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'vendor', label: 'Vendor', type: 'text' },
      { key: 'poNumber', label: 'PO / PI Number', type: 'text' },
      { key: 'orderDate', label: 'Order Date', type: 'date' },
      { key: 'amount', label: 'Original Amount', type: 'money', financial: true },
      { key: 'paid', label: 'Amount Paid', type: 'money', financial: true },
      { key: 'openBalance', label: 'Open Balance', type: 'money', financial: true },
      { key: 'status', label: 'Status', type: 'text' },
    ],
    filterKeys: ['project', 'vendor', 'status'],
    groupByKeys: ['project', 'vendor'],
    searchKeys: ['project', 'vendor', 'poNumber'],
    getRows: (ctx) => ctx.projects.flatMap(p => (p.vendorEstimates || []).map(v => {
      const scope = p.scopes.find(s => s.id === v.scopeId);
      const po = (p.purchaseOrders || []).find(x => x.id === v.poId);
      let status = 'Not Ordered', paid = 0;
      if (po) {
        const paidTerms = po.paymentTerms.filter(t => t.status === 'Paid');
        paid = paidTerms.reduce((s, t) => s + (po.amount * t.pct / 100), 0);
        status = paid >= po.amount ? 'Fully Paid' : paid > 0 ? 'Balance Required' : 'Deposit Required';
      } else if (v.pmApproved || v.ownerApproved) {
        status = 'Deposit Required';
      }
      return {
        _id: v.id, _onOpen: () => ctx.goProjectTab(p.id, 'procurement'),
        project: p.name, scope: scope ? scope.name : '—', vendor: v.vendorName,
        poNumber: po ? po.poNumber : '—', orderDate: po ? po.issuedDate : v.date,
        amount: v.amount, paid, openBalance: v.amount - paid, status,
      };
    })),
    summaryCards: (rows) => [
      { label: 'Total Committed', value: fmtMoney(rows.reduce((s, r) => s + r.amount, 0)), financial: true },
      { label: 'Total Open Balance', value: fmtMoney(rows.reduce((s, r) => s + r.openBalance, 0)), financial: true },
      { label: 'Not Yet Ordered', value: String(rows.filter(r => r.status === 'Not Ordered').length), filterKey: 'status', filterValue: 'Not Ordered' },
    ],
  },

  {
    id: 'estimatesPendingReview', category: 'vendor', name: 'Estimates Pending Review',
    description: 'Vendor and freight estimates not yet approved.',
    canView: canViewOpsReport,
    columns: [
      { key: 'estimateNumber', label: 'Estimate Number', type: 'text' },
      { key: 'partyType', label: 'Type', type: 'text' },
      { key: 'vendor', label: 'Vendor', type: 'text' },
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'date', label: 'Date', type: 'date' },
      { key: 'amount', label: 'Amount', type: 'money', financial: true },
      { key: 'status', label: 'Status', type: 'text' },
    ],
    filterKeys: ['project', 'partyType', 'status'],
    groupByKeys: ['partyType', 'project'],
    searchKeys: ['vendor', 'project', 'estimateNumber'],
    getRows: (ctx) => allEstimates(ctx.projects).filter(e => ['Draft', 'Received', 'Under Review'].includes(e.status)).map(e => {
      const project = ctx.projects.find(p => p.id === e.projectId);
      const scope = project && project.scopes.find(s => s.id === e.scopeId);
      return {
        _id: e.id, _onOpen: () => ctx.goProjectTab(e.projectId, e.partyType === 'Freight' ? 'export' : 'procurement'),
        estimateNumber: e.estimateNumber, partyType: e.partyType, vendor: e.vendorName, project: e.projectName,
        scope: scope ? scope.name : '—', date: e.date, amount: e.amount, status: e.status,
      };
    }),
    summaryCards: (rows) => [
      { label: 'Estimates Pending Review', value: String(rows.length) },
      { label: 'Total Value', value: fmtMoney(rows.reduce((s, r) => s + r.amount, 0)), financial: true },
    ],
  },

  {
    id: 'estimatesNotConverted', category: 'vendor', name: 'Approved Estimates Not Yet Converted to PO',
    description: 'Estimates both approvals have cleared, but no PO has been issued yet.',
    canView: canViewOpsReport,
    columns: [
      { key: 'estimateNumber', label: 'Estimate Number', type: 'text' },
      { key: 'partyType', label: 'Type', type: 'text' },
      { key: 'vendor', label: 'Vendor', type: 'text' },
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'amount', label: 'Amount', type: 'money', financial: true },
    ],
    filterKeys: ['project', 'partyType'],
    groupByKeys: ['partyType', 'project'],
    searchKeys: ['vendor', 'project', 'estimateNumber'],
    getRows: (ctx) => allEstimates(ctx.projects).filter(e => e.status === 'Approved' && !e.poId).map(e => {
      const project = ctx.projects.find(p => p.id === e.projectId);
      const scope = project && project.scopes.find(s => s.id === e.scopeId);
      return {
        _id: e.id, _onOpen: () => ctx.goProjectTab(e.projectId, e.partyType === 'Freight' ? 'export' : 'procurement'),
        estimateNumber: e.estimateNumber, partyType: e.partyType, vendor: e.vendorName, project: e.projectName,
        scope: scope ? scope.name : '—', amount: e.amount,
      };
    }),
    summaryCards: (rows) => [{ label: 'Awaiting Conversion', value: String(rows.length) }, { label: 'Total Value', value: fmtMoney(rows.reduce((s, r) => s + r.amount, 0)), financial: true }],
  },

  {
    id: 'posAwaitingVendorAcceptance', category: 'vendor', name: 'POs Awaiting Vendor Acceptance',
    description: 'Purchase orders sent to the vendor but not yet accepted.',
    canView: canViewOpsReport,
    columns: [
      { key: 'poNumber', label: 'PO Number', type: 'text' },
      { key: 'partyType', label: 'Type', type: 'text' },
      { key: 'vendor', label: 'Vendor', type: 'text' },
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'issuedDate', label: 'Issued Date', type: 'date' },
      { key: 'amount', label: 'Amount', type: 'money', financial: true },
      { key: 'status', label: 'Status', type: 'text' },
    ],
    filterKeys: ['project', 'partyType'],
    groupByKeys: ['partyType'],
    searchKeys: ['vendor', 'project', 'poNumber'],
    getRows: (ctx) => allPOs(ctx.projects).filter(po => po.status === 'Sent to Vendor').map(po => ({
      _id: po.id, _onOpen: () => ctx.goProjectTab(po.projectId, po.partyType === 'Freight' ? 'export' : 'procurement'),
      poNumber: po.poNumber, partyType: po.partyType, vendor: po.vendorName || po.carrier, project: po.projectName,
      issuedDate: po.issuedDate, amount: po.amount, status: po.status,
    })),
    summaryCards: (rows) => [{ label: 'Awaiting Acceptance', value: String(rows.length) }],
  },

  {
    id: 'posWithoutPI', category: 'vendor', name: 'POs Without PI',
    description: 'Approved purchase orders that have not yet been converted into a proforma invoice.',
    canView: canViewOpsReport,
    columns: [
      { key: 'poNumber', label: 'PO Number', type: 'text' },
      { key: 'partyType', label: 'Type', type: 'text' },
      { key: 'vendor', label: 'Vendor', type: 'text' },
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'issuedDate', label: 'Issued Date', type: 'date' },
      { key: 'amount', label: 'Amount', type: 'money', financial: true },
      { key: 'status', label: 'Status', type: 'text' },
    ],
    filterKeys: ['project', 'partyType', 'status'],
    groupByKeys: ['partyType', 'project'],
    searchKeys: ['vendor', 'project', 'poNumber'],
    getRows: (ctx) => allPOs(ctx.projects).filter(po => !['Converted to PI', 'Cancelled', 'Draft'].includes(po.status)).map(po => ({
      _id: po.id, _onOpen: () => ctx.goProjectTab(po.projectId, po.partyType === 'Freight' ? 'export' : 'procurement'),
      poNumber: po.poNumber, partyType: po.partyType, vendor: po.vendorName || po.carrier, project: po.projectName,
      issuedDate: po.issuedDate, amount: po.amount, status: po.status,
    })),
    summaryCards: (rows) => [{ label: 'POs Without PI', value: String(rows.length) }, { label: 'Total Value', value: fmtMoney(rows.reduce((s, r) => s + r.amount, 0)), financial: true }],
  },

  {
    id: 'pisPendingReview', category: 'vendor', name: 'PIs Pending Review',
    description: 'Proforma invoices received from the vendor/freight forwarder, not yet approved.',
    canView: canViewOpsReport,
    columns: [
      { key: 'piNumber', label: 'PI Number', type: 'text' },
      { key: 'partyType', label: 'Type', type: 'text' },
      { key: 'vendor', label: 'Vendor', type: 'text' },
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'piDate', label: 'PI Date', type: 'date' },
      { key: 'amount', label: 'Amount', type: 'money', financial: true },
      { key: 'status', label: 'Status', type: 'text' },
    ],
    filterKeys: ['project', 'partyType'],
    groupByKeys: ['partyType', 'project'],
    searchKeys: ['vendor', 'project', 'piNumber'],
    getRows: (ctx) => allPIs(ctx.projects).filter(pi => ['Received', 'Under Review', 'Revision Requested'].includes(pi.status)).map(pi => ({
      _id: pi.id, _onOpen: () => ctx.goProjectTab(pi.projectId, pi.partyType === 'Freight' ? 'export' : 'procurement'),
      piNumber: pi.piNumber, partyType: pi.partyType, vendor: pi.vendorName, project: pi.projectName,
      piDate: pi.piDate, amount: pi.amount, status: pi.status,
    })),
    summaryCards: (rows) => [{ label: 'PIs Pending Review', value: String(rows.length) }, { label: 'Total Value', value: fmtMoney(rows.reduce((s, r) => s + r.amount, 0)), financial: true }],
  },

  {
    id: 'pisAwaitingPayment', category: 'vendor', name: 'PIs Awaiting Payment',
    description: 'Approved proforma invoices whose linked AP invoice is not yet fully paid.',
    canView: canViewOpsReport,
    columns: [
      { key: 'piNumber', label: 'PI Number', type: 'text' },
      { key: 'partyType', label: 'Type', type: 'text' },
      { key: 'vendor', label: 'Vendor', type: 'text' },
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'amount', label: 'Amount', type: 'money', financial: true },
      { key: 'openBalance', label: 'Open Balance', type: 'money', financial: true },
      { key: 'status', label: 'Status', type: 'text' },
    ],
    filterKeys: ['project', 'partyType'],
    groupByKeys: ['partyType', 'project'],
    searchKeys: ['vendor', 'project', 'piNumber'],
    getRows: (ctx) => allPIs(ctx.projects).filter(pi => pi.status === 'Approved' || pi.status === 'Approved for Payment' || pi.status === 'Partially Paid').map(pi => {
      const project = ctx.projects.find(p => p.id === pi.projectId);
      const inv = project && pi.apInvoiceId ? project.apInvoices.find(i => i.id === pi.apInvoiceId) : null;
      const openBalance = inv ? invoiceOpenBalance(inv) : pi.amount;
      return {
        _id: pi.id, _onOpen: () => ctx.goProjectTab(pi.projectId, 'financials', 'ap'),
        piNumber: pi.piNumber, partyType: pi.partyType, vendor: pi.vendorName, project: pi.projectName,
        amount: pi.amount, openBalance, status: inv ? inv.paymentStatus : pi.status,
      };
    }).filter(r => r.openBalance > 0),
    summaryCards: (rows) => [{ label: 'PIs Awaiting Payment', value: String(rows.length) }, { label: 'Total Open Balance', value: fmtMoney(rows.reduce((s, r) => s + r.openBalance, 0)), financial: true }],
  },

  {
    id: 'estimateVsPoVariance', category: 'vendor', name: 'Estimate vs PO Variance',
    description: 'How much a purchase order differs from the estimate it was issued from.',
    canView: canViewFinReport,
    columns: [
      { key: 'estimateNumber', label: 'Estimate Number', type: 'text' },
      { key: 'poNumber', label: 'PO Number', type: 'text' },
      { key: 'partyType', label: 'Type', type: 'text' },
      { key: 'vendor', label: 'Vendor', type: 'text' },
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'estimateAmount', label: 'Estimate Amount', type: 'money', financial: true },
      { key: 'poAmount', label: 'PO Amount', type: 'money', financial: true },
      { key: 'variance', label: 'Variance', type: 'money', financial: true },
      { key: 'variancePct', label: 'Variance %', type: 'pct', financial: true },
    ],
    filterKeys: ['project', 'partyType'],
    groupByKeys: ['partyType', 'project'],
    searchKeys: ['vendor', 'project', 'estimateNumber', 'poNumber'],
    getRows: (ctx) => allPOs(ctx.projects).map(po => {
      const estimates = po.partyType === 'Freight' ? 'freightEstimates' : 'vendorEstimates';
      const project = ctx.projects.find(p => p.id === po.projectId);
      const estimate = project && (project[estimates] || []).find(e => e.id === (po.vendorEstimateId || po.freightEstimateId));
      if (!estimate) return null;
      const variance = po.amount - estimate.amount;
      return {
        _id: po.id, _onOpen: () => ctx.goProjectTab(po.projectId, po.partyType === 'Freight' ? 'export' : 'procurement'),
        estimateNumber: estimate.estimateNumber, poNumber: po.poNumber, partyType: po.partyType, vendor: po.vendorName || po.carrier, project: po.projectName,
        estimateAmount: estimate.amount, poAmount: po.amount, variance, variancePct: estimate.amount ? (variance / estimate.amount) * 100 : 0,
      };
    }).filter(Boolean),
    summaryCards: (rows) => [{ label: 'Total Variance', value: fmtMoney(rows.reduce((s, r) => s + r.variance, 0)), financial: true }],
  },

  {
    id: 'poVsPiVariance', category: 'vendor', name: 'PO vs PI Variance',
    description: 'How much a vendor’s proforma invoice differs from the purchase order it was converted from.',
    canView: canViewFinReport,
    columns: [
      { key: 'poNumber', label: 'PO Number', type: 'text' },
      { key: 'piNumber', label: 'PI Number', type: 'text' },
      { key: 'partyType', label: 'Type', type: 'text' },
      { key: 'vendor', label: 'Vendor', type: 'text' },
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'poAmount', label: 'PO Amount', type: 'money', financial: true },
      { key: 'piAmount', label: 'PI Amount', type: 'money', financial: true },
      { key: 'variance', label: 'Variance', type: 'money', financial: true },
      { key: 'variancePct', label: 'Variance %', type: 'pct', financial: true },
      { key: 'flagged', label: 'Exceeds PO Threshold', type: 'text' },
    ],
    filterKeys: ['project', 'partyType', 'flagged'],
    groupByKeys: ['partyType', 'project'],
    searchKeys: ['vendor', 'project', 'poNumber', 'piNumber'],
    getRows: (ctx) => allPIs(ctx.projects).map(pi => {
      const project = ctx.projects.find(p => p.id === pi.projectId);
      const { po, overThreshold, diffAmount, diffPct } = ctx.piExceedsApprovedPO(project, pi);
      if (!po) return null;
      return {
        _id: pi.id, _onOpen: () => ctx.goProjectTab(pi.projectId, pi.partyType === 'Freight' ? 'export' : 'procurement'),
        poNumber: po.poNumber, piNumber: pi.piNumber, partyType: pi.partyType, vendor: pi.vendorName, project: pi.projectName,
        poAmount: po.amount, piAmount: pi.amount, variance: diffAmount, variancePct: diffPct, flagged: overThreshold ? 'Yes' : 'No',
      };
    }).filter(Boolean),
    summaryCards: (rows) => [
      { label: 'Total Variance', value: fmtMoney(rows.reduce((s, r) => s + r.variance, 0)), financial: true },
      { label: 'Flagged for Review', value: String(rows.filter(r => r.flagged === 'Yes').length), tone: 'red', filterKey: 'flagged', filterValue: 'Yes' },
    ],
  },

  {
    id: 'procurementRevisionHistory', category: 'vendor', name: 'Procurement Revision History',
    description: 'Every revision recorded against an estimate, PO, or PI, across every project.',
    canView: canViewOpsReport,
    columns: [
      { key: 'document', label: 'Document', type: 'text' },
      { key: 'stage', label: 'Stage', type: 'text' },
      { key: 'partyType', label: 'Type', type: 'text' },
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'revisionNumber', label: 'Revision', type: 'number' },
      { key: 'date', label: 'Date', type: 'date' },
      { key: 'revisedBy', label: 'Revised By', type: 'text' },
      { key: 'reasonForRevision', label: 'Reason', type: 'text' },
      { key: 'previousAmount', label: 'Previous Amount', type: 'money', financial: true },
      { key: 'revisedAmount', label: 'Revised Amount', type: 'money', financial: true },
    ],
    filterKeys: ['project', 'stage', 'partyType'],
    groupByKeys: ['stage', 'project'],
    searchKeys: ['document', 'project'],
    getRows: (ctx) => {
      const out = [];
      allEstimates(ctx.projects).forEach(e => (e.revisions || []).forEach(r => out.push({
        _id: r.id, _onOpen: () => ctx.goProjectTab(e.projectId, e.partyType === 'Freight' ? 'export' : 'procurement'),
        document: e.estimateNumber, stage: 'Estimate', partyType: e.partyType, project: e.projectName,
        revisionNumber: r.revisionNumber, date: r.date, revisedBy: r.revisedBy || '—', reasonForRevision: r.reasonForRevision, previousAmount: r.previousAmount, revisedAmount: r.revisedAmount,
      })));
      allPOs(ctx.projects).forEach(po => (po.revisions || []).forEach(r => out.push({
        _id: r.id, _onOpen: () => ctx.goProjectTab(po.projectId, po.partyType === 'Freight' ? 'export' : 'procurement'),
        document: po.poNumber, stage: 'PO', partyType: po.partyType, project: po.projectName,
        revisionNumber: r.revisionNumber, date: r.date, revisedBy: r.revisedBy || '—', reasonForRevision: r.reasonForRevision, previousAmount: r.previousAmount, revisedAmount: r.revisedAmount,
      })));
      allPIs(ctx.projects).forEach(pi => (pi.revisions || []).forEach(r => out.push({
        _id: r.id, _onOpen: () => ctx.goProjectTab(pi.projectId, pi.partyType === 'Freight' ? 'export' : 'procurement'),
        document: pi.piNumber, stage: 'PI', partyType: pi.partyType, project: pi.projectName,
        revisionNumber: r.revisionNumber, date: r.date, revisedBy: r.revisedBy || '—', reasonForRevision: r.reasonForRevision, previousAmount: r.previousAmount, revisedAmount: r.revisedAmount,
      })));
      return out;
    },
    summaryCards: (rows) => [{ label: 'Total Revisions', value: String(rows.length) }],
  },

  {
    id: 'vendorPerformance', category: 'vendor', name: 'Vendor Performance',
    description: 'Spend, business loss, and remakes attributed to each vendor. On-time %, delays, and formal quality claims are not captured as structured data yet.',
    canView: canViewOpsReport,
    columns: [
      { key: 'vendor', label: 'Vendor', type: 'text' },
      { key: 'totalSpend', label: 'Total Spend', type: 'money', financial: true },
      { key: 'projectCount', label: 'Number of Projects', type: 'number' },
      { key: 'onTimePct', label: 'On-Time Production %', type: 'text' },
      { key: 'businessLoss', label: 'Business Loss Attributed to Vendor', type: 'money', financial: true },
      { key: 'remakes', label: 'Remakes', type: 'number' },
      { key: 'openIssues', label: 'Open Issues', type: 'text' },
    ],
    filterKeys: ['vendor'],
    groupByKeys: [],
    searchKeys: ['vendor'],
    getRows: (ctx) => {
      const byVendor = {};
      ctx.projects.forEach(p => (p.vendorEstimates || []).forEach(v => {
        if (!byVendor[v.vendorName]) byVendor[v.vendorName] = { vendor: v.vendorName, totalSpend: 0, projects: new Set(), businessLoss: 0, remakes: 0 };
        const b = byVendor[v.vendorName];
        b.totalSpend += v.amount; b.projects.add(p.id);
        if (v.recoverability === 'Recoverable from Vendor' || v.recoverability === 'Business Loss') b.businessLoss += v.amount;
        if (v.unplannedReason === 'Remake / Replacement') b.remakes += 1;
      }));
      return Object.values(byVendor).map(b => ({
        _id: b.vendor, _onOpen: () => { const v = ctx.vendors.find(x => x.name === b.vendor); if (v) ctx.goVendorDetail(v.id, 'vendor'); },
        vendor: b.vendor, totalSpend: b.totalSpend, projectCount: b.projects.size,
        onTimePct: 'Not tracked', businessLoss: b.businessLoss, remakes: b.remakes, openIssues: 'Not tracked',
      }));
    },
    summaryCards: (rows) => [
      { label: 'Total Vendor Spend', value: fmtMoney(rows.reduce((s, r) => s + r.totalSpend, 0)), financial: true },
      { label: 'Total Business Loss', value: fmtMoney(rows.reduce((s, r) => s + r.businessLoss, 0)), financial: true, tone: 'red' },
    ],
  },

  // ==== SUBCONTRACTOR =======================================================
  {
    id: 'subPerformance', category: 'subcontractor', name: 'Subcontractor Performance',
    description: 'Invoicing and punch-item activity by subcontractor. Callbacks and formal quality issues are not captured as structured data yet.',
    canView: canViewOpsReport,
    columns: [
      { key: 'subcontractor', label: 'Subcontractor', type: 'text' },
      { key: 'trade', label: 'Trade', type: 'text' },
      { key: 'projectsCompleted', label: 'Projects Completed', type: 'number' },
      { key: 'totalInvoiced', label: 'Total Invoiced', type: 'money', financial: true },
      { key: 'installDelays', label: 'Installation Delays', type: 'text' },
      { key: 'openPunch', label: 'Open Punch Items', type: 'number' },
      { key: 'completedPunch', label: 'Completed Punch Items', type: 'number' },
      { key: 'callbacks', label: 'Callbacks', type: 'text' },
      { key: 'qualityIssues', label: 'Quality Issues', type: 'text' },
    ],
    filterKeys: ['subcontractor', 'trade'],
    groupByKeys: ['trade'],
    searchKeys: ['subcontractor'],
    getRows: (ctx) => {
      const allInvoices = allApInvoices(ctx.projects);
      return ctx.subcontractors.map(sub => {
        const invoices = allInvoices.filter(i => i.vendorId === sub.id);
        const projectsCompleted = new Set(sub.projectIds || []).size;
        let openPunch = 0, completedPunch = 0;
        ctx.projects.forEach(p => (p.installationRecords || []).forEach(r => {
          if (r.assignedSubcontractorId !== sub.id) return;
          (p.punchItems || []).forEach(pi => {
            if (pi.building !== r.building || pi.floor !== r.floor) return;
            if (pi.status === 'Closed') completedPunch++; else openPunch++;
          });
        }));
        return {
          _id: sub.id, _onOpen: () => ctx.goSubcontractorDetail(sub.id),
          subcontractor: sub.companyName, trade: sub.trade, projectsCompleted,
          totalInvoiced: invoices.reduce((s, i) => s + i.amount, 0),
          installDelays: 'Not tracked', openPunch, completedPunch,
          callbacks: 'Not tracked', qualityIssues: 'Not tracked',
        };
      });
    },
    summaryCards: (rows) => [
      { label: 'Total Invoiced', value: fmtMoney(rows.reduce((s, r) => s + r.totalInvoiced, 0)), financial: true },
      { label: 'Open Punch Items', value: String(rows.reduce((s, r) => s + r.openPunch, 0)) },
    ],
  },

  // ==== SALES ===============================================================
  {
    id: 'salesPipeline', category: 'sales', name: 'Quotation / Sales Pipeline',
    description: 'Every quotation, its status, and win/loss tracking. Lost quotations are excluded from Contract Value and backlog.',
    canView: canViewFinReport,
    columns: [
      { key: 'quoteNumber', label: 'Quotation Number', type: 'text' },
      { key: 'client', label: 'Client', type: 'text' },
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'quoteDate', label: 'Quotation Date', type: 'date' },
      { key: 'quotedValue', label: 'Quoted Value', type: 'money', financial: true },
      { key: 'status', label: 'Current Status', type: 'text' },
      { key: 'awardedValue', label: 'Awarded Value', type: 'money', financial: true },
      { key: 'lostValue', label: 'Lost Value', type: 'money', financial: true },
      { key: 'salesperson', label: 'Responsible Salesperson', type: 'text' },
    ],
    filterKeys: ['client', 'status', 'salesperson'],
    groupByKeys: ['status', 'salesperson'],
    searchKeys: ['project', 'client', 'quoteNumber'],
    getRows: (ctx) => ctx.projects.filter(p => (p.quoteRevisions || []).length).map(p => {
      const account = ctx.accounts.find(a => a.id === p.accountId);
      const latest = [...p.quoteRevisions].sort((a, b) => b.revision - a.revision)[0];
      const awarded = p.pipelineStatus === 'Active Job';
      const lost = p.pipelineStatus === 'Lost Job';
      return {
        _id: p.id, _onOpen: () => ctx.goProjectTab(p.id, 'sales', 'quotes'),
        quoteNumber: `${p.projectNumber} R${latest.revision}`, client: account ? account.name : '—', project: p.name,
        quoteDate: latest.date, quotedValue: latest.amount, status: p.pipelineStatus,
        awardedValue: awarded ? revisedContractValue(p) : 0, lostValue: lost ? latest.amount : 0,
        salesperson: personName(ctx.teamDirectory, teamMemberFor(p, 'Sales Person')) || '—',
      };
    }),
    summaryCards: (rows, ctx) => {
      const s = salesValueSummary(ctx.projects);
      return [
        { label: 'Total Quoted Value', value: fmtMoney(s.totalQuoted), financial: true },
        { label: 'Active Quotation Value', value: fmtMoney(s.activeQuotationValue), financial: true },
        { label: 'Contract Value', value: fmtMoney(s.contractValue), financial: true },
        { label: 'Win Rate', value: s.winRate === null ? '—' : fmtPct(s.winRate) },
        { label: 'Loss Rate', value: s.lossRate === null ? '—' : fmtPct(s.lossRate), tone: 'red' },
      ];
    },
  },

  {
    id: 'changeOrders', category: 'sales', name: 'Contract & Change Order',
    description: 'Original vs. revised contract value, with approved/pending/rejected change orders by project.',
    canView: canViewFinReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'originalContract', label: 'Original Contract Value', type: 'money', financial: true },
      { key: 'approvedCO', label: 'Approved Change Orders', type: 'money', financial: true },
      { key: 'pendingCO', label: 'Pending Change Orders', type: 'money', financial: true },
      { key: 'rejectedCO', label: 'Rejected Change Orders', type: 'money', financial: true },
      { key: 'revisedContract', label: 'Revised Contract Value', type: 'money', financial: true },
      { key: 'coProfitImpact', label: 'Change Order Profit Impact', type: 'money', financial: true },
    ],
    filterKeys: ['project'],
    groupByKeys: [],
    searchKeys: ['project'],
    getRows: (ctx) => ctx.projects.filter(p => p.pipelineStatus === 'Active Job').map(p => {
      const cos = (p.changeOrders || []).filter(c => c.type === 'Change Order');
      const sum = st => cos.filter(c => c.status === st).reduce((s, c) => s + c.amount, 0);
      return {
        _id: p.id, _onOpen: () => ctx.goProjectTab(p.id, 'sales', 'changeOrders'),
        project: p.name, originalContract: latestQuoteAmount(p), approvedCO: sum('Approved'),
        pendingCO: sum('Pending'), rejectedCO: sum('Rejected'), revisedContract: revisedContractValue(p),
        coProfitImpact: sum('Approved'),
      };
    }),
    summaryCards: (rows) => [
      { label: 'Total Approved Change Orders', value: fmtMoney(rows.reduce((s, r) => s + r.approvedCO, 0)), financial: true },
      { label: 'Total Pending Change Orders', value: fmtMoney(rows.reduce((s, r) => s + r.pendingCO, 0)), financial: true, tone: 'yellow' },
    ],
  },

  // ==== FINANCIAL ===========================================================
  {
    id: 'profitability', category: 'financial', name: 'Projected vs Actual Profitability',
    description: 'Projected vs. actual cost and profit, by project and by scope. Visible to Admin and Accounting only.',
    canView: canViewFinReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'salesValue', label: 'Contract / Sales Value', type: 'money', financial: true },
      { key: 'projectedCost', label: 'Projected Total Cost', type: 'money', financial: true },
      { key: 'actualCost', label: 'Actual Total Cost', type: 'money', financial: true },
      { key: 'projectedProfit', label: 'Projected Profit', type: 'money', financial: true },
      { key: 'actualProfit', label: 'Actual Profit', type: 'money', financial: true },
      { key: 'projectedMarginPct', label: 'Projected Margin %', type: 'pct', financial: true },
      { key: 'actualMarginPct', label: 'Actual Margin %', type: 'pct', financial: true },
      { key: 'profitVariance', label: 'Profit Variance', type: 'money', financial: true },
    ],
    filterKeys: ['project'],
    groupByKeys: ['project'],
    searchKeys: ['project', 'scope'],
    getRows: (ctx) => ctx.projects.flatMap(p => p.scopes.map(s => {
      const calc = scopeProfitabilityCalc(s);
      const actualCost = scopeActualTotalCost(s);
      const actualProfit = (s.profitability.salesValue || 0) - actualCost;
      const actualMarginPct = s.profitability.salesValue ? (actualProfit / s.profitability.salesValue) * 100 : 0;
      return {
        _id: s.id, _onOpen: () => ctx.goProjectTab(p.id, 'sales', 'profitability'),
        project: p.name, scope: s.name, salesValue: s.profitability.salesValue,
        projectedCost: calc.totalProjectedCost, actualCost,
        projectedProfit: calc.projectedProfit, actualProfit,
        projectedMarginPct: calc.projectedMarginPct, actualMarginPct,
        profitVariance: actualProfit - calc.projectedProfit,
      };
    })),
    summaryCards: (rows) => [
      { label: 'Total Projected Profit', value: fmtMoney(rows.reduce((s, r) => s + r.projectedProfit, 0)), financial: true },
      { label: 'Total Actual Profit', value: fmtMoney(rows.reduce((s, r) => s + r.actualProfit, 0)), financial: true },
    ],
  },

  {
    id: 'marginLeakage', category: 'financial', name: 'Margin Leakage / Business Loss',
    description: 'Unplanned costs and margin loss, by cause, vendor, scope, or project.',
    canView: canViewFinReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'vendor', label: 'Vendor / Subcontractor', type: 'text' },
      { key: 'description', label: 'Cost Description', type: 'text' },
      { key: 'cause', label: 'Cause', type: 'text' },
      { key: 'amount', label: 'Amount', type: 'money', financial: true },
      { key: 'date', label: 'Date', type: 'date' },
      { key: 'recoverability', label: 'Recoverability', type: 'text' },
    ],
    filterKeys: ['project', 'vendor', 'cause', 'recoverability'],
    groupByKeys: ['cause', 'vendor', 'project'],
    searchKeys: ['project', 'vendor', 'description'],
    getRows: (ctx) => ctx.projects.flatMap(p => unplannedCostItems(p).map(v => {
      const scope = p.scopes.find(s => s.id === v.scopeId);
      return {
        _id: v.id, _onOpen: () => ctx.goProjectTab(p.id, 'procurement'),
        project: p.name, scope: scope ? scope.name : '—', vendor: v.vendorName,
        description: v.description, cause: v.unplannedReason || v.category,
        amount: v.amount, date: v.date, recoverability: v.recoverability || 'Pending Determination',
      };
    })),
    summaryCards: (rows) => [
      { label: 'Total Business Loss', value: fmtMoney(rows.filter(r => r.recoverability === 'Business Loss').reduce((s, r) => s + r.amount, 0)), financial: true, tone: 'red' },
      { label: 'Total Unplanned Cost', value: fmtMoney(rows.reduce((s, r) => s + r.amount, 0)), financial: true },
      { label: 'Pending Determination', value: fmtMoney(rows.filter(r => r.recoverability === 'Pending Determination').reduce((s, r) => s + r.amount, 0)), financial: true, filterKey: 'recoverability', filterValue: 'Pending Determination' },
    ],
  },

  {
    id: 'openBills', category: 'financial', name: 'Open Bills / Accounts Payable',
    description: 'All open vendor, freight, and subcontractor bills, company-wide.',
    canView: canViewFinReport,
    columns: [
      { key: 'vendor', label: 'Vendor / Subcontractor', type: 'text' },
      { key: 'invoiceNumber', label: 'Invoice Number', type: 'text' },
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'invoiceDate', label: 'Invoice Date', type: 'date' },
      { key: 'dueDate', label: 'Due Date', type: 'date' },
      { key: 'amount', label: 'Original Amount', type: 'money', financial: true },
      { key: 'paid', label: 'Amount Paid', type: 'money', financial: true },
      { key: 'openBalance', label: 'Open Balance', type: 'money', financial: true },
      { key: 'approvalStatus', label: 'Approval Status', type: 'text' },
      { key: 'paymentStatus', label: 'Payment Status', type: 'text' },
      { key: 'daysPastDue', label: 'Days Past Due', type: 'number' },
    ],
    filterKeys: ['project', 'vendor', 'approvalStatus', 'paymentStatus'],
    groupByKeys: ['paymentStatus', 'project'],
    searchKeys: ['vendor', 'invoiceNumber', 'project'],
    getRows: (ctx) => allApInvoices(ctx.projects).filter(i => invoiceOpenBalance(i) > 0 && i.paymentStatus !== 'Cancelled').map(i => {
      const project = ctx.projects.find(p => p.id === i.projectId);
      const scope = project && project.scopes.find(s => s.id === i.scopeId);
      return {
        _id: i.id, _onOpen: () => ctx.goProjectTab(i.projectId, 'financials', 'ap'),
        vendor: i.vendorName, invoiceNumber: i.invoiceNumber, project: i.projectName, scope: scope ? scope.name : '—',
        invoiceDate: i.invoiceDate, dueDate: i.dueDate, amount: i.amount, paid: invoiceTotalPaid(i),
        openBalance: invoiceOpenBalance(i), approvalStatus: i.approvalStatus, paymentStatus: i.paymentStatus,
        daysPastDue: invoiceDaysPastDue(i),
      };
    }),
    summaryCards: (rows) => [
      { label: 'Total Open Bills', value: String(rows.length), financial: true },
      { label: 'Total Open Balance', value: fmtMoney(rows.reduce((s, r) => s + r.openBalance, 0)), financial: true },
      { label: 'Total Past Due', value: fmtMoney(rows.filter(r => r.daysPastDue > 0).reduce((s, r) => s + r.openBalance, 0)), financial: true, tone: 'red' },
    ],
  },

  {
    id: 'vendorBalance', category: 'financial', name: 'Vendor Open Balance',
    description: 'Every vendor with an open balance — click through to see the underlying invoices.',
    canView: canViewFinReport,
    columns: [
      { key: 'vendor', label: 'Vendor', type: 'text' },
      { key: 'vendorType', label: 'Vendor Type', type: 'text' },
      { key: 'totalInvoiced', label: 'Total Invoiced', type: 'money', financial: true },
      { key: 'totalPaid', label: 'Total Paid', type: 'money', financial: true },
      { key: 'totalOpenBalance', label: 'Open Balance', type: 'money', financial: true },
      { key: 'totalPastDue', label: 'Past Due Balance', type: 'money', financial: true },
      { key: 'openCount', label: 'Number of Open Invoices', type: 'number' },
      { key: 'nextPaymentDue', label: 'Next Payment Due', type: 'date' },
    ],
    filterKeys: ['vendorType'],
    groupByKeys: ['vendorType'],
    searchKeys: ['vendor'],
    getRows: (ctx) => partyBalanceRows(allApInvoices(ctx.projects), 'Vendor').map(r => {
      const vendor = ctx.vendors.find(v => v.name === r.name);
      return { _id: r.name, _onOpen: () => vendor && ctx.goVendorDetail(vendor.id, 'vendor'), vendor: r.name, vendorType: vendor ? vendor.vendorType : '—', ...r };
    }),
    summaryCards: (rows) => [
      { label: 'Total Vendor Open Balance', value: fmtMoney(rows.reduce((s, r) => s + r.totalOpenBalance, 0)), financial: true },
    ],
  },

  {
    id: 'subBalance', category: 'financial', name: 'Subcontractor Open Balance',
    description: 'Every subcontractor with an open balance — click through to see invoice and payment history.',
    canView: canViewFinReport,
    columns: [
      { key: 'subcontractor', label: 'Subcontractor', type: 'text' },
      { key: 'trade', label: 'Trade', type: 'text' },
      { key: 'totalSubmitted', label: 'Total Submitted', type: 'money', financial: true },
      { key: 'totalApproved', label: 'Total Approved', type: 'money', financial: true },
      { key: 'totalPaid', label: 'Total Paid', type: 'money', financial: true },
      { key: 'totalOpenBalance', label: 'Open Balance', type: 'money', financial: true },
      { key: 'totalPastDue', label: 'Past Due', type: 'money', financial: true },
      { key: 'openCount', label: 'Open Invoice Count', type: 'number' },
    ],
    filterKeys: ['trade'],
    groupByKeys: ['trade'],
    searchKeys: ['subcontractor'],
    getRows: (ctx) => partyBalanceRows(allApInvoices(ctx.projects), 'Subcontractor').map(r => {
      const sub = ctx.subcontractors.find(s => s.companyName === r.name);
      return { _id: r.name, _onOpen: () => sub && ctx.goSubcontractorDetail(sub.id), subcontractor: r.name, trade: sub ? sub.trade : '—', ...r };
    }),
    summaryCards: (rows) => [
      { label: 'Total Subcontractor Open Balance', value: fmtMoney(rows.reduce((s, r) => s + r.totalOpenBalance, 0)), financial: true },
    ],
  },

  {
    id: 'freightBalance', category: 'financial', name: 'Freight Open Balance',
    description: 'Every freight/logistics company with an open balance.',
    canView: canViewFinReport,
    columns: [
      { key: 'freightCo', label: 'Freight / Logistics Company', type: 'text' },
      { key: 'totalInvoiced', label: 'Total Invoiced', type: 'money', financial: true },
      { key: 'totalPaid', label: 'Total Paid', type: 'money', financial: true },
      { key: 'totalOpenBalance', label: 'Open Balance', type: 'money', financial: true },
      { key: 'totalPastDue', label: 'Past Due Balance', type: 'money', financial: true },
    ],
    filterKeys: ['freightCo'],
    groupByKeys: [],
    searchKeys: ['freightCo'],
    getRows: (ctx) => partyBalanceRows(allApInvoices(ctx.projects), 'Freight').map(r => {
      const fw = ctx.freightForwarders.find(f => f.name === r.name);
      return { _id: r.name, _onOpen: () => fw && ctx.goVendorDetail(fw.id, 'forwarder'), freightCo: r.name, ...r };
    }),
    summaryCards: (rows) => [
      { label: 'Total Freight Open Balance', value: fmtMoney(rows.reduce((s, r) => s + r.totalOpenBalance, 0)), financial: true },
    ],
  },

  {
    id: 'arAging', category: 'financial', name: 'Accounts Receivable / Client Aging',
    description: 'Client payment terms and open balance. No due-date field is tracked on the receivables side today, so day-based aging buckets are not shown.',
    canView: canViewFinReport,
    columns: [
      { key: 'client', label: 'Client', type: 'text' },
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'term', label: 'Billing Milestone', type: 'text' },
      { key: 'originalAmount', label: 'Original Amount', type: 'money', financial: true },
      { key: 'received', label: 'Amount Received', type: 'money', financial: true },
      { key: 'openBalance', label: 'Open Balance', type: 'money', financial: true },
      { key: 'status', label: 'Status', type: 'text' },
    ],
    filterKeys: ['client', 'project', 'status'],
    groupByKeys: ['project', 'status'],
    searchKeys: ['client', 'project'],
    getRows: (ctx) => ctx.projects.flatMap(p => {
      const account = ctx.accounts.find(a => a.id === p.accountId);
      const contract = revisedContractValue(p);
      return (p.paymentTerms || []).map(t => {
        const amount = contract * (t.pct / 100);
        const received = t.status === 'Paid' ? amount : 0;
        return {
          _id: t.id, _onOpen: () => ctx.goProjectTab(p.id, 'financials', 'ar'),
          client: account ? account.name : '—', project: p.name, term: `${t.label} (${t.trigger})`,
          originalAmount: amount, received, openBalance: amount - received, status: t.status,
        };
      });
    }),
    summaryCards: (rows) => [
      { label: 'Total Outstanding AR', value: fmtMoney(rows.reduce((s, r) => s + r.openBalance, 0)), financial: true },
    ],
  },

  {
    id: 'cashFlow', category: 'financial', name: 'Cash Flow / Upcoming Payments',
    description: 'Money out (vendor/freight/subcontractor bills) is due-date driven and fully tracked. Money in (client billing) has no stored due date today, so it is shown by milestone without a forecast date.',
    canView: canViewFinReport,
    columns: [
      { key: 'direction', label: 'Direction', type: 'text' },
      { key: 'party', label: 'Party', type: 'text' },
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'dueDate', label: 'Due Date', type: 'date' },
      { key: 'amount', label: 'Amount', type: 'money', financial: true },
    ],
    filterKeys: ['direction', 'project'],
    groupByKeys: ['direction'],
    searchKeys: ['party', 'project'],
    getRows: (ctx) => {
      const out = allApInvoices(ctx.projects).filter(i => invoiceOpenBalance(i) > 0 && i.paymentStatus !== 'Cancelled').map(i => ({
        _id: i.id, _onOpen: () => ctx.goProjectTab(i.projectId, 'financials', 'ap'),
        direction: 'Money Out', party: i.vendorName, project: i.projectName, dueDate: i.dueDate, amount: invoiceOpenBalance(i),
      }));
      ctx.projects.forEach(p => {
        const account = ctx.accounts.find(a => a.id === p.accountId);
        const contract = revisedContractValue(p);
        (p.paymentTerms || []).filter(t => t.status !== 'Paid').forEach(t => {
          out.push({ _id: `${p.id}-${t.id}`, _onOpen: () => ctx.goProjectTab(p.id, 'financials', 'ar'), direction: 'Money In', party: account ? account.name : '—', project: p.name, dueDate: null, amount: contract * (t.pct / 100) });
        });
      });
      return out;
    },
    summaryCards: (rows) => {
      const moneyIn = rows.filter(r => r.direction === 'Money In').reduce((s, r) => s + r.amount, 0);
      const moneyOut = rows.filter(r => r.direction === 'Money Out').reduce((s, r) => s + r.amount, 0);
      return [
        { label: 'Money In (Open)', value: fmtMoney(moneyIn), financial: true, filterKey: 'direction', filterValue: 'Money In' },
        { label: 'Money Out (Open)', value: fmtMoney(moneyOut), financial: true, filterKey: 'direction', filterValue: 'Money Out' },
        { label: 'Expected Net Movement', value: fmtMoney(moneyIn - moneyOut), financial: true, tone: moneyIn - moneyOut < 0 ? 'red' : 'green' },
      ];
    },
  },

  {
    id: 'aiaBilling', category: 'financial', name: 'AIA Billing',
    description: 'Schedule of Values and payment application status, for projects using AIA-style billing. Cash actually received against an application is not tracked separately from certified/billed amounts, so "Total Paid" is shown as not tracked.',
    canView: canViewFinReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'originalContract', label: 'Original Contract Value', type: 'money', financial: true },
      { key: 'approvedCO', label: 'Approved Change Orders', type: 'money', financial: true },
      { key: 'currentContract', label: 'Current Contract Value', type: 'money', financial: true },
      { key: 'scheduledValue', label: 'Scheduled Value', type: 'money', financial: true },
      { key: 'previousApplications', label: 'Previous Applications', type: 'money', financial: true },
      { key: 'currentApplication', label: 'Current Application', type: 'money', financial: true },
      { key: 'storedMaterials', label: 'Stored Materials', type: 'money', financial: true },
      { key: 'workCompleted', label: 'Work Completed', type: 'money', financial: true },
      { key: 'retainage', label: 'Retainage', type: 'money', financial: true },
      { key: 'totalPaid', label: 'Total Paid', type: 'text' },
      { key: 'balanceToFinish', label: 'Balance to Finish', type: 'money', financial: true },
    ],
    filterKeys: ['project'],
    groupByKeys: [],
    searchKeys: ['project'],
    getRows: (ctx) => ctx.projects.filter(p => (p.sov || []).length > 0).map(p => {
      const scheduledValue = p.sov.flatMap(c => c.items).reduce((s, i) => s + i.scheduledValue, 0);
      const latestApp = p.applications.length ? p.applications[p.applications.length - 1] : null;
      const summary = latestApp ? applicationSummary(p, latestApp.id) : null;
      const stored = summary ? [...summary.lineResults, ...summary.coLineResults].reduce((s, r) => s + (r.stored || 0), 0) : 0;
      const approvedCO = (p.changeOrders || []).filter(c => c.type === 'Change Order' && c.status === 'Approved').reduce((s, c) => s + c.amount, 0);
      return {
        _id: p.id, _onOpen: () => ctx.goProjectTab(p.id, 'financials', 'billing'),
        project: p.name, originalContract: latestQuoteAmount(p), approvedCO, currentContract: revisedContractValue(p),
        scheduledValue, previousApplications: summary ? summary.previousApplications : 0,
        currentApplication: summary ? summary.currentPaymentDue : 0, storedMaterials: stored,
        workCompleted: summary ? summary.completedToDate : 0, retainage: summary ? summary.retainage : 0,
        totalPaid: 'Not tracked', balanceToFinish: summary ? summary.balanceToFinish : scheduledValue,
      };
    }),
    summaryCards: (rows) => [
      { label: 'Total Scheduled Value', value: fmtMoney(rows.reduce((s, r) => s + r.scheduledValue, 0)), financial: true },
      { label: 'Total Work Completed', value: fmtMoney(rows.reduce((s, r) => s + r.workCompleted, 0)), financial: true },
    ],
  },

  // ==== LOGISTICS ==========================================================
  {
    id: 'logisticsMaster', category: 'logistics', name: 'Master Logistics Status Report',
    description: 'Every active shipment/container across every project, with a plain-language On Track / Attention Required / At Risk / Delayed / Completed status.',
    canView: canViewLogisticsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'countryOfOrigin', label: 'Country of Origin', type: 'text' }, { key: 'shipmentType', label: 'Shipment Type', type: 'text' },
      { key: 'containerNumber', label: 'Container Number', type: 'text' }, { key: 'containerType', label: 'Container Type', type: 'text' },
      { key: 'bookingNumber', label: 'Booking Number', type: 'text' }, { key: 'blNumber', label: 'Bill of Lading', type: 'text' },
      { key: 'freightForwarder', label: 'Freight Forwarder', type: 'text' }, { key: 'carrier', label: 'Carrier', type: 'text' },
      { key: 'fromPort', label: 'Port of Loading', type: 'text' }, { key: 'toPort', label: 'Port of Discharge', type: 'text' },
      { key: 'etd', label: 'ETD', type: 'date' }, { key: 'actualDeparture', label: 'Actual Departure', type: 'date' },
      { key: 'eta', label: 'ETA', type: 'date' }, { key: 'actualArrival', label: 'Actual Arrival', type: 'date' },
      { key: 'customsStatus', label: 'Customs Status', type: 'text' },
      { key: 'warehouseDestination', label: 'Warehouse Destination', type: 'text' }, { key: 'jobsiteDestination', label: 'Jobsite Destination', type: 'text' },
      { key: 'status', label: 'Logistics Status', type: 'text' }, { key: 'responsible', label: 'Responsible Team Member', type: 'text' },
      { key: 'daysDelayed', label: 'Days Delayed', type: 'number' }, { key: 'risk', label: 'Risk Status', type: 'text' },
      { key: 'classificationStatus', label: 'Classification Status', type: 'text' }, { key: 'customsEntryNumber', label: 'Customs Entry Number', type: 'text' },
      { key: 'broker', label: 'Broker', type: 'text' }, { key: 'estimatedTariffs', label: 'Estimated Tariffs', type: 'money', financial: true },
      { key: 'actualTariffs', label: 'Actual Tariffs', type: 'money', financial: true }, { key: 'tariffVariance', label: 'Tariff Variance', type: 'money', financial: true },
    ],
    filterKeys: ['project', 'status', 'risk', 'customsStatus', 'shipmentType', 'classificationStatus'],
    groupByKeys: ['project', 'risk', 'status'],
    searchKeys: ['project', 'scope', 'containerNumber', 'blNumber', 'bookingNumber'],
    dateFilterKey: 'eta',
    // Classification Status / Estimated & Actual Tariffs / Broker / Entry #
    // are pulled from ctx.tariffLines — the same records the Trade
    // Compliance & Tariffs module and the Export tab's "Tariffs & Customs"
    // section read and write. Nothing here is a separate tariff record.
    getRows: (ctx) => allContainers(ctx).map(c => {
      const fwd = ctx.freightForwarders.find(f => f.id === c.freightCompanyId);
      const lines = ctx.tariffLines.filter(l => l.exportContainerId === c.id);
      const clearedLine = lines.find(l => l.actual && l.actual.customsEntryNumber);
      const broker = clearedLine && clearedLine.actual.brokerId ? ctx.freightForwarders.find(f => f.id === clearedLine.actual.brokerId) : null;
      const estimatedTariffs = lines.reduce((s, l) => s + (l.estimatedTariff || 0), 0);
      const clearedLines = lines.filter(l => l.actualTariff !== null && l.actualTariff !== undefined);
      const actualTariffs = clearedLines.reduce((s, l) => s + (l.actualTariff || 0), 0);
      return {
        _id: c.id, _onOpen: () => ctx.goProjectTab(c.projectId, 'export'),
        project: c.projectName, scope: c.scopeNames || '—', countryOfOrigin: c.countryOfOrigin || '—', shipmentType: c.shipmentType,
        containerNumber: c.containerNumber, containerType: c.containerType || '—', bookingNumber: c.bookingNumber || '—', blNumber: c.blNumber || '—',
        freightForwarder: fwd ? fwd.name : '—', carrier: c.carrier || (fwd ? fwd.name : '—'),
        fromPort: c.fromPort || '—', toPort: c.toPort || '—', etd: c.etd, actualDeparture: c.actualDeparture, eta: c.eta, actualArrival: c.actualArrival,
        customsStatus: c.customsStatus, warehouseDestination: c.warehouseDestination || '—', jobsiteDestination: c.jobsiteDestination || '—',
        status: c.status, responsible: (c.assigneeIds && c.assigneeIds.length) ? c.assigneeIds.map(id => personName(ctx.teamDirectory, id)).join(', ') : '—',
        daysDelayed: containerDaysDelayed(c), risk: containerRiskStatus(c),
        classificationStatus: lines.length === 0 ? 'Not Classified' : lines.every(l => l.htsCode || l.hsCode) ? 'Classified' : 'Partially Classified',
        customsEntryNumber: clearedLine ? clearedLine.actual.customsEntryNumber : '—', broker: broker ? broker.name : '—',
        estimatedTariffs, actualTariffs, tariffVariance: clearedLines.length ? actualTariffs - clearedLines.reduce((s, l) => s + (l.estimatedTariff || 0), 0) : null,
      };
    }),
    summaryCards: (rows) => [
      { label: 'On Track', value: String(rows.filter(r => r.risk === 'On Track').length), filterKey: 'risk', filterValue: 'On Track', tone: 'green' },
      { label: 'Attention Required', value: String(rows.filter(r => r.risk === 'Attention Required').length), filterKey: 'risk', filterValue: 'Attention Required' },
      { label: 'At Risk', value: String(rows.filter(r => r.risk === 'At Risk').length), filterKey: 'risk', filterValue: 'At Risk', tone: 'red' },
      { label: 'Delayed', value: String(rows.filter(r => r.risk === 'Delayed').length), filterKey: 'risk', filterValue: 'Delayed', tone: 'red' },
    ],
  },

  {
    id: 'logisticsTracking', category: 'logistics', name: 'Shipment & Container Tracking Report',
    description: 'Every shipment from booking through final delivery, one milestone checklist per container.',
    canView: canViewLogisticsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'containerNumber', label: 'Container', type: 'text' },
      { key: 'bookingConfirmed', label: 'Booking Confirmed', type: 'text' }, { key: 'readyToShip', label: 'Ready to Ship', type: 'text' },
      { key: 'etd', label: 'ETD', type: 'date' }, { key: 'actualDeparture', label: 'Actual Departure', type: 'date' },
      { key: 'inTransit', label: 'In Transit', type: 'text' }, { key: 'eta', label: 'ETA', type: 'date' },
      { key: 'portArrival', label: 'Port Arrival', type: 'text' }, { key: 'customsClearance', label: 'Customs Clearance', type: 'text' },
      { key: 'containerPickup', label: 'Container Pickup', type: 'text' }, { key: 'warehouseDelivery', label: 'Warehouse Delivery', type: 'text' },
      { key: 'jobsiteDelivery', label: 'Jobsite Delivery', type: 'text' }, { key: 'emptyReturn', label: 'Empty Container Return', type: 'text' },
      { key: 'pod', label: 'POD', type: 'text' },
    ],
    filterKeys: ['project'],
    groupByKeys: ['project'],
    searchKeys: ['project', 'containerNumber'],
    getRows: (ctx) => allContainers(ctx).map(c => {
      const stepOk = key => !!(c.documents[key] && c.documents[key].fileUrl);
      return {
        _id: c.id, _onOpen: () => ctx.goProjectTab(c.projectId, 'export'),
        project: c.projectName, containerNumber: c.containerNumber,
        bookingConfirmed: c.bookingNumber ? 'Confirmed' : (['Booked', 'Loaded', 'In Transit', 'Arrived', 'Delivered'].includes(c.status) ? 'Confirmed' : 'Pending'),
        readyToShip: ['Booked', 'Loaded', 'In Transit', 'Arrived', 'Delivered'].includes(c.status) ? 'Yes' : 'No',
        etd: c.etd, actualDeparture: c.actualDeparture,
        inTransit: c.status === 'In Transit' ? 'Yes' : (['Arrived', 'Delivered'].includes(c.status) ? 'Complete' : 'No'),
        eta: c.eta, portArrival: c.status === 'Arrived' || c.status === 'Delivered' ? 'Arrived' : 'Pending',
        customsClearance: c.customsStatus,
        containerPickup: c.actualPickupDate ? `Picked up ${fmtDate(c.actualPickupDate)}` : (c.status === 'Arrived' ? 'Awaiting Pickup' : 'Pending'),
        warehouseDelivery: c.warehouseDestination ? (c.status === 'Delivered' || c.actualPickupDate ? 'Delivered' : 'Pending') : '—',
        jobsiteDelivery: c.jobsiteDestination ? (c.status === 'Delivered' ? 'Delivered' : 'Pending') : '—',
        emptyReturn: c.actualReturnDate ? `Returned ${fmtDate(c.actualReturnDate)}` : (c.emptyReturnDeadline ? `Due ${fmtDate(c.emptyReturnDeadline)}` : 'Pending'),
        pod: stepOk('delivery_pod') ? 'On File' : 'Not on File',
      };
    }),
    summaryCards: (rows) => [
      { label: 'Containers Tracked', value: String(rows.length) },
      { label: 'In Transit', value: String(rows.filter(r => r.inTransit === 'Yes').length) },
      { label: 'Awaiting Pickup', value: String(rows.filter(r => r.containerPickup === 'Awaiting Pickup').length) },
      { label: 'POD On File', value: String(rows.filter(r => r.pod === 'On File').length) },
    ],
  },

  {
    id: 'logisticsByProject', category: 'logistics', name: 'Project Logistics Report',
    description: 'Every scope in every project, and where its materials currently sit in the Procurement → Delivered chain.',
    canView: canViewLogisticsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'materialStatus', label: 'Material Status', type: 'text' }, { key: 'containerCount', label: 'Containers', type: 'number' },
      { key: 'eta', label: 'Next ETA', type: 'date' }, { key: 'responsible', label: 'Responsible', type: 'text' },
    ],
    filterKeys: ['project', 'materialStatus'],
    groupByKeys: ['project', 'materialStatus'],
    searchKeys: ['project', 'scope'],
    getRows: (ctx) => ctx.projects.flatMap(p => p.scopes.map(s => {
      const containers = containersForProjectScope(ctx.exportContainers, p.id, s.id);
      const nextEta = containers.filter(c => c.eta && !c.actualArrival).map(c => c.eta).sort()[0] || null;
      return {
        _id: `${p.id}-${s.id}`, _onOpen: () => ctx.goProjectTab(p.id, 'export'),
        project: p.name, scope: s.name, materialStatus: scopeLogisticsStatus(p, s, ctx.materialAllocations, ctx.exportContainers),
        containerCount: containers.length, eta: nextEta,
        responsible: personName(ctx.teamDirectory, teamMemberFor(p, 'Logistic Manager') || teamMemberFor(p, 'Export Manager')) || '—',
      };
    })),
    summaryCards: (rows) => MATERIAL_STATUS_LADDER.map(status => ({
      label: status, value: String(rows.filter(r => r.materialStatus === status).length), filterKey: 'materialStatus', filterValue: status,
    })),
  },

  {
    id: 'logisticsForecast', category: 'logistics', name: 'Upcoming Shipment & Arrival Forecast',
    description: 'Shipments expected to depart or arrive soon. Use the 7/14/30/60/90-day buttons or a custom date range (filtered on ETA).',
    canView: canViewLogisticsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'scope', label: 'Scope', type: 'text' }, { key: 'vendor', label: 'Vendor', type: 'text' },
      { key: 'countryOfOrigin', label: 'Origin Country', type: 'text' }, { key: 'containerNumber', label: 'Container', type: 'text' },
      { key: 'freightForwarder', label: 'Freight Forwarder', type: 'text' }, { key: 'carrier', label: 'Carrier', type: 'text' },
      { key: 'toPort', label: 'Destination', type: 'text' }, { key: 'etd', label: 'ETD', type: 'date' }, { key: 'eta', label: 'ETA', type: 'date' },
      { key: 'status', label: 'Status', type: 'text' },
    ],
    filterKeys: ['project', 'vendor', 'countryOfOrigin', 'toPort', 'carrier', 'freightForwarder'],
    groupByKeys: ['project'],
    searchKeys: ['project', 'scope', 'containerNumber'],
    dateFilterKey: 'eta',
    getRows: (ctx) => allContainers(ctx).filter(c => !c.actualArrival).map(c => {
      const fwd = ctx.freightForwarders.find(f => f.id === c.freightCompanyId);
      const project = ctx.projects.find(p => p.id === c.projectId);
      const scope = project.scopes.find(s => c.scopeIds.includes(s.id));
      const estimate = scope ? (project.vendorEstimates || []).find(e => e.scopeId === scope.id) : null;
      const vendorName = estimate ? (ctx.vendors.find(v => v.id === estimate.vendorId) || {}).name : null;
      return {
        _id: c.id, _onOpen: () => ctx.goProjectTab(c.projectId, 'export'),
        project: c.projectName, scope: c.scopeNames || '—', vendor: vendorName || '—', countryOfOrigin: c.countryOfOrigin || '—',
        containerNumber: c.containerNumber, freightForwarder: fwd ? fwd.name : '—', carrier: c.carrier || (fwd ? fwd.name : '—'),
        toPort: c.toPort || '—', etd: c.etd, eta: c.eta, status: c.status,
      };
    }),
    summaryCards: (rows) => [{ label: 'Shipments in Range', value: String(rows.length) }],
  },

  {
    id: 'logisticsDelayed', category: 'logistics', name: 'Delayed & At-Risk Shipment Report',
    description: 'Shipments whose ETD/ETA has passed without an actual date on file, or whose customs status needs attention.',
    canView: canViewLogisticsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'containerNumber', label: 'Container', type: 'text' },
      { key: 'originalRequiredDate', label: 'Original Required Date', type: 'date' }, { key: 'currentProjectedDate', label: 'Current Projected Date', type: 'date' },
      { key: 'daysDelayed', label: 'Days Delayed', type: 'number' }, { key: 'reason', label: 'Reason', type: 'text' },
      { key: 'responsible', label: 'Responsible Party', type: 'text' }, { key: 'impactedMilestone', label: 'Impacted Milestone', type: 'text' },
      { key: 'installationImpact', label: 'Installation Impact', type: 'text' }, { key: 'risk', label: 'Risk Status', type: 'text' },
    ],
    filterKeys: ['project', 'risk'],
    groupByKeys: ['project', 'risk'],
    searchKeys: ['project', 'containerNumber'],
    getRows: (ctx) => allContainers(ctx).filter(c => ['At Risk', 'Delayed'].includes(containerRiskStatus(c))).map(c => {
      const project = ctx.projects.find(p => p.id === c.projectId);
      const scope = project.scopes.find(s => c.scopeIds.includes(s.id));
      const stage = scope ? scope.stages.find(st => st.status === 'In Progress' || st.status === 'Delayed') : null;
      let reason = 'ETA passed without confirmed arrival';
      if (c.customsStatus === 'Held / Inspection') reason = 'Held in customs inspection';
      else if (c.customsStatus === 'Rejected') reason = 'Customs entry rejected';
      else if (c.etd && !c.actualDeparture && c.etd < todayISO()) reason = 'Departure overdue';
      return {
        _id: c.id, _onOpen: () => ctx.goProjectTab(c.projectId, 'export'),
        project: c.projectName, containerNumber: c.containerNumber,
        originalRequiredDate: c.eta, currentProjectedDate: c.actualArrival || c.eta,
        daysDelayed: containerDaysDelayed(c), reason,
        responsible: (c.assigneeIds && c.assigneeIds.length) ? c.assigneeIds.map(id => personName(ctx.teamDirectory, id)).join(', ') : '—',
        impactedMilestone: stage ? stage.name : '—',
        installationImpact: stage ? `May push "${stage.name}" (due ${fmtDate(stage.plannedDue)})` : 'None identified',
        risk: containerRiskStatus(c),
      };
    }),
    summaryCards: (rows) => [
      { label: 'Delayed', value: String(rows.filter(r => r.risk === 'Delayed').length), tone: 'red' },
      { label: 'At Risk', value: String(rows.filter(r => r.risk === 'At Risk').length), tone: 'red' },
      { label: 'Avg Days Delayed', value: rows.length ? (rows.reduce((s, r) => s + r.daysDelayed, 0) / rows.length).toFixed(1) : '0' },
    ],
  },

  {
    id: 'logisticsRequiredVsEta', category: 'logistics', name: 'Required-on-Site vs ETA Report',
    description: `Compares each material allocation's Required Date against the soonest ETA of its scope's containers. Projected delivery adds ${LANDED_LOGISTICS_BUFFER_DAYS} days for estimated customs clearance, inland transportation, and warehouse handling — an estimate, not a tracked value, since those durations aren't separately logged per shipment yet.`,
    canView: canViewLogisticsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'requiredOnSite', label: 'Required on Site Date', type: 'date' }, { key: 'projectedDelivery', label: 'Projected Delivery Date', type: 'date' },
      { key: 'bufferDays', label: 'Days of Buffer', type: 'number' }, { key: 'daysLate', label: 'Days Late', type: 'number' }, { key: 'risk', label: 'Risk Level', type: 'text' },
    ],
    filterKeys: ['project', 'risk'],
    groupByKeys: ['project', 'risk'],
    searchKeys: ['project', 'scope'],
    getRows: (ctx) => {
      const out = [];
      ctx.materialAllocations.filter(a => a.requiredDate).forEach(a => {
        const project = ctx.projects.find(p => p.id === a.projectId);
        if (!project) return;
        const scope = project.scopes.find(s => s.id === a.scopeId);
        const containers = a.scopeId ? containersForProjectScope(ctx.exportContainers, project.id, a.scopeId).filter(c => c.eta) : [];
        if (!containers.length) return;
        const soonestEta = [...containers].sort((x, y) => (x.eta < y.eta ? -1 : 1))[0].eta;
        const projectedDelivery = addDays(soonestEta, LANDED_LOGISTICS_BUFFER_DAYS);
        const bufferDays = daysBetween(projectedDelivery, a.requiredDate);
        out.push({
          _id: a.id, _onOpen: () => ctx.goProjectTab(a.projectId, 'export'),
          project: project.name, scope: scope ? scope.name : '—',
          requiredOnSite: a.requiredDate, projectedDelivery, bufferDays,
          daysLate: bufferDays < 0 ? -bufferDays : 0,
          risk: bufferDays < 0 ? 'Delayed' : bufferDays <= 3 ? 'At Risk' : bufferDays <= 7 ? 'Attention Required' : 'On Track',
        });
      });
      return out;
    },
    summaryCards: (rows) => [
      { label: 'On Track', value: String(rows.filter(r => r.risk === 'On Track').length), tone: 'green' },
      { label: 'At Risk / Attention', value: String(rows.filter(r => ['At Risk', 'Attention Required'].includes(r.risk)).length) },
      { label: 'Projected Late', value: String(rows.filter(r => r.risk === 'Delayed').length), tone: 'red' },
    ],
  },

  {
    id: 'warehouseInventory', category: 'logistics', name: 'Warehouse Inventory Report',
    description: 'Full physical inventory on hand, with received/available/allocated/released/delivered quantities and value.',
    canView: canViewLogisticsReport,
    columns: [
      { key: 'material', label: 'Material', type: 'text' }, { key: 'sku', label: 'SKU', type: 'text' },
      { key: 'vendor', label: 'Vendor', type: 'text' }, { key: 'unit', label: 'Unit', type: 'text' },
      { key: 'qtyReceived', label: 'Qty Received', type: 'number' }, { key: 'qtyAvailable', label: 'Qty Available', type: 'number' },
      { key: 'qtyAllocated', label: 'Qty Allocated', type: 'number' }, { key: 'qtyReleased', label: 'Qty Released', type: 'number' },
      { key: 'qtyDelivered', label: 'Qty Delivered', type: 'number' }, { key: 'location', label: 'Warehouse Location', type: 'text' },
      { key: 'unitCost', label: 'Unit Cost', type: 'money', financial: true }, { key: 'totalValue', label: 'Total Inventory Value', type: 'money', financial: true },
      { key: 'receivedDate', label: 'Received Date', type: 'date' }, { key: 'ageDays', label: 'Inventory Age (Days)', type: 'number' },
    ],
    filterKeys: ['vendor', 'location'],
    groupByKeys: ['vendor', 'location'],
    searchKeys: ['material', 'sku'],
    getRows: (ctx) => ctx.warehouseMaterials.filter(m => m.active).map(m => {
      const wh = ctx.warehouses.find(w => w.id === m.warehouseId);
      const allocated = ctx.materialAllocations.filter(a => a.materialId === m.id && a.status !== 'Cancelled');
      const qtyAllocated = allocated.reduce((s, a) => s + (a.quantityAllocated - a.quantityReleased), 0);
      const qtyReleased = allocated.reduce((s, a) => s + (a.quantityReleased - a.quantityDelivered), 0);
      const qtyDelivered = allocated.reduce((s, a) => s + a.quantityDelivered, 0);
      return {
        _id: m.id, _onOpen: () => ctx.goWarehouse(),
        material: m.name, sku: m.itemId || m.referenceNumber || '—', vendor: m.manufacturerVendor || '—', unit: m.unitOfMeasure,
        qtyReceived: m.initialStock ?? m.currentStock, qtyAvailable: availableQuantity(m, ctx.materialAllocations),
        qtyAllocated, qtyReleased, qtyDelivered,
        location: [wh ? wh.name : null, m.storageLocation].filter(Boolean).join(' — ') || '—',
        unitCost: m.unitCost, totalValue: m.currentStock * m.unitCost,
        receivedDate: m.dateReceived, ageDays: m.dateReceived ? daysBetween(m.dateReceived, todayISO()) : 0,
      };
    }),
    summaryCards: (rows) => [
      { label: 'SKUs Tracked', value: String(rows.length) },
      { label: 'Total Inventory Value', value: fmtMoney(rows.reduce((s, r) => s + r.totalValue, 0)), financial: true },
      { label: 'Total Units Available', value: String(rows.reduce((s, r) => s + r.qtyAvailable, 0)) },
    ],
  },

  {
    id: 'allocatedInventory', category: 'logistics', name: 'Allocated Inventory by Project Report',
    description: 'Warehouse materials currently allocated to a project. Allocated cost is included in that project\'s cost the moment it\'s allocated here.',
    canView: canViewLogisticsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'scope', label: 'Scope', type: 'text' }, { key: 'material', label: 'Material', type: 'text' },
      { key: 'qtyAllocated', label: 'Qty Allocated', type: 'number' }, { key: 'allocationDate', label: 'Allocation Date', type: 'date' },
      { key: 'allocatedBy', label: 'Allocated By', type: 'text' }, { key: 'unitCost', label: 'Unit Cost', type: 'money', financial: true },
      { key: 'totalCost', label: 'Total Allocated Cost', type: 'money', financial: true },
      { key: 'releaseStatus', label: 'Release Status', type: 'text' }, { key: 'deliveryStatus', label: 'Delivery Status', type: 'text' },
    ],
    filterKeys: ['project', 'releaseStatus'],
    groupByKeys: ['project'],
    searchKeys: ['project', 'scope', 'material'],
    getRows: (ctx) => ctx.materialAllocations.filter(a => a.status !== 'Cancelled').map(a => {
      const project = ctx.projects.find(p => p.id === a.projectId);
      const scope = project ? project.scopes.find(s => s.id === a.scopeId) : null;
      const material = ctx.warehouseMaterials.find(m => m.id === a.materialId);
      const unitCost = material ? material.unitCost : 0;
      const releaseStatus = a.quantityReleased >= a.quantityAllocated ? 'Fully Released' : a.quantityReleased > 0 ? 'Partially Released' : 'Not Released';
      const deliveryStatus = a.quantityDelivered >= a.quantityAllocated ? 'Delivered' : a.quantityDelivered > 0 ? 'Partially Delivered' : 'Not Delivered';
      return {
        _id: a.id, _onOpen: () => project && ctx.goProjectTab(project.id, 'procurement'),
        project: project ? project.name : '—', scope: scope ? scope.name : '—', material: material ? material.name : '—',
        qtyAllocated: a.quantityAllocated, allocationDate: a.allocationDate, allocatedBy: a.allocatedBy || '—',
        unitCost, totalCost: a.quantityAllocated * unitCost, releaseStatus, deliveryStatus,
      };
    }),
    summaryCards: (rows) => [
      { label: 'Active Allocations', value: String(rows.length) },
      { label: 'Total Allocated Cost', value: fmtMoney(rows.reduce((s, r) => s + r.totalCost, 0)), financial: true },
      { label: 'Fully Released', value: String(rows.filter(r => r.releaseStatus === 'Fully Released').length), filterKey: 'releaseStatus', filterValue: 'Fully Released' },
    ],
  },

  {
    id: 'unallocatedInventory', category: 'logistics', name: 'Unallocated Inventory Report',
    description: 'Warehouse stock not currently allocated to any project.',
    canView: canViewLogisticsReport,
    columns: [
      { key: 'material', label: 'Material', type: 'text' }, { key: 'qtyAvailable', label: 'Qty Available', type: 'number' },
      { key: 'availableValue', label: 'Available Value', type: 'money', financial: true }, { key: 'receivedDate', label: 'Received Date', type: 'date' },
      { key: 'ageDays', label: 'Days in Inventory', type: 'number' }, { key: 'location', label: 'Warehouse Location', type: 'text' },
      { key: 'originalSource', label: 'Original Project / PO', type: 'text' },
    ],
    filterKeys: ['location'],
    groupByKeys: ['location'],
    searchKeys: ['material'],
    getRows: (ctx) => ctx.warehouseMaterials.filter(m => m.active && availableQuantity(m, ctx.materialAllocations) > 0).map(m => {
      const wh = ctx.warehouses.find(w => w.id === m.warehouseId);
      let originalSource = '—';
      if (m.relatedPoId) {
        for (const p of ctx.projects) {
          const po = (p.purchaseOrders || []).find(x => x.id === m.relatedPoId);
          if (po) { originalSource = `${p.name} — ${po.poNumber}`; break; }
        }
      }
      return {
        _id: m.id, _onOpen: () => ctx.goWarehouse(),
        material: m.name, qtyAvailable: availableQuantity(m, ctx.materialAllocations),
        availableValue: availableQuantity(m, ctx.materialAllocations) * m.unitCost,
        receivedDate: m.dateReceived, ageDays: m.dateReceived ? daysBetween(m.dateReceived, todayISO()) : 0,
        location: [wh ? wh.name : null, m.storageLocation].filter(Boolean).join(' — ') || '—', originalSource,
      };
    }),
    summaryCards: (rows) => [
      { label: 'Unallocated SKUs', value: String(rows.length) },
      { label: 'Unallocated Value', value: fmtMoney(rows.reduce((s, r) => s + r.availableValue, 0)), financial: true },
    ],
  },

  {
    id: 'warehouseAging', category: 'logistics', name: 'Warehouse Aging Report',
    description: 'Inventory grouped into age buckets, by quantity and value.',
    canView: canViewLogisticsReport,
    columns: [
      { key: 'bucket', label: 'Age Bucket', type: 'text' }, { key: 'itemCount', label: 'SKUs', type: 'number' },
      { key: 'quantity', label: 'Total Quantity', type: 'number' }, { key: 'value', label: 'Total Value', type: 'money', financial: true },
    ],
    filterKeys: [],
    groupByKeys: [],
    searchKeys: ['bucket'],
    getRows: (ctx) => {
      const buckets = [
        { key: '0-30', label: '0–30 Days', min: 0, max: 30 },
        { key: '31-60', label: '31–60 Days', min: 31, max: 60 },
        { key: '61-90', label: '61–90 Days', min: 61, max: 90 },
        { key: '91-180', label: '91–180 Days', min: 91, max: 180 },
        { key: '180+', label: '180+ Days', min: 181, max: Infinity },
      ];
      const today = todayISO();
      return buckets.map(b => {
        const items = ctx.warehouseMaterials.filter(m => {
          if (!m.active || !m.dateReceived) return false;
          const age = daysBetween(m.dateReceived, today);
          return age >= b.min && age <= b.max;
        });
        return {
          _id: b.key, bucket: b.label, itemCount: items.length,
          quantity: items.reduce((s, m) => s + (m.currentStock || 0), 0),
          value: items.reduce((s, m) => s + (m.currentStock || 0) * (m.unitCost || 0), 0),
        };
      });
    },
    summaryCards: (rows) => [
      { label: 'Total Inventory Value', value: fmtMoney(rows.reduce((s, r) => s + r.value, 0)), financial: true },
      { label: 'Oldest Bucket (180+) Value', value: fmtMoney((rows.find(r => r.bucket === '180+ Days') || { value: 0 }).value), financial: true, tone: 'red' },
    ],
  },

  {
    id: 'receivingReport', category: 'logistics', name: 'Receiving Report',
    description: 'All materials received at the warehouse, with expected vs. received quantity.',
    canView: canViewLogisticsReport,
    columns: [
      { key: 'material', label: 'Material', type: 'text' }, { key: 'company', label: 'Vendor / Company', type: 'text' },
      { key: 'po', label: 'PO', type: 'text' }, { key: 'expectedQty', label: 'Expected Quantity', type: 'number' },
      { key: 'receivedQty', label: 'Received Quantity', type: 'number' }, { key: 'shortQty', label: 'Short Quantity', type: 'number' },
      { key: 'overQty', label: 'Over Quantity', type: 'number' }, { key: 'damagedQty', label: 'Damaged Quantity', type: 'number' },
      { key: 'receivingDate', label: 'Receiving Date', type: 'date' }, { key: 'receiver', label: 'Receiver', type: 'text' }, { key: 'notes', label: 'Notes', type: 'text' },
    ],
    filterKeys: ['company'],
    groupByKeys: ['company'],
    searchKeys: ['material', 'company', 'po'],
    dateFilterKey: 'receivingDate',
    getRows: (ctx) => ctx.inventoryTransactions.filter(t => t.type === 'Receiving').map(t => {
      const m = ctx.warehouseMaterials.find(x => x.id === t.materialId);
      return {
        _id: t.id, _onOpen: () => ctx.goWarehouse(),
        material: m ? m.name : '—', company: t.company || '—', po: t.po || '—',
        expectedQty: t.expectedQuantity, receivedQty: t.quantity, shortQty: t.shortQuantity || 0, overQty: t.overQuantity || 0, damagedQty: t.damagedQuantity || 0,
        receivingDate: t.date, receiver: t.enteredBy || '—', notes: t.notes || '—',
      };
    }),
    summaryCards: (rows) => [
      { label: 'Receipts Logged', value: String(rows.length) },
      { label: 'With Shortage', value: String(rows.filter(r => r.shortQty > 0).length) },
      { label: 'With Damage', value: String(rows.filter(r => r.damagedQty > 0).length) },
    ],
  },

  {
    id: 'logisticsDiscrepancies', category: 'logistics', name: 'Shortage / Damage / Discrepancy Report',
    description: 'Missing, short, damaged, or incorrect materials logged against a shipment.',
    canView: canViewLogisticsReport,
    columns: [
      { key: 'claimNumber', label: 'Reference #', type: 'text' }, { key: 'project', label: 'Project', type: 'text' }, { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'vendor', label: 'Vendor', type: 'text' }, { key: 'containerNumber', label: 'Shipment / Container', type: 'text' },
      { key: 'claimType', label: 'Type', type: 'text' }, { key: 'description', label: 'Description', type: 'text' },
      { key: 'responsibleParty', label: 'Responsible Party', type: 'text' }, { key: 'claimAmount', label: 'Claim Amount', type: 'money', financial: true },
      { key: 'replacementRequired', label: 'Replacement Required', type: 'text' }, { key: 'replacementStatus', label: 'Replacement Status', type: 'text' },
      { key: 'replacementEta', label: 'Replacement ETA', type: 'date' }, { key: 'status', label: 'Resolution Status', type: 'text' },
    ],
    filterKeys: ['project', 'claimType', 'status'],
    groupByKeys: ['project', 'claimType', 'status'],
    searchKeys: ['claimNumber', 'project', 'description'],
    getRows: (ctx) => ctx.logisticsClaims.filter(c => ['Shortage', 'Damage', 'Missing', 'Incorrect Material', 'Wrong Finish', 'Wrong Quantity'].includes(c.claimType)).map(c => {
      const project = ctx.projects.find(p => p.id === c.projectId);
      const scope = project ? project.scopes.find(s => s.id === c.scopeId) : null;
      const vendor = ctx.vendors.find(v => v.id === c.vendorId) || ctx.freightForwarders.find(v => v.id === c.vendorId);
      const container = ctx.exportContainers.find(x => x.id === c.containerId) || null;
      return {
        _id: c.id, _onOpen: () => project && ctx.goProjectTab(project.id, 'issues'),
        claimNumber: c.claimNumber, project: project ? project.name : '—', scope: scope ? scope.name : '—',
        vendor: vendor ? vendor.name : '—', containerNumber: container ? container.containerNumber : '—',
        claimType: c.claimType, description: c.description || '—', responsibleParty: c.responsibleParty || '—',
        claimAmount: c.claimAmount, replacementRequired: c.replacementRequired ? 'Yes' : 'No',
        replacementStatus: c.replacementStatus || '—', replacementEta: c.replacementEta, status: c.status,
      };
    }),
    summaryCards: (rows) => [
      { label: 'Open Cases', value: String(rows.filter(r => !['Resolved', 'Denied'].includes(r.status)).length), tone: 'red' },
      { label: 'Total Claim Value', value: fmtMoney(rows.reduce((s, r) => s + r.claimAmount, 0)), financial: true },
      { label: 'Replacements Pending', value: String(rows.filter(r => r.replacementRequired === 'Yes' && r.replacementStatus !== 'Delivered').length) },
    ],
  },

  {
    id: 'deliverySchedule', category: 'logistics', name: 'Delivery Schedule Report',
    description: 'Upcoming, approved deliveries not yet completed.',
    canView: canViewLogisticsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'deliveryNumber', label: 'Delivery #', type: 'text' }, { key: 'date', label: 'Delivery Date', type: 'date' },
      { key: 'areas', label: 'Areas', type: 'text' }, { key: 'quantity', label: 'Quantity', type: 'text' },
      { key: 'siteContact', label: 'Site Contact', type: 'text' }, { key: 'instructions', label: 'Delivery Instructions', type: 'text' }, { key: 'status', label: 'Status', type: 'text' },
    ],
    filterKeys: ['project', 'status'],
    groupByKeys: ['project'],
    searchKeys: ['project', 'scope', 'deliveryNumber'],
    dateFilterKey: 'date',
    getRows: (ctx) => allDeliveries(ctx.projects).filter(d => d.approvalStatus === 'Approved' && d.deliveryStatus !== 'Delivered').map(d => ({
      _id: d.id, _onOpen: () => ctx.goProjectTab(d.projectId, 'delivery', 'scheduled'),
      project: d.projectName, scope: d.scopeName, deliveryNumber: d.deliveryNumber, date: d.date,
      areas: d.areas || '—', quantity: d.quantity ? `${d.quantity} ${d.unit || ''}`.trim() : '—',
      siteContact: d.receiverName || '—', instructions: d.notes || '—', status: d.deliveryStatus,
    })),
    summaryCards: (rows) => [{ label: 'Scheduled Deliveries', value: String(rows.length) }],
  },

  {
    id: 'deliveryHistory', category: 'logistics', name: 'Delivery History & POD Report',
    description: 'Completed deliveries with proof of delivery.',
    canView: canViewLogisticsReport,
    columns: [
      { key: 'date', label: 'Delivery Date', type: 'date' }, { key: 'project', label: 'Project', type: 'text' }, { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'deliveryNumber', label: 'Delivery #', type: 'text' }, { key: 'recipient', label: 'Recipient', type: 'text' },
      { key: 'signedPod', label: 'Signed POD', type: 'text' }, { key: 'notes', label: 'Delivery Notes', type: 'text' },
    ],
    filterKeys: ['project'],
    groupByKeys: ['project'],
    searchKeys: ['project', 'scope', 'deliveryNumber', 'recipient'],
    dateFilterKey: 'date',
    getRows: (ctx) => allDeliveries(ctx.projects).filter(d => d.deliveryStatus === 'Delivered').map(d => ({
      _id: d.id, _onOpen: () => ctx.goProjectTab(d.projectId, 'delivery', 'delivered'),
      date: d.date, project: d.projectName, scope: d.scopeName, deliveryNumber: d.deliveryNumber,
      recipient: d.receiverName || '—', signedPod: d.clientSignatureUrl ? 'On File' : (d.slipFile ? 'On File' : 'Not on File'), notes: d.notes || '—',
    })),
    summaryCards: (rows) => [
      { label: 'Deliveries Completed', value: String(rows.length) },
      { label: 'POD On File', value: String(rows.filter(r => r.signedPod === 'On File').length) },
    ],
  },

  {
    id: 'openDeliveries', category: 'logistics', name: 'Open Deliveries Report',
    description: 'Materials allocated or released but not yet delivered.',
    canView: canViewLogisticsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'scope', label: 'Scope', type: 'text' }, { key: 'material', label: 'Material', type: 'text' },
      { key: 'qtyOpen', label: 'Qty Not Yet Delivered', type: 'number' }, { key: 'releaseStatus', label: 'Release Status', type: 'text' }, { key: 'requiredDate', label: 'Required Date', type: 'date' },
    ],
    filterKeys: ['project', 'releaseStatus'],
    groupByKeys: ['project'],
    searchKeys: ['project', 'scope', 'material'],
    getRows: (ctx) => ctx.materialAllocations.filter(a => a.status !== 'Cancelled' && a.quantityDelivered < a.quantityAllocated).map(a => {
      const project = ctx.projects.find(p => p.id === a.projectId);
      const scope = project ? project.scopes.find(s => s.id === a.scopeId) : null;
      const material = ctx.warehouseMaterials.find(m => m.id === a.materialId);
      return {
        _id: a.id, _onOpen: () => project && ctx.goProjectTab(project.id, 'procurement'),
        project: project ? project.name : '—', scope: scope ? scope.name : '—', material: material ? material.name : '—',
        qtyOpen: a.quantityAllocated - a.quantityDelivered,
        releaseStatus: a.quantityReleased >= a.quantityAllocated ? 'Fully Released' : a.quantityReleased > 0 ? 'Partially Released' : 'Not Released',
        requiredDate: a.requiredDate,
      };
    }),
    summaryCards: (rows) => [{ label: 'Open Delivery Lines', value: String(rows.length) }],
  },

  {
    id: 'freightCostReport', category: 'logistics', name: 'Freight Cost Report',
    description: 'Actual logistics cost per shipment, broken down by cost category.',
    canView: canViewFinReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'scope', label: 'Scope', type: 'text' }, { key: 'containerNumber', label: 'Container', type: 'text' },
      ...LOGISTICS_COST_FIELDS.map(f => ({ key: f.key, label: f.label, type: 'money', financial: true })),
      { key: 'total', label: 'Total', type: 'money', financial: true },
    ],
    filterKeys: ['project'],
    groupByKeys: ['project'],
    searchKeys: ['project', 'scope', 'containerNumber'],
    getRows: (ctx) => allContainers(ctx).map(c => {
      const row = {
        _id: c.id, _onOpen: () => ctx.goProjectTab(c.projectId, 'export'),
        project: c.projectName, scope: c.scopeNames || '—', containerNumber: c.containerNumber,
        total: logisticsCostTotal(c.costs),
      };
      LOGISTICS_COST_FIELDS.forEach(f => { row[f.key] = (c.costs && c.costs[f.key]) || 0; });
      return row;
    }),
    summaryCards: (rows) => [{ label: 'Total Freight Cost', value: fmtMoney(rows.reduce((s, r) => s + r.total, 0)), financial: true }],
  },

  {
    id: 'estVsActualLogisticsCost', category: 'logistics', name: 'Estimated vs Actual Logistics Cost Report',
    description: "Compares each scope's projected profitability logistics costs (Freight/Tariffs/Duties) against actual costs on the same scope.",
    canView: canViewFinReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'estFreight', label: 'Estimated Freight', type: 'money', financial: true }, { key: 'actFreight', label: 'Actual Freight', type: 'money', financial: true },
      { key: 'estTariffs', label: 'Estimated Tariffs/Duties', type: 'money', financial: true }, { key: 'actTariffs', label: 'Actual Tariffs/Duties', type: 'money', financial: true },
      { key: 'estWarehousing', label: 'Estimated Customs/Warehousing', type: 'money', financial: true }, { key: 'actWarehousing', label: 'Actual Customs/Warehousing', type: 'money', financial: true },
      { key: 'estTotal', label: 'Total Estimated', type: 'money', financial: true }, { key: 'actTotal', label: 'Total Actual', type: 'money', financial: true },
      { key: 'variance', label: 'Dollar Variance', type: 'money', financial: true }, { key: 'variancePct', label: '% Variance', type: 'pct', financial: true },
    ],
    filterKeys: ['project'],
    groupByKeys: ['project'],
    searchKeys: ['project', 'scope'],
    getRows: (ctx) => ctx.projects.flatMap(p => p.scopes.filter(s => s.profitability).map(s => {
      const est = s.profitability.costs, act = s.profitability.actual;
      const estFreight = est.oceanFreight + est.domesticFreight, actFreight = act.oceanFreight + act.domesticFreight;
      const estTariffs = est.tariffs + est.dutiesCustoms, actTariffs = act.tariffs + act.dutiesCustoms;
      const estWarehousing = est.warehousing, actWarehousing = act.warehousing;
      const estTotal = estFreight + estTariffs + estWarehousing, actTotal = actFreight + actTariffs + actWarehousing;
      return {
        _id: s.id, _onOpen: () => ctx.goProjectTab(p.id, 'sales', 'profitability'),
        project: p.name, scope: s.name, estFreight, actFreight, estTariffs, actTariffs, estWarehousing, actWarehousing,
        estTotal, actTotal, variance: actTotal - estTotal, variancePct: estTotal ? ((actTotal - estTotal) / estTotal) * 100 : 0,
      };
    })),
    summaryCards: (rows) => [
      { label: 'Total Estimated', value: fmtMoney(rows.reduce((s, r) => s + r.estTotal, 0)), financial: true },
      { label: 'Total Actual', value: fmtMoney(rows.reduce((s, r) => s + r.actTotal, 0)), financial: true },
      { label: 'Total Variance', value: fmtMoney(rows.reduce((s, r) => s + r.variance, 0)), financial: true },
    ],
  },

  {
    id: 'landedCostReport', category: 'logistics', name: 'Landed Cost Report',
    description: 'True landed cost per scope: material cost (from profitability) plus all actual container-level logistics costs assigned to that scope.',
    canView: canViewFinReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'materialCost', label: 'Material Cost', type: 'money', financial: true }, { key: 'logisticsCost', label: 'Freight + Customs + Warehousing + Other', type: 'money', financial: true },
      { key: 'landedCost', label: 'Landed Cost', type: 'money', financial: true }, { key: 'salesValue', label: 'Sales Value', type: 'money', financial: true },
      { key: 'landedMarginPct', label: 'Margin After Landed Cost', type: 'pct', financial: true },
    ],
    filterKeys: ['project'],
    groupByKeys: ['project'],
    searchKeys: ['project', 'scope'],
    getRows: (ctx) => ctx.projects.flatMap(p => p.scopes.filter(s => s.profitability).map(s => {
      const materialCost = s.profitability.actual.vendorCost || s.profitability.costs.vendorCost;
      const containers = containersForProjectScope(ctx.exportContainers, p.id, s.id);
      const logisticsCost = containers.reduce((sum, c) => sum + logisticsCostTotal(c.costs), 0);
      const landedCost = materialCost + logisticsCost;
      const salesValue = s.profitability.salesValue || 0;
      return {
        _id: s.id, _onOpen: () => ctx.goProjectTab(p.id, 'sales', 'profitability'),
        project: p.name, scope: s.name, materialCost, logisticsCost, landedCost, salesValue,
        landedMarginPct: salesValue ? ((salesValue - landedCost) / salesValue) * 100 : 0,
      };
    })),
    summaryCards: (rows) => [
      { label: 'Total Landed Cost', value: fmtMoney(rows.reduce((s, r) => s + r.landedCost, 0)), financial: true },
      { label: 'Total Sales Value', value: fmtMoney(rows.reduce((s, r) => s + r.salesValue, 0)), financial: true },
    ],
  },

  {
    id: 'tariffDutyReport', category: 'logistics', name: 'Tariff & Duty Report',
    description: 'Duties and tariffs paid per shipment, by country of origin.',
    canView: canViewFinReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'countryOfOrigin', label: 'Country of Origin', type: 'text' }, { key: 'containerNumber', label: 'Container', type: 'text' },
      { key: 'tariffAmount', label: 'Tariff Amount', type: 'money', financial: true }, { key: 'dutyAmount', label: 'Duty Amount', type: 'money', financial: true },
      { key: 'totalPaid', label: 'Total Paid', type: 'money', financial: true },
    ],
    filterKeys: ['project', 'countryOfOrigin'],
    groupByKeys: ['countryOfOrigin', 'project'],
    searchKeys: ['project', 'scope', 'containerNumber'],
    getRows: (ctx) => allContainers(ctx).filter(c => (c.costs && (c.costs.tariffs || c.costs.duties)) || c.countryOfOrigin).map(c => ({
      _id: c.id, _onOpen: () => ctx.goProjectTab(c.projectId, 'export'),
      project: c.projectName, scope: c.scopeNames || '—', countryOfOrigin: c.countryOfOrigin || '—', containerNumber: c.containerNumber,
      tariffAmount: (c.costs && c.costs.tariffs) || 0, dutyAmount: (c.costs && c.costs.duties) || 0,
      totalPaid: ((c.costs && c.costs.tariffs) || 0) + ((c.costs && c.costs.duties) || 0),
    })),
    summaryCards: (rows) => [{ label: 'Total Tariffs + Duties Paid', value: fmtMoney(rows.reduce((s, r) => s + r.totalPaid, 0)), financial: true }],
  },

  {
    id: 'demurrageReport', category: 'logistics', name: 'Demurrage / Detention / Storage Report',
    description: 'Avoidable logistics charges — free-time expirations and actual pickup/return dates, tracked separately from planned freight cost.',
    canView: canViewFinReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'containerNumber', label: 'Container', type: 'text' },
      { key: 'freeTimeExpiration', label: 'Free-Time Expiration', type: 'date' }, { key: 'pickupDeadline', label: 'Pickup Deadline', type: 'date' },
      { key: 'emptyReturnDeadline', label: 'Empty Return Deadline', type: 'date' }, { key: 'actualPickupDate', label: 'Actual Pickup Date', type: 'date' },
      { key: 'actualReturnDate', label: 'Actual Return Date', type: 'date' }, { key: 'demurrage', label: 'Demurrage', type: 'money', financial: true },
      { key: 'detention', label: 'Detention', type: 'money', financial: true }, { key: 'storage', label: 'Storage', type: 'money', financial: true },
      { key: 'responsible', label: 'Responsible Party', type: 'text' },
    ],
    filterKeys: ['project'],
    groupByKeys: ['project'],
    searchKeys: ['project', 'containerNumber'],
    getRows: (ctx) => allContainers(ctx).filter(c => (c.costs && (c.costs.demurrage || c.costs.detention || c.costs.storage)) || c.freeTimeExpiration || c.pickupDeadline).map(c => ({
      _id: c.id, _onOpen: () => ctx.goProjectTab(c.projectId, 'export'),
      project: c.projectName, containerNumber: c.containerNumber,
      freeTimeExpiration: c.freeTimeExpiration, pickupDeadline: c.pickupDeadline, emptyReturnDeadline: c.emptyReturnDeadline,
      actualPickupDate: c.actualPickupDate, actualReturnDate: c.actualReturnDate,
      demurrage: (c.costs && c.costs.demurrage) || 0, detention: (c.costs && c.costs.detention) || 0, storage: (c.costs && c.costs.storage) || 0,
      responsible: (c.assigneeIds && c.assigneeIds.length) ? c.assigneeIds.map(id => personName(ctx.teamDirectory, id)).join(', ') : '—',
    })),
    summaryCards: (rows) => [
      { label: 'Total Demurrage', value: fmtMoney(rows.reduce((s, r) => s + r.demurrage, 0)), financial: true, tone: 'red' },
      { label: 'Total Detention', value: fmtMoney(rows.reduce((s, r) => s + r.detention, 0)), financial: true, tone: 'red' },
      { label: 'Total Storage', value: fmtMoney(rows.reduce((s, r) => s + r.storage, 0)), financial: true },
    ],
  },

  {
    id: 'exportDocsCompleteness', category: 'logistics', name: 'Import / Export Documentation Report',
    description: 'Whether each shipment has all 11 required export documents on file.',
    canView: canViewLogisticsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'containerNumber', label: 'Container', type: 'text' },
      ...EXPORT_WORKFLOW_STEPS.map(s => ({ key: s.key, label: s.name, type: 'text' })),
      { key: 'missingCount', label: 'Missing Documents', type: 'number' }, { key: 'complete', label: 'Complete', type: 'text' },
    ],
    filterKeys: ['project', 'complete'],
    groupByKeys: ['project'],
    searchKeys: ['project', 'containerNumber'],
    getRows: (ctx) => allContainers(ctx).map(c => {
      const row = { _id: c.id, _onOpen: () => ctx.goProjectTab(c.projectId, 'export'), project: c.projectName, containerNumber: c.containerNumber };
      let missing = 0;
      EXPORT_WORKFLOW_STEPS.forEach(s => {
        const onFile = !!(c.documents[s.key] && c.documents[s.key].fileUrl);
        if (!onFile) missing += 1;
        row[s.key] = onFile ? 'On File' : 'Missing';
      });
      row.missingCount = missing;
      row.complete = missing === 0 ? 'Yes' : 'No';
      return row;
    }),
    summaryCards: (rows) => [
      { label: 'Fully Documented', value: String(rows.filter(r => r.complete === 'Yes').length), filterKey: 'complete', filterValue: 'Yes', tone: 'green' },
      { label: 'Missing Documents', value: String(rows.filter(r => r.complete === 'No').length), filterKey: 'complete', filterValue: 'No', tone: 'red' },
    ],
  },

  {
    id: 'logisticsClaimsReport', category: 'logistics', name: 'Logistics Claims Report',
    description: 'All freight, damage, and shortage claims — including pure freight claims not tied to a physical discrepancy.',
    canView: canViewFinReport,
    columns: [
      { key: 'claimNumber', label: 'Claim Number', type: 'text' }, { key: 'project', label: 'Project', type: 'text' },
      { key: 'containerNumber', label: 'Shipment / Container', type: 'text' }, { key: 'vendor', label: 'Vendor', type: 'text' },
      { key: 'carrier', label: 'Carrier', type: 'text' }, { key: 'forwarder', label: 'Freight Forwarder', type: 'text' },
      { key: 'claimType', label: 'Claim Type', type: 'text' }, { key: 'claimAmount', label: 'Claim Amount', type: 'money', financial: true },
      { key: 'amountRecovered', label: 'Amount Recovered', type: 'money', financial: true }, { key: 'dateSubmitted', label: 'Date Submitted', type: 'date' },
      { key: 'status', label: 'Claim Status', type: 'text' }, { key: 'agingDays', label: 'Aging (Days)', type: 'number' },
    ],
    filterKeys: ['project', 'claimType', 'status'],
    groupByKeys: ['project', 'status'],
    searchKeys: ['claimNumber', 'project'],
    getRows: (ctx) => ctx.logisticsClaims.map(c => {
      const project = ctx.projects.find(p => p.id === c.projectId);
      const container = ctx.exportContainers.find(x => x.id === c.containerId) || null;
      const vendor = ctx.vendors.find(v => v.id === c.vendorId);
      const carrier = ctx.freightForwarders.find(f => f.id === c.carrierId);
      return {
        _id: c.id, _onOpen: () => project && ctx.goProjectTab(project.id, 'issues'),
        claimNumber: c.claimNumber, project: project ? project.name : '—', containerNumber: container ? container.containerNumber : '—',
        vendor: vendor ? vendor.name : '—', carrier: carrier ? carrier.name : '—', forwarder: carrier ? carrier.name : '—',
        claimType: c.claimType, claimAmount: c.claimAmount, amountRecovered: c.amountRecovered,
        dateSubmitted: c.dateSubmitted, status: c.status,
        agingDays: ['Resolved', 'Denied'].includes(c.status) ? 0 : daysBetween(c.dateSubmitted, todayISO()),
      };
    }),
    summaryCards: (rows) => [
      { label: 'Open Claims', value: String(rows.filter(r => !['Resolved', 'Denied'].includes(r.status)).length) },
      { label: 'Total Claimed', value: fmtMoney(rows.reduce((s, r) => s + r.claimAmount, 0)), financial: true },
      { label: 'Total Recovered', value: fmtMoney(rows.reduce((s, r) => s + r.amountRecovered, 0)), financial: true },
    ],
  },

  {
    id: 'logisticsCostByProject', category: 'logistics', name: 'Logistics Cost by Project Report',
    description: 'Executive financial summary — total logistics cost per project, and as a % of contract value.',
    canView: canViewFinReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'materialCost', label: 'Material Cost', type: 'money', financial: true },
      { key: 'oceanFreight', label: 'Ocean Freight', type: 'money', financial: true }, { key: 'airFreight', label: 'Air Freight', type: 'money', financial: true },
      { key: 'trucking', label: 'Trucking', type: 'money', financial: true }, { key: 'tariffs', label: 'Tariffs', type: 'money', financial: true },
      { key: 'duties', label: 'Duties', type: 'money', financial: true }, { key: 'customs', label: 'Customs', type: 'money', financial: true },
      { key: 'storage', label: 'Storage', type: 'money', financial: true }, { key: 'demurrage', label: 'Demurrage', type: 'money', financial: true },
      { key: 'detention', label: 'Detention', type: 'money', financial: true }, { key: 'warehouseCosts', label: 'Warehouse Costs', type: 'money', financial: true },
      { key: 'other', label: 'Other Logistics Costs', type: 'money', financial: true }, { key: 'totalLogisticsCost', label: 'Total Logistics Cost', type: 'money', financial: true },
      { key: 'contractValue', label: 'Contract Value', type: 'money', financial: true }, { key: 'logisticsPct', label: 'Logistics % of Contract Value', type: 'pct', financial: true },
    ],
    filterKeys: [],
    groupByKeys: [],
    searchKeys: ['project'],
    getRows: (ctx) => ctx.projects.map(p => {
      const containers = containersForProject(ctx.exportContainers, p.id);
      const sum = key => containers.reduce((s, c) => s + ((c.costs && c.costs[key]) || 0), 0);
      const materialCost = p.scopes.reduce((s, sc) => s + (sc.profitability ? (sc.profitability.actual.vendorCost || sc.profitability.costs.vendorCost) : 0), 0);
      const totalLogisticsCost = LOGISTICS_COST_FIELDS.reduce((s, f) => s + sum(f.key), 0);
      const contractValue = revisedContractValue(p);
      return {
        _id: p.id, _onOpen: () => ctx.goProjectTab(p.id, 'export'),
        project: p.name, materialCost,
        oceanFreight: sum('oceanFreight'), airFreight: sum('airFreight'), trucking: sum('inlandTrucking') + sum('drayage'),
        tariffs: sum('tariffs'), duties: sum('duties'), customs: sum('customsBrokerage'),
        storage: sum('storage'), demurrage: sum('demurrage'), detention: sum('detention'),
        warehouseCosts: sum('warehouseHandling'), other: sum('insurance') + sum('other'),
        totalLogisticsCost, contractValue, logisticsPct: contractValue ? (totalLogisticsCost / contractValue) * 100 : 0,
      };
    }),
    summaryCards: (rows) => [
      { label: 'Total Logistics Cost (All Projects)', value: fmtMoney(rows.reduce((s, r) => s + r.totalLogisticsCost, 0)), financial: true },
      { label: 'Avg Logistics % of Contract', value: rows.length ? fmtPct(rows.reduce((s, r) => s + r.logisticsPct, 0) / rows.length) : '—', financial: true },
    ],
  },

  {
    id: 'forwarderPerformance', category: 'logistics', name: 'Freight Forwarder / Carrier Performance Report',
    description: 'Shipment volume, on-time performance, and cost by freight forwarder.',
    canView: canViewLogisticsReport,
    columns: [
      { key: 'freightCo', label: 'Freight Forwarder', type: 'text' }, { key: 'shipmentCount', label: 'Shipments', type: 'number' },
      { key: 'avgTransitDays', label: 'Avg Transit Time (Days)', type: 'number' }, { key: 'onTimePct', label: 'On-Time %', type: 'pct' },
      { key: 'avgDelayDays', label: 'Avg Delay (Days)', type: 'number' }, { key: 'claimCount', label: 'Claims', type: 'number' },
      { key: 'freightCost', label: 'Freight Cost', type: 'money', financial: true }, { key: 'demurrageDetention', label: 'Demurrage/Detention', type: 'money', financial: true },
    ],
    filterKeys: [],
    groupByKeys: [],
    searchKeys: ['freightCo'],
    getRows: (ctx) => {
      const containers = allContainers(ctx);
      return ctx.freightForwarders.map(fwd => {
        const shipments = containers.filter(c => c.freightCompanyId === fwd.id);
        const withEta = shipments.filter(c => c.eta && c.etd);
        const avgTransitDays = withEta.length ? withEta.reduce((s, c) => s + daysBetween(c.etd, c.eta), 0) / withEta.length : 0;
        const completed = shipments.filter(c => c.actualArrival && c.eta);
        const onTime = completed.filter(c => c.actualArrival <= c.eta);
        const late = completed.filter(c => c.actualArrival > c.eta);
        const claims = ctx.logisticsClaims.filter(c => c.carrierId === fwd.id);
        return {
          _id: fwd.id, _onOpen: () => ctx.goVendorDetail(fwd.id, 'forwarder'),
          freightCo: fwd.name, shipmentCount: shipments.length, avgTransitDays: Math.round(avgTransitDays),
          onTimePct: completed.length ? (onTime.length / completed.length) * 100 : 0,
          avgDelayDays: late.length ? late.reduce((s, c) => s + daysBetween(c.eta, c.actualArrival), 0) / late.length : 0,
          claimCount: claims.length,
          freightCost: shipments.reduce((s, c) => s + logisticsCostTotal(c.costs), 0),
          demurrageDetention: shipments.reduce((s, c) => s + ((c.costs && c.costs.demurrage) || 0) + ((c.costs && c.costs.detention) || 0), 0),
        };
      }).filter(r => r.shipmentCount > 0);
    },
    summaryCards: (rows) => [{ label: 'Forwarders Tracked', value: String(rows.length) }],
  },

  {
    id: 'vendorShippingPerformance', category: 'logistics', name: 'Vendor Shipping Performance Report',
    description: 'Shipping-related discrepancies and claims by vendor — shortages, damages, and replacement shipments.',
    canView: canViewLogisticsReport,
    columns: [
      { key: 'vendor', label: 'Vendor', type: 'text' }, { key: 'shortages', label: 'Shortages', type: 'number' },
      { key: 'damages', label: 'Damages', type: 'number' }, { key: 'documentationErrors', label: 'Other Discrepancies', type: 'number' },
      { key: 'claimCount', label: 'Total Claims', type: 'number' }, { key: 'replacementShipments', label: 'Replacement Shipments', type: 'number' },
      { key: 'totalClaimAmount', label: 'Total Claim Amount', type: 'money', financial: true },
    ],
    filterKeys: [],
    groupByKeys: [],
    searchKeys: ['vendor'],
    getRows: (ctx) => ctx.vendors.map(v => {
      const claims = ctx.logisticsClaims.filter(c => c.vendorId === v.id);
      return {
        _id: v.id, _onOpen: () => ctx.goVendorDetail(v.id, 'vendor'),
        vendor: v.name, shortages: claims.filter(c => c.claimType === 'Shortage').length, damages: claims.filter(c => c.claimType === 'Damage').length,
        documentationErrors: claims.filter(c => !['Shortage', 'Damage'].includes(c.claimType)).length,
        claimCount: claims.length, replacementShipments: claims.filter(c => c.replacementStatus === 'Delivered').length,
        totalClaimAmount: claims.reduce((s, c) => s + c.claimAmount, 0),
      };
    }).filter(r => r.claimCount > 0),
    summaryCards: (rows) => [{ label: 'Vendors With Claims', value: String(rows.length) }],
  },

  // ==== TRADE COMPLIANCE ====================================================
  // All read from the same ctx.tariffLines / ctx.tariffLibrary the Trade
  // Compliance & Tariffs module and the Export tab's per-container "Tariffs
  // & Customs" section use — no parallel tariff data here.
  {
    id: 'tariffLinesMaster', category: 'tariff', name: 'Tariff Lines Master Report',
    description: 'Every tariff/classification line, across every project, scope, vendor, and shipment.',
    canView: canViewTradeComplianceReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'scope', label: 'Scope', type: 'text' }, { key: 'vendor', label: 'Vendor', type: 'text' },
      { key: 'exportNumber', label: 'Export Number', type: 'text' }, { key: 'product', label: 'Product / Material', type: 'text' },
      { key: 'countryOfOrigin', label: 'Country of Origin', type: 'text' }, { key: 'htsCode', label: 'HTS Code', type: 'text' }, { key: 'hsCode', label: 'HS Code', type: 'text' },
      { key: 'customsValue', label: 'Customs Value', type: 'money', financial: true }, { key: 'applicableTariffPct', label: 'Applicable Tariff %', type: 'pct' },
      { key: 'estimatedTariff', label: 'Estimated Tariff', type: 'money', financial: true }, { key: 'actualTariff', label: 'Actual Tariff', type: 'money', financial: true },
      { key: 'variance', label: 'Variance', type: 'money', financial: true }, { key: 'customsEntryNumber', label: 'Customs Entry Number', type: 'text' },
      { key: 'entryDate', label: 'Entry Date', type: 'date' }, { key: 'broker', label: 'Broker', type: 'text' }, { key: 'status', label: 'Status', type: 'text' },
    ],
    filterKeys: ['project', 'countryOfOrigin', 'status', 'broker'],
    groupByKeys: ['project', 'scope', 'status'],
    searchKeys: ['project', 'scope', 'vendor', 'exportNumber', 'htsCode', 'hsCode', 'product'],
    dateFilterKey: 'entryDate',
    getRows: (ctx) => enrichedTariffLines(ctx).map(l => ({
      _id: l.id, _onOpen: () => ctx.goProjectTab(l.projectId, 'export'),
      project: l.projectName, scope: l.scopeName, vendor: l.vendorName, exportNumber: l.containerNumber,
      product: l.productDescription || '—', countryOfOrigin: l.countryOfOrigin || '—', htsCode: l.htsCode || '—', hsCode: l.hsCode || '—',
      customsValue: l.customsValue, applicableTariffPct: l.applicableTariffPct, estimatedTariff: l.estimatedTariff,
      actualTariff: l.actualTariff, variance: l.varianceAmount,
      customsEntryNumber: l.actual ? l.actual.customsEntryNumber : '—', entryDate: l.actual ? l.actual.entryDate : null,
      broker: l.brokerName, status: l.status,
    })),
    summaryCards: (rows) => [
      { label: 'Lines Tracked', value: String(rows.length) },
      { label: 'Total Estimated Tariff', value: fmtMoney(rows.reduce((s, r) => s + r.estimatedTariff, 0)), financial: true },
      { label: 'Total Actual Tariff', value: fmtMoney(rows.filter(r => r.actualTariff !== null).reduce((s, r) => s + r.actualTariff, 0)), financial: true },
    ],
  },

  {
    id: 'tariffByProject', category: 'tariff', name: 'Tariffs by Project',
    description: 'Estimated and actual tariff cost rolled up per project.',
    canView: canViewTradeComplianceReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'lineCount', label: 'Tariff Lines', type: 'number' },
      { key: 'customsValue', label: 'Total Customs Value', type: 'money', financial: true }, { key: 'estimatedTariff', label: 'Estimated Tariff', type: 'money', financial: true },
      { key: 'actualTariff', label: 'Actual Tariff', type: 'money', financial: true }, { key: 'contractValue', label: 'Contract Value', type: 'money', financial: true },
      { key: 'tariffPctOfContract', label: 'Tariff % of Contract Value', type: 'pct', financial: true },
    ],
    filterKeys: [], groupByKeys: [], searchKeys: ['project'],
    getRows: (ctx) => ctx.projects.map(p => {
      const lines = ctx.tariffLines.filter(l => l.projectId === p.id);
      const estimatedTariff = lines.reduce((s, l) => s + l.estimatedTariff, 0);
      const actualTariff = lines.filter(l => l.actualTariff !== null).reduce((s, l) => s + l.actualTariff, 0);
      const contractValue = revisedContractValue(p);
      return {
        _id: p.id, _onOpen: () => ctx.goProjectTab(p.id, 'export'),
        project: p.name, lineCount: lines.length, customsValue: lines.reduce((s, l) => s + l.customsValue, 0),
        estimatedTariff, actualTariff, contractValue, tariffPctOfContract: contractValue ? (actualTariff / contractValue) * 100 : 0,
      };
    }).filter(r => r.lineCount > 0),
    summaryCards: (rows) => [{ label: 'Total Actual Tariff (All Projects)', value: fmtMoney(rows.reduce((s, r) => s + r.actualTariff, 0)), financial: true }],
  },

  {
    id: 'tariffByScope', category: 'tariff', name: 'Tariffs by Scope',
    description: 'Estimated and actual tariff cost rolled up per project scope — the same allocation figures feeding that scope\'s Profitability.',
    canView: canViewTradeComplianceReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'scope', label: 'Scope', type: 'text' }, { key: 'lineCount', label: 'Tariff Lines', type: 'number' },
      { key: 'estimatedTariff', label: 'Estimated Tariff', type: 'money', financial: true }, { key: 'actualTariff', label: 'Actual Tariff', type: 'money', financial: true },
      { key: 'materialCost', label: 'Material Cost', type: 'money', financial: true }, { key: 'tariffPctOfMaterial', label: 'Tariff % of Material Cost', type: 'pct', financial: true },
    ],
    filterKeys: ['project'], groupByKeys: ['project'], searchKeys: ['project', 'scope'],
    getRows: (ctx) => ctx.projects.flatMap(p => p.scopes.map(s => {
      const lines = ctx.tariffLines.filter(l => l.projectId === p.id && l.scopeId === s.id);
      if (!lines.length) return null;
      const estimatedTariff = lines.reduce((s2, l) => s2 + l.estimatedTariff, 0);
      const actualTariff = lines.filter(l => l.actualTariff !== null).reduce((s2, l) => s2 + l.actualTariff, 0);
      const materialCost = s.profitability ? (s.profitability.actual.vendorCost || s.profitability.costs.vendorCost) : 0;
      return {
        _id: s.id, _onOpen: () => ctx.goProjectTab(p.id, 'sales', 'profitability'),
        project: p.name, scope: s.name, lineCount: lines.length, estimatedTariff, actualTariff,
        materialCost, tariffPctOfMaterial: materialCost ? (actualTariff / materialCost) * 100 : 0,
      };
    }).filter(Boolean)),
    summaryCards: (rows) => [{ label: 'Total Actual Tariff (All Scopes)', value: fmtMoney(rows.reduce((s, r) => s + r.actualTariff, 0)), financial: true }],
  },

  {
    id: 'tariffByVendorCountry', category: 'tariff', name: 'Tariffs by Vendor & Country of Origin',
    description: 'Tariff exposure rolled up by vendor and by country of origin.',
    canView: canViewTradeComplianceReport,
    columns: [
      { key: 'vendor', label: 'Vendor', type: 'text' }, { key: 'countryOfOrigin', label: 'Country of Origin', type: 'text' },
      { key: 'lineCount', label: 'Tariff Lines', type: 'number' }, { key: 'estimatedTariff', label: 'Estimated Tariff', type: 'money', financial: true },
      { key: 'actualTariff', label: 'Actual Tariff', type: 'money', financial: true },
    ],
    filterKeys: ['vendor', 'countryOfOrigin'], groupByKeys: ['vendor', 'countryOfOrigin'], searchKeys: ['vendor', 'countryOfOrigin'],
    getRows: (ctx) => enrichedTariffLines(ctx).map(l => ({
      _id: l.id, _onOpen: () => ctx.goProjectTab(l.projectId, 'export'),
      vendor: l.vendorName, countryOfOrigin: l.countryOfOrigin || '—', lineCount: 1,
      estimatedTariff: l.estimatedTariff, actualTariff: l.actualTariff || 0,
    })),
    summaryCards: (rows) => [{ label: 'Total Estimated Tariff', value: fmtMoney(rows.reduce((s, r) => s + r.estimatedTariff, 0)), financial: true }],
  },

  {
    id: 'tariffByHts', category: 'tariff', name: 'Tariffs by HTS Classification',
    description: 'Tariff exposure rolled up by HTS/HS classification, with the current library rate for reference.',
    canView: canViewTradeComplianceReport,
    columns: [
      { key: 'htsCode', label: 'HTS / HS Code', type: 'text' }, { key: 'productDescription', label: 'Product Description', type: 'text' },
      { key: 'currentRate', label: 'Current Library Rate %', type: 'pct' }, { key: 'lineCount', label: 'Tariff Lines', type: 'number' },
      { key: 'estimatedTariff', label: 'Estimated Tariff', type: 'money', financial: true }, { key: 'actualTariff', label: 'Actual Tariff', type: 'money', financial: true },
    ],
    filterKeys: [], groupByKeys: [], searchKeys: ['htsCode', 'productDescription'],
    getRows: (ctx) => ctx.tariffLibrary.map(cls => {
      const lines = ctx.tariffLines.filter(l => l.tariffClassificationId === cls.id);
      const current = currentTariffVersion(cls);
      return {
        _id: cls.id, _onOpen: () => ctx.goTradeCompliance('library'),
        htsCode: cls.htsCode || cls.hsCode || '—', productDescription: cls.productDescription,
        currentRate: current ? current.totalEstimatedDutyPct : 0, lineCount: lines.length,
        estimatedTariff: lines.reduce((s, l) => s + l.estimatedTariff, 0),
        actualTariff: lines.filter(l => l.actualTariff !== null).reduce((s, l) => s + l.actualTariff, 0),
      };
    }),
    summaryCards: (rows) => [{ label: 'Classifications', value: String(rows.length) }],
  },

  {
    id: 'tariffVarianceReport', category: 'tariff', name: 'Estimated vs Actual Tariff Variance Report',
    description: 'Cleared tariff lines only — how estimating accuracy compares to what customs actually charged.',
    canView: canViewTradeComplianceReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'scope', label: 'Scope', type: 'text' }, { key: 'htsCode', label: 'HTS/HS', type: 'text' },
      { key: 'estimatedTariff', label: 'Estimated Tariff', type: 'money', financial: true }, { key: 'actualTariff', label: 'Actual Tariff', type: 'money', financial: true },
      { key: 'variance', label: 'Variance $', type: 'money', financial: true }, { key: 'variancePct', label: 'Variance %', type: 'pct', financial: true },
      { key: 'entryDate', label: 'Entry Date', type: 'date' },
    ],
    filterKeys: ['project'], groupByKeys: ['project'], searchKeys: ['project', 'scope', 'htsCode'],
    dateFilterKey: 'entryDate',
    getRows: (ctx) => enrichedTariffLines(ctx).filter(l => l.status === 'Cleared').map(l => ({
      _id: l.id, _onOpen: () => ctx.goProjectTab(l.projectId, 'export'),
      project: l.projectName, scope: l.scopeName, htsCode: l.htsCode || l.hsCode || '—',
      estimatedTariff: l.estimatedTariff, actualTariff: l.actualTariff, variance: l.varianceAmount, variancePct: l.variancePct,
      entryDate: l.actual ? l.actual.entryDate : null,
    })),
    summaryCards: (rows) => [
      { label: 'Total Variance', value: fmtMoney(rows.reduce((s, r) => s + (r.variance || 0), 0)), financial: true },
      { label: 'Avg Variance %', value: rows.length ? fmtPct(rows.reduce((s, r) => s + (r.variancePct || 0), 0) / rows.length) : '—', financial: true },
    ],
  },

  {
    id: 'tariffExposureReport', category: 'tariff', name: 'Tariff Exposure Report',
    description: 'Open (not yet cleared) tariff lines — procurement placed but not yet imported.',
    canView: canViewTradeComplianceReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' }, { key: 'scope', label: 'Scope', type: 'text' }, { key: 'vendor', label: 'Vendor', type: 'text' },
      { key: 'htsCode', label: 'HTS/HS', type: 'text' }, { key: 'customsValue', label: 'Customs Value', type: 'money', financial: true },
      { key: 'applicableTariffPct', label: 'Rate %', type: 'pct' }, { key: 'estimatedTariff', label: 'Estimated Tariff Exposure', type: 'money', financial: true }, { key: 'status', label: 'Status', type: 'text' },
    ],
    filterKeys: ['project', 'status'], groupByKeys: ['project'], searchKeys: ['project', 'scope', 'vendor', 'htsCode'],
    getRows: (ctx) => enrichedTariffLines(ctx).filter(l => l.status !== 'Cleared').map(l => ({
      _id: l.id, _onOpen: () => ctx.goProjectTab(l.projectId, 'export'),
      project: l.projectName, scope: l.scopeName, vendor: l.vendorName, htsCode: l.htsCode || l.hsCode || '—',
      customsValue: l.customsValue, applicableTariffPct: l.applicableTariffPct, estimatedTariff: l.estimatedTariff, status: l.status,
    })),
    summaryCards: (rows) => [
      { label: 'Open Lines', value: String(rows.length) },
      { label: 'Total Exposure', value: fmtMoney(rows.reduce((s, r) => s + r.estimatedTariff, 0)), financial: true, tone: 'red' },
    ],
  },

  {
    id: 'tariffLibraryReport', category: 'tariff', name: 'Tariff Classification Library Report',
    description: 'Every tracked HTS/HS classification, current rate, and how many tariff lines reference it.',
    canView: canViewTradeComplianceReport,
    columns: [
      { key: 'htsCode', label: 'HTS/HS Code', type: 'text' }, { key: 'productDescription', label: 'Product Description', type: 'text' },
      { key: 'category', label: 'Category', type: 'text' }, { key: 'countryOfOrigin', label: 'Country of Origin', type: 'text' },
      { key: 'currentRate', label: 'Current Total Duty %', type: 'pct' }, { key: 'status', label: 'Status', type: 'text' },
      { key: 'effectiveFrom', label: 'Effective From', type: 'date' }, { key: 'revisionCount', label: 'Revisions', type: 'number' },
      { key: 'lineCount', label: 'Lines Using This', type: 'number' }, { key: 'lastVerified', label: 'Last Verified', type: 'date' },
    ],
    filterKeys: ['status', 'countryOfOrigin'], groupByKeys: ['status'], searchKeys: ['htsCode', 'productDescription', 'category'],
    getRows: (ctx) => ctx.tariffLibrary.map(cls => {
      const current = currentTariffVersion(cls);
      return {
        _id: cls.id, _onOpen: () => ctx.goTradeCompliance('library'),
        htsCode: cls.htsCode || cls.hsCode || '—', productDescription: cls.productDescription, category: cls.materialCategory,
        countryOfOrigin: cls.countryOfOrigin || '—', currentRate: current ? current.totalEstimatedDutyPct : 0, status: current ? current.status : '—',
        effectiveFrom: current ? current.effectiveFrom : null, revisionCount: cls.versions.length,
        lineCount: ctx.tariffLines.filter(l => l.tariffClassificationId === cls.id).length,
        lastVerified: current ? current.lastVerifiedDate : null,
      };
    }),
    summaryCards: (rows) => [{ label: 'Classifications Tracked', value: String(rows.length) }],
  },

  // ==== Added after the cash-planning and permissions work — each of these
  // covers something the app now tracks but had no report for. ==============

  {
    id: 'bankHolds', category: 'financial', name: 'Bank Holds & Release Schedule',
    description: 'Client payments the bank is holding, and when each phase is due to be released. A held receipt is money you have been paid but cannot yet spend.',
    canView: canViewFinReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'client', label: 'Client', type: 'text' },
      { key: 'source', label: 'Against', type: 'text' },
      { key: 'bank', label: 'Held By', type: 'text' },
      { key: 'receivedDate', label: 'Client Paid', type: 'date' },
      { key: 'phase', label: 'Phase', type: 'text' },
      { key: 'plannedDate', label: 'Planned Release', type: 'date' },
      { key: 'releasedDate', label: 'Actually Released', type: 'date' },
      { key: 'amount', label: 'Phase Amount', type: 'money', financial: true },
      { key: 'releasedAmount', label: 'Released', type: 'money', financial: true },
      { key: 'state', label: 'State', type: 'text' },
      { key: 'daysLate', label: 'Days Late', type: 'text' },
    ],
    filterKeys: ['state', 'bank', 'client'],
    groupByKeys: ['project', 'state', 'bank'],
    searchKeys: ['project', 'client', 'phase', 'bank'],
    dateFilterKey: 'plannedDate',
    getRows: (ctx) => {
      const today = todayISO();
      const rows = [];
      ctx.projects.forEach(p => {
        const account = ctx.accounts.find(a => a.id === p.accountId);
        const held = [
          ...(p.paymentTerms || []).filter(isBankHeld).map(t => ({ rec: t, label: t.label || 'Payment' })),
          ...(p.paymentRequisitions || []).filter(isBankHeld).map(r => ({ rec: r, label: `Requisition R${r.revision}` })),
        ];
        held.forEach(({ rec, label }) => {
          (rec.bankHold.releases || []).forEach((rel, i) => {
            const done = !!rel.releasedDate;
            const due = done ? null : daysBetween(today, rel.plannedDate || today);
            rows.push({
              _id: rel.id, _onOpen: () => ctx.goProjectTab(p.id, 'financials'),
              project: p.name, client: account ? account.name : '—',
              source: label, bank: rec.bankHold.bankName || '—',
              receivedDate: rec.receivedDate,
              phase: rel.name || `Release ${i + 1}`,
              plannedDate: rel.plannedDate || null,
              releasedDate: rel.releasedDate || null,
              amount: Number(rel.amount) || 0,
              releasedAmount: done ? (rel.releasedAmount != null ? Number(rel.releasedAmount) : Number(rel.amount) || 0) : 0,
              state: done ? 'Released' : (due < 0 ? 'Overdue' : due <= 7 ? 'Due Soon' : 'Scheduled'),
              daysLate: done || due >= 0 ? '—' : String(Math.abs(due)),
            });
          });
        });
      });
      return rows;
    },
    summaryCards: (rows) => [
      { label: 'Still Held', value: fmtMoney(rows.filter(r => r.state !== 'Released').reduce((s, r) => s + r.amount, 0)), financial: true },
      { label: 'Released to Date', value: fmtMoney(rows.reduce((s, r) => s + r.releasedAmount, 0)), financial: true },
      { label: 'Overdue Releases', value: String(rows.filter(r => r.state === 'Overdue').length), filterKey: 'state', filterValue: 'Overdue' },
      { label: 'Due Within 7 Days', value: String(rows.filter(r => r.state === 'Due Soon').length), filterKey: 'state', filterValue: 'Due Soon' },
    ],
  },

  {
    id: 'dateChanges', category: 'financial', name: 'Payment Date Change Log',
    description: 'Every time a forecast payment date was moved — what it was, what it became, who moved it and why. A date that has slipped three times is the point of this report.',
    canView: canViewFinReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'direction', label: 'In / Out', type: 'text' },
      { key: 'item', label: 'Item', type: 'text' },
      { key: 'from', label: 'Moved From', type: 'date' },
      { key: 'to', label: 'Moved To', type: 'date' },
      { key: 'slipDays', label: 'Slip (days)', type: 'text' },
      { key: 'moves', label: 'Times Moved', type: 'text' },
      { key: 'reason', label: 'Reason Given', type: 'text' },
      { key: 'by', label: 'Moved By', type: 'text' },
      { key: 'when', label: 'Logged', type: 'date' },
    ],
    filterKeys: ['direction', 'by', 'project'],
    groupByKeys: ['project', 'direction', 'by'],
    searchKeys: ['project', 'item', 'reason', 'by'],
    dateFilterKey: 'when',
    getRows: (ctx) => {
      const rows = [];
      function push(p, direction, item, hist) {
        (hist || []).forEach(h => rows.push({
          _id: `${item}-${h.date}-${h.to}`, _onOpen: () => ctx.goProjectTab(p.id, 'financials'),
          project: p.name, direction, item,
          from: h.from, to: h.to,
          slipDays: h.from && h.to ? String(daysBetween(h.from, h.to)) : '—',
          moves: String(hist.length),
          reason: h.reason || '— none given —', by: h.by || '—', when: h.date,
        }));
      }
      ctx.projects.forEach(p => {
        (p.paymentTerms || []).forEach(t => push(p, 'Money In', t.label || 'Payment', t.expectedDateHistory));
        (p.apInvoices || []).forEach(inv => push(p, 'Money Out', `${inv.vendorName || ''} ${inv.invoiceNumber}`.trim(), inv.dueDateHistory));
        const holders = [...(p.paymentTerms || []), ...(p.paymentRequisitions || [])].filter(isBankHeld);
        holders.forEach(rec => (rec.bankHold.releases || []).forEach(rel =>
          push(p, 'Money In', `Bank release — ${rel.name || 'release'}`, rel.plannedDateHistory)));
      });
      return rows;
    },
    summaryCards: (rows) => [
      { label: 'Date Moves Logged', value: String(rows.length) },
      { label: 'Without a Reason', value: String(rows.filter(r => r.reason === '— none given —').length) },
      { label: 'Money In', value: String(rows.filter(r => r.direction === 'Money In').length), filterKey: 'direction', filterValue: 'Money In' },
      { label: 'Money Out', value: String(rows.filter(r => r.direction === 'Money Out').length), filterKey: 'direction', filterValue: 'Money Out' },
    ],
  },

  {
    id: 'teamWorkload', category: 'operations', name: 'Team Workload & Overdue',
    description: 'What every person is carrying right now — open items, how many are late, and the next thing due. Read it before handing anyone else a task.',
    canView: canViewOpsReport,
    columns: [
      { key: 'person', label: 'Person', type: 'text' },
      { key: 'role', label: 'Role', type: 'text' },
      { key: 'departments', label: 'Departments', type: 'text' },
      { key: 'open', label: 'Open Items', type: 'text' },
      { key: 'overdue', label: 'Overdue', type: 'text' },
      { key: 'dueWeek', label: 'Due This Week', type: 'text' },
      { key: 'nextDue', label: 'Next Due', type: 'date' },
      { key: 'nextItem', label: 'Next Item', type: 'text' },
      { key: 'projects', label: 'Projects Involved', type: 'text' },
    ],
    filterKeys: ['role', 'departments'],
    groupByKeys: ['role', 'departments'],
    searchKeys: ['person', 'role', 'nextItem'],
    getRows: (ctx) => {
      const today = todayISO();
      const weekOut = addDays(today, 7);
      return ctx.teamDirectory.filter(p => p.active).map(person => {
        // Same definition of "my work" the person sees in My To-Do.
        const items = collectMyItems(ctx, person.id).filter(i => !i.completed);
        const dated = items.filter(i => i.date).sort((a, b) => (a.date < b.date ? -1 : 1));
        const next = dated[0];
        return {
          _id: person.id,
          person: person.name, role: person.securityRole,
          departments: (person.departments || []).join(' + ') || '—',
          open: String(items.length),
          overdue: String(items.filter(i => i.date && i.date < today).length),
          dueWeek: String(items.filter(i => i.date && i.date >= today && i.date <= weekOut).length),
          nextDue: next ? next.date : null,
          nextItem: next ? `${next.kind}: ${next.title}` : '—',
          projects: [...new Set(items.map(i => i.projectName).filter(Boolean))].join(', ') || '—',
        };
      });
    },
    summaryCards: (rows) => [
      { label: 'People With Open Work', value: String(rows.filter(r => Number(r.open) > 0).length) },
      { label: 'Total Open Items', value: String(rows.reduce((s, r) => s + Number(r.open), 0)) },
      { label: 'Total Overdue', value: String(rows.reduce((s, r) => s + Number(r.overdue), 0)) },
      { label: 'People With Nothing Assigned', value: String(rows.filter(r => Number(r.open) === 0).length) },
    ],
  },

  {
    id: 'scheduleAccuracy', category: 'operations', name: 'Schedule Accuracy & Lead-Time Variance',
    description: 'Planned vs actual duration for every completed stage. Stages that consistently run over are telling you the lead-time library is wrong — this is the report you tune it from.',
    canView: canViewOpsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'family', label: 'Scope Family', type: 'text' },
      { key: 'department', label: 'Department', type: 'text' },
      { key: 'stage', label: 'Stage', type: 'text' },
      { key: 'plannedDue', label: 'Planned', type: 'date' },
      { key: 'actual', label: 'Actual', type: 'date' },
      { key: 'varianceDays', label: 'Variance (days)', type: 'text' },
      { key: 'outcome', label: 'Outcome', type: 'text' },
    ],
    filterKeys: ['outcome', 'family', 'department', 'stage'],
    groupByKeys: ['stage', 'family', 'outcome', 'department'],
    searchKeys: ['project', 'scope', 'stage'],
    getRows: (ctx) => {
      const rows = [];
      ctx.projects.forEach(p => (p.scopes || []).forEach(sc => (sc.stages || []).forEach(st => {
        if (!st.actualCompletion || !st.plannedDue) return;   // only finished stages can be scored
        const v = daysBetween(st.plannedDue, st.actualCompletion);
        rows.push({
          _id: st.id, _onOpen: () => ctx.goProjectTab(p.id, 'scopes'),
          project: p.name, scope: sc.name, family: sc.familyName || '—',
          department: scopeDepartment(sc), stage: st.name,
          plannedDue: st.plannedDue, actual: st.actualCompletion,
          varianceDays: (v > 0 ? '+' : '') + v,
          outcome: v > 0 ? 'Late' : v < 0 ? 'Early' : 'On Time',
        });
      })));
      return rows;
    },
    summaryCards: (rows) => {
      const nums = rows.map(r => Number(r.varianceDays));
      const avg = nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;
      return [
        { label: 'Stages Measured', value: String(rows.length) },
        { label: 'Average Variance', value: `${avg > 0 ? '+' : ''}${avg.toFixed(1)} days` },
        { label: 'Finished Late', value: String(rows.filter(r => r.outcome === 'Late').length), filterKey: 'outcome', filterValue: 'Late' },
        { label: 'On Time or Early', value: String(rows.filter(r => r.outcome !== 'Late').length) },
      ];
    },
  },

  {
    id: 'punchCloseout', category: 'operations', name: 'Punch List & Closeout Aging',
    description: 'Every punch item that is not closed, and how long it has been open. Closeout is where jobs quietly stall.',
    canView: canViewOpsReport,
    columns: [
      { key: 'project', label: 'Project', type: 'text' },
      { key: 'scope', label: 'Scope', type: 'text' },
      { key: 'location', label: 'Location', type: 'text' },
      { key: 'item', label: 'Item', type: 'text' },
      { key: 'priority', label: 'Priority', type: 'text' },
      { key: 'status', label: 'Status', type: 'text' },
      { key: 'raised', label: 'Raised', type: 'date' },
      { key: 'ageDays', label: 'Age (days)', type: 'text' },
      { key: 'owner', label: 'Waiting On', type: 'text' },
    ],
    filterKeys: ['status', 'priority', 'project'],
    groupByKeys: ['project', 'status', 'priority'],
    searchKeys: ['project', 'scope', 'item', 'location'],
    dateFilterKey: 'raised',
    getRows: (ctx) => {
      const today = todayISO();
      const rows = [];
      ctx.projects.forEach(p => (p.punchItems || []).forEach(pi => {
        if (pi.status === 'Closed') return;
        const scope = (p.scopes || []).find(sc => sc.id === pi.scopeId);
        const raised = pi.dateRaised || pi.createdDate || null;
        rows.push({
          _id: pi.id, _onOpen: () => ctx.goProjectTab(p.id, 'installation'),
          project: p.name, scope: scope ? scope.name : '—',
          location: [pi.building, pi.floor, pi.unit, pi.room].filter(Boolean).join(' · ') || '—',
          item: pi.item || pi.description || '—',
          priority: pi.priority || '—', status: pi.status,
          raised,
          ageDays: raised ? String(Math.abs(daysBetween(today, raised))) : '—',
          owner: pi.status === 'Completed – Awaiting Verification'
            ? personName(ctx.teamDirectory, teamMemberFor(p, 'Project Coordinator')) || 'Project Coordinator'
            : (pi.assignedTo || 'Field crew'),
        });
      }));
      return rows;
    },
    summaryCards: (rows) => [
      { label: 'Open Punch Items', value: String(rows.length) },
      { label: 'Awaiting Verification', value: String(rows.filter(r => r.status !== 'Open').length) },
      { label: 'Open Over 30 Days', value: String(rows.filter(r => Number(r.ageDays) > 30).length) },
      { label: 'High Priority', value: String(rows.filter(r => r.priority === 'High').length), filterKey: 'priority', filterValue: 'High' },
    ],
  },

  {
    id: 'accessAudit', category: 'operations', name: 'User Access Audit',
    description: 'Who holds which role, which departments they cover, and exactly what that role can edit — read live from Role Permissions. The report to review before signing off on the access model.',
    canView: (role) => canViewModule(role, 'users'),
    columns: [
      { key: 'person', label: 'Person', type: 'text' },
      { key: 'role', label: 'Security Role', type: 'text' },
      { key: 'departments', label: 'Departments', type: 'text' },
      { key: 'active', label: 'Active', type: 'text' },
      { key: 'seesMoney', label: 'Sees Money', type: 'text' },
      { key: 'editCount', label: 'Modules Can Edit', type: 'text' },
      { key: 'viewCount', label: 'Modules View Only', type: 'text' },
      { key: 'capabilities', label: 'Special Capabilities', type: 'text' },
      { key: 'overrides', label: 'Per-User Overrides', type: 'text' },
      { key: 'reportsTo', label: 'Reports To', type: 'text' },
    ],
    filterKeys: ['role', 'departments', 'seesMoney', 'active'],
    groupByKeys: ['role', 'departments', 'seesMoney'],
    searchKeys: ['person', 'role', 'capabilities'],
    getRows: (ctx) => ctx.teamDirectory.map(person => {
      const role = person.securityRole;
      const levels = ALL_MODULE_KEYS.map(m => roleModuleLevel(role, m.key));
      const caps = CAPABILITY_DEFS.filter(c => roleHasCapability(role, c.key));
      const ov = person.permissionOverrides || {};
      const ovKeys = Object.keys(ov);
      return {
        _id: person.id,
        person: person.name, role,
        departments: (person.departments || []).join(' + ') || '—',
        active: person.active ? 'Yes' : 'No',
        seesMoney: canSeeFinancials(role) ? 'Yes' : 'No',
        editCount: String(levels.filter(l => l === 'edit').length),
        viewCount: String(levels.filter(l => l === 'view').length),
        capabilities: caps.length ? caps.map(c => c.label).join(', ') : '—',
        overrides: ovKeys.length ? ovKeys.map(k => `${k}:${ov[k]}`).join(', ') : '—',
        reportsTo: person.reportsToId ? personName(ctx.teamDirectory, person.reportsToId) : '— top level —',
      };
    }),
    summaryCards: (rows) => [
      { label: 'Active Accounts', value: String(rows.filter(r => r.active === 'Yes').length) },
      { label: 'Can See Money', value: String(rows.filter(r => r.seesMoney === 'Yes').length), filterKey: 'seesMoney', filterValue: 'Yes' },
      { label: 'With Per-User Overrides', value: String(rows.filter(r => r.overrides !== '—').length) },
      { label: 'No Manager Set', value: String(rows.filter(r => r.reportsTo === '— top level —').length) },
    ],
  },

];

// ---------------------------------------------------------------------------
// Report engine
// ---------------------------------------------------------------------------
function ReportEngine({ reportDef, ctx, initialFilters, onBack }) {
  const [filters, setFilters] = useState(() => ({ ...(initialFilters || {}) }));
  const [searchText, setSearchText] = useState('');
  const [sortKey, setSortKey] = useState(null);
  const [sortDir, setSortDir] = useState('asc');
  const [groupBy, setGroupBy] = useState('');
  // Date-range filtering — a single reportDef.dateFilterKey column key drives
  // both the 7/14/30/60/90-day quick-forecast presets and a manual from/to
  // range, so any report (arrivals, deliveries, claims...) opts in with one
  // config line rather than each needing its own bespoke filter UI.
  const [dateFrom, setDateFrom] = useState('');
  const [dateTo, setDateTo] = useState('');
  const [quickDays, setQuickDays] = useState(null);
  useEffect(() => { setDateFrom(''); setDateTo(''); setQuickDays(null); }, [reportDef]);
  function applyQuickDays(n) {
    setQuickDays(n);
    setDateFrom(todayISO());
    setDateTo(addDays(todayISO(), n));
  }

  const columns = useMemo(() => reportDef.columns.filter(c => !c.financial || ctx.canSeeFin), [reportDef, ctx.canSeeFin]);
  const allRows = useMemo(() => reportDef.getRows(ctx), [reportDef, ctx.projects, ctx.vendors, ctx.subcontractors, ctx.accounts, ctx.teamDirectory, ctx.materialLibrary, ctx.freightForwarders, ctx.warehouseMaterials, ctx.materialAllocations, ctx.inventoryTransactions, ctx.logisticsClaims]);

  const filterOptions = useMemo(() => {
    const out = {};
    (reportDef.filterKeys || []).forEach(key => {
      out[key] = [...new Set(allRows.map(r => r[key]).filter(v => v !== undefined && v !== null && v !== ''))].sort();
    });
    return out;
  }, [allRows, reportDef]);

  let rows = allRows;
  Object.entries(filters).forEach(([key, value]) => {
    if (value === undefined || value === null || value === '') return;
    rows = rows.filter(r => r[key] === value);
  });
  if (reportDef.dateFilterKey && (dateFrom || dateTo)) {
    rows = rows.filter(r => {
      const d = r[reportDef.dateFilterKey];
      if (!d) return false;
      if (dateFrom && d < dateFrom) return false;
      if (dateTo && d > dateTo) return false;
      return true;
    });
  }
  if (searchText.trim()) {
    const q = searchText.trim().toLowerCase();
    const keys = reportDef.searchKeys || columns.map(c => c.key);
    rows = rows.filter(r => keys.some(k => String(r[k] ?? '').toLowerCase().includes(q)));
  }
  if (sortKey) {
    rows = [...rows].sort((a, b) => {
      const av = a[sortKey], bv = b[sortKey];
      if (av === bv) return 0;
      if (av === null || av === undefined) return 1;
      if (bv === null || bv === undefined) return -1;
      const cmp = typeof av === 'number' ? av - bv : String(av).localeCompare(String(bv));
      return sortDir === 'asc' ? cmp : -cmp;
    });
  }

  const summaryCards = (reportDef.summaryCards ? reportDef.summaryCards(rows, ctx) : []).filter(c => !c.financial || ctx.canSeeFin);

  function toggleSort(key) {
    if (sortKey === key) setSortDir(d => (d === 'asc' ? 'desc' : 'asc'));
    else { setSortKey(key); setSortDir('asc'); }
  }
  function setFilter(key, value) { setFilters(f => ({ ...f, [key]: value || undefined })); }
  function clearFilters() { setFilters({}); setSearchText(''); }
  const activeFilterCount = Object.values(filters).filter(v => v !== undefined && v !== null && v !== '').length;

  function exportCsv() { downloadCsv(reportDef.name, columns, rows); }

  const groups = groupBy ? Array.from(new Set(rows.map(r => r[groupBy] ?? '—'))) : null;

  function renderTable(tableRows) {
    return (
      <div className="overflow-x-auto bg-white border border-[var(--leon-line)] rounded-xl">
        <table className="w-full text-xs">
          <thead className="bg-[var(--leon-cream)]">
            <tr className="text-left text-[var(--leon-black)]/50 uppercase">
              {columns.map(c => (
                <th key={c.key} className="px-3 py-2 cursor-pointer select-none whitespace-nowrap" onClick={() => toggleSort(c.key)}>
                  {c.label}{sortKey === c.key ? (sortDir === 'asc' ? ' ▲' : ' ▼') : ''}
                </th>
              ))}
            </tr>
          </thead>
          <tbody>
            {tableRows.map(r => (
              <tr key={r._id} className={`border-t border-[var(--leon-line)] ${r._onOpen ? 'cursor-pointer hover:bg-[var(--leon-cream)]' : ''}`} onClick={r._onOpen}>
                {columns.map(c => <td key={c.key} className="px-3 py-2 whitespace-nowrap">{fmtReportValue(c, r[c.key])}</td>)}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    );
  }

  return (
    <div>
      <button onClick={onBack} className="no-print text-sm text-[var(--leon-brown)] font-semibold mb-3">← Back to Reports</button>
      <div className="flex items-center justify-between flex-wrap gap-3 mb-4">
        <div>
          <h1 className="text-2xl font-bold">{reportDef.name}</h1>
          {reportDef.description && <p className="text-sm text-[var(--leon-black)]/50 max-w-3xl">{reportDef.description}</p>}
        </div>
        <div className="no-print flex items-center gap-2">
          <Button variant="outline" size="sm" onClick={exportCsv}>⬇ Export to Excel</Button>
          {/* subjectKey scopes the share history to THIS report, so the count
              beside the button answers "has this already gone out?". */}
          <ShareButton ctx={ctx} subjectKey={`report:${reportDef.id}`}
            subject={reportDef.name}
            summary={`${rows.length} row${rows.length === 1 ? '' : 's'} — ${reportDef.description || ''}`.trim()} />
          <PrintButton onClick={() => window.print()} label="Print / PDF" />
        </div>
      </div>

      {summaryCards.length > 0 && (
        <div className="grid sm:grid-cols-3 lg:grid-cols-5 gap-3 mb-4">
          {summaryCards.map((c, i) => (
            <button
              key={i}
              onClick={() => c.filterKey !== undefined && setFilter(c.filterKey, c.filterValue)}
              disabled={c.filterKey === undefined}
              className={`text-left border border-[var(--leon-line)] rounded-lg p-3 bg-white ${c.filterKey !== undefined ? 'hover:border-[var(--leon-brown)] cursor-pointer' : 'cursor-default'}`}
            >
              <p className="text-[11px] uppercase text-[var(--leon-black)]/50 font-semibold">{c.label}</p>
              <p className={`text-base font-bold ${c.tone === 'red' ? 'text-[var(--leon-red)]' : c.tone === 'green' ? 'text-[var(--leon-green)]' : ''}`}>{c.value}</p>
            </button>
          ))}
        </div>
      )}

      <div className="no-print flex items-center gap-2 flex-wrap mb-3">
        <TextInput placeholder="Search…" value={searchText} onChange={e => setSearchText(e.target.value)} className="!w-48 !py-1 !text-xs" />
        {(reportDef.filterKeys || []).map(key => (
          <Select key={key} value={filters[key] || ''} onChange={e => setFilter(key, e.target.value)} className="!w-auto !py-1 !text-xs">
            <option value="">All {reportDef.columns.find(c => c.key === key)?.label || key}</option>
            {(filterOptions[key] || []).map(v => <option key={v} value={v}>{v}</option>)}
          </Select>
        ))}
        {reportDef.dateFilterKey && (
          <div className="flex items-center gap-1.5">
            {[7, 14, 30, 60, 90].map(n => (
              <button
                key={n}
                onClick={() => applyQuickDays(n)}
                className={`px-2 py-1 text-xs font-semibold rounded-full border ${quickDays === n ? 'bg-[var(--leon-black)] text-white border-[var(--leon-black)]' : 'border-[var(--leon-line)] text-[var(--leon-black)]/60 hover:bg-[var(--leon-cream)]'}`}
              >
                {n}d
              </button>
            ))}
            <TextInput type="date" value={dateFrom} onChange={e => { setDateFrom(e.target.value); setQuickDays(null); }} className="!w-auto !py-1 !text-xs" />
            <span className="text-xs text-[var(--leon-black)]/40">to</span>
            <TextInput type="date" value={dateTo} onChange={e => { setDateTo(e.target.value); setQuickDays(null); }} className="!w-auto !py-1 !text-xs" />
            {(dateFrom || dateTo) && <button onClick={() => { setDateFrom(''); setDateTo(''); setQuickDays(null); }} className="text-xs font-semibold text-[var(--leon-brown)] hover:underline">Clear dates</button>}
          </div>
        )}
        {(reportDef.groupByKeys || []).length > 0 && (
          <Select value={groupBy} onChange={e => setGroupBy(e.target.value)} className="!w-auto !py-1 !text-xs">
            <option value="">No Grouping</option>
            {reportDef.groupByKeys.map(key => <option key={key} value={key}>Group by {reportDef.columns.find(c => c.key === key)?.label || key}</option>)}
          </Select>
        )}
        {activeFilterCount > 0 && <button onClick={clearFilters} className="text-xs font-semibold text-[var(--leon-brown)] hover:underline">Clear filters</button>}
        <span className="text-xs text-[var(--leon-black)]/40 ml-auto">{rows.length} of {allRows.length}</span>
      </div>

      {rows.length === 0 ? <EmptyState text="No records match." /> : groups ? (
        <div>
          {groups.map(g => (
            <Collapsible key={g} id={`report-${reportDef.id}-group-${groupBy}-${g}`} title={String(g)} count={rows.filter(r => (r[groupBy] ?? '—') === g).length}>
              {renderTable(rows.filter(r => (r[groupBy] ?? '—') === g))}
            </Collapsible>
          ))}
        </div>
      ) : renderTable(rows)}
    </div>
  );
}

function ReportsHubTab({ ctx, initialFilters, pendingReportNav, onConsumeReportNav }) {
  const [selectedId, setSelectedId] = useState(pendingReportNav ? pendingReportNav.reportId : null);
  const [navFilters, setNavFilters] = useState(pendingReportNav ? pendingReportNav.filters : null);
  const visibleDefs = REPORT_DEFINITIONS.filter(r => r.canView(ctx.currentRole));
  const selected = REPORT_DEFINITIONS.find(r => r.id === selectedId);
  // A KPI-card deep link (Logistics Dashboard) can fire again while this
  // hub is already mounted — e.g. clicking a second card without leaving
  // Reports first. navFilters is copied into local state (not read from the
  // prop at render time) so it survives the parent clearing pendingReportNav
  // via onConsumeReportNav a beat later.
  useEffect(() => {
    if (pendingReportNav) {
      setSelectedId(pendingReportNav.reportId);
      setNavFilters(pendingReportNav.filters);
      onConsumeReportNav && onConsumeReportNav();
    }
  }, [pendingReportNav]);

  if (selected) {
    return <ReportEngine reportDef={selected} ctx={ctx} initialFilters={navFilters || initialFilters} onBack={() => { setSelectedId(null); setNavFilters(null); }} />;
  }

  return (
    <div>
      <div className="mb-5">
        <h1 className="text-2xl font-bold">Reports</h1>
        <p className="text-sm text-[var(--leon-black)]/50">One reporting center, reading live from the same project, vendor, and financial records used everywhere else.</p>
      </div>
      {visibleDefs.length === 0 ? <LockedNotice label="No reports are available for your role." /> : (
        REPORT_CATEGORIES.map(cat => {
          const defs = visibleDefs.filter(r => r.category === cat.key);
          if (!defs.length) return null;
          return (
            <Collapsible key={cat.key} id={`reports-category-${cat.key}`} title={cat.label} count={defs.length}>
              <div className="space-y-1.5">
                {defs.map(r => (
                  <button key={r.id} onClick={() => setSelectedId(r.id)} className="w-full text-left border border-[var(--leon-line)] rounded-lg px-3 py-2 hover:bg-[var(--leon-cream)] hover:border-[var(--leon-brown)]">
                    <p className="text-sm font-semibold">{r.name}</p>
                    {r.description && <p className="text-xs text-[var(--leon-black)]/50">{r.description}</p>}
                  </button>
                ))}
              </div>
            </Collapsible>
          );
        })
      )}
    </div>
  );
}
