@burdenoff/microfe-assethandler 2026.817.2 → 2026.820.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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EsgDashboardPage.js","names":[],"sources":["../../../src/assethandler/pages/EsgDashboardPage.tsx"],"sourcesContent":["import { Leaf, Plus, TrendingUp, TrendingDown, Minus } from 'lucide-react';\nimport type React from 'react';\nimport {\n Button,\n IllustratedEmptyState,\n GlassCard,\n PagePurpose,\n Input,\n Label,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { useMemo, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Pagination,\n usePagination,\n SectionHeader,\n StatTile,\n ChartCard,\n DonutChart,\n BarsChart,\n seriesColor,\n formatNumber,\n type DonutSlice,\n} from '../components';\nimport { cn } from '../utils/cn';\nimport { notify } from '../utils/toast';\n\nconst ESG_METRICS_QUERY = gql`\n query AssetHandlerEsgMetrics {\n esgMetrics {\n id\n metricKey\n metricName\n category\n unit\n value\n target\n trend\n periodStart\n periodEnd\n }\n }\n`;\n\nconst UPSERT_ESG_METRIC_MUTATION = gql`\n mutation AssetHandlerUpsertEsgMetric($input: UpsertEsgMetricInput!) {\n upsertEsgMetric(input: $input) {\n id\n metricName\n }\n }\n`;\n\ninterface EsgMetric {\n id: string;\n metricKey: string;\n metricName: string;\n category: string;\n unit: string;\n value: number;\n target: number | null;\n trend: string | null;\n periodStart: string;\n periodEnd: string;\n}\n\ninterface UpsertEsgMetricResult {\n upsertEsgMetric: { id: string; metricName: string };\n}\n\nconst ESG_CATEGORIES = ['environment', 'social', 'governance'] as const;\nconst ESG_TRENDS = ['improving', 'stable', 'declining'] as const;\n\nconst TREND_META: Record<\n string,\n {\n label: string;\n icon: typeof TrendingUp;\n tone: 'positive' | 'negative' | 'neutral';\n dir: 'up' | 'down' | 'neutral';\n }\n> = {\n improving: { label: 'Improving', icon: TrendingUp, tone: 'positive', dir: 'up' },\n declining: { label: 'Declining', icon: TrendingDown, tone: 'negative', dir: 'down' },\n stable: { label: 'Stable', icon: Minus, tone: 'neutral', dir: 'neutral' },\n};\n\nconst TREND_PILL: Record<'positive' | 'negative' | 'neutral', string> = {\n positive: 'bg-status-success-bg-subtle text-status-success-text',\n negative: 'bg-status-error-bg-subtle text-status-error-text',\n neutral: 'bg-bg-sunken text-text-secondary',\n};\n\nconst slugify = (value: string): string =>\n value\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n\nfunction firstOfMonthISO(): string {\n const now = new Date();\n return new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10);\n}\n\nfunction todayISO(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction TrendBadge({ trend }: { trend: string | null }) {\n const meta = TREND_META[trend ?? 'stable'] ?? TREND_META.stable;\n const Icon = meta.icon;\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium',\n TREND_PILL[meta.tone]\n )}\n >\n <Icon className=\"h-3 w-3\" />\n {meta.label}\n </span>\n );\n}\n\nfunction RecordMetricDialog({\n open,\n onOpenChange,\n onSaved,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onSaved: () => void;\n}) {\n const [metricName, setMetricName] = useState('');\n const [category, setCategory] = useState<(typeof ESG_CATEGORIES)[number]>('environment');\n const [unit, setUnit] = useState('');\n const [value, setValue] = useState('');\n const [target, setTarget] = useState('');\n const [trend, setTrend] = useState<(typeof ESG_TRENDS)[number]>('stable');\n const [periodStart, setPeriodStart] = useState(firstOfMonthISO());\n const [periodEnd, setPeriodEnd] = useState(todayISO());\n\n const [upsertEsgMetric, { loading: saving }] = useMutation<UpsertEsgMetricResult>(\n UPSERT_ESG_METRIC_MUTATION,\n {\n onCompleted: (result) => {\n notify.success('Metric saved', `${result.upsertEsgMetric.metricName} was recorded.`);\n setMetricName('');\n setUnit('');\n setValue('');\n setTarget('');\n onOpenChange(false);\n onSaved();\n },\n onError: (error) => notify.error('Could not save metric', error.message),\n }\n );\n\n const handleSubmit = (event: React.FormEvent) => {\n event.preventDefault();\n const parsedValue = Number(value);\n if (!metricName.trim() || !unit.trim()) {\n notify.warning('Missing details', 'Metric name and unit are required.');\n return;\n }\n if (!Number.isFinite(parsedValue) || value.trim() === '') {\n notify.warning('Invalid value', 'Enter a numeric metric value.');\n return;\n }\n if (!periodStart || !periodEnd) {\n notify.warning('Missing period', 'Period start and end are required.');\n return;\n }\n const parsedTarget = target.trim() === '' ? null : Number(target);\n void upsertEsgMetric({\n variables: {\n input: {\n metricKey: slugify(metricName),\n metricName: metricName.trim(),\n category,\n unit: unit.trim(),\n value: parsedValue,\n target: parsedTarget != null && Number.isFinite(parsedTarget) ? parsedTarget : null,\n trend,\n periodStart: new Date(periodStart).toISOString(),\n periodEnd: new Date(periodEnd).toISOString(),\n },\n },\n });\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-2xl\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Save Metric</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Record an ESG metric to track progress toward your sustainability targets.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <form className=\"space-y-3\" onSubmit={handleSubmit}>\n <div className=\"grid gap-3 md:grid-cols-3\">\n <div className=\"space-y-2\">\n <Label>Metric Name *</Label>\n <Input\n placeholder=\"e.g., Scope 2 carbon emissions\"\n value={metricName}\n onChange={(e) => setMetricName(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Category</Label>\n <Select value={category} onValueChange={(v) => setCategory(v as typeof category)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ESG_CATEGORIES.map((c) => (\n <SelectItem key={c} value={c} className=\"capitalize\">\n {c}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label>Unit *</Label>\n <Input\n placeholder=\"e.g., tCO2e\"\n value={unit}\n onChange={(e) => setUnit(e.target.value)}\n />\n </div>\n </div>\n <div className=\"grid gap-3 md:grid-cols-3\">\n <div className=\"space-y-2\">\n <Label>Value *</Label>\n <Input\n type=\"number\"\n step=\"any\"\n value={value}\n onChange={(e) => setValue(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Target</Label>\n <Input\n type=\"number\"\n step=\"any\"\n value={target}\n onChange={(e) => setTarget(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Trend</Label>\n <Select value={trend} onValueChange={(v) => setTrend(v as typeof trend)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ESG_TRENDS.map((t) => (\n <SelectItem key={t} value={t} className=\"capitalize\">\n {t}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label>Period Start *</Label>\n <Input\n type=\"date\"\n value={periodStart}\n onChange={(e) => setPeriodStart(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Period End *</Label>\n <Input type=\"date\" value={periodEnd} onChange={(e) => setPeriodEnd(e.target.value)} />\n </div>\n </div>\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={saving}\n >\n Cancel\n </Button>\n <Button type=\"submit\" disabled={saving}>\n {saving ? 'Saving…' : 'Save Metric'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nexport function EsgDashboardPage() {\n const [dialogOpen, setDialogOpen] = useState(false);\n const [category, setCategory] = useState<string>('ALL');\n\n const { data, loading, refetch } = useQuery<{ esgMetrics: EsgMetric[] }>(ESG_METRICS_QUERY, {\n fetchPolicy: 'cache-and-network',\n });\n\n const metrics = useMemo(() => data?.esgMetrics ?? [], [data?.esgMetrics]);\n\n const headline = useMemo(() => metrics.slice(0, 4), [metrics]);\n\n const filtered = useMemo(\n () => (category === 'ALL' ? metrics : metrics.filter((m) => m.category === category)),\n [metrics, category]\n );\n\n const categoryMix = useMemo<DonutSlice[]>(() => {\n const counts = metrics.reduce<Record<string, number>>((acc, m) => {\n acc[m.category] = (acc[m.category] ?? 0) + 1;\n return acc;\n }, {});\n return Object.entries(counts).map(([cat, value], i) => ({\n name: cat.charAt(0).toUpperCase() + cat.slice(1),\n value,\n color: seriesColor(i),\n }));\n }, [metrics]);\n\n const valueVsTarget = useMemo(\n () =>\n metrics\n .filter((m) => m.target != null)\n .slice(0, 8)\n .map((m) => ({ name: m.metricName, value: m.value, target: m.target as number })),\n [metrics]\n );\n\n const categoryCounts = useMemo(() => {\n const counts: Record<string, number> = { ALL: metrics.length };\n for (const m of metrics) counts[m.category] = (counts[m.category] ?? 0) + 1;\n return counts;\n }, [metrics]);\n\n const pagination = usePagination(filtered, { pageSize: 8, resetKey: category });\n\n return (\n <div className=\"space-y-5 p-4 sm:p-6\">\n <SectionHeader\n hero\n icon={Leaf}\n title=\"ESG Dashboard\"\n description=\"Measure the environmental and governance impact of your estate against targets.\"\n actions={\n <Button className=\"w-full sm:w-auto\" onClick={() => setDialogOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" />\n Save Metric\n </Button>\n }\n />\n <PagePurpose>\n Track energy use, carbon emissions and responsible disposal against targets — and produce\n the evidence ESG reporting and audits require.\n </PagePurpose>\n\n <RecordMetricDialog\n open={dialogOpen}\n onOpenChange={setDialogOpen}\n onSaved={() => void refetch()}\n />\n\n {loading && metrics.length === 0 ? (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {Array.from({ length: 4 }).map((_, i) => (\n <div key={i} className=\"h-28 rounded-xl bg-bg-sunken motion-safe:animate-vc-shimmer\" />\n ))}\n </div>\n ) : null}\n\n {!loading && metrics.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No ESG metrics recorded yet\"\n description=\"Save a metric to start tracking sustainability progress against your targets.\"\n />\n ) : null}\n\n {headline.length > 0 ? (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {headline.map((m) => {\n const meta = TREND_META[m.trend ?? 'stable'] ?? TREND_META.stable;\n return (\n <StatTile\n key={m.id}\n label={`${m.metricName} (${m.unit})`}\n value={m.value}\n format={formatNumber}\n delta={meta.label}\n deltaDirection={meta.dir}\n deltaTone={meta.tone}\n icon={Leaf}\n />\n );\n })}\n </div>\n ) : null}\n\n {metrics.length > 0 ? (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <ChartCard title=\"Value vs. target\" subtitle=\"Metrics measured against their targets\">\n {valueVsTarget.length > 0 ? (\n <BarsChart\n data={valueVsTarget}\n xKey=\"name\"\n series={[\n { key: 'value', label: 'Value' },\n { key: 'target', label: 'Target' },\n ]}\n orientation=\"horizontal\"\n height={240}\n categoryWidth={150}\n ariaLabel=\"Metric value versus target\"\n />\n ) : (\n <div className=\"flex h-[240px] items-center justify-center text-sm text-text-secondary\">\n Set a target on a metric to compare value against target.\n </div>\n )}\n </ChartCard>\n <ChartCard title=\"By category\" subtitle=\"Metrics across E / S / G\">\n <DonutChart\n data={categoryMix}\n height={240}\n centerLabel=\"Metrics\"\n centerValue={metrics.length}\n ariaLabel=\"Metrics by category\"\n />\n </ChartCard>\n </div>\n ) : null}\n\n {metrics.length > 0 ? (\n <>\n <div className=\"flex flex-wrap items-center gap-2\">\n {(['ALL', ...ESG_CATEGORIES] as const).map((c) => (\n <button\n key={c}\n type=\"button\"\n onClick={() => setCategory(c)}\n className={cn(\n 'rounded-full px-3.5 py-1.5 text-sm font-medium capitalize transition-colors',\n category === c\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {c === 'ALL' ? 'All' : c}\n <span className=\"ml-1.5 text-xs opacity-70 tabular-nums\">\n {categoryCounts[c] ?? 0}\n </span>\n </button>\n ))}\n </div>\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {pagination.pageItems.map((metric) => {\n const pct =\n metric.target && metric.target > 0\n ? Math.min(100, Math.round((metric.value / metric.target) * 100))\n : null;\n return (\n <GlassCard key={metric.id} treatment=\"glass\" className=\"border-border-seam\">\n <div className=\"space-y-3 p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <p className=\"text-xs uppercase tracking-wide text-text-secondary\">\n {metric.category}\n </p>\n <h2 className=\"mt-0.5 text-base font-semibold text-text-primary\">\n {metric.metricName}\n </h2>\n </div>\n <TrendBadge trend={metric.trend} />\n </div>\n <div className=\"flex items-baseline gap-2\">\n <span className=\"text-2xl font-bold tabular-nums text-text-primary\">\n {formatNumber(metric.value)}\n </span>\n <span className=\"text-sm text-text-secondary\">{metric.unit}</span>\n {metric.target != null ? (\n <span className=\"text-xs text-text-muted\">\n target {formatNumber(metric.target)}\n </span>\n ) : null}\n </div>\n {pct != null ? (\n <div className=\"space-y-1\">\n <div className=\"flex items-center justify-between text-xs text-text-secondary\">\n <span>Progress to target</span>\n <span className=\"font-medium tabular-nums\">{pct}%</span>\n </div>\n <div className=\"h-2 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n ) : null}\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n <Pagination\n page={pagination.page}\n pageCount={pagination.pageCount}\n rangeStart={pagination.rangeStart}\n rangeEnd={pagination.rangeEnd}\n totalItems={pagination.totalItems}\n canPrev={pagination.canPrev}\n canNext={pagination.canNext}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel=\"metrics\"\n />\n </>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EsgDashboardPage.js","names":[],"sources":["../../../src/assethandler/pages/EsgDashboardPage.tsx"],"sourcesContent":["import { Leaf, Plus, TrendingUp, TrendingDown, Minus } from 'lucide-react';\nimport type React from 'react';\nimport {\n Button,\n IllustratedEmptyState,\n GlassCard,\n PagePurpose,\n Input,\n Label,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { useMemo, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Pagination,\n usePagination,\n SectionHeader,\n StatTile,\n ChartCard,\n DonutChart,\n BarsChart,\n seriesColor,\n formatNumber,\n type DonutSlice,\n} from '../components';\nimport { cn } from '../utils/cn';\nimport { notify } from '../utils/toast';\n\nconst ESG_METRICS_QUERY = gql`\n query AssetHandlerEsgMetrics {\n esgMetrics {\n id\n metricKey\n metricName\n category\n unit\n value\n target\n trend\n periodStart\n periodEnd\n }\n }\n`;\n\nconst UPSERT_ESG_METRIC_MUTATION = gql`\n mutation AssetHandlerUpsertEsgMetric($input: UpsertEsgMetricInput!) {\n upsertEsgMetric(input: $input) {\n id\n metricName\n }\n }\n`;\n\ninterface EcoMetric {\n id: string;\n metricKey: string;\n metricName: string;\n category: string;\n unit: string;\n value: number;\n target: number | null;\n trend: string | null;\n periodStart: string;\n periodEnd: string;\n}\n\ninterface UpsertEsgMetricResult {\n upsertEsgMetric: { id: string; metricName: string };\n}\n\nconst ESG_CATEGORIES = ['environment', 'social', 'governance'] as const;\nconst ESG_TRENDS = ['improving', 'stable', 'declining'] as const;\n\nconst TREND_META: Record<\n string,\n {\n label: string;\n icon: typeof TrendingUp;\n tone: 'positive' | 'negative' | 'neutral';\n dir: 'up' | 'down' | 'neutral';\n }\n> = {\n improving: { label: 'Improving', icon: TrendingUp, tone: 'positive', dir: 'up' },\n declining: { label: 'Declining', icon: TrendingDown, tone: 'negative', dir: 'down' },\n stable: { label: 'Stable', icon: Minus, tone: 'neutral', dir: 'neutral' },\n};\n\nconst TREND_PILL: Record<'positive' | 'negative' | 'neutral', string> = {\n positive: 'bg-status-success-bg-subtle text-status-success-text',\n negative: 'bg-status-error-bg-subtle text-status-error-text',\n neutral: 'bg-bg-sunken text-text-secondary',\n};\n\nconst slugify = (value: string): string =>\n value\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n\nfunction firstOfMonthISO(): string {\n const now = new Date();\n return new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10);\n}\n\nfunction todayISO(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction TrendBadge({ trend }: { trend: string | null }) {\n const meta = TREND_META[trend ?? 'stable'] ?? TREND_META.stable;\n const Icon = meta.icon;\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium',\n TREND_PILL[meta.tone]\n )}\n >\n <Icon className=\"h-3 w-3\" />\n {meta.label}\n </span>\n );\n}\n\nfunction RecordMetricDialog({\n open,\n onOpenChange,\n onSaved,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onSaved: () => void;\n}) {\n const [metricName, setMetricName] = useState('');\n const [category, setCategory] = useState<(typeof ESG_CATEGORIES)[number]>('environment');\n const [unit, setUnit] = useState('');\n const [value, setValue] = useState('');\n const [target, setTarget] = useState('');\n const [trend, setTrend] = useState<(typeof ESG_TRENDS)[number]>('stable');\n const [periodStart, setPeriodStart] = useState(firstOfMonthISO());\n const [periodEnd, setPeriodEnd] = useState(todayISO());\n\n const [upsertEsgMetric, { loading: saving }] = useMutation<UpsertEsgMetricResult>(\n UPSERT_ESG_METRIC_MUTATION,\n {\n onCompleted: (result) => {\n notify.success('Metric saved', `${result.upsertEsgMetric.metricName} was recorded.`);\n setMetricName('');\n setUnit('');\n setValue('');\n setTarget('');\n onOpenChange(false);\n onSaved();\n },\n onError: (error) => notify.error('Could not save metric', error.message),\n }\n );\n\n const handleSubmit = (event: React.FormEvent) => {\n event.preventDefault();\n const parsedValue = Number(value);\n if (!metricName.trim() || !unit.trim()) {\n notify.warning('Missing details', 'Metric name and unit are required.');\n return;\n }\n if (!Number.isFinite(parsedValue) || value.trim() === '') {\n notify.warning('Invalid value', 'Enter a numeric metric value.');\n return;\n }\n if (!periodStart || !periodEnd) {\n notify.warning('Missing period', 'Period start and end are required.');\n return;\n }\n const parsedTarget = target.trim() === '' ? null : Number(target);\n void upsertEsgMetric({\n variables: {\n input: {\n metricKey: slugify(metricName),\n metricName: metricName.trim(),\n category,\n unit: unit.trim(),\n value: parsedValue,\n target: parsedTarget != null && Number.isFinite(parsedTarget) ? parsedTarget : null,\n trend,\n periodStart: new Date(periodStart).toISOString(),\n periodEnd: new Date(periodEnd).toISOString(),\n },\n },\n });\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-2xl\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Save Metric</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Record an ESG metric to track progress toward your sustainability targets.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <form className=\"space-y-3\" onSubmit={handleSubmit}>\n <div className=\"grid gap-3 md:grid-cols-3\">\n <div className=\"space-y-2\">\n <Label>Metric Name *</Label>\n <Input\n placeholder=\"e.g., Scope 2 carbon emissions\"\n value={metricName}\n onChange={(e) => setMetricName(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Category</Label>\n <Select value={category} onValueChange={(v) => setCategory(v as typeof category)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ESG_CATEGORIES.map((c) => (\n <SelectItem key={c} value={c} className=\"capitalize\">\n {c}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label>Unit *</Label>\n <Input\n placeholder=\"e.g., tCO2e\"\n value={unit}\n onChange={(e) => setUnit(e.target.value)}\n />\n </div>\n </div>\n <div className=\"grid gap-3 md:grid-cols-3\">\n <div className=\"space-y-2\">\n <Label>Value *</Label>\n <Input\n type=\"number\"\n step=\"any\"\n value={value}\n onChange={(e) => setValue(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Target</Label>\n <Input\n type=\"number\"\n step=\"any\"\n value={target}\n onChange={(e) => setTarget(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Trend</Label>\n <Select value={trend} onValueChange={(v) => setTrend(v as typeof trend)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ESG_TRENDS.map((t) => (\n <SelectItem key={t} value={t} className=\"capitalize\">\n {t}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label>Period Start *</Label>\n <Input\n type=\"date\"\n value={periodStart}\n onChange={(e) => setPeriodStart(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Period End *</Label>\n <Input type=\"date\" value={periodEnd} onChange={(e) => setPeriodEnd(e.target.value)} />\n </div>\n </div>\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={saving}\n >\n Cancel\n </Button>\n <Button type=\"submit\" disabled={saving}>\n {saving ? 'Saving…' : 'Save Metric'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nexport function EsgDashboardPage() {\n const [dialogOpen, setDialogOpen] = useState(false);\n const [category, setCategory] = useState<string>('ALL');\n\n const { data, loading, refetch } = useQuery<{ esgMetrics: EcoMetric[] }>(ESG_METRICS_QUERY, {\n fetchPolicy: 'cache-and-network',\n });\n\n const metrics = useMemo(() => data?.esgMetrics ?? [], [data?.esgMetrics]);\n\n const headline = useMemo(() => metrics.slice(0, 4), [metrics]);\n\n const filtered = useMemo(\n () => (category === 'ALL' ? metrics : metrics.filter((m) => m.category === category)),\n [metrics, category]\n );\n\n const categoryMix = useMemo<DonutSlice[]>(() => {\n const counts = metrics.reduce<Record<string, number>>((acc, m) => {\n acc[m.category] = (acc[m.category] ?? 0) + 1;\n return acc;\n }, {});\n return Object.entries(counts).map(([cat, value], i) => ({\n name: cat.charAt(0).toUpperCase() + cat.slice(1),\n value,\n color: seriesColor(i),\n }));\n }, [metrics]);\n\n const valueVsTarget = useMemo(\n () =>\n metrics\n .filter((m) => m.target != null)\n .slice(0, 8)\n .map((m) => ({ name: m.metricName, value: m.value, target: m.target as number })),\n [metrics]\n );\n\n const categoryCounts = useMemo(() => {\n const counts: Record<string, number> = { ALL: metrics.length };\n for (const m of metrics) counts[m.category] = (counts[m.category] ?? 0) + 1;\n return counts;\n }, [metrics]);\n\n const pagination = usePagination(filtered, { pageSize: 8, resetKey: category });\n\n return (\n <div className=\"space-y-5 p-4 sm:p-6\">\n <SectionHeader\n hero\n icon={Leaf}\n title=\"ESG Dashboard\"\n description=\"Measure the environmental and governance impact of your estate against targets.\"\n actions={\n <Button className=\"w-full sm:w-auto\" onClick={() => setDialogOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" />\n Save Metric\n </Button>\n }\n />\n <PagePurpose>\n Track energy use, carbon emissions and responsible disposal against targets — and produce\n the evidence ESG reporting and audits require.\n </PagePurpose>\n\n <RecordMetricDialog\n open={dialogOpen}\n onOpenChange={setDialogOpen}\n onSaved={() => void refetch()}\n />\n\n {loading && metrics.length === 0 ? (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {Array.from({ length: 4 }).map((_, i) => (\n <div key={i} className=\"h-28 rounded-xl bg-bg-sunken motion-safe:animate-vc-shimmer\" />\n ))}\n </div>\n ) : null}\n\n {!loading && metrics.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No ESG metrics recorded yet\"\n description=\"Save a metric to start tracking sustainability progress against your targets.\"\n />\n ) : null}\n\n {headline.length > 0 ? (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {headline.map((m) => {\n const meta = TREND_META[m.trend ?? 'stable'] ?? TREND_META.stable;\n return (\n <StatTile\n key={m.id}\n label={`${m.metricName} (${m.unit})`}\n value={m.value}\n format={formatNumber}\n delta={meta.label}\n deltaDirection={meta.dir}\n deltaTone={meta.tone}\n icon={Leaf}\n />\n );\n })}\n </div>\n ) : null}\n\n {metrics.length > 0 ? (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <ChartCard title=\"Value vs. target\" subtitle=\"Metrics measured against their targets\">\n {valueVsTarget.length > 0 ? (\n <BarsChart\n data={valueVsTarget}\n xKey=\"name\"\n series={[\n { key: 'value', label: 'Value' },\n { key: 'target', label: 'Target' },\n ]}\n orientation=\"horizontal\"\n height={240}\n categoryWidth={150}\n ariaLabel=\"Metric value versus target\"\n />\n ) : (\n <div className=\"flex h-[240px] items-center justify-center text-sm text-text-secondary\">\n Set a target on a metric to compare value against target.\n </div>\n )}\n </ChartCard>\n <ChartCard title=\"By category\" subtitle=\"Metrics across E / S / G\">\n <DonutChart\n data={categoryMix}\n height={240}\n centerLabel=\"Metrics\"\n centerValue={metrics.length}\n ariaLabel=\"Metrics by category\"\n />\n </ChartCard>\n </div>\n ) : null}\n\n {metrics.length > 0 ? (\n <>\n <div className=\"flex flex-wrap items-center gap-2\">\n {(['ALL', ...ESG_CATEGORIES] as const).map((c) => (\n <button\n key={c}\n type=\"button\"\n onClick={() => setCategory(c)}\n className={cn(\n 'rounded-full px-3.5 py-1.5 text-sm font-medium capitalize transition-colors',\n category === c\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {c === 'ALL' ? 'All' : c}\n <span className=\"ml-1.5 text-xs opacity-70 tabular-nums\">\n {categoryCounts[c] ?? 0}\n </span>\n </button>\n ))}\n </div>\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {pagination.pageItems.map((metric) => {\n const pct =\n metric.target && metric.target > 0\n ? Math.min(100, Math.round((metric.value / metric.target) * 100))\n : null;\n return (\n <GlassCard key={metric.id} treatment=\"glass\" className=\"border-border-seam\">\n <div className=\"space-y-3 p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <p className=\"text-xs uppercase tracking-wide text-text-secondary\">\n {metric.category}\n </p>\n <h2 className=\"mt-0.5 text-base font-semibold text-text-primary\">\n {metric.metricName}\n </h2>\n </div>\n <TrendBadge trend={metric.trend} />\n </div>\n <div className=\"flex items-baseline gap-2\">\n <span className=\"text-2xl font-bold tabular-nums text-text-primary\">\n {formatNumber(metric.value)}\n </span>\n <span className=\"text-sm text-text-secondary\">{metric.unit}</span>\n {metric.target != null ? (\n <span className=\"text-xs text-text-muted\">\n target {formatNumber(metric.target)}\n </span>\n ) : null}\n </div>\n {pct != null ? (\n <div className=\"space-y-1\">\n <div className=\"flex items-center justify-between text-xs text-text-secondary\">\n <span>Progress to target</span>\n <span className=\"font-medium tabular-nums\">{pct}%</span>\n </div>\n <div className=\"h-2 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n ) : null}\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n <Pagination\n page={pagination.page}\n pageCount={pagination.pageCount}\n rangeStart={pagination.rangeStart}\n rangeEnd={pagination.rangeEnd}\n totalItems={pagination.totalItems}\n canPrev={pagination.canPrev}\n canNext={pagination.canNext}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel=\"metrics\"\n />\n </>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|