@burdenoff/microfe-assethandler 2026.826.1 → 2026.826.2
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,4 +1,47 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
2
|
|
|
3
|
+
declare const ESG_CATEGORIES: readonly ["environment", "social", "governance"];
|
|
4
|
+
declare const ESG_TRENDS: readonly ["improving", "stable", "declining"];
|
|
5
|
+
interface MetricFormValues {
|
|
6
|
+
metricName: string;
|
|
7
|
+
category: (typeof ESG_CATEGORIES)[number];
|
|
8
|
+
unit: string;
|
|
9
|
+
value: number;
|
|
10
|
+
target: number | null;
|
|
11
|
+
trend: (typeof ESG_TRENDS)[number];
|
|
12
|
+
periodStart: string;
|
|
13
|
+
periodEnd: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function buildMetricMutationVariables(metricId: string | null, form: MetricFormValues): {
|
|
16
|
+
kind: "update";
|
|
17
|
+
variables: {
|
|
18
|
+
id: string;
|
|
19
|
+
input: {
|
|
20
|
+
metricName: string;
|
|
21
|
+
category: "environment" | "social" | "governance";
|
|
22
|
+
unit: string;
|
|
23
|
+
value: number;
|
|
24
|
+
target: number | null;
|
|
25
|
+
trend: "improving" | "stable" | "declining";
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
} | {
|
|
29
|
+
kind: "create";
|
|
30
|
+
variables: {
|
|
31
|
+
input: {
|
|
32
|
+
metricKey: string;
|
|
33
|
+
periodStart: string;
|
|
34
|
+
periodEnd: string;
|
|
35
|
+
metricName: string;
|
|
36
|
+
category: "environment" | "social" | "governance";
|
|
37
|
+
unit: string;
|
|
38
|
+
value: number;
|
|
39
|
+
target: number | null;
|
|
40
|
+
trend: "improving" | "stable" | "declining";
|
|
41
|
+
};
|
|
42
|
+
id?: undefined;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
3
45
|
export declare function EsgDashboardPage(): React.JSX.Element;
|
|
46
|
+
export {};
|
|
4
47
|
//# sourceMappingURL=EsgDashboardPage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EsgDashboardPage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/pages/EsgDashboardPage.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"EsgDashboardPage.d.ts","sourceRoot":"","sources":["../../../src/assethandler/pages/EsgDashboardPage.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAoG/B,QAAA,MAAM,cAAc,kDAAmD,CAAC;AACxE,QAAA,MAAM,UAAU,+CAAgD,CAAC;AA6BjE,UAAU,gBAAgB;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsB3F;AAwOD,wBAAgB,gBAAgB,sBAyT/B"}
|
|
@@ -9,13 +9,13 @@ import { ChartCard as l } from "../components/ChartCard.js";
|
|
|
9
9
|
import { SectionHeader as u } from "../components/SectionHeader.js";
|
|
10
10
|
import "../components/index.js";
|
|
11
11
|
import { useMemo as d, useState as f } from "react";
|
|
12
|
-
import {
|
|
13
|
-
import { gql as
|
|
14
|
-
import { useMutation as
|
|
15
|
-
import { Fragment as
|
|
16
|
-
import { Button as
|
|
12
|
+
import { Archive as p, Leaf as m, Minus as h, Pencil as g, Plus as _, TrendingDown as v, TrendingUp as y } from "lucide-react";
|
|
13
|
+
import { gql as b } from "@apollo/client";
|
|
14
|
+
import { useMutation as x, useQuery as S } from "@apollo/client/react";
|
|
15
|
+
import { Fragment as C, jsx as w, jsxs as T } from "react/jsx-runtime";
|
|
16
|
+
import { Button as E, GlassCard as D, IllustratedEmptyState as O, Input as k, Label as A, PagePurpose as j, ResponsiveDialog as M, ResponsiveDialogContent as N, ResponsiveDialogDescription as P, ResponsiveDialogFooter as F, ResponsiveDialogHeader as I, ResponsiveDialogTitle as L, Select as R, SelectContent as z, SelectItem as B, SelectTrigger as V, SelectValue as H } from "@burdenoff/fe-libs/ui";
|
|
17
17
|
//#region src/assethandler/pages/EsgDashboardPage.tsx
|
|
18
|
-
var
|
|
18
|
+
var U = b`
|
|
19
19
|
query AssetHandlerEsgMetrics {
|
|
20
20
|
esgMetrics {
|
|
21
21
|
id
|
|
@@ -30,162 +30,202 @@ var V = v`
|
|
|
30
30
|
periodEnd
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
`,
|
|
34
|
-
mutation
|
|
35
|
-
|
|
33
|
+
`, W = b`
|
|
34
|
+
mutation AssetHandlerCreateEsgMetric($input: CreateEsgMetricInput!) {
|
|
35
|
+
createEsgMetric(input: $input) {
|
|
36
36
|
id
|
|
37
37
|
metricName
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
`,
|
|
40
|
+
`, G = b`
|
|
41
|
+
mutation AssetHandlerUpdateEsgMetric($id: ID!, $input: UpdateEsgMetricInput!) {
|
|
42
|
+
updateEsgMetric(id: $id, input: $input) {
|
|
43
|
+
id
|
|
44
|
+
metricName
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
`, K = b`
|
|
48
|
+
mutation AssetHandlerArchiveEsgMetric($id: ID!) {
|
|
49
|
+
archiveEsgMetric(id: $id)
|
|
50
|
+
}
|
|
51
|
+
`, q = [
|
|
41
52
|
"environment",
|
|
42
53
|
"social",
|
|
43
54
|
"governance"
|
|
44
|
-
],
|
|
55
|
+
], J = [
|
|
45
56
|
"improving",
|
|
46
57
|
"stable",
|
|
47
58
|
"declining"
|
|
48
|
-
],
|
|
59
|
+
], Y = {
|
|
49
60
|
improving: {
|
|
50
61
|
label: "Improving",
|
|
51
|
-
icon:
|
|
62
|
+
icon: y,
|
|
52
63
|
tone: "positive",
|
|
53
64
|
dir: "up"
|
|
54
65
|
},
|
|
55
66
|
declining: {
|
|
56
67
|
label: "Declining",
|
|
57
|
-
icon:
|
|
68
|
+
icon: v,
|
|
58
69
|
tone: "negative",
|
|
59
70
|
dir: "down"
|
|
60
71
|
},
|
|
61
72
|
stable: {
|
|
62
73
|
label: "Stable",
|
|
63
|
-
icon:
|
|
74
|
+
icon: h,
|
|
64
75
|
tone: "neutral",
|
|
65
76
|
dir: "neutral"
|
|
66
77
|
}
|
|
67
|
-
},
|
|
78
|
+
}, X = {
|
|
68
79
|
positive: "bg-status-success-bg-subtle text-status-success-text",
|
|
69
80
|
negative: "bg-status-error-bg-subtle text-status-error-text",
|
|
70
81
|
neutral: "bg-bg-sunken text-text-secondary"
|
|
71
|
-
},
|
|
72
|
-
function
|
|
82
|
+
}, Z = (e) => e.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
83
|
+
function ee(e, t) {
|
|
84
|
+
let n = {
|
|
85
|
+
metricName: t.metricName.trim(),
|
|
86
|
+
category: t.category,
|
|
87
|
+
unit: t.unit.trim(),
|
|
88
|
+
value: t.value,
|
|
89
|
+
target: t.target,
|
|
90
|
+
trend: t.trend
|
|
91
|
+
};
|
|
92
|
+
return e ? {
|
|
93
|
+
kind: "update",
|
|
94
|
+
variables: {
|
|
95
|
+
id: e,
|
|
96
|
+
input: n
|
|
97
|
+
}
|
|
98
|
+
} : {
|
|
99
|
+
kind: "create",
|
|
100
|
+
variables: { input: {
|
|
101
|
+
...n,
|
|
102
|
+
metricKey: Z(t.metricName),
|
|
103
|
+
periodStart: new Date(t.periodStart).toISOString(),
|
|
104
|
+
periodEnd: new Date(t.periodEnd).toISOString()
|
|
105
|
+
} }
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function Q() {
|
|
73
109
|
let e = /* @__PURE__ */ new Date();
|
|
74
110
|
return new Date(e.getFullYear(), e.getMonth(), 1).toISOString().slice(0, 10);
|
|
75
111
|
}
|
|
76
|
-
function
|
|
112
|
+
function te() {
|
|
77
113
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
78
114
|
}
|
|
79
|
-
function
|
|
80
|
-
let t =
|
|
81
|
-
return /* @__PURE__ */
|
|
82
|
-
className: n("inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium",
|
|
83
|
-
children: [/* @__PURE__ */
|
|
115
|
+
function ne({ trend: e }) {
|
|
116
|
+
let t = Y[e ?? "stable"] ?? Y.stable, r = t.icon;
|
|
117
|
+
return /* @__PURE__ */ T("span", {
|
|
118
|
+
className: n("inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium", X[t.tone]),
|
|
119
|
+
children: [/* @__PURE__ */ w(r, { className: "h-3 w-3" }), t.label]
|
|
84
120
|
});
|
|
85
121
|
}
|
|
86
|
-
function
|
|
87
|
-
let [
|
|
122
|
+
function re({ open: e, metric: t, onOpenChange: n, onSaved: i }) {
|
|
123
|
+
let [a, o] = f(t?.metricName ?? ""), [s, c] = f(t?.category ?? "environment"), [l, u] = f(t?.unit ?? ""), [d, p] = f(t ? String(t.value) : ""), [m, h] = f(t?.target == null ? "" : String(t.target)), [g, _] = f(t?.trend ?? "stable"), [v, y] = f(t?.periodStart.slice(0, 10) ?? Q()), [b, S] = f(t?.periodEnd.slice(0, 10) ?? te()), C = { onError: (e) => r.error("Could not save metric", e.message) }, [D, { loading: O }] = x(W, {
|
|
124
|
+
...C,
|
|
88
125
|
onCompleted: (e) => {
|
|
89
|
-
r.success("Metric saved", `${e.
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
126
|
+
r.success("Metric saved", `${e.createEsgMetric.metricName} was recorded.`), n(!1), i();
|
|
127
|
+
}
|
|
128
|
+
}), [j, { loading: U }] = x(G, {
|
|
129
|
+
...C,
|
|
130
|
+
onCompleted: (e) => {
|
|
131
|
+
r.success("Metric updated", `${e.updateEsgMetric.metricName} was updated.`), n(!1), i();
|
|
132
|
+
}
|
|
133
|
+
}), K = O || U;
|
|
134
|
+
return /* @__PURE__ */ w(M, {
|
|
94
135
|
open: e,
|
|
95
|
-
onOpenChange:
|
|
96
|
-
children: /* @__PURE__ */
|
|
136
|
+
onOpenChange: n,
|
|
137
|
+
children: /* @__PURE__ */ T(N, {
|
|
97
138
|
className: "max-h-[85vh] overflow-y-auto sm:max-w-2xl",
|
|
98
|
-
children: [/* @__PURE__ */
|
|
139
|
+
children: [/* @__PURE__ */ T(I, { children: [/* @__PURE__ */ w(L, { children: t ? "Edit Metric" : "Save Metric" }), /* @__PURE__ */ w(P, { children: t ? "Update this metric’s values. Its source identity and reporting period stay unchanged." : "Record an ESG metric to track progress toward your sustainability targets." })] }), /* @__PURE__ */ T("form", {
|
|
99
140
|
className: "space-y-3",
|
|
100
141
|
onSubmit: (e) => {
|
|
101
142
|
e.preventDefault();
|
|
102
|
-
let
|
|
103
|
-
if (!
|
|
143
|
+
let n = Number(d);
|
|
144
|
+
if (!a.trim() || !l.trim()) {
|
|
104
145
|
r.warning("Missing details", "Metric name and unit are required.");
|
|
105
146
|
return;
|
|
106
147
|
}
|
|
107
|
-
if (!Number.isFinite(
|
|
148
|
+
if (!Number.isFinite(n) || d.trim() === "") {
|
|
108
149
|
r.warning("Invalid value", "Enter a numeric metric value.");
|
|
109
150
|
return;
|
|
110
151
|
}
|
|
111
|
-
if (!
|
|
152
|
+
if (!v || !b) {
|
|
112
153
|
r.warning("Missing period", "Period start and end are required.");
|
|
113
154
|
return;
|
|
114
155
|
}
|
|
115
|
-
let
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
} } });
|
|
156
|
+
let i = m.trim() === "" ? null : Number(m), o = ee(t?.id ?? null, {
|
|
157
|
+
metricName: a,
|
|
158
|
+
category: s,
|
|
159
|
+
unit: l,
|
|
160
|
+
value: n,
|
|
161
|
+
target: i != null && Number.isFinite(i) ? i : null,
|
|
162
|
+
trend: g,
|
|
163
|
+
periodStart: v,
|
|
164
|
+
periodEnd: b
|
|
165
|
+
});
|
|
166
|
+
o.kind === "update" ? j(o) : D(o);
|
|
127
167
|
},
|
|
128
168
|
children: [
|
|
129
|
-
/* @__PURE__ */
|
|
169
|
+
/* @__PURE__ */ T("div", {
|
|
130
170
|
className: "grid gap-3 md:grid-cols-3",
|
|
131
171
|
children: [
|
|
132
|
-
/* @__PURE__ */
|
|
172
|
+
/* @__PURE__ */ T("div", {
|
|
133
173
|
className: "space-y-2",
|
|
134
|
-
children: [/* @__PURE__ */
|
|
174
|
+
children: [/* @__PURE__ */ w(A, { children: "Metric Name *" }), /* @__PURE__ */ w(k, {
|
|
135
175
|
placeholder: "e.g., Scope 2 carbon emissions",
|
|
136
|
-
value:
|
|
137
|
-
onChange: (e) =>
|
|
176
|
+
value: a,
|
|
177
|
+
onChange: (e) => o(e.target.value)
|
|
138
178
|
})]
|
|
139
179
|
}),
|
|
140
|
-
/* @__PURE__ */
|
|
180
|
+
/* @__PURE__ */ T("div", {
|
|
141
181
|
className: "space-y-2",
|
|
142
|
-
children: [/* @__PURE__ */
|
|
143
|
-
value:
|
|
144
|
-
onValueChange: (e) =>
|
|
145
|
-
children: [/* @__PURE__ */
|
|
182
|
+
children: [/* @__PURE__ */ w(A, { children: "Category" }), /* @__PURE__ */ T(R, {
|
|
183
|
+
value: s,
|
|
184
|
+
onValueChange: (e) => c(e),
|
|
185
|
+
children: [/* @__PURE__ */ w(V, { children: /* @__PURE__ */ w(H, {}) }), /* @__PURE__ */ w(z, { children: q.map((e) => /* @__PURE__ */ w(B, {
|
|
146
186
|
value: e,
|
|
147
187
|
className: "capitalize",
|
|
148
188
|
children: e
|
|
149
189
|
}, e)) })]
|
|
150
190
|
})]
|
|
151
191
|
}),
|
|
152
|
-
/* @__PURE__ */
|
|
192
|
+
/* @__PURE__ */ T("div", {
|
|
153
193
|
className: "space-y-2",
|
|
154
|
-
children: [/* @__PURE__ */
|
|
194
|
+
children: [/* @__PURE__ */ w(A, { children: "Unit *" }), /* @__PURE__ */ w(k, {
|
|
155
195
|
placeholder: "e.g., tCO2e",
|
|
156
|
-
value:
|
|
157
|
-
onChange: (e) =>
|
|
196
|
+
value: l,
|
|
197
|
+
onChange: (e) => u(e.target.value)
|
|
158
198
|
})]
|
|
159
199
|
})
|
|
160
200
|
]
|
|
161
201
|
}),
|
|
162
|
-
/* @__PURE__ */
|
|
202
|
+
/* @__PURE__ */ T("div", {
|
|
163
203
|
className: "grid gap-3 md:grid-cols-3",
|
|
164
204
|
children: [
|
|
165
|
-
/* @__PURE__ */
|
|
205
|
+
/* @__PURE__ */ T("div", {
|
|
166
206
|
className: "space-y-2",
|
|
167
|
-
children: [/* @__PURE__ */
|
|
207
|
+
children: [/* @__PURE__ */ w(A, { children: "Value *" }), /* @__PURE__ */ w(k, {
|
|
168
208
|
type: "number",
|
|
169
209
|
step: "any",
|
|
170
|
-
value:
|
|
171
|
-
onChange: (e) =>
|
|
210
|
+
value: d,
|
|
211
|
+
onChange: (e) => p(e.target.value)
|
|
172
212
|
})]
|
|
173
213
|
}),
|
|
174
|
-
/* @__PURE__ */
|
|
214
|
+
/* @__PURE__ */ T("div", {
|
|
175
215
|
className: "space-y-2",
|
|
176
|
-
children: [/* @__PURE__ */
|
|
216
|
+
children: [/* @__PURE__ */ w(A, { children: "Target" }), /* @__PURE__ */ w(k, {
|
|
177
217
|
type: "number",
|
|
178
218
|
step: "any",
|
|
179
|
-
value:
|
|
180
|
-
onChange: (e) =>
|
|
219
|
+
value: m,
|
|
220
|
+
onChange: (e) => h(e.target.value)
|
|
181
221
|
})]
|
|
182
222
|
}),
|
|
183
|
-
/* @__PURE__ */
|
|
223
|
+
/* @__PURE__ */ T("div", {
|
|
184
224
|
className: "space-y-2",
|
|
185
|
-
children: [/* @__PURE__ */
|
|
186
|
-
value:
|
|
187
|
-
onValueChange: (e) =>
|
|
188
|
-
children: [/* @__PURE__ */
|
|
225
|
+
children: [/* @__PURE__ */ w(A, { children: "Trend" }), /* @__PURE__ */ T(R, {
|
|
226
|
+
value: g,
|
|
227
|
+
onValueChange: (e) => _(e),
|
|
228
|
+
children: [/* @__PURE__ */ w(V, { children: /* @__PURE__ */ w(H, {}) }), /* @__PURE__ */ w(z, { children: J.map((e) => /* @__PURE__ */ w(B, {
|
|
189
229
|
value: e,
|
|
190
230
|
className: "capitalize",
|
|
191
231
|
children: e
|
|
@@ -194,111 +234,146 @@ function Z({ open: e, onOpenChange: t, onSaved: n }) {
|
|
|
194
234
|
})
|
|
195
235
|
]
|
|
196
236
|
}),
|
|
197
|
-
/* @__PURE__ */
|
|
237
|
+
/* @__PURE__ */ T("div", {
|
|
198
238
|
className: "grid gap-3 md:grid-cols-2",
|
|
199
|
-
children: [/* @__PURE__ */
|
|
239
|
+
children: [/* @__PURE__ */ T("div", {
|
|
200
240
|
className: "space-y-2",
|
|
201
|
-
children: [/* @__PURE__ */
|
|
241
|
+
children: [/* @__PURE__ */ w(A, { children: "Period Start *" }), /* @__PURE__ */ w(k, {
|
|
202
242
|
type: "date",
|
|
203
|
-
value:
|
|
204
|
-
onChange: (e) =>
|
|
243
|
+
value: v,
|
|
244
|
+
onChange: (e) => y(e.target.value),
|
|
245
|
+
disabled: t != null
|
|
205
246
|
})]
|
|
206
|
-
}), /* @__PURE__ */
|
|
247
|
+
}), /* @__PURE__ */ T("div", {
|
|
207
248
|
className: "space-y-2",
|
|
208
|
-
children: [/* @__PURE__ */
|
|
249
|
+
children: [/* @__PURE__ */ w(A, { children: "Period End *" }), /* @__PURE__ */ w(k, {
|
|
209
250
|
type: "date",
|
|
210
251
|
value: b,
|
|
211
|
-
onChange: (e) =>
|
|
252
|
+
onChange: (e) => S(e.target.value),
|
|
253
|
+
disabled: t != null
|
|
212
254
|
})]
|
|
213
255
|
})]
|
|
214
256
|
}),
|
|
215
|
-
/* @__PURE__ */
|
|
257
|
+
/* @__PURE__ */ T(F, { children: [/* @__PURE__ */ w(E, {
|
|
216
258
|
type: "button",
|
|
217
259
|
variant: "outline",
|
|
218
|
-
onClick: () =>
|
|
219
|
-
disabled:
|
|
260
|
+
onClick: () => n(!1),
|
|
261
|
+
disabled: K,
|
|
220
262
|
children: "Cancel"
|
|
221
|
-
}), /* @__PURE__ */
|
|
263
|
+
}), /* @__PURE__ */ w(E, {
|
|
222
264
|
type: "submit",
|
|
223
|
-
disabled:
|
|
224
|
-
children:
|
|
265
|
+
disabled: K,
|
|
266
|
+
children: K ? "Saving…" : t ? "Update Metric" : "Save Metric"
|
|
225
267
|
})] })
|
|
226
268
|
]
|
|
227
269
|
})]
|
|
228
270
|
})
|
|
229
271
|
});
|
|
230
272
|
}
|
|
231
|
-
function
|
|
232
|
-
let [
|
|
233
|
-
|
|
273
|
+
function $() {
|
|
274
|
+
let [h, v] = f(!1), [y, b] = f(null), [k, A] = f(null), [R, z] = f("ALL"), { data: B, loading: V, refetch: H } = S(U, { fetchPolicy: "cache-and-network" }), W = d(() => B?.esgMetrics ?? [], [B?.esgMetrics]), [G, { loading: J }] = x(K, {
|
|
275
|
+
onCompleted: () => {
|
|
276
|
+
r.success("Metric archived", "The metric was removed from the active ESG dashboard."), A(null), H();
|
|
277
|
+
},
|
|
278
|
+
onError: (e) => r.error("Could not archive metric", e.message)
|
|
279
|
+
}), X = d(() => W.slice(0, 4), [W]), Z = d(() => R === "ALL" ? W : W.filter((e) => e.category === R), [W, R]), ee = d(() => {
|
|
280
|
+
let e = W.reduce((e, t) => (e[t.category] = (e[t.category] ?? 0) + 1, e), {});
|
|
234
281
|
return Object.entries(e).map(([e, n], r) => ({
|
|
235
282
|
name: e.charAt(0).toUpperCase() + e.slice(1),
|
|
236
283
|
value: n,
|
|
237
284
|
color: t(r)
|
|
238
285
|
}));
|
|
239
|
-
}, [
|
|
286
|
+
}, [W]), Q = d(() => W.filter((e) => e.target != null).slice(0, 8).map((e) => ({
|
|
240
287
|
name: e.metricName,
|
|
241
288
|
value: e.value,
|
|
242
289
|
target: e.target
|
|
243
|
-
})), [
|
|
244
|
-
let e = { ALL:
|
|
245
|
-
for (let t of
|
|
290
|
+
})), [W]), te = d(() => {
|
|
291
|
+
let e = { ALL: W.length };
|
|
292
|
+
for (let t of W) e[t.category] = (e[t.category] ?? 0) + 1;
|
|
246
293
|
return e;
|
|
247
|
-
}, [
|
|
294
|
+
}, [W]), $ = s(Z, {
|
|
248
295
|
pageSize: 8,
|
|
249
|
-
resetKey:
|
|
296
|
+
resetKey: R
|
|
250
297
|
});
|
|
251
|
-
return /* @__PURE__ */
|
|
298
|
+
return /* @__PURE__ */ T("div", {
|
|
252
299
|
className: "space-y-5 p-4 sm:p-6",
|
|
253
300
|
children: [
|
|
254
|
-
/* @__PURE__ */
|
|
301
|
+
/* @__PURE__ */ w(u, {
|
|
255
302
|
hero: !0,
|
|
256
|
-
icon:
|
|
303
|
+
icon: m,
|
|
257
304
|
title: "ESG Dashboard",
|
|
258
305
|
description: "Measure the environmental and governance impact of your estate against targets.",
|
|
259
|
-
actions: /* @__PURE__ */
|
|
306
|
+
actions: /* @__PURE__ */ T(E, {
|
|
260
307
|
className: "w-full sm:w-auto",
|
|
261
|
-
onClick: () =>
|
|
262
|
-
|
|
308
|
+
onClick: () => {
|
|
309
|
+
b(null), v(!0);
|
|
310
|
+
},
|
|
311
|
+
children: [/* @__PURE__ */ w(_, { className: "mr-2 h-4 w-4" }), "Save Metric"]
|
|
263
312
|
})
|
|
264
313
|
}),
|
|
265
|
-
/* @__PURE__ */
|
|
266
|
-
/* @__PURE__ */
|
|
267
|
-
open:
|
|
268
|
-
|
|
269
|
-
|
|
314
|
+
/* @__PURE__ */ w(j, { children: "Track energy use, carbon emissions and responsible disposal against targets — and produce the evidence ESG reporting and audits require." }),
|
|
315
|
+
h ? /* @__PURE__ */ w(re, {
|
|
316
|
+
open: !0,
|
|
317
|
+
metric: y,
|
|
318
|
+
onOpenChange: (e) => {
|
|
319
|
+
v(e), e || b(null);
|
|
320
|
+
},
|
|
321
|
+
onSaved: () => void H()
|
|
322
|
+
}) : null,
|
|
323
|
+
/* @__PURE__ */ w(M, {
|
|
324
|
+
open: k != null,
|
|
325
|
+
onOpenChange: (e) => {
|
|
326
|
+
e || A(null);
|
|
327
|
+
},
|
|
328
|
+
children: /* @__PURE__ */ T(N, { children: [/* @__PURE__ */ T(I, { children: [/* @__PURE__ */ w(L, { children: "Archive metric" }), /* @__PURE__ */ T(P, { children: [
|
|
329
|
+
"Archive ",
|
|
330
|
+
k?.metricName ?? "this metric",
|
|
331
|
+
"? It will leave the active ESG dashboard. Archiving does not refund monthly usage."
|
|
332
|
+
] })] }), /* @__PURE__ */ T(F, { children: [/* @__PURE__ */ w(E, {
|
|
333
|
+
variant: "outline",
|
|
334
|
+
onClick: () => A(null),
|
|
335
|
+
disabled: J,
|
|
336
|
+
children: "Cancel"
|
|
337
|
+
}), /* @__PURE__ */ w(E, {
|
|
338
|
+
variant: "destructive",
|
|
339
|
+
disabled: J || k == null,
|
|
340
|
+
onClick: () => {
|
|
341
|
+
k && G({ variables: { id: k.id } });
|
|
342
|
+
},
|
|
343
|
+
children: J ? "Archiving…" : "Archive Metric"
|
|
344
|
+
})] })] })
|
|
270
345
|
}),
|
|
271
|
-
|
|
346
|
+
V && W.length === 0 ? /* @__PURE__ */ w("div", {
|
|
272
347
|
className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-4",
|
|
273
|
-
children: Array.from({ length: 4 }).map((e, t) => /* @__PURE__ */
|
|
348
|
+
children: Array.from({ length: 4 }).map((e, t) => /* @__PURE__ */ w("div", { className: "h-28 rounded-xl bg-bg-sunken motion-safe:animate-vc-shimmer" }, t))
|
|
274
349
|
}) : null,
|
|
275
|
-
!
|
|
350
|
+
!V && W.length === 0 ? /* @__PURE__ */ w(O, {
|
|
276
351
|
illustration: "empty-data",
|
|
277
352
|
title: "No ESG metrics recorded yet",
|
|
278
353
|
description: "Save a metric to start tracking sustainability progress against your targets."
|
|
279
354
|
}) : null,
|
|
280
|
-
|
|
355
|
+
X.length > 0 ? /* @__PURE__ */ w("div", {
|
|
281
356
|
className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-4",
|
|
282
|
-
children:
|
|
283
|
-
let n =
|
|
284
|
-
return /* @__PURE__ */
|
|
357
|
+
children: X.map((t) => {
|
|
358
|
+
let n = Y[t.trend ?? "stable"] ?? Y.stable;
|
|
359
|
+
return /* @__PURE__ */ w(c, {
|
|
285
360
|
label: `${t.metricName} (${t.unit})`,
|
|
286
361
|
value: t.value,
|
|
287
362
|
format: e,
|
|
288
363
|
delta: n.label,
|
|
289
364
|
deltaDirection: n.dir,
|
|
290
365
|
deltaTone: n.tone,
|
|
291
|
-
icon:
|
|
366
|
+
icon: m
|
|
292
367
|
}, t.id);
|
|
293
368
|
})
|
|
294
369
|
}) : null,
|
|
295
|
-
|
|
370
|
+
W.length > 0 ? /* @__PURE__ */ T("div", {
|
|
296
371
|
className: "grid gap-4 lg:grid-cols-2",
|
|
297
|
-
children: [/* @__PURE__ */
|
|
372
|
+
children: [/* @__PURE__ */ w(l, {
|
|
298
373
|
title: "Value vs. target",
|
|
299
374
|
subtitle: "Metrics measured against their targets",
|
|
300
|
-
children:
|
|
301
|
-
data:
|
|
375
|
+
children: Q.length > 0 ? /* @__PURE__ */ w(i, {
|
|
376
|
+
data: Q,
|
|
302
377
|
xKey: "name",
|
|
303
378
|
series: [{
|
|
304
379
|
key: "value",
|
|
@@ -311,86 +386,109 @@ function Q() {
|
|
|
311
386
|
height: 240,
|
|
312
387
|
categoryWidth: 150,
|
|
313
388
|
ariaLabel: "Metric value versus target"
|
|
314
|
-
}) : /* @__PURE__ */
|
|
389
|
+
}) : /* @__PURE__ */ w("div", {
|
|
315
390
|
className: "flex h-[240px] items-center justify-center text-sm text-text-secondary",
|
|
316
391
|
children: "Set a target on a metric to compare value against target."
|
|
317
392
|
})
|
|
318
|
-
}), /* @__PURE__ */
|
|
393
|
+
}), /* @__PURE__ */ w(l, {
|
|
319
394
|
title: "By category",
|
|
320
395
|
subtitle: "Metrics across E / S / G",
|
|
321
|
-
children: /* @__PURE__ */
|
|
322
|
-
data:
|
|
396
|
+
children: /* @__PURE__ */ w(a, {
|
|
397
|
+
data: ee,
|
|
323
398
|
height: 240,
|
|
324
399
|
centerLabel: "Metrics",
|
|
325
|
-
centerValue:
|
|
400
|
+
centerValue: W.length,
|
|
326
401
|
ariaLabel: "Metrics by category"
|
|
327
402
|
})
|
|
328
403
|
})]
|
|
329
404
|
}) : null,
|
|
330
|
-
|
|
331
|
-
/* @__PURE__ */
|
|
405
|
+
W.length > 0 ? /* @__PURE__ */ T(C, { children: [
|
|
406
|
+
/* @__PURE__ */ w("div", {
|
|
332
407
|
className: "flex flex-wrap items-center gap-2",
|
|
333
|
-
children: ["ALL", ...
|
|
408
|
+
children: ["ALL", ...q].map((e) => /* @__PURE__ */ T("button", {
|
|
334
409
|
type: "button",
|
|
335
|
-
onClick: () =>
|
|
336
|
-
className: n("rounded-full px-3.5 py-1.5 text-sm font-medium capitalize transition-colors",
|
|
337
|
-
children: [e === "ALL" ? "All" : e, /* @__PURE__ */
|
|
410
|
+
onClick: () => z(e),
|
|
411
|
+
className: n("rounded-full px-3.5 py-1.5 text-sm font-medium capitalize transition-colors", R === e ? "bg-action-primary-bg text-action-primary-text" : "bg-bg-sunken text-text-secondary hover:text-text-primary"),
|
|
412
|
+
children: [e === "ALL" ? "All" : e, /* @__PURE__ */ w("span", {
|
|
338
413
|
className: "ml-1.5 text-xs opacity-70 tabular-nums",
|
|
339
|
-
children:
|
|
414
|
+
children: te[e] ?? 0
|
|
340
415
|
})]
|
|
341
416
|
}, e))
|
|
342
417
|
}),
|
|
343
|
-
/* @__PURE__ */
|
|
418
|
+
/* @__PURE__ */ w("div", {
|
|
344
419
|
className: "grid gap-4 sm:grid-cols-2",
|
|
345
|
-
children:
|
|
420
|
+
children: $.pageItems.map((t) => {
|
|
346
421
|
let n = t.target && t.target > 0 ? Math.min(100, Math.round(t.value / t.target * 100)) : null;
|
|
347
|
-
return /* @__PURE__ */
|
|
422
|
+
return /* @__PURE__ */ w(D, {
|
|
348
423
|
treatment: "glass",
|
|
349
424
|
className: "border-border-seam",
|
|
350
|
-
children: /* @__PURE__ */
|
|
425
|
+
children: /* @__PURE__ */ T("div", {
|
|
351
426
|
className: "space-y-3 p-4",
|
|
352
427
|
children: [
|
|
353
|
-
/* @__PURE__ */
|
|
428
|
+
/* @__PURE__ */ T("div", {
|
|
354
429
|
className: "flex items-start justify-between gap-2",
|
|
355
|
-
children: [/* @__PURE__ */
|
|
430
|
+
children: [/* @__PURE__ */ T("div", {
|
|
356
431
|
className: "min-w-0",
|
|
357
|
-
children: [/* @__PURE__ */
|
|
432
|
+
children: [/* @__PURE__ */ w("p", {
|
|
358
433
|
className: "text-xs uppercase tracking-wide text-text-secondary",
|
|
359
434
|
children: t.category
|
|
360
|
-
}), /* @__PURE__ */
|
|
435
|
+
}), /* @__PURE__ */ w("h2", {
|
|
361
436
|
className: "mt-0.5 text-base font-semibold text-text-primary",
|
|
362
437
|
children: t.metricName
|
|
363
438
|
})]
|
|
364
|
-
}), /* @__PURE__ */
|
|
439
|
+
}), /* @__PURE__ */ T("div", {
|
|
440
|
+
className: "flex items-center gap-1",
|
|
441
|
+
children: [
|
|
442
|
+
/* @__PURE__ */ w(ne, { trend: t.trend }),
|
|
443
|
+
/* @__PURE__ */ w(E, {
|
|
444
|
+
type: "button",
|
|
445
|
+
variant: "ghost",
|
|
446
|
+
size: "icon",
|
|
447
|
+
"aria-label": `Edit ${t.metricName}`,
|
|
448
|
+
onClick: () => {
|
|
449
|
+
b(t), v(!0);
|
|
450
|
+
},
|
|
451
|
+
children: /* @__PURE__ */ w(g, { className: "h-4 w-4" })
|
|
452
|
+
}),
|
|
453
|
+
/* @__PURE__ */ w(E, {
|
|
454
|
+
type: "button",
|
|
455
|
+
variant: "ghost",
|
|
456
|
+
size: "icon",
|
|
457
|
+
"aria-label": `Archive ${t.metricName}`,
|
|
458
|
+
onClick: () => A(t),
|
|
459
|
+
children: /* @__PURE__ */ w(p, { className: "h-4 w-4" })
|
|
460
|
+
})
|
|
461
|
+
]
|
|
462
|
+
})]
|
|
365
463
|
}),
|
|
366
|
-
/* @__PURE__ */
|
|
464
|
+
/* @__PURE__ */ T("div", {
|
|
367
465
|
className: "flex items-baseline gap-2",
|
|
368
466
|
children: [
|
|
369
|
-
/* @__PURE__ */
|
|
467
|
+
/* @__PURE__ */ w("span", {
|
|
370
468
|
className: "text-2xl font-bold tabular-nums text-text-primary",
|
|
371
469
|
children: e(t.value)
|
|
372
470
|
}),
|
|
373
|
-
/* @__PURE__ */
|
|
471
|
+
/* @__PURE__ */ w("span", {
|
|
374
472
|
className: "text-sm text-text-secondary",
|
|
375
473
|
children: t.unit
|
|
376
474
|
}),
|
|
377
|
-
t.target == null ? null : /* @__PURE__ */
|
|
475
|
+
t.target == null ? null : /* @__PURE__ */ T("span", {
|
|
378
476
|
className: "text-xs text-text-muted",
|
|
379
477
|
children: ["target ", e(t.target)]
|
|
380
478
|
})
|
|
381
479
|
]
|
|
382
480
|
}),
|
|
383
|
-
n == null ? null : /* @__PURE__ */
|
|
481
|
+
n == null ? null : /* @__PURE__ */ T("div", {
|
|
384
482
|
className: "space-y-1",
|
|
385
|
-
children: [/* @__PURE__ */
|
|
483
|
+
children: [/* @__PURE__ */ T("div", {
|
|
386
484
|
className: "flex items-center justify-between text-xs text-text-secondary",
|
|
387
|
-
children: [/* @__PURE__ */
|
|
485
|
+
children: [/* @__PURE__ */ w("span", { children: "Progress to target" }), /* @__PURE__ */ T("span", {
|
|
388
486
|
className: "font-medium tabular-nums",
|
|
389
487
|
children: [n, "%"]
|
|
390
488
|
})]
|
|
391
|
-
}), /* @__PURE__ */
|
|
489
|
+
}), /* @__PURE__ */ w("div", {
|
|
392
490
|
className: "h-2 overflow-hidden rounded-full bg-bg-sunken",
|
|
393
|
-
children: /* @__PURE__ */
|
|
491
|
+
children: /* @__PURE__ */ w("div", {
|
|
394
492
|
className: "h-full rounded-full bg-action-primary-bg",
|
|
395
493
|
style: { width: `${n}%` }
|
|
396
494
|
})
|
|
@@ -401,16 +499,16 @@ function Q() {
|
|
|
401
499
|
}, t.id);
|
|
402
500
|
})
|
|
403
501
|
}),
|
|
404
|
-
/* @__PURE__ */
|
|
405
|
-
page:
|
|
406
|
-
pageCount:
|
|
407
|
-
rangeStart:
|
|
408
|
-
rangeEnd:
|
|
409
|
-
totalItems:
|
|
410
|
-
canPrev:
|
|
411
|
-
canNext:
|
|
412
|
-
onPrev:
|
|
413
|
-
onNext:
|
|
502
|
+
/* @__PURE__ */ w(o, {
|
|
503
|
+
page: $.page,
|
|
504
|
+
pageCount: $.pageCount,
|
|
505
|
+
rangeStart: $.rangeStart,
|
|
506
|
+
rangeEnd: $.rangeEnd,
|
|
507
|
+
totalItems: $.totalItems,
|
|
508
|
+
canPrev: $.canPrev,
|
|
509
|
+
canNext: $.canNext,
|
|
510
|
+
onPrev: $.prevPage,
|
|
511
|
+
onNext: $.nextPage,
|
|
414
512
|
itemLabel: "metrics"
|
|
415
513
|
})
|
|
416
514
|
] }) : null
|
|
@@ -418,6 +516,6 @@ function Q() {
|
|
|
418
516
|
});
|
|
419
517
|
}
|
|
420
518
|
//#endregion
|
|
421
|
-
export {
|
|
519
|
+
export { $ as EsgDashboardPage };
|
|
422
520
|
|
|
423
521
|
//# sourceMappingURL=EsgDashboardPage.js.map
|
|
@@ -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 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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"}
|
|
1
|
+
{"version":3,"file":"EsgDashboardPage.js","names":[],"sources":["../../../src/assethandler/pages/EsgDashboardPage.tsx"],"sourcesContent":["import { Archive, Leaf, Minus, Pencil, Plus, TrendingDown, TrendingUp } 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 CREATE_ESG_METRIC_MUTATION = gql`\n mutation AssetHandlerCreateEsgMetric($input: CreateEsgMetricInput!) {\n createEsgMetric(input: $input) {\n id\n metricName\n }\n }\n`;\n\nconst UPDATE_ESG_METRIC_MUTATION = gql`\n mutation AssetHandlerUpdateEsgMetric($id: ID!, $input: UpdateEsgMetricInput!) {\n updateEsgMetric(id: $id, input: $input) {\n id\n metricName\n }\n }\n`;\n\nconst ARCHIVE_ESG_METRIC_MUTATION = gql`\n mutation AssetHandlerArchiveEsgMetric($id: ID!) {\n archiveEsgMetric(id: $id)\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 CreateEsgMetricResult {\n createEsgMetric: { id: string; metricName: string };\n}\n\ninterface UpdateEsgMetricResult {\n updateEsgMetric: { 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\ninterface MetricFormValues {\n metricName: string;\n category: (typeof ESG_CATEGORIES)[number];\n unit: string;\n value: number;\n target: number | null;\n trend: (typeof ESG_TRENDS)[number];\n periodStart: string;\n periodEnd: string;\n}\n\nexport function buildMetricMutationVariables(metricId: string | null, form: MetricFormValues) {\n const mutableInput = {\n metricName: form.metricName.trim(),\n category: form.category,\n unit: form.unit.trim(),\n value: form.value,\n target: form.target,\n trend: form.trend,\n };\n if (metricId)\n return { kind: 'update' as const, variables: { id: metricId, input: mutableInput } };\n return {\n kind: 'create' as const,\n variables: {\n input: {\n ...mutableInput,\n metricKey: slugify(form.metricName),\n periodStart: new Date(form.periodStart).toISOString(),\n periodEnd: new Date(form.periodEnd).toISOString(),\n },\n },\n };\n}\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 metric,\n onOpenChange,\n onSaved,\n}: {\n open: boolean;\n metric: EcoMetric | null;\n onOpenChange: (open: boolean) => void;\n onSaved: () => void;\n}) {\n const [metricName, setMetricName] = useState(metric?.metricName ?? '');\n const [category, setCategory] = useState<(typeof ESG_CATEGORIES)[number]>(\n (metric?.category as (typeof ESG_CATEGORIES)[number]) ?? 'environment'\n );\n const [unit, setUnit] = useState(metric?.unit ?? '');\n const [value, setValue] = useState(metric ? String(metric.value) : '');\n const [target, setTarget] = useState(metric?.target == null ? '' : String(metric.target));\n const [trend, setTrend] = useState<(typeof ESG_TRENDS)[number]>(\n (metric?.trend as (typeof ESG_TRENDS)[number]) ?? 'stable'\n );\n const [periodStart, setPeriodStart] = useState(\n metric?.periodStart.slice(0, 10) ?? firstOfMonthISO()\n );\n const [periodEnd, setPeriodEnd] = useState(metric?.periodEnd.slice(0, 10) ?? todayISO());\n\n const mutationOptions = {\n onError: (error: Error) => notify.error('Could not save metric', error.message),\n };\n const [createEsgMetric, { loading: creating }] = useMutation<CreateEsgMetricResult>(\n CREATE_ESG_METRIC_MUTATION,\n {\n ...mutationOptions,\n onCompleted: (result) => {\n notify.success('Metric saved', `${result.createEsgMetric.metricName} was recorded.`);\n onOpenChange(false);\n onSaved();\n },\n }\n );\n const [updateEsgMetric, { loading: updating }] = useMutation<UpdateEsgMetricResult>(\n UPDATE_ESG_METRIC_MUTATION,\n {\n ...mutationOptions,\n onCompleted: (result) => {\n notify.success('Metric updated', `${result.updateEsgMetric.metricName} was updated.`);\n onOpenChange(false);\n onSaved();\n },\n }\n );\n const saving = creating || updating;\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 const mutation = buildMetricMutationVariables(metric?.id ?? null, {\n metricName,\n category,\n unit,\n value: parsedValue,\n target: parsedTarget != null && Number.isFinite(parsedTarget) ? parsedTarget : null,\n trend,\n periodStart,\n periodEnd,\n });\n if (mutation.kind === 'update') {\n void updateEsgMetric(mutation);\n } else {\n void createEsgMetric(mutation);\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>{metric ? 'Edit Metric' : 'Save Metric'}</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {metric\n ? 'Update this metric’s values. Its source identity and reporting period stay unchanged.'\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 disabled={metric != null}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>Period End *</Label>\n <Input\n type=\"date\"\n value={periodEnd}\n onChange={(e) => setPeriodEnd(e.target.value)}\n disabled={metric != null}\n />\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…' : metric ? 'Update Metric' : '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 [editingMetric, setEditingMetric] = useState<EcoMetric | null>(null);\n const [archivingMetric, setArchivingMetric] = useState<EcoMetric | null>(null);\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 [archiveEsgMetric, { loading: archiving }] = useMutation<{ archiveEsgMetric: boolean }>(\n ARCHIVE_ESG_METRIC_MUTATION,\n {\n onCompleted: () => {\n notify.success('Metric archived', 'The metric was removed from the active ESG dashboard.');\n setArchivingMetric(null);\n void refetch();\n },\n onError: (error) => notify.error('Could not archive metric', error.message),\n }\n );\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\n className=\"w-full sm:w-auto\"\n onClick={() => {\n setEditingMetric(null);\n setDialogOpen(true);\n }}\n >\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 {dialogOpen ? (\n <RecordMetricDialog\n open\n metric={editingMetric}\n onOpenChange={(nextOpen) => {\n setDialogOpen(nextOpen);\n if (!nextOpen) setEditingMetric(null);\n }}\n onSaved={() => void refetch()}\n />\n ) : null}\n\n <ResponsiveDialog\n open={archivingMetric != null}\n onOpenChange={(nextOpen) => {\n if (!nextOpen) setArchivingMetric(null);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Archive metric</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Archive {archivingMetric?.metricName ?? 'this metric'}? It will leave the active ESG\n dashboard. Archiving does not refund monthly usage.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setArchivingMetric(null)} disabled={archiving}>\n Cancel\n </Button>\n <Button\n variant=\"destructive\"\n disabled={archiving || archivingMetric == null}\n onClick={() => {\n if (archivingMetric) {\n void archiveEsgMetric({ variables: { id: archivingMetric.id } });\n }\n }}\n >\n {archiving ? 'Archiving…' : 'Archive Metric'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\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 <div className=\"flex items-center gap-1\">\n <TrendBadge trend={metric.trend} />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n aria-label={`Edit ${metric.metricName}`}\n onClick={() => {\n setEditingMetric(metric);\n setDialogOpen(true);\n }}\n >\n <Pencil className=\"h-4 w-4\" />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n aria-label={`Archive ${metric.metricName}`}\n onClick={() => setArchivingMetric(metric)}\n >\n <Archive className=\"h-4 w-4\" />\n </Button>\n </div>\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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"}
|