@burdenoff/microfe-theglobalfuel 2026.1014.1 → 2026.1015.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/theglobalfuel/components/viz/AvatarStack.js +1 -0
- package/dist/theglobalfuel/components/viz/CertificateCard.js +1 -0
- package/dist/theglobalfuel/components/viz/HBarFunnel.js +1 -0
- package/dist/theglobalfuel/components/viz/Heatmap.js +1 -0
- package/dist/theglobalfuel/components/viz/KanbanBoard.js +1 -0
- package/dist/theglobalfuel/components/viz/OrgTree.js +1 -0
- package/dist/theglobalfuel/components/viz/ProgressRing.js +1 -0
- package/dist/theglobalfuel/components/viz/RatingStars.js +1 -0
- package/dist/theglobalfuel/components/viz/SegmentedTabs.js +1 -0
- package/dist/theglobalfuel/components/viz/SkillBar.js +1 -0
- package/dist/theglobalfuel/components/viz/lib/Avatar.js +1 -0
- package/dist/theglobalfuel/index.js +1 -0
- package/dist/theglobalfuel/pages/alerts/AlertsPage.js +1 -0
- package/dist/theglobalfuel/pages/alerts/AlertsPage.js.map +1 -1
- package/dist/theglobalfuel/pages/analytics/AnalyticsPage.js +1 -0
- package/dist/theglobalfuel/pages/analytics/AnalyticsPage.js.map +1 -1
- package/dist/theglobalfuel/pages/assets/AssetDetailPage.js +1 -0
- package/dist/theglobalfuel/pages/assets/AssetDetailPage.js.map +1 -1
- package/dist/theglobalfuel/pages/assets/AssetsPage.js +1 -0
- package/dist/theglobalfuel/pages/assets/AssetsPage.js.map +1 -1
- package/dist/theglobalfuel/pages/compliance/CompliancePage.js +1 -0
- package/dist/theglobalfuel/pages/compliance/CompliancePage.js.map +1 -1
- package/dist/theglobalfuel/pages/contracts/ContractDetailPage.js +1 -0
- package/dist/theglobalfuel/pages/contracts/ContractDetailPage.js.map +1 -1
- package/dist/theglobalfuel/pages/contracts/ContractsPage.js +1 -0
- package/dist/theglobalfuel/pages/contracts/ContractsPage.js.map +1 -1
- package/dist/theglobalfuel/pages/esg-disclosures/EsgDisclosuresPage.js +1 -0
- package/dist/theglobalfuel/pages/esg-disclosures/EsgDisclosuresPage.js.map +1 -1
- package/dist/theglobalfuel/pages/inventory/InventoryPage.js +1 -0
- package/dist/theglobalfuel/pages/inventory/InventoryPage.js.map +1 -1
- package/dist/theglobalfuel/pages/production/ProductionPage.js +1 -0
- package/dist/theglobalfuel/pages/production/ProductionPage.js.map +1 -1
- package/dist/theglobalfuel/pages/provenance/ProvenancePage.js +1 -0
- package/dist/theglobalfuel/pages/provenance/ProvenancePage.js.map +1 -1
- package/dist/theglobalfuel/pages/reports/ReportsPage.js +1 -0
- package/dist/theglobalfuel/pages/reports/ReportsPage.js.map +1 -1
- package/dist/theglobalfuel/pages/reserves/ReservesPage.js +1 -0
- package/dist/theglobalfuel/pages/reserves/ReservesPage.js.map +1 -1
- package/dist/theglobalfuel/pages/shipments/ShipmentsPage.js +1 -0
- package/dist/theglobalfuel/pages/shipments/ShipmentsPage.js.map +1 -1
- package/dist/theglobalfuel/utils/cn.js +2 -2
- package/dist/theglobalfuel/utils/index.js +7 -0
- package/dist/theglobalfuel/utils/provenance.js +1 -1
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ import { useTheAssets as l, useTheComplianceCerts as u, useTheContracts as ee, u
|
|
|
9
9
|
import { useThePageView as ie } from "../../hooks/useThePageView.js";
|
|
10
10
|
import { useTheTranslation as f } from "../../hooks/useTheTranslation.js";
|
|
11
11
|
import "../../hooks/index.js";
|
|
12
|
+
import "../../utils/index.js";
|
|
12
13
|
import { FilterTabs as p } from "../../components/FilterTabs.js";
|
|
13
14
|
import "../../components/index.js";
|
|
14
15
|
import { useMemo as m, useState as h } from "react";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AlertsPage.js","names":[],"sources":["../../../../src/theglobalfuel/pages/alerts/AlertsPage.tsx"],"sourcesContent":["/**\n * Alerts — operational, commercial and compliance exceptions.\n * @module theglobalfuel/pages/alerts\n *\n * A deterministic rule engine over the live `the*` records. Every alert states\n * the rule that produced it and links to the record that triggered it, so an\n * operator can always answer \"why am I seeing this\" and act on it in one click.\n *\n * The rules run client-side over data already fetched for the rest of the\n * product — no alert store, no polling job, and therefore no possibility of an\n * alert disagreeing with the record it points at. Rules that would need history\n * (trend breaks, anomaly detection) deliberately are NOT approximated here;\n * those belong to the backend decision engine.\n */\n\nimport { useMemo, useState } from 'react';\nimport { BellRing, ShieldAlert, Ship, FileSignature, Leaf, Gauge, Boxes } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n LoadingView,\n ErrorView,\n EmptyState,\n FilterTabs,\n type FilterOption,\n} from '../../components';\nimport { useTheGlobalFuel } from '../../context';\nimport {\n useTheAssets,\n useTheProductionRecords,\n useTheEmissionRecords,\n useTheComplianceCerts,\n useTheContracts,\n useTheShipments,\n useTheInventoryLots,\n useThePageView,\n useTheTranslation,\n} from '../../hooks';\nimport { cn } from '../../utils/cn';\nimport { formatNumber, formatDate, formatEnumLabel } from '../../utils';\n\nconst PAGE: { limit: number } = { limit: 200 };\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/** Days a certificate may have left before it is flagged. */\nconst CERT_EXPIRY_WARNING_DAYS = 90;\n/** Days a shipment may sit IN_TRANSIT before it is flagged as stalled. */\nconst SHIPMENT_STALE_DAYS = 30;\n/** Downtime hours in a single production record that count as significant. */\nconst DOWNTIME_ALERT_HOURS = 72;\n/** Fulfilment fraction a contract must reach when inside the closing window. */\nconst CONTRACT_AT_RISK_FRACTION = 0.75;\n/** Days before a contract's end date that its fulfilment starts being checked. */\nconst CONTRACT_CLOSING_DAYS = 60;\n\ntype Severity = 'critical' | 'warning' | 'info';\n\ninterface Alert {\n id: string;\n severity: Severity;\n category: string;\n icon: React.ReactNode;\n title: string;\n detail: string;\n /** The rule that produced this alert, shown verbatim to the operator. */\n rule: string;\n /** In-product route to the record that triggered the alert. */\n route: string;\n}\n\nconst SEVERITY_ORDER: Record<Severity, number> = { critical: 0, warning: 1, info: 2 };\n\nconst SEVERITY_STYLE: Record<Severity, string> = {\n critical: 'border-status-error-border bg-status-error-bg-subtle text-status-error-text',\n warning: 'border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text',\n info: 'border-border-seam bg-bg-sunken text-text-secondary',\n};\n\nconst SEVERITY_LABEL: Record<Severity, string> = {\n critical: 'Critical',\n warning: 'Warning',\n info: 'Info',\n};\n\n/** Days from `now` until `iso`; negative when already past. */\nfunction daysUntil(iso: string, now: number): number {\n return (new Date(iso).getTime() - now) / DAY_MS;\n}\n\n/** Days elapsed between `iso` and `now`; negative when in the future. */\nfunction daysSince(iso: string, now: number): number {\n return (now - new Date(iso).getTime()) / DAY_MS;\n}\n\nexport function AlertsPage() {\n useThePageView('alerts');\n const t = useTheTranslation();\n const { navigateTo } = useTheGlobalFuel();\n // Every rule below is evaluated against one clock reading taken at mount, so\n // the whole alert set is internally consistent and cannot shift mid-render.\n const [evaluatedAt] = useState(() => Date.now());\n\n const assets = useTheAssets({ pagination: PAGE });\n const production = useTheProductionRecords({ pagination: PAGE });\n const emissions = useTheEmissionRecords({ pagination: PAGE });\n const compliance = useTheComplianceCerts(undefined, undefined, undefined, PAGE);\n const contracts = useTheContracts({ pagination: PAGE });\n const shipments = useTheShipments(undefined, undefined, PAGE);\n const inventory = useTheInventoryLots({ pagination: PAGE });\n\n const sources = [assets, production, emissions, compliance, contracts, shipments, inventory];\n const isLoading = sources.some((s) => s.isLoading);\n const isError = sources.every((s) => s.isError);\n\n const assetName = useMemo(() => {\n const map = new Map<string, string>();\n for (const a of assets.data?.items ?? []) map.set(a.id, a.name);\n return map;\n }, [assets.data]);\n\n const alerts = useMemo<Alert[]>(() => {\n const out: Alert[] = [];\n\n // ── Compliance: expired and expiring certificates ────────────────────────\n for (const cert of compliance.data?.items ?? []) {\n const label = cert.certNumber ? `${cert.title} (${cert.certNumber})` : cert.title;\n if (cert.status === 'EXPIRED' || cert.status === 'REVOKED') {\n out.push({\n id: `cert-status-${cert.id}`,\n severity: 'critical',\n category: 'Compliance',\n icon: <ShieldAlert className=\"h-4 w-4\" />,\n title: `${formatEnumLabel(cert.category)} is ${formatEnumLabel(cert.status).toLowerCase()}`,\n detail: label,\n rule: 'Certificate status is EXPIRED or REVOKED.',\n route: 'compliance',\n });\n continue;\n }\n if (!cert.expiresAt) continue;\n const left = daysUntil(cert.expiresAt, evaluatedAt);\n if (left < 0) {\n out.push({\n id: `cert-past-${cert.id}`,\n severity: 'critical',\n category: 'Compliance',\n icon: <ShieldAlert className=\"h-4 w-4\" />,\n title: `${formatEnumLabel(cert.category)} expired ${formatNumber(Math.abs(Math.floor(left)))} days ago`,\n detail: `${label} · expired ${formatDate(cert.expiresAt)}`,\n rule: 'Certificate expiry date is in the past but its status has not been updated.',\n route: 'compliance',\n });\n } else if (left <= CERT_EXPIRY_WARNING_DAYS) {\n out.push({\n id: `cert-soon-${cert.id}`,\n severity: 'warning',\n category: 'Compliance',\n icon: <ShieldAlert className=\"h-4 w-4\" />,\n title: `${formatEnumLabel(cert.category)} expires in ${formatNumber(Math.floor(left))} days`,\n detail: `${label} · expires ${formatDate(cert.expiresAt)}`,\n rule: `Certificate expires within ${CERT_EXPIRY_WARNING_DAYS} days.`,\n route: 'compliance',\n });\n }\n }\n\n // ── Commerce: contracts at risk and over-delivered ───────────────────────\n for (const c of contracts.data?.items ?? []) {\n if (c.contractedVolume <= 0) continue;\n const fraction = c.deliveredVolume / c.contractedVolume;\n\n if (fraction > 1) {\n out.push({\n id: `contract-over-${c.id}`,\n severity: 'warning',\n category: 'Contracts',\n icon: <FileSignature className=\"h-4 w-4\" />,\n title: `${c.reference} is over-delivered`,\n detail: `${formatNumber(c.deliveredVolume)} of ${formatNumber(c.contractedVolume)} ${formatEnumLabel(c.unit)} delivered to ${c.counterparty}.`,\n rule: 'Delivered volume exceeds the contracted volume.',\n route: `contracts/${c.id}`,\n });\n }\n\n if ((c.status === 'ACTIVE' || c.status === 'SIGNED') && c.endDate) {\n const left = daysUntil(c.endDate, evaluatedAt);\n if (left < 0 && fraction < 1) {\n out.push({\n id: `contract-lapsed-${c.id}`,\n severity: 'critical',\n category: 'Contracts',\n icon: <FileSignature className=\"h-4 w-4\" />,\n title: `${c.reference} passed its end date under-delivered`,\n detail: `${Math.round(fraction * 100)}% delivered · ended ${formatDate(c.endDate)}.`,\n rule: 'Contract end date has passed while the contract is still live and under-delivered.',\n route: `contracts/${c.id}`,\n });\n } else if (\n left >= 0 &&\n left <= CONTRACT_CLOSING_DAYS &&\n fraction < CONTRACT_AT_RISK_FRACTION\n ) {\n out.push({\n id: `contract-risk-${c.id}`,\n severity: 'warning',\n category: 'Contracts',\n icon: <FileSignature className=\"h-4 w-4\" />,\n title: `${c.reference} is behind schedule`,\n detail: `${Math.round(fraction * 100)}% delivered with ${formatNumber(Math.floor(left))} days left.`,\n rule: `Under ${Math.round(CONTRACT_AT_RISK_FRACTION * 100)}% delivered inside the final ${CONTRACT_CLOSING_DAYS} days.`,\n route: `contracts/${c.id}`,\n });\n }\n }\n }\n\n // ── Logistics: stalled movements ─────────────────────────────────────────\n for (const s of shipments.data?.items ?? []) {\n if (s.status !== 'IN_TRANSIT' && s.status !== 'LOADING') continue;\n const reference = s.departedAt ?? s.scheduledAt ?? s.createdAt;\n const elapsed = daysSince(reference, evaluatedAt);\n if (elapsed <= SHIPMENT_STALE_DAYS) continue;\n out.push({\n id: `shipment-stalled-${s.id}`,\n severity: 'warning',\n category: 'Shipments',\n icon: <Ship className=\"h-4 w-4\" />,\n title: `${s.reference} has been ${formatEnumLabel(s.status).toLowerCase()} for ${formatNumber(Math.floor(elapsed))} days`,\n detail: [s.carrier, s.trackingId, [s.origin, s.destination].filter(Boolean).join(' → ')]\n .filter(Boolean)\n .join(' · '),\n rule: `Shipment has been LOADING or IN_TRANSIT for more than ${SHIPMENT_STALE_DAYS} days.`,\n route: 'shipments',\n });\n }\n\n // ── Production: significant downtime ─────────────────────────────────────\n for (const r of production.data?.items ?? []) {\n if ((r.downtimeHours ?? 0) < DOWNTIME_ALERT_HOURS) continue;\n out.push({\n id: `downtime-${r.id}`,\n severity: 'warning',\n category: 'Production',\n icon: <Gauge className=\"h-4 w-4\" />,\n title: `${formatNumber(r.downtimeHours)} h downtime recorded`,\n detail: `${assetName.get(r.assetId) ?? r.assetId} · ${formatDate(r.periodStart)} – ${formatDate(r.periodEnd)}`,\n rule: `A single production record carries at least ${DOWNTIME_ALERT_HOURS} hours of downtime.`,\n route: `assets/${r.assetId}`,\n });\n }\n\n // ── ESG: unverified emissions ────────────────────────────────────────────\n const emissionRows = emissions.data?.items ?? [];\n const unverified = emissionRows.filter((e) => !e.verified);\n if (unverified.length > 0) {\n const tonnes = unverified.reduce((sum, e) => sum + (e.co2eTonnes ?? 0), 0);\n out.push({\n id: 'emissions-unverified',\n severity: 'info',\n category: 'ESG',\n icon: <Leaf className=\"h-4 w-4\" />,\n title: `${formatNumber(unverified.length)} emission records are not third-party verified`,\n detail: `${formatNumber(tonnes)} tCO₂e reported but unverified — these cannot be presented as assured in a disclosure.`,\n rule: 'Emission record has verified = false.',\n route: 'emissions',\n });\n }\n\n // ── Inventory: written-off lots ──────────────────────────────────────────\n const writtenOff = (inventory.data?.items ?? []).filter((l) => l.status === 'WRITTEN_OFF');\n if (writtenOff.length > 0) {\n out.push({\n id: 'inventory-written-off',\n severity: 'info',\n category: 'Inventory',\n icon: <Boxes className=\"h-4 w-4\" />,\n title: `${formatNumber(writtenOff.length)} inventory lots written off`,\n detail: writtenOff\n .slice(0, 4)\n .map((l) => l.lotCode)\n .join(', '),\n rule: 'Inventory lot status is WRITTEN_OFF.',\n route: 'inventory',\n });\n }\n\n return out.sort(\n (a, b) =>\n SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity] || a.title.localeCompare(b.title)\n );\n }, [\n compliance.data,\n contracts.data,\n shipments.data,\n production.data,\n emissions.data,\n inventory.data,\n assetName,\n evaluatedAt,\n ]);\n\n const counts = useMemo(\n () => ({\n all: alerts.length,\n critical: alerts.filter((a) => a.severity === 'critical').length,\n warning: alerts.filter((a) => a.severity === 'warning').length,\n info: alerts.filter((a) => a.severity === 'info').length,\n }),\n [alerts]\n );\n\n const [filter, setFilter] = useState<Severity | 'all'>('all');\n const filterOptions: FilterOption<Severity | 'all'>[] = [\n { value: 'all', label: 'All', count: counts.all },\n { value: 'critical', label: 'Critical', count: counts.critical },\n { value: 'warning', label: 'Warning', count: counts.warning },\n { value: 'info', label: 'Info', count: counts.info },\n ];\n\n const visible = filter === 'all' ? alerts : alerts.filter((a) => a.severity === filter);\n\n const description = t(\n 'theglobalfuel.pages.alerts.description',\n 'Compliance, commercial and operational exceptions detected across your records.'\n );\n const pageTitle = t('theglobalfuel.pages.alerts.title', 'Alerts');\n\n if (isLoading) {\n return (\n <PageLayout title={pageTitle} description={description} dataTour=\"theglobalfuel-alerts-page\">\n <LoadingView label=\"Evaluating alert rules…\" />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title={pageTitle} description={description} dataTour=\"theglobalfuel-alerts-page\">\n <ErrorView\n message=\"Couldn't load the records alerts are evaluated over.\"\n onRetry={() => sources.forEach((s) => void s.refetch())}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={pageTitle}\n description={description}\n dataTour=\"theglobalfuel-alerts-page\"\n actions={<FilterTabs options={filterOptions} value={filter} onChange={setFilter} />}\n >\n {alerts.length === 0 ? (\n <EmptyState\n illustration=\"empty-notifications\"\n icon={<BellRing className=\"h-10 w-10\" />}\n title=\"No open exceptions\"\n description=\"No certificate is expiring, no contract is behind, no shipment is stalled and no production record shows significant downtime.\"\n />\n ) : visible.length === 0 ? (\n <EmptyState\n illustration=\"empty-search\"\n icon={<BellRing className=\"h-10 w-10\" />}\n title={`No ${SEVERITY_LABEL[filter as Severity].toLowerCase()} alerts`}\n description=\"Switch the filter to see the other severities.\"\n />\n ) : (\n <div className=\"flex flex-col gap-3\">\n {visible.map((alert) => (\n <GlassCard key={alert.id} treatment=\"glass\" className=\"p-0\">\n <button\n type=\"button\"\n onClick={() => navigateTo(alert.route)}\n className=\"flex w-full items-start gap-4 p-4 text-left transition-colors hover:bg-bg-sunken\"\n aria-label={`Open the record for: ${alert.title}`}\n >\n <span\n className={cn(\n 'mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-lg border',\n SEVERITY_STYLE[alert.severity]\n )}\n >\n {alert.icon}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span\n className={cn(\n 'rounded-full border px-2 py-0.5 text-xs font-medium',\n SEVERITY_STYLE[alert.severity]\n )}\n >\n {SEVERITY_LABEL[alert.severity]}\n </span>\n <span className=\"text-xs uppercase tracking-wide text-text-tertiary\">\n {alert.category}\n </span>\n </div>\n <p className=\"mt-1.5 text-sm font-semibold text-text-primary\">{alert.title}</p>\n {alert.detail && (\n <p className=\"mt-0.5 truncate text-sm text-text-secondary\">{alert.detail}</p>\n )}\n <p className=\"mt-1.5 text-xs text-text-tertiary\">Rule: {alert.rule}</p>\n </div>\n </button>\n </GlassCard>\n ))}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AlertsPage.js","names":[],"sources":["../../../../src/theglobalfuel/pages/alerts/AlertsPage.tsx"],"sourcesContent":["/**\n * Alerts — operational, commercial and compliance exceptions.\n * @module theglobalfuel/pages/alerts\n *\n * A deterministic rule engine over the live `the*` records. Every alert states\n * the rule that produced it and links to the record that triggered it, so an\n * operator can always answer \"why am I seeing this\" and act on it in one click.\n *\n * The rules run client-side over data already fetched for the rest of the\n * product — no alert store, no polling job, and therefore no possibility of an\n * alert disagreeing with the record it points at. Rules that would need history\n * (trend breaks, anomaly detection) deliberately are NOT approximated here;\n * those belong to the backend decision engine.\n */\n\nimport { useMemo, useState } from 'react';\nimport { BellRing, ShieldAlert, Ship, FileSignature, Leaf, Gauge, Boxes } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n LoadingView,\n ErrorView,\n EmptyState,\n FilterTabs,\n type FilterOption,\n} from '../../components';\nimport { useTheGlobalFuel } from '../../context';\nimport {\n useTheAssets,\n useTheProductionRecords,\n useTheEmissionRecords,\n useTheComplianceCerts,\n useTheContracts,\n useTheShipments,\n useTheInventoryLots,\n useThePageView,\n useTheTranslation,\n} from '../../hooks';\nimport { cn } from '../../utils/cn';\nimport { formatNumber, formatDate, formatEnumLabel } from '../../utils';\n\nconst PAGE: { limit: number } = { limit: 200 };\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/** Days a certificate may have left before it is flagged. */\nconst CERT_EXPIRY_WARNING_DAYS = 90;\n/** Days a shipment may sit IN_TRANSIT before it is flagged as stalled. */\nconst SHIPMENT_STALE_DAYS = 30;\n/** Downtime hours in a single production record that count as significant. */\nconst DOWNTIME_ALERT_HOURS = 72;\n/** Fulfilment fraction a contract must reach when inside the closing window. */\nconst CONTRACT_AT_RISK_FRACTION = 0.75;\n/** Days before a contract's end date that its fulfilment starts being checked. */\nconst CONTRACT_CLOSING_DAYS = 60;\n\ntype Severity = 'critical' | 'warning' | 'info';\n\ninterface Alert {\n id: string;\n severity: Severity;\n category: string;\n icon: React.ReactNode;\n title: string;\n detail: string;\n /** The rule that produced this alert, shown verbatim to the operator. */\n rule: string;\n /** In-product route to the record that triggered the alert. */\n route: string;\n}\n\nconst SEVERITY_ORDER: Record<Severity, number> = { critical: 0, warning: 1, info: 2 };\n\nconst SEVERITY_STYLE: Record<Severity, string> = {\n critical: 'border-status-error-border bg-status-error-bg-subtle text-status-error-text',\n warning: 'border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text',\n info: 'border-border-seam bg-bg-sunken text-text-secondary',\n};\n\nconst SEVERITY_LABEL: Record<Severity, string> = {\n critical: 'Critical',\n warning: 'Warning',\n info: 'Info',\n};\n\n/** Days from `now` until `iso`; negative when already past. */\nfunction daysUntil(iso: string, now: number): number {\n return (new Date(iso).getTime() - now) / DAY_MS;\n}\n\n/** Days elapsed between `iso` and `now`; negative when in the future. */\nfunction daysSince(iso: string, now: number): number {\n return (now - new Date(iso).getTime()) / DAY_MS;\n}\n\nexport function AlertsPage() {\n useThePageView('alerts');\n const t = useTheTranslation();\n const { navigateTo } = useTheGlobalFuel();\n // Every rule below is evaluated against one clock reading taken at mount, so\n // the whole alert set is internally consistent and cannot shift mid-render.\n const [evaluatedAt] = useState(() => Date.now());\n\n const assets = useTheAssets({ pagination: PAGE });\n const production = useTheProductionRecords({ pagination: PAGE });\n const emissions = useTheEmissionRecords({ pagination: PAGE });\n const compliance = useTheComplianceCerts(undefined, undefined, undefined, PAGE);\n const contracts = useTheContracts({ pagination: PAGE });\n const shipments = useTheShipments(undefined, undefined, PAGE);\n const inventory = useTheInventoryLots({ pagination: PAGE });\n\n const sources = [assets, production, emissions, compliance, contracts, shipments, inventory];\n const isLoading = sources.some((s) => s.isLoading);\n const isError = sources.every((s) => s.isError);\n\n const assetName = useMemo(() => {\n const map = new Map<string, string>();\n for (const a of assets.data?.items ?? []) map.set(a.id, a.name);\n return map;\n }, [assets.data]);\n\n const alerts = useMemo<Alert[]>(() => {\n const out: Alert[] = [];\n\n // ── Compliance: expired and expiring certificates ────────────────────────\n for (const cert of compliance.data?.items ?? []) {\n const label = cert.certNumber ? `${cert.title} (${cert.certNumber})` : cert.title;\n if (cert.status === 'EXPIRED' || cert.status === 'REVOKED') {\n out.push({\n id: `cert-status-${cert.id}`,\n severity: 'critical',\n category: 'Compliance',\n icon: <ShieldAlert className=\"h-4 w-4\" />,\n title: `${formatEnumLabel(cert.category)} is ${formatEnumLabel(cert.status).toLowerCase()}`,\n detail: label,\n rule: 'Certificate status is EXPIRED or REVOKED.',\n route: 'compliance',\n });\n continue;\n }\n if (!cert.expiresAt) continue;\n const left = daysUntil(cert.expiresAt, evaluatedAt);\n if (left < 0) {\n out.push({\n id: `cert-past-${cert.id}`,\n severity: 'critical',\n category: 'Compliance',\n icon: <ShieldAlert className=\"h-4 w-4\" />,\n title: `${formatEnumLabel(cert.category)} expired ${formatNumber(Math.abs(Math.floor(left)))} days ago`,\n detail: `${label} · expired ${formatDate(cert.expiresAt)}`,\n rule: 'Certificate expiry date is in the past but its status has not been updated.',\n route: 'compliance',\n });\n } else if (left <= CERT_EXPIRY_WARNING_DAYS) {\n out.push({\n id: `cert-soon-${cert.id}`,\n severity: 'warning',\n category: 'Compliance',\n icon: <ShieldAlert className=\"h-4 w-4\" />,\n title: `${formatEnumLabel(cert.category)} expires in ${formatNumber(Math.floor(left))} days`,\n detail: `${label} · expires ${formatDate(cert.expiresAt)}`,\n rule: `Certificate expires within ${CERT_EXPIRY_WARNING_DAYS} days.`,\n route: 'compliance',\n });\n }\n }\n\n // ── Commerce: contracts at risk and over-delivered ───────────────────────\n for (const c of contracts.data?.items ?? []) {\n if (c.contractedVolume <= 0) continue;\n const fraction = c.deliveredVolume / c.contractedVolume;\n\n if (fraction > 1) {\n out.push({\n id: `contract-over-${c.id}`,\n severity: 'warning',\n category: 'Contracts',\n icon: <FileSignature className=\"h-4 w-4\" />,\n title: `${c.reference} is over-delivered`,\n detail: `${formatNumber(c.deliveredVolume)} of ${formatNumber(c.contractedVolume)} ${formatEnumLabel(c.unit)} delivered to ${c.counterparty}.`,\n rule: 'Delivered volume exceeds the contracted volume.',\n route: `contracts/${c.id}`,\n });\n }\n\n if ((c.status === 'ACTIVE' || c.status === 'SIGNED') && c.endDate) {\n const left = daysUntil(c.endDate, evaluatedAt);\n if (left < 0 && fraction < 1) {\n out.push({\n id: `contract-lapsed-${c.id}`,\n severity: 'critical',\n category: 'Contracts',\n icon: <FileSignature className=\"h-4 w-4\" />,\n title: `${c.reference} passed its end date under-delivered`,\n detail: `${Math.round(fraction * 100)}% delivered · ended ${formatDate(c.endDate)}.`,\n rule: 'Contract end date has passed while the contract is still live and under-delivered.',\n route: `contracts/${c.id}`,\n });\n } else if (\n left >= 0 &&\n left <= CONTRACT_CLOSING_DAYS &&\n fraction < CONTRACT_AT_RISK_FRACTION\n ) {\n out.push({\n id: `contract-risk-${c.id}`,\n severity: 'warning',\n category: 'Contracts',\n icon: <FileSignature className=\"h-4 w-4\" />,\n title: `${c.reference} is behind schedule`,\n detail: `${Math.round(fraction * 100)}% delivered with ${formatNumber(Math.floor(left))} days left.`,\n rule: `Under ${Math.round(CONTRACT_AT_RISK_FRACTION * 100)}% delivered inside the final ${CONTRACT_CLOSING_DAYS} days.`,\n route: `contracts/${c.id}`,\n });\n }\n }\n }\n\n // ── Logistics: stalled movements ─────────────────────────────────────────\n for (const s of shipments.data?.items ?? []) {\n if (s.status !== 'IN_TRANSIT' && s.status !== 'LOADING') continue;\n const reference = s.departedAt ?? s.scheduledAt ?? s.createdAt;\n const elapsed = daysSince(reference, evaluatedAt);\n if (elapsed <= SHIPMENT_STALE_DAYS) continue;\n out.push({\n id: `shipment-stalled-${s.id}`,\n severity: 'warning',\n category: 'Shipments',\n icon: <Ship className=\"h-4 w-4\" />,\n title: `${s.reference} has been ${formatEnumLabel(s.status).toLowerCase()} for ${formatNumber(Math.floor(elapsed))} days`,\n detail: [s.carrier, s.trackingId, [s.origin, s.destination].filter(Boolean).join(' → ')]\n .filter(Boolean)\n .join(' · '),\n rule: `Shipment has been LOADING or IN_TRANSIT for more than ${SHIPMENT_STALE_DAYS} days.`,\n route: 'shipments',\n });\n }\n\n // ── Production: significant downtime ─────────────────────────────────────\n for (const r of production.data?.items ?? []) {\n if ((r.downtimeHours ?? 0) < DOWNTIME_ALERT_HOURS) continue;\n out.push({\n id: `downtime-${r.id}`,\n severity: 'warning',\n category: 'Production',\n icon: <Gauge className=\"h-4 w-4\" />,\n title: `${formatNumber(r.downtimeHours)} h downtime recorded`,\n detail: `${assetName.get(r.assetId) ?? r.assetId} · ${formatDate(r.periodStart)} – ${formatDate(r.periodEnd)}`,\n rule: `A single production record carries at least ${DOWNTIME_ALERT_HOURS} hours of downtime.`,\n route: `assets/${r.assetId}`,\n });\n }\n\n // ── ESG: unverified emissions ────────────────────────────────────────────\n const emissionRows = emissions.data?.items ?? [];\n const unverified = emissionRows.filter((e) => !e.verified);\n if (unverified.length > 0) {\n const tonnes = unverified.reduce((sum, e) => sum + (e.co2eTonnes ?? 0), 0);\n out.push({\n id: 'emissions-unverified',\n severity: 'info',\n category: 'ESG',\n icon: <Leaf className=\"h-4 w-4\" />,\n title: `${formatNumber(unverified.length)} emission records are not third-party verified`,\n detail: `${formatNumber(tonnes)} tCO₂e reported but unverified — these cannot be presented as assured in a disclosure.`,\n rule: 'Emission record has verified = false.',\n route: 'emissions',\n });\n }\n\n // ── Inventory: written-off lots ──────────────────────────────────────────\n const writtenOff = (inventory.data?.items ?? []).filter((l) => l.status === 'WRITTEN_OFF');\n if (writtenOff.length > 0) {\n out.push({\n id: 'inventory-written-off',\n severity: 'info',\n category: 'Inventory',\n icon: <Boxes className=\"h-4 w-4\" />,\n title: `${formatNumber(writtenOff.length)} inventory lots written off`,\n detail: writtenOff\n .slice(0, 4)\n .map((l) => l.lotCode)\n .join(', '),\n rule: 'Inventory lot status is WRITTEN_OFF.',\n route: 'inventory',\n });\n }\n\n return out.sort(\n (a, b) =>\n SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity] || a.title.localeCompare(b.title)\n );\n }, [\n compliance.data,\n contracts.data,\n shipments.data,\n production.data,\n emissions.data,\n inventory.data,\n assetName,\n evaluatedAt,\n ]);\n\n const counts = useMemo(\n () => ({\n all: alerts.length,\n critical: alerts.filter((a) => a.severity === 'critical').length,\n warning: alerts.filter((a) => a.severity === 'warning').length,\n info: alerts.filter((a) => a.severity === 'info').length,\n }),\n [alerts]\n );\n\n const [filter, setFilter] = useState<Severity | 'all'>('all');\n const filterOptions: FilterOption<Severity | 'all'>[] = [\n { value: 'all', label: 'All', count: counts.all },\n { value: 'critical', label: 'Critical', count: counts.critical },\n { value: 'warning', label: 'Warning', count: counts.warning },\n { value: 'info', label: 'Info', count: counts.info },\n ];\n\n const visible = filter === 'all' ? alerts : alerts.filter((a) => a.severity === filter);\n\n const description = t(\n 'theglobalfuel.pages.alerts.description',\n 'Compliance, commercial and operational exceptions detected across your records.'\n );\n const pageTitle = t('theglobalfuel.pages.alerts.title', 'Alerts');\n\n if (isLoading) {\n return (\n <PageLayout title={pageTitle} description={description} dataTour=\"theglobalfuel-alerts-page\">\n <LoadingView label=\"Evaluating alert rules…\" />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title={pageTitle} description={description} dataTour=\"theglobalfuel-alerts-page\">\n <ErrorView\n message=\"Couldn't load the records alerts are evaluated over.\"\n onRetry={() => sources.forEach((s) => void s.refetch())}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={pageTitle}\n description={description}\n dataTour=\"theglobalfuel-alerts-page\"\n actions={<FilterTabs options={filterOptions} value={filter} onChange={setFilter} />}\n >\n {alerts.length === 0 ? (\n <EmptyState\n illustration=\"empty-notifications\"\n icon={<BellRing className=\"h-10 w-10\" />}\n title=\"No open exceptions\"\n description=\"No certificate is expiring, no contract is behind, no shipment is stalled and no production record shows significant downtime.\"\n />\n ) : visible.length === 0 ? (\n <EmptyState\n illustration=\"empty-search\"\n icon={<BellRing className=\"h-10 w-10\" />}\n title={`No ${SEVERITY_LABEL[filter as Severity].toLowerCase()} alerts`}\n description=\"Switch the filter to see the other severities.\"\n />\n ) : (\n <div className=\"flex flex-col gap-3\">\n {visible.map((alert) => (\n <GlassCard key={alert.id} treatment=\"glass\" className=\"p-0\">\n <button\n type=\"button\"\n onClick={() => navigateTo(alert.route)}\n className=\"flex w-full items-start gap-4 p-4 text-left transition-colors hover:bg-bg-sunken\"\n aria-label={`Open the record for: ${alert.title}`}\n >\n <span\n className={cn(\n 'mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-lg border',\n SEVERITY_STYLE[alert.severity]\n )}\n >\n {alert.icon}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span\n className={cn(\n 'rounded-full border px-2 py-0.5 text-xs font-medium',\n SEVERITY_STYLE[alert.severity]\n )}\n >\n {SEVERITY_LABEL[alert.severity]}\n </span>\n <span className=\"text-xs uppercase tracking-wide text-text-tertiary\">\n {alert.category}\n </span>\n </div>\n <p className=\"mt-1.5 text-sm font-semibold text-text-primary\">{alert.title}</p>\n {alert.detail && (\n <p className=\"mt-0.5 truncate text-sm text-text-secondary\">{alert.detail}</p>\n )}\n <p className=\"mt-1.5 text-xs text-text-tertiary\">Rule: {alert.rule}</p>\n </div>\n </button>\n </GlassCard>\n ))}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAyCA,IAAM,IAA0B,EAAE,OAAO,KAAK,EACxC,IAAS,OAAU,KAAK,KAGxB,IAA2B,IAE3B,IAAsB,IAEtB,IAAuB,IAEvB,IAA4B,KAE5B,IAAwB,IAiBxB,IAA2C;CAAE,UAAU;CAAG,SAAS;CAAG,MAAM;CAAG,EAE/E,IAA2C;CAC/C,UAAU;CACV,SAAS;CACT,MAAM;CACP,EAEK,IAA2C;CAC/C,UAAU;CACV,SAAS;CACT,MAAM;CACP;AAGD,SAAS,EAAU,GAAa,GAAqB;AACnD,SAAQ,IAAI,KAAK,EAAI,CAAC,SAAS,GAAG,KAAO;;AAI3C,SAAS,GAAU,GAAa,GAAqB;AACnD,SAAQ,IAAM,IAAI,KAAK,EAAI,CAAC,SAAS,IAAI;;AAG3C,SAAgB,IAAa;AAC3B,IAAe,SAAS;CACxB,IAAM,IAAI,GAAmB,EACvB,EAAE,kBAAe,GAAkB,EAGnC,CAAC,KAAe,QAAe,KAAK,KAAK,CAAC,EAE1C,IAAS,EAAa,EAAE,YAAY,GAAM,CAAC,EAC3C,IAAa,EAAwB,EAAE,YAAY,GAAM,CAAC,EAC1D,IAAY,GAAsB,EAAE,YAAY,GAAM,CAAC,EACvD,IAAa,EAAsB,KAAA,GAAW,KAAA,GAAW,KAAA,GAAW,EAAK,EACzE,IAAY,GAAgB,EAAE,YAAY,GAAM,CAAC,EACjD,IAAY,GAAgB,KAAA,GAAW,KAAA,GAAW,EAAK,EACvD,IAAY,GAAoB,EAAE,YAAY,GAAM,CAAC,EAErD,IAAU;EAAC;EAAQ;EAAY;EAAW;EAAY;EAAW;EAAW;EAAU,EACtF,IAAY,EAAQ,MAAM,MAAM,EAAE,UAAU,EAC5C,KAAU,EAAQ,OAAO,MAAM,EAAE,QAAQ,EAEzC,IAAY,QAAc;EAC9B,IAAM,oBAAM,IAAI,KAAqB;AACrC,OAAK,IAAM,KAAK,EAAO,MAAM,SAAS,EAAE,CAAE,GAAI,IAAI,EAAE,IAAI,EAAE,KAAK;AAC/D,SAAO;IACN,CAAC,EAAO,KAAK,CAAC,EAEX,IAAS,QAAuB;EACpC,IAAM,IAAe,EAAE;AAGvB,OAAK,IAAM,KAAQ,EAAW,MAAM,SAAS,EAAE,EAAE;GAC/C,IAAM,IAAQ,EAAK,aAAa,GAAG,EAAK,MAAM,IAAI,EAAK,WAAW,KAAK,EAAK;AAC5E,OAAI,EAAK,WAAW,aAAa,EAAK,WAAW,WAAW;AAC1D,MAAI,KAAK;KACP,IAAI,eAAe,EAAK;KACxB,UAAU;KACV,UAAU;KACV,MAAM,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA;KACzC,OAAO,GAAG,EAAgB,EAAK,SAAS,CAAC,MAAM,EAAgB,EAAK,OAAO,CAAC,aAAa;KACzF,QAAQ;KACR,MAAM;KACN,OAAO;KACR,CAAC;AACF;;AAEF,OAAI,CAAC,EAAK,UAAW;GACrB,IAAM,IAAO,EAAU,EAAK,WAAW,EAAY;AACnD,GAAI,IAAO,IACT,EAAI,KAAK;IACP,IAAI,aAAa,EAAK;IACtB,UAAU;IACV,UAAU;IACV,MAAM,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA;IACzC,OAAO,GAAG,EAAgB,EAAK,SAAS,CAAC,WAAW,EAAa,KAAK,IAAI,KAAK,MAAM,EAAK,CAAC,CAAC,CAAC;IAC7F,QAAQ,GAAG,EAAM,aAAa,EAAW,EAAK,UAAU;IACxD,MAAM;IACN,OAAO;IACR,CAAC,GACO,KAAQ,KACjB,EAAI,KAAK;IACP,IAAI,aAAa,EAAK;IACtB,UAAU;IACV,UAAU;IACV,MAAM,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA;IACzC,OAAO,GAAG,EAAgB,EAAK,SAAS,CAAC,cAAc,EAAa,KAAK,MAAM,EAAK,CAAC,CAAC;IACtF,QAAQ,GAAG,EAAM,aAAa,EAAW,EAAK,UAAU;IACxD,MAAM,8BAA8B,EAAyB;IAC7D,OAAO;IACR,CAAC;;AAKN,OAAK,IAAM,KAAK,EAAU,MAAM,SAAS,EAAE,EAAE;AAC3C,OAAI,EAAE,oBAAoB,EAAG;GAC7B,IAAM,IAAW,EAAE,kBAAkB,EAAE;AAevC,OAbI,IAAW,KACb,EAAI,KAAK;IACP,IAAI,iBAAiB,EAAE;IACvB,UAAU;IACV,UAAU;IACV,MAAM,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;IAC3C,OAAO,GAAG,EAAE,UAAU;IACtB,QAAQ,GAAG,EAAa,EAAE,gBAAgB,CAAC,MAAM,EAAa,EAAE,iBAAiB,CAAC,GAAG,EAAgB,EAAE,KAAK,CAAC,gBAAgB,EAAE,aAAa;IAC5I,MAAM;IACN,OAAO,aAAa,EAAE;IACvB,CAAC,GAGC,EAAE,WAAW,YAAY,EAAE,WAAW,aAAa,EAAE,SAAS;IACjE,IAAM,IAAO,EAAU,EAAE,SAAS,EAAY;AAC9C,IAAI,IAAO,KAAK,IAAW,IACzB,EAAI,KAAK;KACP,IAAI,mBAAmB,EAAE;KACzB,UAAU;KACV,UAAU;KACV,MAAM,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;KAC3C,OAAO,GAAG,EAAE,UAAU;KACtB,QAAQ,GAAG,KAAK,MAAM,IAAW,IAAI,CAAC,sBAAsB,EAAW,EAAE,QAAQ,CAAC;KAClF,MAAM;KACN,OAAO,aAAa,EAAE;KACvB,CAAC,GAEF,KAAQ,KACR,KAAQ,KACR,IAAW,KAEX,EAAI,KAAK;KACP,IAAI,iBAAiB,EAAE;KACvB,UAAU;KACV,UAAU;KACV,MAAM,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;KAC3C,OAAO,GAAG,EAAE,UAAU;KACtB,QAAQ,GAAG,KAAK,MAAM,IAAW,IAAI,CAAC,mBAAmB,EAAa,KAAK,MAAM,EAAK,CAAC,CAAC;KACxF,MAAM,SAAS,KAAK,MAAM,IAA4B,IAAI,CAAC,+BAA+B,EAAsB;KAChH,OAAO,aAAa,EAAE;KACvB,CAAC;;;AAMR,OAAK,IAAM,KAAK,EAAU,MAAM,SAAS,EAAE,EAAE;AAC3C,OAAI,EAAE,WAAW,gBAAgB,EAAE,WAAW,UAAW;GAEzD,IAAM,IAAU,GADE,EAAE,cAAc,EAAE,eAAe,EAAE,WAChB,EAAY;AAC7C,QAAW,KACf,EAAI,KAAK;IACP,IAAI,oBAAoB,EAAE;IAC1B,UAAU;IACV,UAAU;IACV,MAAM,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;IAClC,OAAO,GAAG,EAAE,UAAU,YAAY,EAAgB,EAAE,OAAO,CAAC,aAAa,CAAC,OAAO,EAAa,KAAK,MAAM,EAAQ,CAAC,CAAC;IACnH,QAAQ;KAAC,EAAE;KAAS,EAAE;KAAY,CAAC,EAAE,QAAQ,EAAE,YAAY,CAAC,OAAO,QAAQ,CAAC,KAAK,MAAM;KAAC,CACrF,OAAO,QAAQ,CACf,KAAK,MAAM;IACd,MAAM,yDAAyD,EAAoB;IACnF,OAAO;IACR,CAAC;;AAIJ,OAAK,IAAM,KAAK,EAAW,MAAM,SAAS,EAAE,CAC1C,EAAK,EAAE,iBAAiB,KAAK,KAC7B,EAAI,KAAK;GACP,IAAI,YAAY,EAAE;GAClB,UAAU;GACV,UAAU;GACV,MAAM,kBAAC,IAAD,EAAO,WAAU,WAAY,CAAA;GACnC,OAAO,GAAG,EAAa,EAAE,cAAc,CAAC;GACxC,QAAQ,GAAG,EAAU,IAAI,EAAE,QAAQ,IAAI,EAAE,QAAQ,KAAK,EAAW,EAAE,YAAY,CAAC,KAAK,EAAW,EAAE,UAAU;GAC5G,MAAM,+CAA+C,EAAqB;GAC1E,OAAO,UAAU,EAAE;GACpB,CAAC;EAKJ,IAAM,KADe,EAAU,MAAM,SAAS,EAAE,EAChB,QAAQ,MAAM,CAAC,EAAE,SAAS;AAC1D,MAAI,EAAW,SAAS,GAAG;GACzB,IAAM,IAAS,EAAW,QAAQ,GAAK,MAAM,KAAO,EAAE,cAAc,IAAI,EAAE;AAC1E,KAAI,KAAK;IACP,IAAI;IACJ,UAAU;IACV,UAAU;IACV,MAAM,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;IAClC,OAAO,GAAG,EAAa,EAAW,OAAO,CAAC;IAC1C,QAAQ,GAAG,EAAa,EAAO,CAAC;IAChC,MAAM;IACN,OAAO;IACR,CAAC;;EAIJ,IAAM,KAAc,EAAU,MAAM,SAAS,EAAE,EAAE,QAAQ,MAAM,EAAE,WAAW,cAAc;AAiB1F,SAhBI,EAAW,SAAS,KACtB,EAAI,KAAK;GACP,IAAI;GACJ,UAAU;GACV,UAAU;GACV,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;GACnC,OAAO,GAAG,EAAa,EAAW,OAAO,CAAC;GAC1C,QAAQ,EACL,MAAM,GAAG,EAAE,CACX,KAAK,MAAM,EAAE,QAAQ,CACrB,KAAK,KAAK;GACb,MAAM;GACN,OAAO;GACR,CAAC,EAGG,EAAI,MACR,GAAG,MACF,EAAe,EAAE,YAAY,EAAe,EAAE,aAAa,EAAE,MAAM,cAAc,EAAE,MAAM,CAC5F;IACA;EACD,EAAW;EACX,EAAU;EACV,EAAU;EACV,EAAW;EACX,EAAU;EACV,EAAU;EACV;EACA;EACD,CAAC,EAEI,IAAS,SACN;EACL,KAAK,EAAO;EACZ,UAAU,EAAO,QAAQ,MAAM,EAAE,aAAa,WAAW,CAAC;EAC1D,SAAS,EAAO,QAAQ,MAAM,EAAE,aAAa,UAAU,CAAC;EACxD,MAAM,EAAO,QAAQ,MAAM,EAAE,aAAa,OAAO,CAAC;EACnD,GACD,CAAC,EAAO,CACT,EAEK,CAAC,GAAQ,MAAa,EAA2B,MAAM,EACvD,KAAkD;EACtD;GAAE,OAAO;GAAO,OAAO;GAAO,OAAO,EAAO;GAAK;EACjD;GAAE,OAAO;GAAY,OAAO;GAAY,OAAO,EAAO;GAAU;EAChE;GAAE,OAAO;GAAW,OAAO;GAAW,OAAO,EAAO;GAAS;EAC7D;GAAE,OAAO;GAAQ,OAAO;GAAQ,OAAO,EAAO;GAAM;EACrD,EAEK,IAAU,MAAW,QAAQ,IAAS,EAAO,QAAQ,MAAM,EAAE,aAAa,EAAO,EAEjF,IAAc,EAClB,0CACA,kFACD,EACK,IAAY,EAAE,oCAAoC,SAAS;AAqBjE,QAnBI,IAEA,kBAAC,GAAD;EAAY,OAAO;EAAwB;EAAa,UAAS;YAC/D,kBAAC,GAAD,EAAa,OAAM,2BAA4B,CAAA;EACpC,CAAA,GAIb,KAEA,kBAAC,GAAD;EAAY,OAAO;EAAwB;EAAa,UAAS;YAC/D,kBAAC,GAAD;GACE,SAAQ;GACR,eAAe,EAAQ,SAAS,MAAM,KAAK,EAAE,SAAS,CAAC;GACvD,CAAA;EACS,CAAA,GAKf,kBAAC,GAAD;EACE,OAAO;EACM;EACb,UAAS;EACT,SAAS,kBAAC,GAAD;GAAY,SAAS;GAAe,OAAO;GAAQ,UAAU;GAAa,CAAA;YAElF,EAAO,WAAW,IACjB,kBAAC,GAAD;GACE,cAAa;GACb,MAAM,kBAAC,GAAD,EAAU,WAAU,aAAc,CAAA;GACxC,OAAM;GACN,aAAY;GACZ,CAAA,GACA,EAAQ,WAAW,IACrB,kBAAC,GAAD;GACE,cAAa;GACb,MAAM,kBAAC,GAAD,EAAU,WAAU,aAAc,CAAA;GACxC,OAAO,MAAM,EAAe,GAAoB,aAAa,CAAC;GAC9D,aAAY;GACZ,CAAA,GAEF,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAQ,KAAK,MACZ,kBAAC,GAAD;IAA0B,WAAU;IAAQ,WAAU;cACpD,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAW,EAAM,MAAM;KACtC,WAAU;KACV,cAAY,wBAAwB,EAAM;eAJ5C,CAME,kBAAC,QAAD;MACE,WAAW,EACT,6EACA,EAAe,EAAM,UACtB;gBAEA,EAAM;MACF,CAAA,EACP,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,QAAD;SACE,WAAW,EACT,uDACA,EAAe,EAAM,UACtB;mBAEA,EAAe,EAAM;SACjB,CAAA,EACP,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAM;SACF,CAAA,CACH;;OACN,kBAAC,KAAD;QAAG,WAAU;kBAAkD,EAAM;QAAU,CAAA;OAC9E,EAAM,UACL,kBAAC,KAAD;QAAG,WAAU;kBAA+C,EAAM;QAAW,CAAA;OAE/E,kBAAC,KAAD;QAAG,WAAU;kBAAb,CAAiD,UAAO,EAAM,KAAS;;OACnE;QACC;;IACC,EApCI,EAAM,GAoCV,CACZ;GACE,CAAA;EAEG,CAAA"}
|
|
@@ -14,6 +14,7 @@ import { useTheTranslation as he } from "../../hooks/useTheTranslation.js";
|
|
|
14
14
|
import "../../hooks/index.js";
|
|
15
15
|
import { toCsv as ge } from "../../utils/csv.js";
|
|
16
16
|
import { downloadTextFile as _e } from "../../utils/provenance.js";
|
|
17
|
+
import "../../utils/index.js";
|
|
17
18
|
import { FilterTabs as ve } from "../../components/FilterTabs.js";
|
|
18
19
|
import "../../components/index.js";
|
|
19
20
|
import { useMemo as l, useState as u } from "react";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../../src/theglobalfuel/pages/analytics/AnalyticsPage.tsx"],"sourcesContent":["/**\n * Operational Analytics — utilization, fulfilment and movement performance.\n * @module theglobalfuel/pages/analytics\n *\n * Where the dashboard answers \"what is my portfolio\", this page answers \"how\n * well is it running\". Every figure is aggregated client-side from the same\n * real `the*` list endpoints the rest of the product uses — there is no\n * separate analytics store and no mock data. The window selector re-slices the\n * already-fetched production/shipment rows rather than refetching, so switching\n * periods is instant and costs no extra backend calls.\n *\n * Unit discipline: volumes are NEVER summed across units of measure and money is\n * never summed across currencies. Where a figure cannot be stated honestly for\n * the data on hand (no capacity on record, emissions that carry no asset, lots\n * with no unit value) it renders as \"—\" with the reason, rather than as a zero.\n */\n\nimport { useMemo, useState } from 'react';\nimport {\n Activity,\n Gauge,\n Ship,\n Package,\n TrendingUp,\n Timer,\n Leaf,\n Coins,\n Download,\n} from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n LoadingView,\n ErrorView,\n EmptyState,\n FilterTabs,\n MiniBar,\n type FilterOption,\n AreaChart,\n BarChart,\n DonutChart,\n StatCard,\n type Segment,\n type LabeledValue,\n} from '../../components';\nimport {\n useTheAssets,\n useTheProductionRecords,\n useTheShipments,\n useTheContracts,\n useTheInventoryLots,\n useTheEmissionRecords,\n useNotify,\n useThePageView,\n useTheTranslation,\n} from '../../hooks';\nimport {\n downloadTextFile,\n formatEnumLabel,\n formatMoney,\n formatNumber,\n formatPercent,\n formatTonnesCo2e,\n formatUnit,\n formatVolume,\n toCsv,\n} from '../../utils';\nimport type {\n TheEmissionScope,\n TheInventoryStatus,\n TheProductionRecord,\n TheUnit,\n} from '../../types';\n\nconst PAGE: { limit: number } = { limit: 200 };\n\n/** Analysis window, in days back from now. */\ntype WindowKey = '30' | '90' | '365';\n\nconst WINDOWS: FilterOption<WindowKey>[] = [\n { value: '30', label: 'Last 30 days' },\n { value: '90', label: 'Last 90 days' },\n { value: '365', label: 'Last 12 months' },\n];\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/** GHG scope → donut segment color token (Scope 1 direct · 2 energy · 3 chain). */\nconst SCOPE_COLOR: Record<TheEmissionScope, string> = {\n SCOPE_1: 'var(--color-status-error-text)',\n SCOPE_2: 'var(--color-status-warning-text)',\n SCOPE_3: 'var(--color-accent-blue)',\n};\n\nconst ALL_SCOPES: TheEmissionScope[] = ['SCOPE_1', 'SCOPE_2', 'SCOPE_3'];\n\n/**\n * Lot states that still represent stock you hold. CONSUMED / WRITTEN_OFF lots\n * have left the book, so counting their value would overstate the position.\n */\nconst ON_HAND_LOT_STATES = new Set<TheInventoryStatus>(['AVAILABLE', 'RESERVED', 'IN_TRANSIT']);\n\n/** Caption used wherever a stat carries a period-over-period comparison. */\nconst PRIOR_WINDOW_LABEL = 'vs the preceding window';\n\n/** Contract states that are live enough for fulfilment to be meaningful. */\nconst LIVE_CONTRACT_STATES = new Set(['ACTIVE', 'SIGNED']);\n\n/** Month bucket key (YYYY-MM) for a date string; '' when unparseable. */\nfunction monthKey(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return '';\n return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\n/** Short month label (e.g. \"Mar 26\") for a YYYY-MM key. */\nfunction monthLabel(key: string): string {\n const [year, month] = key.split('-');\n const d = new Date(Date.UTC(Number(year), Number(month) - 1, 1));\n if (Number.isNaN(d.getTime())) return key;\n return d.toLocaleString(undefined, { month: 'short', year: '2-digit', timeZone: 'UTC' });\n}\n\n/**\n * Group volumes by unit. Assets report in BARRELS / TONNES / MCF / MWH …, and\n * summing across them would produce a meaningless number, so every total on\n * this page is either unit-scoped or unitless (hours, counts, percentages).\n */\nfunction volumeByUnit(rows: readonly { volume: number; unit: TheUnit }[]): Map<TheUnit, number> {\n const out = new Map<TheUnit, number>();\n for (const row of rows) {\n out.set(row.unit, (out.get(row.unit) ?? 0) + (row.volume ?? 0));\n }\n return out;\n}\n\n/** The unit carrying the largest share of production — the page's lead unit. */\nfunction dominantUnit(byUnit: Map<TheUnit, number>): TheUnit | null {\n let best: TheUnit | null = null;\n let bestValue = -1;\n for (const [unit, value] of byUnit) {\n if (value > bestValue) {\n best = unit;\n bestValue = value;\n }\n }\n return best;\n}\n\nexport function AnalyticsPage() {\n useThePageView('analytics');\n const t = useTheTranslation();\n const notify = useNotify();\n\n const [windowKey, setWindowKey] = useState<WindowKey>('90');\n const windowDays = Number(windowKey);\n // Sample the clock exactly once, when the page mounts. Reading Date.now()\n // during render would make every aggregate below re-derive against a moving\n // reference point, so a record could silently drop out of the window on an\n // unrelated re-render. A lazy useState initializer pins it for the visit.\n const [evaluatedAt] = useState(() => Date.now());\n const cutoff = useMemo(() => evaluatedAt - windowDays * DAY_MS, [evaluatedAt, windowDays]);\n // Start of the immediately preceding window of equal length, used for the\n // period-over-period deltas below.\n const priorCutoff = useMemo(\n () => evaluatedAt - 2 * windowDays * DAY_MS,\n [evaluatedAt, windowDays]\n );\n\n const assets = useTheAssets({ pagination: PAGE });\n const production = useTheProductionRecords({ pagination: PAGE });\n const shipments = useTheShipments(undefined, undefined, PAGE);\n const contracts = useTheContracts({ pagination: PAGE });\n const inventory = useTheInventoryLots({ pagination: PAGE });\n const emissions = useTheEmissionRecords({ pagination: PAGE });\n\n const isLoading =\n assets.isLoading ||\n production.isLoading ||\n shipments.isLoading ||\n contracts.isLoading ||\n inventory.isLoading ||\n emissions.isLoading;\n const isError =\n assets.isError ||\n production.isError ||\n shipments.isError ||\n contracts.isError ||\n inventory.isError ||\n emissions.isError;\n\n const assetRows = useMemo(() => assets.data?.items ?? [], [assets.data]);\n const contractRows = useMemo(() => contracts.data?.items ?? [], [contracts.data]);\n const lotRows = useMemo(() => inventory.data?.items ?? [], [inventory.data]);\n\n // ── Window-scoped slices ───────────────────────────────────────────────────\n const productionRows = useMemo<TheProductionRecord[]>(\n () =>\n (production.data?.items ?? []).filter((r) => {\n const ts = new Date(r.periodEnd ?? r.createdAt).getTime();\n return !Number.isNaN(ts) && ts >= cutoff;\n }),\n [production.data, cutoff]\n );\n\n const shipmentRows = useMemo(\n () =>\n (shipments.data?.items ?? []).filter((s) => {\n const ts = new Date(\n s.deliveredAt ?? s.departedAt ?? s.scheduledAt ?? s.createdAt\n ).getTime();\n return !Number.isNaN(ts) && ts >= cutoff;\n }),\n [shipments.data, cutoff]\n );\n\n const emissionRows = useMemo(\n () =>\n (emissions.data?.items ?? []).filter((e) => {\n const ts = new Date(e.periodEnd ?? e.createdAt).getTime();\n return !Number.isNaN(ts) && ts >= cutoff;\n }),\n [emissions.data, cutoff]\n );\n\n /**\n * A period-over-period delta is only honest when the ENTIRE record set is on\n * hand: these lists are fetched with a fixed page size and no date filter, so\n * if the backend has more rows than one page the preceding window may be\n * partly missing and a comparison would understate it. When the connection\n * still reports `hasMore`, no delta is shown at all.\n */\n const productionComplete = production.data?.pageInfo?.hasMore === false;\n const shipmentsComplete = shipments.data?.pageInfo?.hasMore === false;\n\n const priorProductionRows = useMemo(\n () =>\n (production.data?.items ?? []).filter((r) => {\n const ts = new Date(r.periodEnd ?? r.createdAt).getTime();\n return !Number.isNaN(ts) && ts >= priorCutoff && ts < cutoff;\n }),\n [production.data, priorCutoff, cutoff]\n );\n\n const priorShipmentRows = useMemo(\n () =>\n (shipments.data?.items ?? []).filter((s) => {\n const ts = new Date(\n s.deliveredAt ?? s.departedAt ?? s.scheduledAt ?? s.createdAt\n ).getTime();\n return !Number.isNaN(ts) && ts >= priorCutoff && ts < cutoff;\n }),\n [shipments.data, priorCutoff, cutoff]\n );\n\n // ── Utilization: produced volume vs nameplate capacity over the window ─────\n // Only assets whose reporting unit matches the production record's unit are\n // counted, so utilization never mixes units. Assets with no capacity on\n // record are excluded rather than counted as 0% (which would be a lie).\n const utilization = useMemo(() => {\n const byAsset = new Map<string, { produced: number; unit: TheUnit }>();\n for (const r of productionRows) {\n const prev = byAsset.get(r.assetId);\n if (prev && prev.unit !== r.unit) continue;\n byAsset.set(r.assetId, { produced: (prev?.produced ?? 0) + (r.volume ?? 0), unit: r.unit });\n }\n\n const rows: { name: string; pct: number }[] = [];\n let totalProduced = 0;\n let totalCapacity = 0;\n for (const asset of assetRows) {\n const agg = byAsset.get(asset.id);\n if (!agg || asset.capacityPerDay <= 0 || asset.capacityUnit !== agg.unit) continue;\n const capacity = asset.capacityPerDay * windowDays;\n if (capacity <= 0) continue;\n totalProduced += agg.produced;\n totalCapacity += capacity;\n rows.push({ name: asset.name, pct: (agg.produced / capacity) * 100 });\n }\n return {\n overallPct: totalCapacity > 0 ? (totalProduced / totalCapacity) * 100 : null,\n measuredAssets: rows.length,\n top: rows.sort((a, b) => b.pct - a.pct).slice(0, 8),\n };\n }, [productionRows, assetRows, windowDays]);\n\n // ── Downtime ───────────────────────────────────────────────────────────────\n const downtimeHours = useMemo(\n () => productionRows.reduce((sum, r) => sum + (r.downtimeHours ?? 0), 0),\n [productionRows]\n );\n\n /** Percent change in recorded downtime vs the preceding window (null = not stateable). */\n const downtimeDelta = useMemo<number | null>(() => {\n if (!productionComplete) return null;\n const prior = priorProductionRows.reduce((sum, r) => sum + (r.downtimeHours ?? 0), 0);\n // A change \"from zero\" has no percentage — say nothing rather than invent ∞.\n if (prior <= 0) return null;\n return ((downtimeHours - prior) / prior) * 100;\n }, [productionComplete, priorProductionRows, downtimeHours]);\n\n // ── Production trend (monthly, dominant unit only) ─────────────────────────\n const productionByUnit = useMemo(() => volumeByUnit(productionRows), [productionRows]);\n const leadUnit = useMemo(() => dominantUnit(productionByUnit), [productionByUnit]);\n\n const trendSeries = useMemo<LabeledValue[]>(() => {\n if (!leadUnit) return [];\n const buckets = new Map<string, number>();\n for (const r of productionRows) {\n if (r.unit !== leadUnit) continue;\n const key = monthKey(r.periodEnd ?? r.createdAt);\n if (!key) continue;\n buckets.set(key, (buckets.get(key) ?? 0) + (r.volume ?? 0));\n }\n return [...buckets.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ({ label: monthLabel(key), value }));\n }, [productionRows, leadUnit]);\n\n // ── Carbon intensity: tonnes CO₂e per unit produced ────────────────────────\n // Both sides of the ratio are restricted to the SAME set of assets — those\n // producing in the lead unit inside the window — so the numerator can never\n // include emissions from an asset whose output is missing from the\n // denominator. Emission records that carry no assetId cannot be attributed to\n // production and are excluded (and counted, so the caption can say so).\n const carbonIntensity = useMemo(() => {\n if (!leadUnit) return null;\n const leadAssets = new Set<string>();\n let volume = 0;\n for (const r of productionRows) {\n if (r.unit !== leadUnit) continue;\n leadAssets.add(r.assetId);\n volume += r.volume ?? 0;\n }\n if (volume <= 0 || leadAssets.size === 0) return null;\n\n let tonnes = 0;\n let attributed = 0;\n let unattributed = 0;\n for (const e of emissionRows) {\n if (!e.assetId) {\n unattributed += 1;\n continue;\n }\n if (!leadAssets.has(e.assetId)) continue;\n tonnes += e.co2eTonnes ?? 0;\n attributed += 1;\n }\n if (attributed === 0) return null;\n return {\n perUnit: tonnes / volume,\n tonnes,\n volume,\n unit: leadUnit,\n assets: leadAssets.size,\n attributed,\n unattributed,\n };\n }, [productionRows, emissionRows, leadUnit]);\n\n // ── Emissions by GHG scope (tonnes CO₂e in the window) ─────────────────────\n const emissionsByScope = useMemo<Segment[]>(() => {\n if (emissionRows.length === 0) return [];\n const totals: Record<TheEmissionScope, number> = { SCOPE_1: 0, SCOPE_2: 0, SCOPE_3: 0 };\n for (const e of emissionRows) totals[e.scope] += e.co2eTonnes ?? 0;\n return ALL_SCOPES.filter((scope) => totals[scope] > 0).map((scope) => ({\n label: formatEnumLabel(scope),\n value: totals[scope],\n color: SCOPE_COLOR[scope],\n }));\n }, [emissionRows]);\n\n const emissionsTotal = useMemo(\n () => emissionRows.reduce((sum, e) => sum + (e.co2eTonnes ?? 0), 0),\n [emissionRows]\n );\n\n const verifiedEmissions = useMemo(\n () => emissionRows.filter((e) => e.verified).length,\n [emissionRows]\n );\n\n // ── Inventory value: quantity × unit value, grouped by currency ────────────\n // Money is never summed across currencies. The headline is the currency\n // carrying the largest position; the rest are listed beside it.\n const inventoryValue = useMemo(() => {\n const byCurrency = new Map<string, number>();\n let valuedLots = 0;\n let unvaluedLots = 0;\n for (const lot of lotRows) {\n if (!ON_HAND_LOT_STATES.has(lot.status)) continue;\n if (!(lot.unitValue > 0) || !(lot.quantity > 0)) {\n unvaluedLots += 1;\n continue;\n }\n const currency = lot.currency || 'USD';\n byCurrency.set(currency, (byCurrency.get(currency) ?? 0) + lot.quantity * lot.unitValue);\n valuedLots += 1;\n }\n const ranked = [...byCurrency.entries()].sort((a, b) => b[1] - a[1]);\n return {\n lead: ranked[0] ?? null,\n others: ranked.slice(1),\n valuedLots,\n unvaluedLots,\n };\n }, [lotRows]);\n\n // ── Contract fulfilment ────────────────────────────────────────────────────\n const fulfilment = useMemo(() => {\n const live = contractRows.filter(\n (c) => c.contractedVolume > 0 && (c.status === 'ACTIVE' || c.status === 'SIGNED')\n );\n const contracted = live.reduce((s, c) => s + c.contractedVolume, 0);\n const delivered = live.reduce((s, c) => s + c.deliveredVolume, 0);\n const atRisk = live.filter((c) => {\n if (!c.endDate) return false;\n const daysLeft = (new Date(c.endDate).getTime() - evaluatedAt) / DAY_MS;\n const pct = c.deliveredVolume / c.contractedVolume;\n return daysLeft > 0 && daysLeft <= 60 && pct < 0.75;\n }).length;\n return {\n pct: contracted > 0 ? (delivered / contracted) * 100 : null,\n liveCount: live.length,\n atRisk,\n top: live\n .map((c) => ({\n label: c.reference,\n value: Math.min(100, (c.deliveredVolume / c.contractedVolume) * 100),\n }))\n .sort((a, b) => a.value - b.value)\n .slice(0, 8),\n };\n }, [contractRows, evaluatedAt]);\n\n // ── Fulfilment by counterparty ─────────────────────────────────────────────\n // Per-contract fulfilment is a unitless ratio, so counterparties whose book\n // spans several units of measure can still be compared. Ratios are averaged\n // across a counterparty's live contracts — volumes are deliberately NOT\n // summed, which would mix barrels with tonnes.\n const counterpartyFulfilment = useMemo(() => {\n const byCounterparty = new Map<string, { total: number; count: number }>();\n for (const c of contractRows) {\n if (!LIVE_CONTRACT_STATES.has(c.status) || c.contractedVolume <= 0) continue;\n const name = c.counterparty.trim() || 'Unnamed counterparty';\n const pct = Math.min(100, (c.deliveredVolume / c.contractedVolume) * 100);\n const prev = byCounterparty.get(name) ?? { total: 0, count: 0 };\n byCounterparty.set(name, { total: prev.total + pct, count: prev.count + 1 });\n }\n return [...byCounterparty.entries()]\n .map(([name, agg]) => ({ name, pct: agg.total / agg.count, contracts: agg.count }))\n .sort((a, b) => b.pct - a.pct)\n .slice(0, 8);\n }, [contractRows]);\n\n // ── Shipment mode mix + delivery rate ──────────────────────────────────────\n const shipmentMix = useMemo<Segment[]>(() => {\n const buckets = new Map<string, number>();\n for (const s of shipmentRows) {\n buckets.set(s.mode, (buckets.get(s.mode) ?? 0) + 1);\n }\n return [...buckets.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([mode, value]) => ({ label: formatEnumLabel(mode), value }));\n }, [shipmentRows]);\n\n const deliveryRate = useMemo(() => {\n const terminalOrDone = shipmentRows.filter((s) => s.status !== 'CANCELLED');\n if (terminalOrDone.length === 0) return null;\n const delivered = terminalOrDone.filter((s) => s.status === 'DELIVERED').length;\n return (delivered / terminalOrDone.length) * 100;\n }, [shipmentRows]);\n\n /** Change in delivery rate vs the preceding window, in percentage POINTS. */\n const deliveryRateDelta = useMemo<number | null>(() => {\n if (!shipmentsComplete || deliveryRate == null) return null;\n const terminalOrDone = priorShipmentRows.filter((s) => s.status !== 'CANCELLED');\n if (terminalOrDone.length === 0) return null;\n const delivered = terminalOrDone.filter((s) => s.status === 'DELIVERED').length;\n return deliveryRate - (delivered / terminalOrDone.length) * 100;\n }, [shipmentsComplete, priorShipmentRows, deliveryRate]);\n\n const inTransit = useMemo(\n () => shipmentRows.filter((s) => s.status === 'IN_TRANSIT' || s.status === 'LOADING').length,\n [shipmentRows]\n );\n\n // ── Inventory position ─────────────────────────────────────────────────────\n const inventoryMix = useMemo<Segment[]>(() => {\n const buckets = new Map<string, number>();\n for (const l of lotRows) {\n buckets.set(l.status, (buckets.get(l.status) ?? 0) + 1);\n }\n return [...buckets.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([status, value]) => ({ label: formatEnumLabel(status), value }));\n }, [lotRows]);\n\n // ── Export ─────────────────────────────────────────────────────────────────\n /**\n * Download the figures on screen as a metric/value CSV. Every row restates a\n * number this page already computed and displays — nothing is recomputed for\n * the file, so the export can never disagree with the view.\n */\n const handleExport = () => {\n const windowLabel = WINDOWS.find((w) => w.value === windowKey)?.label ?? `${windowDays} days`;\n const rows: { metric: string; value: string; detail: string }[] = [\n {\n metric: 'Window',\n value: windowLabel,\n detail: `Evaluated ${new Date(evaluatedAt).toISOString()}`,\n },\n {\n metric: 'Capacity utilization',\n value: utilization.overallPct != null ? formatPercent(utilization.overallPct) : '',\n detail: `${utilization.measuredAssets} assets with capacity on record`,\n },\n {\n metric: 'Contract fulfilment',\n value: fulfilment.pct != null ? formatPercent(fulfilment.pct) : '',\n detail: `${fulfilment.liveCount} live, ${fulfilment.atRisk} at risk`,\n },\n {\n metric: 'Delivery rate',\n value: deliveryRate != null ? formatPercent(deliveryRate) : '',\n detail:\n deliveryRateDelta != null\n ? `${Math.round(deliveryRateDelta)} pp ${PRIOR_WINDOW_LABEL}; ${inTransit} currently moving`\n : `${inTransit} currently moving`,\n },\n {\n metric: 'Recorded downtime (hours)',\n value: String(downtimeHours),\n detail:\n downtimeDelta != null\n ? `${Math.round(downtimeDelta)}% ${PRIOR_WINDOW_LABEL}; ${productionRows.length} production records`\n : `${productionRows.length} production records`,\n },\n {\n metric: carbonIntensity\n ? `Carbon intensity (t CO2e per ${formatUnit(carbonIntensity.unit)})`\n : 'Carbon intensity',\n value: carbonIntensity ? String(carbonIntensity.perUnit) : '',\n detail: carbonIntensity\n ? `${carbonIntensity.tonnes} t CO2e over ${carbonIntensity.volume} ${formatUnit(carbonIntensity.unit)} across ${carbonIntensity.assets} assets`\n : 'Not computable for this window',\n },\n {\n metric: inventoryValue.lead\n ? `Inventory on hand (${inventoryValue.lead[0]})`\n : 'Inventory on hand',\n value: inventoryValue.lead ? String(inventoryValue.lead[1]) : '',\n detail: `${inventoryValue.valuedLots} valued lots, ${inventoryValue.unvaluedLots} without a unit value`,\n },\n ...inventoryValue.others.map(([currency, total]) => ({\n metric: `Inventory on hand (${currency})`,\n value: String(total),\n detail: '',\n })),\n ...emissionsByScope.map((segment) => ({\n metric: `Emissions ${segment.label} (t CO2e)`,\n value: String(segment.value),\n detail: '',\n })),\n ...counterpartyFulfilment.map((row) => ({\n metric: `Fulfilment: ${row.name}`,\n value: formatPercent(row.pct),\n detail: `${row.contracts} live ${row.contracts === 1 ? 'contract' : 'contracts'}`,\n })),\n ];\n\n const csv = toCsv<{ metric: string; value: string; detail: string }>(\n [\n { header: 'Metric', value: (r) => r.metric },\n { header: 'Value', value: (r) => r.value },\n { header: 'Detail', value: (r) => r.detail },\n ],\n rows\n );\n const ok = downloadTextFile(`theglobalfuel-analytics-${windowDays}d.csv`, 'text/csv', csv);\n if (ok) notify.success('Analytics exported');\n else notify.error(\"This browser can't download the export.\");\n };\n\n const description = t(\n 'theglobalfuel.pages.analytics.description',\n 'Utilization, fulfilment and movement performance across the portfolio.'\n );\n const pageTitle = t('theglobalfuel.pages.analytics.title', 'Operational Analytics');\n\n if (isLoading) {\n return (\n <PageLayout\n title={pageTitle}\n description={description}\n dataTour=\"theglobalfuel-analytics-page\"\n >\n <LoadingView label=\"Aggregating operational data…\" />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout\n title={pageTitle}\n description={description}\n dataTour=\"theglobalfuel-analytics-page\"\n >\n <ErrorView\n message=\"Couldn't load the operational data for analytics.\"\n onRetry={() => {\n void assets.refetch();\n void production.refetch();\n void shipments.refetch();\n void contracts.refetch();\n void inventory.refetch();\n void emissions.refetch();\n }}\n />\n </PageLayout>\n );\n }\n\n const hasAnything =\n assetRows.length > 0 ||\n productionRows.length > 0 ||\n contractRows.length > 0 ||\n shipmentRows.length > 0 ||\n emissionRows.length > 0 ||\n lotRows.length > 0;\n\n return (\n <PageLayout\n title={pageTitle}\n description={description}\n dataTour=\"theglobalfuel-analytics-page\"\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <FilterTabs options={WINDOWS} value={windowKey} onChange={setWindowKey} />\n <Button variant=\"outline\" onClick={handleExport} disabled={!hasAnything}>\n <Download className=\"mr-1.5 h-4 w-4\" /> Export\n </Button>\n </div>\n }\n >\n {!hasAnything ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Activity className=\"h-10 w-10\" />}\n title=\"Nothing to analyse yet\"\n description=\"Register assets and record production, contracts and shipments — analytics is computed from those records.\"\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3\">\n <StatCard\n label=\"Capacity utilization\"\n value={utilization.overallPct != null ? formatPercent(utilization.overallPct) : '—'}\n icon={<Gauge className=\"h-4 w-4\" />}\n deltaLabel={\n utilization.overallPct != null\n ? `${formatNumber(utilization.measuredAssets)} assets with capacity on record`\n : 'No asset has both capacity and matching-unit production'\n }\n />\n <StatCard\n label=\"Contract fulfilment\"\n value={fulfilment.pct != null ? formatPercent(fulfilment.pct) : '—'}\n icon={<TrendingUp className=\"h-4 w-4\" />}\n deltaLabel={`${formatNumber(fulfilment.liveCount)} live · ${formatNumber(fulfilment.atRisk)} at risk`}\n />\n <StatCard\n label=\"Delivery rate\"\n value={deliveryRate != null ? formatPercent(deliveryRate) : '—'}\n icon={<Ship className=\"h-4 w-4\" />}\n {...(deliveryRateDelta != null\n ? { delta: Math.round(deliveryRateDelta), deltaSuffix: ' pp' }\n : {})}\n deltaLabel={\n deliveryRateDelta != null\n ? `${PRIOR_WINDOW_LABEL} · ${formatNumber(inTransit)} currently moving`\n : `${formatNumber(inTransit)} currently moving`\n }\n />\n <StatCard\n label=\"Recorded downtime\"\n value={`${formatNumber(downtimeHours)} h`}\n icon={<Timer className=\"h-4 w-4\" />}\n invertDelta\n {...(downtimeDelta != null ? { delta: Math.round(downtimeDelta) } : {})}\n deltaLabel={\n downtimeDelta != null\n ? `${PRIOR_WINDOW_LABEL} · ${formatNumber(productionRows.length)} records`\n : `Across ${formatNumber(productionRows.length)} production records`\n }\n />\n <StatCard\n label=\"Carbon intensity\"\n value={\n carbonIntensity\n ? `${formatNumber(carbonIntensity.perUnit, { maximumFractionDigits: 4 })} t CO₂e/${formatUnit(carbonIntensity.unit)}`\n : '—'\n }\n icon={<Leaf className=\"h-4 w-4\" />}\n invertDelta\n deltaLabel={\n carbonIntensity\n ? [\n `${formatTonnesCo2e(carbonIntensity.tonnes)} against ${formatVolume(carbonIntensity.volume, carbonIntensity.unit)} from ${formatNumber(carbonIntensity.assets)} assets`,\n carbonIntensity.unattributed > 0\n ? `${formatNumber(carbonIntensity.unattributed)} unattributed ${carbonIntensity.unattributed === 1 ? 'record' : 'records'} excluded`\n : null,\n ]\n .filter(Boolean)\n .join(' · ')\n : 'Needs emissions logged against an asset that also reports production'\n }\n />\n <StatCard\n label=\"Inventory on hand\"\n value={\n inventoryValue.lead\n ? formatMoney(inventoryValue.lead[1], inventoryValue.lead[0])\n : '—'\n }\n icon={<Coins className=\"h-4 w-4\" />}\n deltaLabel={\n inventoryValue.lead\n ? [\n `${formatNumber(inventoryValue.valuedLots)} valued ${inventoryValue.valuedLots === 1 ? 'lot' : 'lots'}`,\n ...inventoryValue.others.map(\n ([currency, total]) => `${formatMoney(total, currency)}`\n ),\n ].join(' · ')\n : 'No lot on hand carries a unit value'\n }\n />\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-5 lg:col-span-2\">\n <div className=\"mb-4\">\n <h2 className=\"text-base font-semibold text-text-primary\">Production trend</h2>\n <p className=\"text-xs text-text-tertiary\">\n {leadUnit\n ? `Monthly volume in ${formatEnumLabel(leadUnit)} — the unit carrying most of the portfolio. Other units are charted on their own assets.`\n : 'No production recorded in this window.'}\n </p>\n </div>\n {trendSeries.length > 0 ? (\n <AreaChart series={trendSeries} height={220} title=\"Monthly production volume\" />\n ) : (\n <div className=\"rounded-xl border border-dashed border-border-seam py-10 text-center text-sm text-text-secondary\">\n No production records in this window.\n </div>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-5\">\n <div className=\"mb-4\">\n <h2 className=\"text-base font-semibold text-text-primary\">Movement mix</h2>\n <p className=\"text-xs text-text-tertiary\">Shipments by transport mode.</p>\n </div>\n {shipmentMix.length > 0 ? (\n <DonutChart\n segments={shipmentMix}\n centerLabel={formatNumber(shipmentRows.length)}\n centerCaption=\"shipments\"\n showLegend\n title=\"Shipments by transport mode\"\n />\n ) : (\n <div className=\"rounded-xl border border-dashed border-border-seam py-10 text-center text-sm text-text-secondary\">\n No shipments in this window.\n </div>\n )}\n </GlassCard>\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-5\">\n <div className=\"mb-4\">\n <h2 className=\"text-base font-semibold text-text-primary\">Utilization by asset</h2>\n <p className=\"text-xs text-text-tertiary\">\n Produced volume against nameplate capacity for the window. Assets without a\n capacity figure, or whose production is recorded in a different unit, are omitted.\n </p>\n </div>\n {utilization.top.length > 0 ? (\n <BarChart\n series={utilization.top.map((r) => ({ label: r.name, value: Math.round(r.pct) }))}\n height={220}\n colorful\n valueFormatter={(v) => `${v}%`}\n title=\"Capacity utilization by asset\"\n />\n ) : (\n <div className=\"rounded-xl border border-dashed border-border-seam py-10 text-center text-sm text-text-secondary\">\n No asset has both a capacity figure and matching-unit production in this window.\n </div>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-5\">\n <div className=\"mb-4\">\n <h2 className=\"text-base font-semibold text-text-primary\">\n Least-fulfilled contracts\n </h2>\n <p className=\"text-xs text-text-tertiary\">\n Delivered against contracted volume for live (signed/active) contracts.\n </p>\n </div>\n {fulfilment.top.length > 0 ? (\n <BarChart\n series={fulfilment.top.map((r) => ({\n label: r.label,\n value: Math.round(r.value),\n }))}\n height={220}\n colorful\n valueFormatter={(v) => `${v}%`}\n title=\"Contract fulfilment\"\n />\n ) : (\n <div className=\"rounded-xl border border-dashed border-border-seam py-10 text-center text-sm text-text-secondary\">\n No live contracts with a contracted volume.\n </div>\n )}\n </GlassCard>\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-5\">\n <div className=\"mb-4\">\n <h2 className=\"text-base font-semibold text-text-primary\">Emissions by scope</h2>\n <p className=\"text-xs text-text-tertiary\">\n Tonnes CO₂e logged in this window, split by GHG protocol scope.\n {emissionRows.length > 0\n ? ` ${formatNumber(verifiedEmissions)} of ${formatNumber(emissionRows.length)} records verified.`\n : ''}\n </p>\n </div>\n {emissionsByScope.length > 0 ? (\n <DonutChart\n segments={emissionsByScope}\n centerLabel={formatNumber(emissionsTotal, { maximumFractionDigits: 0 })}\n centerCaption=\"t CO₂e\"\n showLegend\n valueFormatter={(v) => formatNumber(v, { maximumFractionDigits: 1 })}\n title=\"Emissions by GHG scope\"\n />\n ) : (\n <div className=\"rounded-xl border border-dashed border-border-seam py-10 text-center text-sm text-text-secondary\">\n No emissions recorded in this window.\n </div>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-5\">\n <div className=\"mb-4\">\n <h2 className=\"text-base font-semibold text-text-primary\">\n Fulfilment by counterparty\n </h2>\n <p className=\"text-xs text-text-tertiary\">\n Mean delivered-against-contracted across each counterparty's live\n (signed/active) contracts. Ratios are unitless, so no volumes are summed across\n units of measure.\n </p>\n </div>\n {counterpartyFulfilment.length > 0 ? (\n <ul className=\"space-y-3.5\">\n {counterpartyFulfilment.map((row) => (\n <li key={row.name}>\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"truncate text-sm text-text-secondary\">{row.name}</span>\n <span className=\"shrink-0 text-sm font-medium tabular-nums text-text-primary\">\n {formatPercent(row.pct)}\n </span>\n </div>\n <MiniBar\n value={row.pct}\n className=\"mt-1.5\"\n barClassName=\"bg-status-success-text\"\n />\n <p className=\"mt-1 text-[11px] text-text-tertiary\">\n {formatNumber(row.contracts)} live{' '}\n {row.contracts === 1 ? 'contract' : 'contracts'}\n </p>\n </li>\n ))}\n </ul>\n ) : (\n <div className=\"rounded-xl border border-dashed border-border-seam py-10 text-center text-sm text-text-secondary\">\n No live contracts with a contracted volume.\n </div>\n )}\n </GlassCard>\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-5\">\n <div className=\"mb-4 flex items-center gap-2\">\n <Package className=\"h-4 w-4 text-text-secondary\" />\n <h2 className=\"text-base font-semibold text-text-primary\">Inventory position</h2>\n </div>\n {inventoryMix.length > 0 ? (\n <DonutChart\n segments={inventoryMix}\n centerLabel={formatNumber(lotRows.length)}\n centerCaption=\"lots\"\n showLegend\n title=\"Inventory lots by status\"\n />\n ) : (\n <div className=\"rounded-xl border border-dashed border-border-seam py-10 text-center text-sm text-text-secondary\">\n No inventory lots recorded.\n </div>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"glass\" className=\"p-5 lg:col-span-2\">\n <h2 className=\"mb-4 text-base font-semibold text-text-primary\">\n Production volume by unit\n </h2>\n <p className=\"mb-4 text-xs text-text-tertiary\">\n Volumes are never summed across units of measure. Each row is an independent total.\n </p>\n {productionByUnit.size > 0 ? (\n <ul className=\"divide-y divide-border-subtle\">\n {[...productionByUnit.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([unit, value]) => (\n <li key={unit} className=\"flex items-center justify-between py-2.5\">\n <span className=\"text-sm text-text-secondary\">{formatEnumLabel(unit)}</span>\n <span className=\"text-sm font-medium tabular-nums text-text-primary\">\n {formatVolume(value, unit)}\n </span>\n </li>\n ))}\n </ul>\n ) : (\n <div className=\"rounded-xl border border-dashed border-border-seam py-10 text-center text-sm text-text-secondary\">\n No production records in this window.\n </div>\n )}\n </GlassCard>\n </div>\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../../src/theglobalfuel/pages/analytics/AnalyticsPage.tsx"],"sourcesContent":["/**\n * Operational Analytics — utilization, fulfilment and movement performance.\n * @module theglobalfuel/pages/analytics\n *\n * Where the dashboard answers \"what is my portfolio\", this page answers \"how\n * well is it running\". Every figure is aggregated client-side from the same\n * real `the*` list endpoints the rest of the product uses — there is no\n * separate analytics store and no mock data. The window selector re-slices the\n * already-fetched production/shipment rows rather than refetching, so switching\n * periods is instant and costs no extra backend calls.\n *\n * Unit discipline: volumes are NEVER summed across units of measure and money is\n * never summed across currencies. Where a figure cannot be stated honestly for\n * the data on hand (no capacity on record, emissions that carry no asset, lots\n * with no unit value) it renders as \"—\" with the reason, rather than as a zero.\n */\n\nimport { useMemo, useState } from 'react';\nimport {\n Activity,\n Gauge,\n Ship,\n Package,\n TrendingUp,\n Timer,\n Leaf,\n Coins,\n Download,\n} from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n LoadingView,\n ErrorView,\n EmptyState,\n FilterTabs,\n MiniBar,\n type FilterOption,\n AreaChart,\n BarChart,\n DonutChart,\n StatCard,\n type Segment,\n type LabeledValue,\n} from '../../components';\nimport {\n useTheAssets,\n useTheProductionRecords,\n useTheShipments,\n useTheContracts,\n useTheInventoryLots,\n useTheEmissionRecords,\n useNotify,\n useThePageView,\n useTheTranslation,\n} from '../../hooks';\nimport {\n downloadTextFile,\n formatEnumLabel,\n formatMoney,\n formatNumber,\n formatPercent,\n formatTonnesCo2e,\n formatUnit,\n formatVolume,\n toCsv,\n} from '../../utils';\nimport type {\n TheEmissionScope,\n TheInventoryStatus,\n TheProductionRecord,\n TheUnit,\n} from '../../types';\n\nconst PAGE: { limit: number } = { limit: 200 };\n\n/** Analysis window, in days back from now. */\ntype WindowKey = '30' | '90' | '365';\n\nconst WINDOWS: FilterOption<WindowKey>[] = [\n { value: '30', label: 'Last 30 days' },\n { value: '90', label: 'Last 90 days' },\n { value: '365', label: 'Last 12 months' },\n];\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/** GHG scope → donut segment color token (Scope 1 direct · 2 energy · 3 chain). */\nconst SCOPE_COLOR: Record<TheEmissionScope, string> = {\n SCOPE_1: 'var(--color-status-error-text)',\n SCOPE_2: 'var(--color-status-warning-text)',\n SCOPE_3: 'var(--color-accent-blue)',\n};\n\nconst ALL_SCOPES: TheEmissionScope[] = ['SCOPE_1', 'SCOPE_2', 'SCOPE_3'];\n\n/**\n * Lot states that still represent stock you hold. CONSUMED / WRITTEN_OFF lots\n * have left the book, so counting their value would overstate the position.\n */\nconst ON_HAND_LOT_STATES = new Set<TheInventoryStatus>(['AVAILABLE', 'RESERVED', 'IN_TRANSIT']);\n\n/** Caption used wherever a stat carries a period-over-period comparison. */\nconst PRIOR_WINDOW_LABEL = 'vs the preceding window';\n\n/** Contract states that are live enough for fulfilment to be meaningful. */\nconst LIVE_CONTRACT_STATES = new Set(['ACTIVE', 'SIGNED']);\n\n/** Month bucket key (YYYY-MM) for a date string; '' when unparseable. */\nfunction monthKey(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return '';\n return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\n/** Short month label (e.g. \"Mar 26\") for a YYYY-MM key. */\nfunction monthLabel(key: string): string {\n const [year, month] = key.split('-');\n const d = new Date(Date.UTC(Number(year), Number(month) - 1, 1));\n if (Number.isNaN(d.getTime())) return key;\n return d.toLocaleString(undefined, { month: 'short', year: '2-digit', timeZone: 'UTC' });\n}\n\n/**\n * Group volumes by unit. Assets report in BARRELS / TONNES / MCF / MWH …, and\n * summing across them would produce a meaningless number, so every total on\n * this page is either unit-scoped or unitless (hours, counts, percentages).\n */\nfunction volumeByUnit(rows: readonly { volume: number; unit: TheUnit }[]): Map<TheUnit, number> {\n const out = new Map<TheUnit, number>();\n for (const row of rows) {\n out.set(row.unit, (out.get(row.unit) ?? 0) + (row.volume ?? 0));\n }\n return out;\n}\n\n/** The unit carrying the largest share of production — the page's lead unit. */\nfunction dominantUnit(byUnit: Map<TheUnit, number>): TheUnit | null {\n let best: TheUnit | null = null;\n let bestValue = -1;\n for (const [unit, value] of byUnit) {\n if (value > bestValue) {\n best = unit;\n bestValue = value;\n }\n }\n return best;\n}\n\nexport function AnalyticsPage() {\n useThePageView('analytics');\n const t = useTheTranslation();\n const notify = useNotify();\n\n const [windowKey, setWindowKey] = useState<WindowKey>('90');\n const windowDays = Number(windowKey);\n // Sample the clock exactly once, when the page mounts. Reading Date.now()\n // during render would make every aggregate below re-derive against a moving\n // reference point, so a record could silently drop out of the window on an\n // unrelated re-render. A lazy useState initializer pins it for the visit.\n const [evaluatedAt] = useState(() => Date.now());\n const cutoff = useMemo(() => evaluatedAt - windowDays * DAY_MS, [evaluatedAt, windowDays]);\n // Start of the immediately preceding window of equal length, used for the\n // period-over-period deltas below.\n const priorCutoff = useMemo(\n () => evaluatedAt - 2 * windowDays * DAY_MS,\n [evaluatedAt, windowDays]\n );\n\n const assets = useTheAssets({ pagination: PAGE });\n const production = useTheProductionRecords({ pagination: PAGE });\n const shipments = useTheShipments(undefined, undefined, PAGE);\n const contracts = useTheContracts({ pagination: PAGE });\n const inventory = useTheInventoryLots({ pagination: PAGE });\n const emissions = useTheEmissionRecords({ pagination: PAGE });\n\n const isLoading =\n assets.isLoading ||\n production.isLoading ||\n shipments.isLoading ||\n contracts.isLoading ||\n inventory.isLoading ||\n emissions.isLoading;\n const isError =\n assets.isError ||\n production.isError ||\n shipments.isError ||\n contracts.isError ||\n inventory.isError ||\n emissions.isError;\n\n const assetRows = useMemo(() => assets.data?.items ?? [], [assets.data]);\n const contractRows = useMemo(() => contracts.data?.items ?? [], [contracts.data]);\n const lotRows = useMemo(() => inventory.data?.items ?? [], [inventory.data]);\n\n // ── Window-scoped slices ───────────────────────────────────────────────────\n const productionRows = useMemo<TheProductionRecord[]>(\n () =>\n (production.data?.items ?? []).filter((r) => {\n const ts = new Date(r.periodEnd ?? r.createdAt).getTime();\n return !Number.isNaN(ts) && ts >= cutoff;\n }),\n [production.data, cutoff]\n );\n\n const shipmentRows = useMemo(\n () =>\n (shipments.data?.items ?? []).filter((s) => {\n const ts = new Date(\n s.deliveredAt ?? s.departedAt ?? s.scheduledAt ?? s.createdAt\n ).getTime();\n return !Number.isNaN(ts) && ts >= cutoff;\n }),\n [shipments.data, cutoff]\n );\n\n const emissionRows = useMemo(\n () =>\n (emissions.data?.items ?? []).filter((e) => {\n const ts = new Date(e.periodEnd ?? e.createdAt).getTime();\n return !Number.isNaN(ts) && ts >= cutoff;\n }),\n [emissions.data, cutoff]\n );\n\n /**\n * A period-over-period delta is only honest when the ENTIRE record set is on\n * hand: these lists are fetched with a fixed page size and no date filter, so\n * if the backend has more rows than one page the preceding window may be\n * partly missing and a comparison would understate it. When the connection\n * still reports `hasMore`, no delta is shown at all.\n */\n const productionComplete = production.data?.pageInfo?.hasMore === false;\n const shipmentsComplete = shipments.data?.pageInfo?.hasMore === false;\n\n const priorProductionRows = useMemo(\n () =>\n (production.data?.items ?? []).filter((r) => {\n const ts = new Date(r.periodEnd ?? r.createdAt).getTime();\n return !Number.isNaN(ts) && ts >= priorCutoff && ts < cutoff;\n }),\n [production.data, priorCutoff, cutoff]\n );\n\n const priorShipmentRows = useMemo(\n () =>\n (shipments.data?.items ?? []).filter((s) => {\n const ts = new Date(\n s.deliveredAt ?? s.departedAt ?? s.scheduledAt ?? s.createdAt\n ).getTime();\n return !Number.isNaN(ts) && ts >= priorCutoff && ts < cutoff;\n }),\n [shipments.data, priorCutoff, cutoff]\n );\n\n // ── Utilization: produced volume vs nameplate capacity over the window ─────\n // Only assets whose reporting unit matches the production record's unit are\n // counted, so utilization never mixes units. Assets with no capacity on\n // record are excluded rather than counted as 0% (which would be a lie).\n const utilization = useMemo(() => {\n const byAsset = new Map<string, { produced: number; unit: TheUnit }>();\n for (const r of productionRows) {\n const prev = byAsset.get(r.assetId);\n if (prev && prev.unit !== r.unit) continue;\n byAsset.set(r.assetId, { produced: (prev?.produced ?? 0) + (r.volume ?? 0), unit: r.unit });\n }\n\n const rows: { name: string; pct: number }[] = [];\n let totalProduced = 0;\n let totalCapacity = 0;\n for (const asset of assetRows) {\n const agg = byAsset.get(asset.id);\n if (!agg || asset.capacityPerDay <= 0 || asset.capacityUnit !== agg.unit) continue;\n const capacity = asset.capacityPerDay * windowDays;\n if (capacity <= 0) continue;\n totalProduced += agg.produced;\n totalCapacity += capacity;\n rows.push({ name: asset.name, pct: (agg.produced / capacity) * 100 });\n }\n return {\n overallPct: totalCapacity > 0 ? (totalProduced / totalCapacity) * 100 : null,\n measuredAssets: rows.length,\n top: rows.sort((a, b) => b.pct - a.pct).slice(0, 8),\n };\n }, [productionRows, assetRows, windowDays]);\n\n // ── Downtime ───────────────────────────────────────────────────────────────\n const downtimeHours = useMemo(\n () => productionRows.reduce((sum, r) => sum + (r.downtimeHours ?? 0), 0),\n [productionRows]\n );\n\n /** Percent change in recorded downtime vs the preceding window (null = not stateable). */\n const downtimeDelta = useMemo<number | null>(() => {\n if (!productionComplete) return null;\n const prior = priorProductionRows.reduce((sum, r) => sum + (r.downtimeHours ?? 0), 0);\n // A change \"from zero\" has no percentage — say nothing rather than invent ∞.\n if (prior <= 0) return null;\n return ((downtimeHours - prior) / prior) * 100;\n }, [productionComplete, priorProductionRows, downtimeHours]);\n\n // ── Production trend (monthly, dominant unit only) ─────────────────────────\n const productionByUnit = useMemo(() => volumeByUnit(productionRows), [productionRows]);\n const leadUnit = useMemo(() => dominantUnit(productionByUnit), [productionByUnit]);\n\n const trendSeries = useMemo<LabeledValue[]>(() => {\n if (!leadUnit) return [];\n const buckets = new Map<string, number>();\n for (const r of productionRows) {\n if (r.unit !== leadUnit) continue;\n const key = monthKey(r.periodEnd ?? r.createdAt);\n if (!key) continue;\n buckets.set(key, (buckets.get(key) ?? 0) + (r.volume ?? 0));\n }\n return [...buckets.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ({ label: monthLabel(key), value }));\n }, [productionRows, leadUnit]);\n\n // ── Carbon intensity: tonnes CO₂e per unit produced ────────────────────────\n // Both sides of the ratio are restricted to the SAME set of assets — those\n // producing in the lead unit inside the window — so the numerator can never\n // include emissions from an asset whose output is missing from the\n // denominator. Emission records that carry no assetId cannot be attributed to\n // production and are excluded (and counted, so the caption can say so).\n const carbonIntensity = useMemo(() => {\n if (!leadUnit) return null;\n const leadAssets = new Set<string>();\n let volume = 0;\n for (const r of productionRows) {\n if (r.unit !== leadUnit) continue;\n leadAssets.add(r.assetId);\n volume += r.volume ?? 0;\n }\n if (volume <= 0 || leadAssets.size === 0) return null;\n\n let tonnes = 0;\n let attributed = 0;\n let unattributed = 0;\n for (const e of emissionRows) {\n if (!e.assetId) {\n unattributed += 1;\n continue;\n }\n if (!leadAssets.has(e.assetId)) continue;\n tonnes += e.co2eTonnes ?? 0;\n attributed += 1;\n }\n if (attributed === 0) return null;\n return {\n perUnit: tonnes / volume,\n tonnes,\n volume,\n unit: leadUnit,\n assets: leadAssets.size,\n attributed,\n unattributed,\n };\n }, [productionRows, emissionRows, leadUnit]);\n\n // ── Emissions by GHG scope (tonnes CO₂e in the window) ─────────────────────\n const emissionsByScope = useMemo<Segment[]>(() => {\n if (emissionRows.length === 0) return [];\n const totals: Record<TheEmissionScope, number> = { SCOPE_1: 0, SCOPE_2: 0, SCOPE_3: 0 };\n for (const e of emissionRows) totals[e.scope] += e.co2eTonnes ?? 0;\n return ALL_SCOPES.filter((scope) => totals[scope] > 0).map((scope) => ({\n label: formatEnumLabel(scope),\n value: totals[scope],\n color: SCOPE_COLOR[scope],\n }));\n }, [emissionRows]);\n\n const emissionsTotal = useMemo(\n () => emissionRows.reduce((sum, e) => sum + (e.co2eTonnes ?? 0), 0),\n [emissionRows]\n );\n\n const verifiedEmissions = useMemo(\n () => emissionRows.filter((e) => e.verified).length,\n [emissionRows]\n );\n\n // ── Inventory value: quantity × unit value, grouped by currency ────────────\n // Money is never summed across currencies. The headline is the currency\n // carrying the largest position; the rest are listed beside it.\n const inventoryValue = useMemo(() => {\n const byCurrency = new Map<string, number>();\n let valuedLots = 0;\n let unvaluedLots = 0;\n for (const lot of lotRows) {\n if (!ON_HAND_LOT_STATES.has(lot.status)) continue;\n if (!(lot.unitValue > 0) || !(lot.quantity > 0)) {\n unvaluedLots += 1;\n continue;\n }\n const currency = lot.currency || 'USD';\n byCurrency.set(currency, (byCurrency.get(currency) ?? 0) + lot.quantity * lot.unitValue);\n valuedLots += 1;\n }\n const ranked = [...byCurrency.entries()].sort((a, b) => b[1] - a[1]);\n return {\n lead: ranked[0] ?? null,\n others: ranked.slice(1),\n valuedLots,\n unvaluedLots,\n };\n }, [lotRows]);\n\n // ── Contract fulfilment ────────────────────────────────────────────────────\n const fulfilment = useMemo(() => {\n const live = contractRows.filter(\n (c) => c.contractedVolume > 0 && (c.status === 'ACTIVE' || c.status === 'SIGNED')\n );\n const contracted = live.reduce((s, c) => s + c.contractedVolume, 0);\n const delivered = live.reduce((s, c) => s + c.deliveredVolume, 0);\n const atRisk = live.filter((c) => {\n if (!c.endDate) return false;\n const daysLeft = (new Date(c.endDate).getTime() - evaluatedAt) / DAY_MS;\n const pct = c.deliveredVolume / c.contractedVolume;\n return daysLeft > 0 && daysLeft <= 60 && pct < 0.75;\n }).length;\n return {\n pct: contracted > 0 ? (delivered / contracted) * 100 : null,\n liveCount: live.length,\n atRisk,\n top: live\n .map((c) => ({\n label: c.reference,\n value: Math.min(100, (c.deliveredVolume / c.contractedVolume) * 100),\n }))\n .sort((a, b) => a.value - b.value)\n .slice(0, 8),\n };\n }, [contractRows, evaluatedAt]);\n\n // ── Fulfilment by counterparty ─────────────────────────────────────────────\n // Per-contract fulfilment is a unitless ratio, so counterparties whose book\n // spans several units of measure can still be compared. Ratios are averaged\n // across a counterparty's live contracts — volumes are deliberately NOT\n // summed, which would mix barrels with tonnes.\n const counterpartyFulfilment = useMemo(() => {\n const byCounterparty = new Map<string, { total: number; count: number }>();\n for (const c of contractRows) {\n if (!LIVE_CONTRACT_STATES.has(c.status) || c.contractedVolume <= 0) continue;\n const name = c.counterparty.trim() || 'Unnamed counterparty';\n const pct = Math.min(100, (c.deliveredVolume / c.contractedVolume) * 100);\n const prev = byCounterparty.get(name) ?? { total: 0, count: 0 };\n byCounterparty.set(name, { total: prev.total + pct, count: prev.count + 1 });\n }\n return [...byCounterparty.entries()]\n .map(([name, agg]) => ({ name, pct: agg.total / agg.count, contracts: agg.count }))\n .sort((a, b) => b.pct - a.pct)\n .slice(0, 8);\n }, [contractRows]);\n\n // ── Shipment mode mix + delivery rate ──────────────────────────────────────\n const shipmentMix = useMemo<Segment[]>(() => {\n const buckets = new Map<string, number>();\n for (const s of shipmentRows) {\n buckets.set(s.mode, (buckets.get(s.mode) ?? 0) + 1);\n }\n return [...buckets.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([mode, value]) => ({ label: formatEnumLabel(mode), value }));\n }, [shipmentRows]);\n\n const deliveryRate = useMemo(() => {\n const terminalOrDone = shipmentRows.filter((s) => s.status !== 'CANCELLED');\n if (terminalOrDone.length === 0) return null;\n const delivered = terminalOrDone.filter((s) => s.status === 'DELIVERED').length;\n return (delivered / terminalOrDone.length) * 100;\n }, [shipmentRows]);\n\n /** Change in delivery rate vs the preceding window, in percentage POINTS. */\n const deliveryRateDelta = useMemo<number | null>(() => {\n if (!shipmentsComplete || deliveryRate == null) return null;\n const terminalOrDone = priorShipmentRows.filter((s) => s.status !== 'CANCELLED');\n if (terminalOrDone.length === 0) return null;\n const delivered = terminalOrDone.filter((s) => s.status === 'DELIVERED').length;\n return deliveryRate - (delivered / terminalOrDone.length) * 100;\n }, [shipmentsComplete, priorShipmentRows, deliveryRate]);\n\n const inTransit = useMemo(\n () => shipmentRows.filter((s) => s.status === 'IN_TRANSIT' || s.status === 'LOADING').length,\n [shipmentRows]\n );\n\n // ── Inventory position ─────────────────────────────────────────────────────\n const inventoryMix = useMemo<Segment[]>(() => {\n const buckets = new Map<string, number>();\n for (const l of lotRows) {\n buckets.set(l.status, (buckets.get(l.status) ?? 0) + 1);\n }\n return [...buckets.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([status, value]) => ({ label: formatEnumLabel(status), value }));\n }, [lotRows]);\n\n // ── Export ─────────────────────────────────────────────────────────────────\n /**\n * Download the figures on screen as a metric/value CSV. Every row restates a\n * number this page already computed and displays — nothing is recomputed for\n * the file, so the export can never disagree with the view.\n */\n const handleExport = () => {\n const windowLabel = WINDOWS.find((w) => w.value === windowKey)?.label ?? `${windowDays} days`;\n const rows: { metric: string; value: string; detail: string }[] = [\n {\n metric: 'Window',\n value: windowLabel,\n detail: `Evaluated ${new Date(evaluatedAt).toISOString()}`,\n },\n {\n metric: 'Capacity utilization',\n value: utilization.overallPct != null ? formatPercent(utilization.overallPct) : '',\n detail: `${utilization.measuredAssets} assets with capacity on record`,\n },\n {\n metric: 'Contract fulfilment',\n value: fulfilment.pct != null ? formatPercent(fulfilment.pct) : '',\n detail: `${fulfilment.liveCount} live, ${fulfilment.atRisk} at risk`,\n },\n {\n metric: 'Delivery rate',\n value: deliveryRate != null ? formatPercent(deliveryRate) : '',\n detail:\n deliveryRateDelta != null\n ? `${Math.round(deliveryRateDelta)} pp ${PRIOR_WINDOW_LABEL}; ${inTransit} currently moving`\n : `${inTransit} currently moving`,\n },\n {\n metric: 'Recorded downtime (hours)',\n value: String(downtimeHours),\n detail:\n downtimeDelta != null\n ? `${Math.round(downtimeDelta)}% ${PRIOR_WINDOW_LABEL}; ${productionRows.length} production records`\n : `${productionRows.length} production records`,\n },\n {\n metric: carbonIntensity\n ? `Carbon intensity (t CO2e per ${formatUnit(carbonIntensity.unit)})`\n : 'Carbon intensity',\n value: carbonIntensity ? String(carbonIntensity.perUnit) : '',\n detail: carbonIntensity\n ? `${carbonIntensity.tonnes} t CO2e over ${carbonIntensity.volume} ${formatUnit(carbonIntensity.unit)} across ${carbonIntensity.assets} assets`\n : 'Not computable for this window',\n },\n {\n metric: inventoryValue.lead\n ? `Inventory on hand (${inventoryValue.lead[0]})`\n : 'Inventory on hand',\n value: inventoryValue.lead ? String(inventoryValue.lead[1]) : '',\n detail: `${inventoryValue.valuedLots} valued lots, ${inventoryValue.unvaluedLots} without a unit value`,\n },\n ...inventoryValue.others.map(([currency, total]) => ({\n metric: `Inventory on hand (${currency})`,\n value: String(total),\n detail: '',\n })),\n ...emissionsByScope.map((segment) => ({\n metric: `Emissions ${segment.label} (t CO2e)`,\n value: String(segment.value),\n detail: '',\n })),\n ...counterpartyFulfilment.map((row) => ({\n metric: `Fulfilment: ${row.name}`,\n value: formatPercent(row.pct),\n detail: `${row.contracts} live ${row.contracts === 1 ? 'contract' : 'contracts'}`,\n })),\n ];\n\n const csv = toCsv<{ metric: string; value: string; detail: string }>(\n [\n { header: 'Metric', value: (r) => r.metric },\n { header: 'Value', value: (r) => r.value },\n { header: 'Detail', value: (r) => r.detail },\n ],\n rows\n );\n const ok = downloadTextFile(`theglobalfuel-analytics-${windowDays}d.csv`, 'text/csv', csv);\n if (ok) notify.success('Analytics exported');\n else notify.error(\"This browser can't download the export.\");\n };\n\n const description = t(\n 'theglobalfuel.pages.analytics.description',\n 'Utilization, fulfilment and movement performance across the portfolio.'\n );\n const pageTitle = t('theglobalfuel.pages.analytics.title', 'Operational Analytics');\n\n if (isLoading) {\n return (\n <PageLayout\n title={pageTitle}\n description={description}\n dataTour=\"theglobalfuel-analytics-page\"\n >\n <LoadingView label=\"Aggregating operational data…\" />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout\n title={pageTitle}\n description={description}\n dataTour=\"theglobalfuel-analytics-page\"\n >\n <ErrorView\n message=\"Couldn't load the operational data for analytics.\"\n onRetry={() => {\n void assets.refetch();\n void production.refetch();\n void shipments.refetch();\n void contracts.refetch();\n void inventory.refetch();\n void emissions.refetch();\n }}\n />\n </PageLayout>\n );\n }\n\n const hasAnything =\n assetRows.length > 0 ||\n productionRows.length > 0 ||\n contractRows.length > 0 ||\n shipmentRows.length > 0 ||\n emissionRows.length > 0 ||\n lotRows.length > 0;\n\n return (\n <PageLayout\n title={pageTitle}\n description={description}\n dataTour=\"theglobalfuel-analytics-page\"\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <FilterTabs options={WINDOWS} value={windowKey} onChange={setWindowKey} />\n <Button variant=\"outline\" onClick={handleExport} disabled={!hasAnything}>\n <Download className=\"mr-1.5 h-4 w-4\" /> Export\n </Button>\n </div>\n }\n >\n {!hasAnything ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Activity className=\"h-10 w-10\" />}\n title=\"Nothing to analyse yet\"\n description=\"Register assets and record production, contracts and shipments — analytics is computed from those records.\"\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3\">\n <StatCard\n label=\"Capacity utilization\"\n value={utilization.overallPct != null ? formatPercent(utilization.overallPct) : '—'}\n icon={<Gauge className=\"h-4 w-4\" />}\n deltaLabel={\n utilization.overallPct != null\n ? `${formatNumber(utilization.measuredAssets)} assets with capacity on record`\n : 'No asset has both capacity and matching-unit production'\n }\n />\n <StatCard\n label=\"Contract fulfilment\"\n value={fulfilment.pct != null ? formatPercent(fulfilment.pct) : '—'}\n icon={<TrendingUp className=\"h-4 w-4\" />}\n deltaLabel={`${formatNumber(fulfilment.liveCount)} live · ${formatNumber(fulfilment.atRisk)} at risk`}\n />\n <StatCard\n label=\"Delivery rate\"\n value={deliveryRate != null ? formatPercent(deliveryRate) : '—'}\n icon={<Ship className=\"h-4 w-4\" />}\n {...(deliveryRateDelta != null\n ? { delta: Math.round(deliveryRateDelta), deltaSuffix: ' pp' }\n : {})}\n deltaLabel={\n deliveryRateDelta != null\n ? `${PRIOR_WINDOW_LABEL} · ${formatNumber(inTransit)} currently moving`\n : `${formatNumber(inTransit)} currently moving`\n }\n />\n <StatCard\n label=\"Recorded downtime\"\n value={`${formatNumber(downtimeHours)} h`}\n icon={<Timer className=\"h-4 w-4\" />}\n invertDelta\n {...(downtimeDelta != null ? { delta: Math.round(downtimeDelta) } : {})}\n deltaLabel={\n downtimeDelta != null\n ? `${PRIOR_WINDOW_LABEL} · ${formatNumber(productionRows.length)} records`\n : `Across ${formatNumber(productionRows.length)} production records`\n }\n />\n <StatCard\n label=\"Carbon intensity\"\n value={\n carbonIntensity\n ? `${formatNumber(carbonIntensity.perUnit, { maximumFractionDigits: 4 })} t CO₂e/${formatUnit(carbonIntensity.unit)}`\n : '—'\n }\n icon={<Leaf className=\"h-4 w-4\" />}\n invertDelta\n deltaLabel={\n carbonIntensity\n ? [\n `${formatTonnesCo2e(carbonIntensity.tonnes)} against ${formatVolume(carbonIntensity.volume, carbonIntensity.unit)} from ${formatNumber(carbonIntensity.assets)} assets`,\n carbonIntensity.unattributed > 0\n ? `${formatNumber(carbonIntensity.unattributed)} unattributed ${carbonIntensity.unattributed === 1 ? 'record' : 'records'} excluded`\n : null,\n ]\n .filter(Boolean)\n .join(' · ')\n : 'Needs emissions logged against an asset that also reports production'\n }\n />\n <StatCard\n label=\"Inventory on hand\"\n value={\n inventoryValue.lead\n ? formatMoney(inventoryValue.lead[1], inventoryValue.lead[0])\n : '—'\n }\n icon={<Coins className=\"h-4 w-4\" />}\n deltaLabel={\n inventoryValue.lead\n ? [\n `${formatNumber(inventoryValue.valuedLots)} valued ${inventoryValue.valuedLots === 1 ? 'lot' : 'lots'}`,\n ...inventoryValue.others.map(\n ([currency, total]) => `${formatMoney(total, currency)}`\n ),\n ].join(' · ')\n : 'No lot on hand carries a unit value'\n }\n />\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-5 lg:col-span-2\">\n <div className=\"mb-4\">\n <h2 className=\"text-base font-semibold text-text-primary\">Production trend</h2>\n <p className=\"text-xs text-text-tertiary\">\n {leadUnit\n ? `Monthly volume in ${formatEnumLabel(leadUnit)} — the unit carrying most of the portfolio. Other units are charted on their own assets.`\n : 'No production recorded in this window.'}\n </p>\n </div>\n {trendSeries.length > 0 ? (\n <AreaChart series={trendSeries} height={220} title=\"Monthly production volume\" />\n ) : (\n <div className=\"rounded-xl border border-dashed border-border-seam py-10 text-center text-sm text-text-secondary\">\n No production records in this window.\n </div>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-5\">\n <div className=\"mb-4\">\n <h2 className=\"text-base font-semibold text-text-primary\">Movement mix</h2>\n <p className=\"text-xs text-text-tertiary\">Shipments by transport mode.</p>\n </div>\n {shipmentMix.length > 0 ? (\n <DonutChart\n segments={shipmentMix}\n centerLabel={formatNumber(shipmentRows.length)}\n centerCaption=\"shipments\"\n showLegend\n title=\"Shipments by transport mode\"\n />\n ) : (\n <div className=\"rounded-xl border border-dashed border-border-seam py-10 text-center text-sm text-text-secondary\">\n No shipments in this window.\n </div>\n )}\n </GlassCard>\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-5\">\n <div className=\"mb-4\">\n <h2 className=\"text-base font-semibold text-text-primary\">Utilization by asset</h2>\n <p className=\"text-xs text-text-tertiary\">\n Produced volume against nameplate capacity for the window. Assets without a\n capacity figure, or whose production is recorded in a different unit, are omitted.\n </p>\n </div>\n {utilization.top.length > 0 ? (\n <BarChart\n series={utilization.top.map((r) => ({ label: r.name, value: Math.round(r.pct) }))}\n height={220}\n colorful\n valueFormatter={(v) => `${v}%`}\n title=\"Capacity utilization by asset\"\n />\n ) : (\n <div className=\"rounded-xl border border-dashed border-border-seam py-10 text-center text-sm text-text-secondary\">\n No asset has both a capacity figure and matching-unit production in this window.\n </div>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-5\">\n <div className=\"mb-4\">\n <h2 className=\"text-base font-semibold text-text-primary\">\n Least-fulfilled contracts\n </h2>\n <p className=\"text-xs text-text-tertiary\">\n Delivered against contracted volume for live (signed/active) contracts.\n </p>\n </div>\n {fulfilment.top.length > 0 ? (\n <BarChart\n series={fulfilment.top.map((r) => ({\n label: r.label,\n value: Math.round(r.value),\n }))}\n height={220}\n colorful\n valueFormatter={(v) => `${v}%`}\n title=\"Contract fulfilment\"\n />\n ) : (\n <div className=\"rounded-xl border border-dashed border-border-seam py-10 text-center text-sm text-text-secondary\">\n No live contracts with a contracted volume.\n </div>\n )}\n </GlassCard>\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-5\">\n <div className=\"mb-4\">\n <h2 className=\"text-base font-semibold text-text-primary\">Emissions by scope</h2>\n <p className=\"text-xs text-text-tertiary\">\n Tonnes CO₂e logged in this window, split by GHG protocol scope.\n {emissionRows.length > 0\n ? ` ${formatNumber(verifiedEmissions)} of ${formatNumber(emissionRows.length)} records verified.`\n : ''}\n </p>\n </div>\n {emissionsByScope.length > 0 ? (\n <DonutChart\n segments={emissionsByScope}\n centerLabel={formatNumber(emissionsTotal, { maximumFractionDigits: 0 })}\n centerCaption=\"t CO₂e\"\n showLegend\n valueFormatter={(v) => formatNumber(v, { maximumFractionDigits: 1 })}\n title=\"Emissions by GHG scope\"\n />\n ) : (\n <div className=\"rounded-xl border border-dashed border-border-seam py-10 text-center text-sm text-text-secondary\">\n No emissions recorded in this window.\n </div>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-5\">\n <div className=\"mb-4\">\n <h2 className=\"text-base font-semibold text-text-primary\">\n Fulfilment by counterparty\n </h2>\n <p className=\"text-xs text-text-tertiary\">\n Mean delivered-against-contracted across each counterparty's live\n (signed/active) contracts. Ratios are unitless, so no volumes are summed across\n units of measure.\n </p>\n </div>\n {counterpartyFulfilment.length > 0 ? (\n <ul className=\"space-y-3.5\">\n {counterpartyFulfilment.map((row) => (\n <li key={row.name}>\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"truncate text-sm text-text-secondary\">{row.name}</span>\n <span className=\"shrink-0 text-sm font-medium tabular-nums text-text-primary\">\n {formatPercent(row.pct)}\n </span>\n </div>\n <MiniBar\n value={row.pct}\n className=\"mt-1.5\"\n barClassName=\"bg-status-success-text\"\n />\n <p className=\"mt-1 text-[11px] text-text-tertiary\">\n {formatNumber(row.contracts)} live{' '}\n {row.contracts === 1 ? 'contract' : 'contracts'}\n </p>\n </li>\n ))}\n </ul>\n ) : (\n <div className=\"rounded-xl border border-dashed border-border-seam py-10 text-center text-sm text-text-secondary\">\n No live contracts with a contracted volume.\n </div>\n )}\n </GlassCard>\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-5\">\n <div className=\"mb-4 flex items-center gap-2\">\n <Package className=\"h-4 w-4 text-text-secondary\" />\n <h2 className=\"text-base font-semibold text-text-primary\">Inventory position</h2>\n </div>\n {inventoryMix.length > 0 ? (\n <DonutChart\n segments={inventoryMix}\n centerLabel={formatNumber(lotRows.length)}\n centerCaption=\"lots\"\n showLegend\n title=\"Inventory lots by status\"\n />\n ) : (\n <div className=\"rounded-xl border border-dashed border-border-seam py-10 text-center text-sm text-text-secondary\">\n No inventory lots recorded.\n </div>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"glass\" className=\"p-5 lg:col-span-2\">\n <h2 className=\"mb-4 text-base font-semibold text-text-primary\">\n Production volume by unit\n </h2>\n <p className=\"mb-4 text-xs text-text-tertiary\">\n Volumes are never summed across units of measure. Each row is an independent total.\n </p>\n {productionByUnit.size > 0 ? (\n <ul className=\"divide-y divide-border-subtle\">\n {[...productionByUnit.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([unit, value]) => (\n <li key={unit} className=\"flex items-center justify-between py-2.5\">\n <span className=\"text-sm text-text-secondary\">{formatEnumLabel(unit)}</span>\n <span className=\"text-sm font-medium tabular-nums text-text-primary\">\n {formatVolume(value, unit)}\n </span>\n </li>\n ))}\n </ul>\n ) : (\n <div className=\"rounded-xl border border-dashed border-border-seam py-10 text-center text-sm text-text-secondary\">\n No production records in this window.\n </div>\n )}\n </GlassCard>\n </div>\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -15,6 +15,7 @@ import { useThePageView as pe } from "../../hooks/useThePageView.js";
|
|
|
15
15
|
import { useTheNativeControls as me } from "../../hooks/useTheNativeControls.js";
|
|
16
16
|
import "../../hooks/index.js";
|
|
17
17
|
import { ASSET_TRANSITIONS as he, nextStatuses as ge } from "../../utils/transitions.js";
|
|
18
|
+
import "../../utils/index.js";
|
|
18
19
|
import { Tabs as _e } from "../../components/Tabs.js";
|
|
19
20
|
import { Pagination as ve } from "../../components/Pagination.js";
|
|
20
21
|
import { TheDiscussion as ye } from "../../components/TheDiscussion.js";
|