@burdenoff/microfe-proserviceworld 2026.1026.1 → 2026.1028.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/proserviceworld/hooks/useProServiceWorldApi.d.ts +37 -0
- package/dist/proserviceworld/hooks/useProServiceWorldApi.d.ts.map +1 -1
- package/dist/proserviceworld/hooks/useProServiceWorldApi.js +112 -72
- package/dist/proserviceworld/hooks/useProServiceWorldApi.js.map +1 -1
- package/dist/proserviceworld/pages/AnalyticsPage.d.ts.map +1 -1
- package/dist/proserviceworld/pages/AnalyticsPage.js +193 -121
- package/dist/proserviceworld/pages/AnalyticsPage.js.map +1 -1
- package/dist/proserviceworld/pages/InvoicesPage.d.ts.map +1 -1
- package/dist/proserviceworld/pages/InvoicesPage.js +331 -161
- package/dist/proserviceworld/pages/InvoicesPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,169 +1,241 @@
|
|
|
1
|
-
import { useDashboardStats as e } from "../hooks/useProServiceWorldApi.js";
|
|
2
|
-
import { cn as
|
|
3
|
-
import { ErrorState as
|
|
4
|
-
import { BarChart3 as
|
|
5
|
-
import { jsx as
|
|
1
|
+
import { useDashboardStats as e, useWriteOffs as t } from "../hooks/useProServiceWorldApi.js";
|
|
2
|
+
import { cn as n } from "../utils/cn.js";
|
|
3
|
+
import { Empty as r, ErrorState as i, LoadingBlock as a, MetricCard as o, PageHeader as s, SectionCard as c, StatusPill as l, TableWrap as u, Td as d, Th as f, fmtDate as p, fmtHours as m, fmtMoney as h } from "../components/ui.js";
|
|
4
|
+
import { AlertTriangle as g, BarChart3 as _, Clock as v, Scale as y, TrendingUp as b, Wallet as x } from "lucide-react";
|
|
5
|
+
import { jsx as S, jsxs as C } from "react/jsx-runtime";
|
|
6
6
|
//#region src/proserviceworld/pages/AnalyticsPage.tsx
|
|
7
|
-
var
|
|
8
|
-
let { data:
|
|
9
|
-
if (
|
|
10
|
-
if (
|
|
11
|
-
error:
|
|
12
|
-
onRetry: () => void
|
|
7
|
+
var w = () => {
|
|
8
|
+
let { data: n, isLoading: l, error: w, refetch: D } = e(), j = t({ pagination: { limit: 10 } });
|
|
9
|
+
if (l) return /* @__PURE__ */ S(a, { label: "Crunching numbers…" });
|
|
10
|
+
if (w) return /* @__PURE__ */ S(i, {
|
|
11
|
+
error: w,
|
|
12
|
+
onRetry: () => void D()
|
|
13
13
|
});
|
|
14
|
-
if (!
|
|
15
|
-
let
|
|
16
|
-
return /* @__PURE__ */
|
|
14
|
+
if (!n) return null;
|
|
15
|
+
let M = n.unbilledHours + n.billedHours, N = M > 0 ? Math.round(n.billedHours / M * 100) : 0, P = n.paidInvoiceTotal + n.outstandingInvoiceTotal, F = P > 0 ? Math.round(n.paidInvoiceTotal / P * 100) : 0;
|
|
16
|
+
return /* @__PURE__ */ C("div", {
|
|
17
17
|
className: "space-y-6",
|
|
18
18
|
children: [
|
|
19
|
-
/* @__PURE__ */
|
|
19
|
+
/* @__PURE__ */ S(s, {
|
|
20
20
|
title: "Analytics",
|
|
21
21
|
subtitle: "Real, live metrics across the practice — no estimates, straight from your records.",
|
|
22
|
-
icon: /* @__PURE__ */
|
|
22
|
+
icon: /* @__PURE__ */ S(_, { className: "h-6 w-6" })
|
|
23
23
|
}),
|
|
24
|
-
/* @__PURE__ */
|
|
24
|
+
/* @__PURE__ */ C("div", {
|
|
25
25
|
className: "grid grid-cols-2 gap-3 lg:grid-cols-4",
|
|
26
26
|
children: [
|
|
27
|
-
/* @__PURE__ */
|
|
27
|
+
/* @__PURE__ */ S(o, {
|
|
28
28
|
label: "Collected revenue",
|
|
29
|
-
value:
|
|
30
|
-
hint: `${
|
|
31
|
-
icon: /* @__PURE__ */
|
|
29
|
+
value: h(n.paidInvoiceTotal),
|
|
30
|
+
hint: `${h(P)} invoiced`,
|
|
31
|
+
icon: /* @__PURE__ */ S(x, { className: "h-4 w-4" }),
|
|
32
32
|
tone: "success"
|
|
33
33
|
}),
|
|
34
|
-
/* @__PURE__ */
|
|
34
|
+
/* @__PURE__ */ S(o, {
|
|
35
35
|
label: "Collection rate",
|
|
36
|
-
value: `${
|
|
37
|
-
hint: `${
|
|
38
|
-
icon: /* @__PURE__ */
|
|
36
|
+
value: `${F}%`,
|
|
37
|
+
hint: `${h(n.outstandingInvoiceTotal)} outstanding`,
|
|
38
|
+
icon: /* @__PURE__ */ S(b, { className: "h-4 w-4" }),
|
|
39
39
|
tone: "accent"
|
|
40
40
|
}),
|
|
41
|
-
/* @__PURE__ */
|
|
41
|
+
/* @__PURE__ */ S(o, {
|
|
42
42
|
label: "Utilization",
|
|
43
|
-
value: `${
|
|
44
|
-
hint: `${
|
|
45
|
-
icon: /* @__PURE__ */
|
|
43
|
+
value: `${N}%`,
|
|
44
|
+
hint: `${m(n.billedHours)} of ${m(M)}`,
|
|
45
|
+
icon: /* @__PURE__ */ S(v, { className: "h-4 w-4" }),
|
|
46
46
|
tone: "info"
|
|
47
47
|
}),
|
|
48
|
-
/* @__PURE__ */
|
|
48
|
+
/* @__PURE__ */ S(o, {
|
|
49
49
|
label: "Active engagements",
|
|
50
|
-
value:
|
|
51
|
-
hint: `${
|
|
52
|
-
icon: /* @__PURE__ */
|
|
50
|
+
value: n.activeEngagements,
|
|
51
|
+
hint: `${n.openMatters} open matters`,
|
|
52
|
+
icon: /* @__PURE__ */ S(_, { className: "h-4 w-4" }),
|
|
53
53
|
tone: "warning"
|
|
54
54
|
})
|
|
55
55
|
]
|
|
56
56
|
}),
|
|
57
|
-
/* @__PURE__ */
|
|
57
|
+
/* @__PURE__ */ C("div", {
|
|
58
58
|
className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
|
|
59
|
-
children: [/* @__PURE__ */
|
|
59
|
+
children: [/* @__PURE__ */ S(c, {
|
|
60
60
|
title: "Collection rate",
|
|
61
61
|
description: "Paid vs. total invoiced.",
|
|
62
|
-
children: /* @__PURE__ */
|
|
63
|
-
pct:
|
|
62
|
+
children: /* @__PURE__ */ S(E, {
|
|
63
|
+
pct: F,
|
|
64
64
|
tone: "accent",
|
|
65
|
-
primary:
|
|
65
|
+
primary: h(n.paidInvoiceTotal),
|
|
66
66
|
primaryLabel: "Collected",
|
|
67
|
-
secondary:
|
|
67
|
+
secondary: h(n.outstandingInvoiceTotal),
|
|
68
68
|
secondaryLabel: "Outstanding"
|
|
69
69
|
})
|
|
70
|
-
}), /* @__PURE__ */
|
|
70
|
+
}), /* @__PURE__ */ S(c, {
|
|
71
71
|
title: "Utilization",
|
|
72
72
|
description: "Billed vs. total logged hours.",
|
|
73
|
-
children: /* @__PURE__ */
|
|
74
|
-
pct:
|
|
73
|
+
children: /* @__PURE__ */ S(E, {
|
|
74
|
+
pct: N,
|
|
75
75
|
tone: "info",
|
|
76
|
-
primary:
|
|
76
|
+
primary: m(n.billedHours),
|
|
77
77
|
primaryLabel: "Billed",
|
|
78
|
-
secondary:
|
|
78
|
+
secondary: m(n.unbilledHours),
|
|
79
79
|
secondaryLabel: "Unbilled"
|
|
80
80
|
})
|
|
81
81
|
})]
|
|
82
82
|
}),
|
|
83
|
-
/* @__PURE__ */
|
|
83
|
+
/* @__PURE__ */ S(c, {
|
|
84
84
|
title: "Revenue by month",
|
|
85
85
|
description: "Invoiced totals across recent months.",
|
|
86
|
-
children: /* @__PURE__ */
|
|
86
|
+
children: /* @__PURE__ */ S(O, { data: n.revenueByMonth })
|
|
87
87
|
}),
|
|
88
|
-
/* @__PURE__ */
|
|
88
|
+
/* @__PURE__ */ C("div", {
|
|
89
89
|
className: "grid grid-cols-1 gap-4 lg:grid-cols-3",
|
|
90
90
|
children: [
|
|
91
|
-
/* @__PURE__ */
|
|
91
|
+
/* @__PURE__ */ S(c, {
|
|
92
92
|
title: "Engagements",
|
|
93
93
|
description: "By status",
|
|
94
|
-
children: /* @__PURE__ */
|
|
94
|
+
children: /* @__PURE__ */ S(k, { items: n.engagementsByStatus })
|
|
95
95
|
}),
|
|
96
|
-
/* @__PURE__ */
|
|
96
|
+
/* @__PURE__ */ S(c, {
|
|
97
97
|
title: "Matters",
|
|
98
98
|
description: "By status",
|
|
99
|
-
children: /* @__PURE__ */
|
|
99
|
+
children: /* @__PURE__ */ S(k, { items: n.mattersByStatus })
|
|
100
100
|
}),
|
|
101
|
-
/* @__PURE__ */
|
|
101
|
+
/* @__PURE__ */ S(c, {
|
|
102
102
|
title: "Invoices",
|
|
103
103
|
description: "By status",
|
|
104
|
-
children: /* @__PURE__ */
|
|
104
|
+
children: /* @__PURE__ */ S(k, { items: n.invoicesByStatus })
|
|
105
105
|
})
|
|
106
106
|
]
|
|
107
107
|
}),
|
|
108
|
-
/* @__PURE__ */
|
|
108
|
+
/* @__PURE__ */ S(c, {
|
|
109
109
|
title: "Time & billing",
|
|
110
110
|
description: "Billable capacity across the practice.",
|
|
111
|
-
children: /* @__PURE__ */
|
|
111
|
+
children: /* @__PURE__ */ C("div", {
|
|
112
112
|
className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
|
|
113
|
-
children: [/* @__PURE__ */
|
|
113
|
+
children: [/* @__PURE__ */ S(A, {
|
|
114
114
|
label: "Billed hours",
|
|
115
|
-
value:
|
|
116
|
-
total:
|
|
115
|
+
value: n.billedHours,
|
|
116
|
+
total: M,
|
|
117
117
|
tone: "accent"
|
|
118
|
-
}), /* @__PURE__ */
|
|
118
|
+
}), /* @__PURE__ */ S(A, {
|
|
119
119
|
label: "Unbilled hours",
|
|
120
|
-
value:
|
|
121
|
-
total:
|
|
120
|
+
value: n.unbilledHours,
|
|
121
|
+
total: M,
|
|
122
122
|
tone: "warning"
|
|
123
123
|
})]
|
|
124
124
|
})
|
|
125
|
+
}),
|
|
126
|
+
/* @__PURE__ */ C("div", {
|
|
127
|
+
className: "grid grid-cols-2 gap-3 lg:grid-cols-4",
|
|
128
|
+
children: [/* @__PURE__ */ S(o, {
|
|
129
|
+
label: "Realization",
|
|
130
|
+
value: `${Math.round(n.realizationPct)}%`,
|
|
131
|
+
hint: `${h(n.billedValue)} of ${h(n.workedValue)} worked`,
|
|
132
|
+
icon: /* @__PURE__ */ S(y, { className: "h-4 w-4" }),
|
|
133
|
+
tone: "accent"
|
|
134
|
+
}), /* @__PURE__ */ S(o, {
|
|
135
|
+
label: "Write-offs",
|
|
136
|
+
value: h(n.writeOffTotal),
|
|
137
|
+
hint: "Total written off",
|
|
138
|
+
icon: /* @__PURE__ */ S(g, { className: "h-4 w-4" }),
|
|
139
|
+
tone: "warning"
|
|
140
|
+
})]
|
|
141
|
+
}),
|
|
142
|
+
/* @__PURE__ */ S(c, {
|
|
143
|
+
title: "Aged receivables",
|
|
144
|
+
description: "Outstanding invoice value by age — the margin-leak early warning.",
|
|
145
|
+
children: /* @__PURE__ */ C("div", {
|
|
146
|
+
className: "grid grid-cols-2 gap-3 lg:grid-cols-4",
|
|
147
|
+
children: [
|
|
148
|
+
/* @__PURE__ */ S(T, {
|
|
149
|
+
label: "Current",
|
|
150
|
+
bucket: n.agedReceivables.current,
|
|
151
|
+
tone: "success"
|
|
152
|
+
}),
|
|
153
|
+
/* @__PURE__ */ S(T, {
|
|
154
|
+
label: "30 days",
|
|
155
|
+
bucket: n.agedReceivables.thirty,
|
|
156
|
+
tone: "info"
|
|
157
|
+
}),
|
|
158
|
+
/* @__PURE__ */ S(T, {
|
|
159
|
+
label: "60 days",
|
|
160
|
+
bucket: n.agedReceivables.sixty,
|
|
161
|
+
tone: "warning"
|
|
162
|
+
}),
|
|
163
|
+
/* @__PURE__ */ S(T, {
|
|
164
|
+
label: "90+ days",
|
|
165
|
+
bucket: n.agedReceivables.ninetyPlus,
|
|
166
|
+
tone: "danger"
|
|
167
|
+
})
|
|
168
|
+
]
|
|
169
|
+
})
|
|
170
|
+
}),
|
|
171
|
+
/* @__PURE__ */ S(c, {
|
|
172
|
+
title: "Write-offs",
|
|
173
|
+
description: "Append-only audit of written-off amounts.",
|
|
174
|
+
children: j.isLoading ? /* @__PURE__ */ S(a, { label: "Loading write-offs…" }) : j.data?.length ? /* @__PURE__ */ C(u, { children: [/* @__PURE__ */ S("thead", { children: /* @__PURE__ */ C("tr", { children: [
|
|
175
|
+
/* @__PURE__ */ S(f, { children: "Date" }),
|
|
176
|
+
/* @__PURE__ */ S(f, { children: "Reason" }),
|
|
177
|
+
/* @__PURE__ */ S(f, {
|
|
178
|
+
className: "text-right",
|
|
179
|
+
children: "Amount"
|
|
180
|
+
})
|
|
181
|
+
] }) }), /* @__PURE__ */ S("tbody", { children: j.data.map((e) => /* @__PURE__ */ C("tr", { children: [
|
|
182
|
+
/* @__PURE__ */ S(d, { children: p(e.createdAt) }),
|
|
183
|
+
/* @__PURE__ */ S(d, { children: e.reason }),
|
|
184
|
+
/* @__PURE__ */ S(d, {
|
|
185
|
+
className: "text-right",
|
|
186
|
+
children: h(e.amount)
|
|
187
|
+
})
|
|
188
|
+
] }, e.id)) })] }) : /* @__PURE__ */ S(r, {
|
|
189
|
+
title: "No write-offs",
|
|
190
|
+
description: "Nothing has been written off yet."
|
|
191
|
+
})
|
|
125
192
|
})
|
|
126
193
|
]
|
|
127
194
|
});
|
|
128
|
-
},
|
|
195
|
+
}, T = ({ label: e, bucket: t, tone: n }) => /* @__PURE__ */ S(o, {
|
|
196
|
+
label: e,
|
|
197
|
+
value: h(t.amount),
|
|
198
|
+
hint: `${t.count} invoice${t.count === 1 ? "" : "s"}`,
|
|
199
|
+
tone: n
|
|
200
|
+
}), E = ({ pct: e, tone: t, primary: n, primaryLabel: r, secondary: i, secondaryLabel: a }) => /* @__PURE__ */ C("div", {
|
|
129
201
|
className: "flex items-center gap-5",
|
|
130
|
-
children: [/* @__PURE__ */
|
|
202
|
+
children: [/* @__PURE__ */ S(D, {
|
|
131
203
|
pct: e,
|
|
132
204
|
tone: t
|
|
133
|
-
}), /* @__PURE__ */
|
|
205
|
+
}), /* @__PURE__ */ C("div", {
|
|
134
206
|
className: "flex flex-1 flex-col gap-2.5",
|
|
135
207
|
children: [
|
|
136
|
-
/* @__PURE__ */
|
|
208
|
+
/* @__PURE__ */ C("div", {
|
|
137
209
|
className: "flex items-baseline justify-between gap-3",
|
|
138
|
-
children: [/* @__PURE__ */
|
|
210
|
+
children: [/* @__PURE__ */ S("span", {
|
|
139
211
|
className: "text-xs font-medium uppercase tracking-wider text-text-tertiary",
|
|
140
212
|
children: r
|
|
141
|
-
}), /* @__PURE__ */
|
|
213
|
+
}), /* @__PURE__ */ S("span", {
|
|
142
214
|
className: "text-sm font-semibold tabular-nums text-text-primary",
|
|
143
215
|
children: n
|
|
144
216
|
})]
|
|
145
217
|
}),
|
|
146
|
-
/* @__PURE__ */
|
|
147
|
-
/* @__PURE__ */
|
|
218
|
+
/* @__PURE__ */ S("div", { className: "h-px bg-border-seam" }),
|
|
219
|
+
/* @__PURE__ */ C("div", {
|
|
148
220
|
className: "flex items-baseline justify-between gap-3",
|
|
149
|
-
children: [/* @__PURE__ */
|
|
221
|
+
children: [/* @__PURE__ */ S("span", {
|
|
150
222
|
className: "text-xs font-medium uppercase tracking-wider text-text-tertiary",
|
|
151
223
|
children: a
|
|
152
|
-
}), /* @__PURE__ */
|
|
224
|
+
}), /* @__PURE__ */ S("span", {
|
|
153
225
|
className: "text-sm font-semibold tabular-nums text-text-secondary",
|
|
154
226
|
children: i
|
|
155
227
|
})]
|
|
156
228
|
})
|
|
157
229
|
]
|
|
158
230
|
})]
|
|
159
|
-
}),
|
|
231
|
+
}), D = ({ pct: e, tone: t }) => {
|
|
160
232
|
let r = Math.max(0, Math.min(100, e)), i = 2 * Math.PI * 34, a = r / 100 * i;
|
|
161
|
-
return /* @__PURE__ */
|
|
162
|
-
className:
|
|
163
|
-
children: [/* @__PURE__ */
|
|
233
|
+
return /* @__PURE__ */ C("div", {
|
|
234
|
+
className: n("relative h-24 w-24 shrink-0", t === "accent" ? "text-accent" : "text-text-link"),
|
|
235
|
+
children: [/* @__PURE__ */ C("svg", {
|
|
164
236
|
viewBox: "0 0 80 80",
|
|
165
237
|
className: "h-full w-full -rotate-90",
|
|
166
|
-
children: [/* @__PURE__ */
|
|
238
|
+
children: [/* @__PURE__ */ S("circle", {
|
|
167
239
|
cx: "40",
|
|
168
240
|
cy: "40",
|
|
169
241
|
r: 34,
|
|
@@ -171,7 +243,7 @@ var g = () => {
|
|
|
171
243
|
strokeWidth: "8",
|
|
172
244
|
className: "text-bg-sunken",
|
|
173
245
|
stroke: "currentColor"
|
|
174
|
-
}), /* @__PURE__ */
|
|
246
|
+
}), /* @__PURE__ */ S("circle", {
|
|
175
247
|
cx: "40",
|
|
176
248
|
cy: "40",
|
|
177
249
|
r: 34,
|
|
@@ -182,74 +254,74 @@ var g = () => {
|
|
|
182
254
|
strokeDasharray: `${a} ${i}`,
|
|
183
255
|
className: "transition-[stroke-dasharray] duration-700"
|
|
184
256
|
})]
|
|
185
|
-
}), /* @__PURE__ */
|
|
257
|
+
}), /* @__PURE__ */ S("div", {
|
|
186
258
|
className: "absolute inset-0 flex items-center justify-center",
|
|
187
|
-
children: /* @__PURE__ */
|
|
259
|
+
children: /* @__PURE__ */ C("span", {
|
|
188
260
|
className: "text-lg font-bold tabular-nums text-text-primary",
|
|
189
261
|
children: [r, "%"]
|
|
190
262
|
})
|
|
191
263
|
})]
|
|
192
264
|
});
|
|
193
|
-
},
|
|
194
|
-
if (!e.length) return /* @__PURE__ */
|
|
265
|
+
}, O = ({ data: e }) => {
|
|
266
|
+
if (!e.length) return /* @__PURE__ */ S("p", {
|
|
195
267
|
className: "text-sm text-text-tertiary",
|
|
196
268
|
children: "No invoiced revenue yet."
|
|
197
269
|
});
|
|
198
270
|
let t = Math.max(...e.map((e) => e.amount), 1);
|
|
199
|
-
return /* @__PURE__ */
|
|
271
|
+
return /* @__PURE__ */ S("div", {
|
|
200
272
|
className: "overflow-x-auto",
|
|
201
|
-
children: /* @__PURE__ */
|
|
273
|
+
children: /* @__PURE__ */ C("div", {
|
|
202
274
|
className: "min-w-[520px]",
|
|
203
|
-
children: [/* @__PURE__ */
|
|
275
|
+
children: [/* @__PURE__ */ S("div", {
|
|
204
276
|
className: "flex h-60 items-end gap-2 border-b border-border-seam",
|
|
205
|
-
children: e.map((e) => /* @__PURE__ */
|
|
277
|
+
children: e.map((e) => /* @__PURE__ */ C("div", {
|
|
206
278
|
className: "group flex h-full flex-1 flex-col items-center justify-end gap-1.5",
|
|
207
|
-
children: [/* @__PURE__ */
|
|
279
|
+
children: [/* @__PURE__ */ S("span", {
|
|
208
280
|
className: "whitespace-nowrap text-[10px] font-semibold tabular-nums text-text-secondary",
|
|
209
|
-
children:
|
|
210
|
-
}), /* @__PURE__ */
|
|
281
|
+
children: h(e.amount)
|
|
282
|
+
}), /* @__PURE__ */ S("div", {
|
|
211
283
|
className: "w-full max-w-[3rem] rounded-t-lg bg-gradient-to-t from-accent/50 to-accent shadow-sm transition-all duration-200 group-hover:-translate-y-1 group-hover:shadow-md",
|
|
212
284
|
style: { height: `${Math.max(e.amount / t * 90, 2)}%` },
|
|
213
|
-
title:
|
|
285
|
+
title: h(e.amount)
|
|
214
286
|
})]
|
|
215
287
|
}, e.month))
|
|
216
|
-
}), /* @__PURE__ */
|
|
288
|
+
}), /* @__PURE__ */ S("div", {
|
|
217
289
|
className: "mt-2 flex gap-2",
|
|
218
|
-
children: e.map((e) => /* @__PURE__ */
|
|
290
|
+
children: e.map((e) => /* @__PURE__ */ S("span", {
|
|
219
291
|
className: "flex-1 text-center text-xs text-text-tertiary",
|
|
220
|
-
children:
|
|
292
|
+
children: P(e.month)
|
|
221
293
|
}, e.month))
|
|
222
294
|
})]
|
|
223
295
|
})
|
|
224
296
|
});
|
|
225
|
-
},
|
|
226
|
-
if (!e.length) return /* @__PURE__ */
|
|
297
|
+
}, k = ({ items: e }) => {
|
|
298
|
+
if (!e.length) return /* @__PURE__ */ S("p", {
|
|
227
299
|
className: "text-sm text-text-tertiary",
|
|
228
300
|
children: "No records yet."
|
|
229
301
|
});
|
|
230
|
-
let
|
|
231
|
-
return /* @__PURE__ */
|
|
302
|
+
let t = Math.max(...e.map((e) => e.count), 1), r = e.reduce((e, t) => e + t.count, 0) || 1;
|
|
303
|
+
return /* @__PURE__ */ S("ul", {
|
|
232
304
|
className: "space-y-3.5",
|
|
233
|
-
children: e.map((e) => /* @__PURE__ */
|
|
305
|
+
children: e.map((e) => /* @__PURE__ */ C("li", {
|
|
234
306
|
className: "grid grid-cols-[7.5rem_minmax(0,1fr)_auto] items-center gap-3",
|
|
235
307
|
children: [
|
|
236
|
-
/* @__PURE__ */
|
|
308
|
+
/* @__PURE__ */ S(l, {
|
|
237
309
|
status: e.status,
|
|
238
310
|
className: "justify-self-start"
|
|
239
311
|
}),
|
|
240
|
-
/* @__PURE__ */
|
|
312
|
+
/* @__PURE__ */ S("div", {
|
|
241
313
|
className: "h-2.5 overflow-hidden rounded-full bg-bg-sunken",
|
|
242
|
-
children: /* @__PURE__ */
|
|
243
|
-
className:
|
|
244
|
-
style: { width: `${Math.max(e.count /
|
|
314
|
+
children: /* @__PURE__ */ S("div", {
|
|
315
|
+
className: n("h-full rounded-full bg-gradient-to-r transition-all duration-500", N(e.status)),
|
|
316
|
+
style: { width: `${Math.max(e.count / t * 100, 4)}%` }
|
|
245
317
|
})
|
|
246
318
|
}),
|
|
247
|
-
/* @__PURE__ */
|
|
319
|
+
/* @__PURE__ */ C("span", {
|
|
248
320
|
className: "flex items-baseline justify-end gap-1.5 tabular-nums",
|
|
249
|
-
children: [/* @__PURE__ */
|
|
321
|
+
children: [/* @__PURE__ */ S("span", {
|
|
250
322
|
className: "text-sm font-semibold text-text-primary",
|
|
251
323
|
children: e.count
|
|
252
|
-
}), /* @__PURE__ */
|
|
324
|
+
}), /* @__PURE__ */ C("span", {
|
|
253
325
|
className: "text-xs text-text-tertiary",
|
|
254
326
|
children: [Math.round(e.count / r * 100), "%"]
|
|
255
327
|
})]
|
|
@@ -257,40 +329,40 @@ var g = () => {
|
|
|
257
329
|
]
|
|
258
330
|
}, e.status))
|
|
259
331
|
});
|
|
260
|
-
},
|
|
261
|
-
let a = r > 0 ? Math.round(
|
|
262
|
-
return /* @__PURE__ */
|
|
332
|
+
}, A = ({ label: e, value: t, total: r, tone: i }) => {
|
|
333
|
+
let a = r > 0 ? Math.round(t / r * 100) : 0, o = i === "accent" ? "from-accent/60 to-accent" : "from-status-warning-text/60 to-status-warning-text";
|
|
334
|
+
return /* @__PURE__ */ C("div", {
|
|
263
335
|
className: "rounded-xl border border-border-seam bg-bg-canvas p-4",
|
|
264
|
-
children: [/* @__PURE__ */
|
|
336
|
+
children: [/* @__PURE__ */ C("div", {
|
|
265
337
|
className: "mb-2.5 flex items-center justify-between gap-3",
|
|
266
|
-
children: [/* @__PURE__ */
|
|
338
|
+
children: [/* @__PURE__ */ S("span", {
|
|
267
339
|
className: "text-sm font-medium text-text-secondary",
|
|
268
340
|
children: e
|
|
269
|
-
}), /* @__PURE__ */
|
|
341
|
+
}), /* @__PURE__ */ C("span", {
|
|
270
342
|
className: "text-sm font-semibold tabular-nums text-text-primary",
|
|
271
343
|
children: [
|
|
272
|
-
|
|
344
|
+
m(t),
|
|
273
345
|
" · ",
|
|
274
346
|
a,
|
|
275
347
|
"%"
|
|
276
348
|
]
|
|
277
349
|
})]
|
|
278
|
-
}), /* @__PURE__ */
|
|
350
|
+
}), /* @__PURE__ */ S("div", {
|
|
279
351
|
className: "h-3 overflow-hidden rounded-full bg-bg-sunken",
|
|
280
|
-
children: /* @__PURE__ */
|
|
281
|
-
className:
|
|
352
|
+
children: /* @__PURE__ */ S("div", {
|
|
353
|
+
className: n("h-full rounded-full bg-gradient-to-r transition-all duration-700", o),
|
|
282
354
|
style: { width: `${a}%` }
|
|
283
355
|
})
|
|
284
356
|
})]
|
|
285
357
|
});
|
|
286
|
-
},
|
|
358
|
+
}, j = {
|
|
287
359
|
success: "from-status-success-text/55 to-status-success-text",
|
|
288
360
|
warning: "from-status-warning-text/55 to-status-warning-text",
|
|
289
361
|
danger: "from-status-error-text/55 to-status-error-text",
|
|
290
362
|
info: "from-text-link/55 to-text-link",
|
|
291
363
|
accent: "from-accent/55 to-accent",
|
|
292
364
|
neutral: "from-text-tertiary/30 to-text-tertiary/70"
|
|
293
|
-
},
|
|
365
|
+
}, M = {
|
|
294
366
|
ACTIVE: "success",
|
|
295
367
|
DELIVERED: "success",
|
|
296
368
|
INVOICED: "success",
|
|
@@ -330,16 +402,16 @@ var g = () => {
|
|
|
330
402
|
DRAFT: "neutral",
|
|
331
403
|
WAIVED: "neutral"
|
|
332
404
|
};
|
|
333
|
-
function
|
|
334
|
-
return
|
|
405
|
+
function N(e) {
|
|
406
|
+
return j[M[e] ?? "accent"];
|
|
335
407
|
}
|
|
336
|
-
function
|
|
408
|
+
function P(e) {
|
|
337
409
|
let [t, n] = e.split("-").map(Number);
|
|
338
410
|
if (!t || !n) return e;
|
|
339
411
|
let r = new Date(t, n - 1, 1);
|
|
340
412
|
return Number.isNaN(r.getTime()) ? e : r.toLocaleDateString(void 0, { month: "short" });
|
|
341
413
|
}
|
|
342
414
|
//#endregion
|
|
343
|
-
export {
|
|
415
|
+
export { w as AnalyticsPage };
|
|
344
416
|
|
|
345
417
|
//# sourceMappingURL=AnalyticsPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../src/proserviceworld/pages/AnalyticsPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { BarChart3, TrendingUp, Clock, Wallet } from 'lucide-react';\nimport { useDashboardStats } from '../hooks/useProServiceWorldApi';\nimport { cn } from '../utils/cn';\nimport {\n PageHeader,\n SectionCard,\n MetricCard,\n LoadingBlock,\n ErrorState,\n StatusPill,\n fmtMoney,\n fmtHours,\n} from '../components/ui';\n\nexport const AnalyticsPage: FC = () => {\n const { data, isLoading, error, refetch } = useDashboardStats();\n if (isLoading) return <LoadingBlock label=\"Crunching numbers…\" />;\n if (error) return <ErrorState error={error} onRetry={() => void refetch()} />;\n if (!data) return null;\n\n const totalHours = data.unbilledHours + data.billedHours;\n const utilization = totalHours > 0 ? Math.round((data.billedHours / totalHours) * 100) : 0;\n const billableBase = data.paidInvoiceTotal + data.outstandingInvoiceTotal;\n const collectionRate =\n billableBase > 0 ? Math.round((data.paidInvoiceTotal / billableBase) * 100) : 0;\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Analytics\"\n subtitle=\"Real, live metrics across the practice — no estimates, straight from your records.\"\n icon={<BarChart3 className=\"h-6 w-6\" />}\n />\n\n <div className=\"grid grid-cols-2 gap-3 lg:grid-cols-4\">\n <MetricCard\n label=\"Collected revenue\"\n value={fmtMoney(data.paidInvoiceTotal)}\n hint={`${fmtMoney(billableBase)} invoiced`}\n icon={<Wallet className=\"h-4 w-4\" />}\n tone=\"success\"\n />\n <MetricCard\n label=\"Collection rate\"\n value={`${collectionRate}%`}\n hint={`${fmtMoney(data.outstandingInvoiceTotal)} outstanding`}\n icon={<TrendingUp className=\"h-4 w-4\" />}\n tone=\"accent\"\n />\n <MetricCard\n label=\"Utilization\"\n value={`${utilization}%`}\n hint={`${fmtHours(data.billedHours)} of ${fmtHours(totalHours)}`}\n icon={<Clock className=\"h-4 w-4\" />}\n tone=\"info\"\n />\n <MetricCard\n label=\"Active engagements\"\n value={data.activeEngagements}\n hint={`${data.openMatters} open matters`}\n icon={<BarChart3 className=\"h-4 w-4\" />}\n tone=\"warning\"\n />\n </div>\n\n {/* Practice-health meters — collection & utilization at a glance */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <SectionCard title=\"Collection rate\" description=\"Paid vs. total invoiced.\">\n <GaugeRow\n pct={collectionRate}\n tone=\"accent\"\n primary={fmtMoney(data.paidInvoiceTotal)}\n primaryLabel=\"Collected\"\n secondary={fmtMoney(data.outstandingInvoiceTotal)}\n secondaryLabel=\"Outstanding\"\n />\n </SectionCard>\n <SectionCard title=\"Utilization\" description=\"Billed vs. total logged hours.\">\n <GaugeRow\n pct={utilization}\n tone=\"info\"\n primary={fmtHours(data.billedHours)}\n primaryLabel=\"Billed\"\n secondary={fmtHours(data.unbilledHours)}\n secondaryLabel=\"Unbilled\"\n />\n </SectionCard>\n </div>\n\n <SectionCard title=\"Revenue by month\" description=\"Invoiced totals across recent months.\">\n <RevenueChart data={data.revenueByMonth} />\n </SectionCard>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <SectionCard title=\"Engagements\" description=\"By status\">\n <Distribution items={data.engagementsByStatus} />\n </SectionCard>\n <SectionCard title=\"Matters\" description=\"By status\">\n <Distribution items={data.mattersByStatus} />\n </SectionCard>\n <SectionCard title=\"Invoices\" description=\"By status\">\n <Distribution items={data.invoicesByStatus} />\n </SectionCard>\n </div>\n\n <SectionCard title=\"Time & billing\" description=\"Billable capacity across the practice.\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <HoursBar\n label=\"Billed hours\"\n value={data.billedHours}\n total={totalHours}\n tone=\"accent\"\n />\n <HoursBar\n label=\"Unbilled hours\"\n value={data.unbilledHours}\n total={totalHours}\n tone=\"warning\"\n />\n </div>\n </SectionCard>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Radial gauge with a supporting two-number legend\n// ---------------------------------------------------------------------------\nconst GaugeRow: FC<{\n pct: number;\n tone: 'accent' | 'info';\n primary: string;\n primaryLabel: string;\n secondary: string;\n secondaryLabel: string;\n}> = ({ pct, tone, primary, primaryLabel, secondary, secondaryLabel }) => (\n <div className=\"flex items-center gap-5\">\n <Gauge pct={pct} tone={tone} />\n <div className=\"flex flex-1 flex-col gap-2.5\">\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"text-xs font-medium uppercase tracking-wider text-text-tertiary\">\n {primaryLabel}\n </span>\n <span className=\"text-sm font-semibold tabular-nums text-text-primary\">{primary}</span>\n </div>\n <div className=\"h-px bg-border-seam\" />\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"text-xs font-medium uppercase tracking-wider text-text-tertiary\">\n {secondaryLabel}\n </span>\n <span className=\"text-sm font-semibold tabular-nums text-text-secondary\">{secondary}</span>\n </div>\n </div>\n </div>\n);\n\nconst Gauge: FC<{ pct: number; tone: 'accent' | 'info' }> = ({ pct, tone }) => {\n const clamped = Math.max(0, Math.min(100, pct));\n const r = 34;\n const c = 2 * Math.PI * r;\n const dash = (clamped / 100) * c;\n return (\n <div\n className={cn(\n 'relative h-24 w-24 shrink-0',\n tone === 'accent' ? 'text-accent' : 'text-text-link'\n )}\n >\n <svg viewBox=\"0 0 80 80\" className=\"h-full w-full -rotate-90\">\n <circle\n cx=\"40\"\n cy=\"40\"\n r={r}\n fill=\"none\"\n strokeWidth=\"8\"\n className=\"text-bg-sunken\"\n stroke=\"currentColor\"\n />\n <circle\n cx=\"40\"\n cy=\"40\"\n r={r}\n fill=\"none\"\n strokeWidth=\"8\"\n strokeLinecap=\"round\"\n stroke=\"currentColor\"\n strokeDasharray={`${dash} ${c}`}\n className=\"transition-[stroke-dasharray] duration-700\"\n />\n </svg>\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <span className=\"text-lg font-bold tabular-nums text-text-primary\">{clamped}%</span>\n </div>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Revenue chart — gradient columns with value labels + month axis\n// ---------------------------------------------------------------------------\nconst RevenueChart: FC<{ data: { month: string; amount: number }[] }> = ({ data }) => {\n if (!data.length) return <p className=\"text-sm text-text-tertiary\">No invoiced revenue yet.</p>;\n const peak = Math.max(...data.map((d) => d.amount), 1);\n return (\n <div className=\"overflow-x-auto\">\n <div className=\"min-w-[520px]\">\n <div className=\"flex h-60 items-end gap-2 border-b border-border-seam\">\n {data.map((d) => (\n <div\n key={d.month}\n className=\"group flex h-full flex-1 flex-col items-center justify-end gap-1.5\"\n >\n <span className=\"whitespace-nowrap text-[10px] font-semibold tabular-nums text-text-secondary\">\n {fmtMoney(d.amount)}\n </span>\n <div\n className=\"w-full max-w-[3rem] rounded-t-lg bg-gradient-to-t from-accent/50 to-accent shadow-sm transition-all duration-200 group-hover:-translate-y-1 group-hover:shadow-md\"\n style={{ height: `${Math.max((d.amount / peak) * 90, 2)}%` }}\n title={fmtMoney(d.amount)}\n />\n </div>\n ))}\n </div>\n <div className=\"mt-2 flex gap-2\">\n {data.map((d) => (\n <span key={d.month} className=\"flex-1 text-center text-xs text-text-tertiary\">\n {monthShort(d.month)}\n </span>\n ))}\n </div>\n </div>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Distribution — tone-colored gradient bars with count + share\n// ---------------------------------------------------------------------------\nconst Distribution: FC<{ items: { status: string; count: number }[] }> = ({ items }) => {\n if (!items.length) return <p className=\"text-sm text-text-tertiary\">No records yet.</p>;\n const max = Math.max(...items.map((i) => i.count), 1);\n const total = items.reduce((s, i) => s + i.count, 0) || 1;\n return (\n <ul className=\"space-y-3.5\">\n {items.map((i) => (\n <li\n key={i.status}\n className=\"grid grid-cols-[7.5rem_minmax(0,1fr)_auto] items-center gap-3\"\n >\n <StatusPill status={i.status} className=\"justify-self-start\" />\n <div className=\"h-2.5 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className={cn(\n 'h-full rounded-full bg-gradient-to-r transition-all duration-500',\n barGradientFor(i.status)\n )}\n style={{ width: `${Math.max((i.count / max) * 100, 4)}%` }}\n />\n </div>\n <span className=\"flex items-baseline justify-end gap-1.5 tabular-nums\">\n <span className=\"text-sm font-semibold text-text-primary\">{i.count}</span>\n <span className=\"text-xs text-text-tertiary\">\n {Math.round((i.count / total) * 100)}%\n </span>\n </span>\n </li>\n ))}\n </ul>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Hours bar — big gradient progress meter (billed vs unbilled)\n// ---------------------------------------------------------------------------\nconst HoursBar: FC<{ label: string; value: number; total: number; tone: 'accent' | 'warning' }> = ({\n label,\n value,\n total,\n tone,\n}) => {\n const pct = total > 0 ? Math.round((value / total) * 100) : 0;\n const grad =\n tone === 'accent'\n ? 'from-accent/60 to-accent'\n : 'from-status-warning-text/60 to-status-warning-text';\n return (\n <div className=\"rounded-xl border border-border-seam bg-bg-canvas p-4\">\n <div className=\"mb-2.5 flex items-center justify-between gap-3\">\n <span className=\"text-sm font-medium text-text-secondary\">{label}</span>\n <span className=\"text-sm font-semibold tabular-nums text-text-primary\">\n {fmtHours(value)} · {pct}%\n </span>\n </div>\n <div className=\"h-3 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className={cn('h-full rounded-full bg-gradient-to-r transition-all duration-700', grad)}\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Chart helpers (token-driven; self-contained to this page)\n// ---------------------------------------------------------------------------\ntype BarTone = 'success' | 'warning' | 'danger' | 'info' | 'accent' | 'neutral';\n\nconst BAR_GRADIENT: Record<BarTone, string> = {\n success: 'from-status-success-text/55 to-status-success-text',\n warning: 'from-status-warning-text/55 to-status-warning-text',\n danger: 'from-status-error-text/55 to-status-error-text',\n info: 'from-text-link/55 to-text-link',\n accent: 'from-accent/55 to-accent',\n neutral: 'from-text-tertiary/30 to-text-tertiary/70',\n};\n\n/** Mirror of the shared StatusPill tone mapping, used to tint chart bars. */\nconst STATUS_BAR_TONE: Record<string, BarTone> = {\n ACTIVE: 'success',\n DELIVERED: 'success',\n INVOICED: 'success',\n PAID: 'success',\n APPROVED: 'success',\n PASSED: 'success',\n CLEAR: 'success',\n COMPLETED: 'success',\n FINAL: 'success',\n PROSPECT: 'info',\n PROPOSAL: 'info',\n MEDIUM: 'info',\n INTERNAL_REVIEW: 'info',\n SUBMITTED: 'info',\n SENT: 'info',\n IN_REVIEW: 'info',\n REVIEW: 'warning',\n ON_HOLD: 'warning',\n HIGH: 'warning',\n CLIENT_REVIEW: 'warning',\n PENDING: 'warning',\n POTENTIAL_CONFLICT: 'warning',\n CANCELLED: 'danger',\n CRITICAL: 'danger',\n REJECTED: 'danger',\n OVERDUE: 'danger',\n FAILED: 'danger',\n CONFLICT: 'danger',\n ACCEPTED: 'accent',\n IN_PROGRESS: 'accent',\n OPEN: 'accent',\n BILLED: 'accent',\n INACTIVE: 'neutral',\n ARCHIVED: 'neutral',\n CLOSED: 'neutral',\n LOW: 'neutral',\n DRAFT: 'neutral',\n WAIVED: 'neutral',\n};\n\nfunction barGradientFor(status: string): string {\n return BAR_GRADIENT[STATUS_BAR_TONE[status] ?? 'accent'];\n}\n\n/** \"2026-07\" → \"Jul\" (falls back to the raw value if unparseable). */\nfunction monthShort(m: string): string {\n const [y, mo] = m.split('-').map(Number);\n if (!y || !mo) return m;\n const d = new Date(y, mo - 1, 1);\n return Number.isNaN(d.getTime()) ? m : d.toLocaleDateString(undefined, { month: 'short' });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../src/proserviceworld/pages/AnalyticsPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { BarChart3, TrendingUp, Clock, Wallet, Scale, AlertTriangle } from 'lucide-react';\nimport { useDashboardStats, useWriteOffs } from '../hooks/useProServiceWorldApi';\nimport { cn } from '../utils/cn';\nimport {\n PageHeader,\n SectionCard,\n MetricCard,\n LoadingBlock,\n ErrorState,\n StatusPill,\n Empty,\n TableWrap,\n Th,\n Td,\n fmtMoney,\n fmtHours,\n fmtDate,\n} from '../components/ui';\n\nexport const AnalyticsPage: FC = () => {\n const { data, isLoading, error, refetch } = useDashboardStats();\n const writeOffs = useWriteOffs({ pagination: { limit: 10 } });\n if (isLoading) return <LoadingBlock label=\"Crunching numbers…\" />;\n if (error) return <ErrorState error={error} onRetry={() => void refetch()} />;\n if (!data) return null;\n\n const totalHours = data.unbilledHours + data.billedHours;\n const utilization = totalHours > 0 ? Math.round((data.billedHours / totalHours) * 100) : 0;\n const billableBase = data.paidInvoiceTotal + data.outstandingInvoiceTotal;\n const collectionRate =\n billableBase > 0 ? Math.round((data.paidInvoiceTotal / billableBase) * 100) : 0;\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Analytics\"\n subtitle=\"Real, live metrics across the practice — no estimates, straight from your records.\"\n icon={<BarChart3 className=\"h-6 w-6\" />}\n />\n\n <div className=\"grid grid-cols-2 gap-3 lg:grid-cols-4\">\n <MetricCard\n label=\"Collected revenue\"\n value={fmtMoney(data.paidInvoiceTotal)}\n hint={`${fmtMoney(billableBase)} invoiced`}\n icon={<Wallet className=\"h-4 w-4\" />}\n tone=\"success\"\n />\n <MetricCard\n label=\"Collection rate\"\n value={`${collectionRate}%`}\n hint={`${fmtMoney(data.outstandingInvoiceTotal)} outstanding`}\n icon={<TrendingUp className=\"h-4 w-4\" />}\n tone=\"accent\"\n />\n <MetricCard\n label=\"Utilization\"\n value={`${utilization}%`}\n hint={`${fmtHours(data.billedHours)} of ${fmtHours(totalHours)}`}\n icon={<Clock className=\"h-4 w-4\" />}\n tone=\"info\"\n />\n <MetricCard\n label=\"Active engagements\"\n value={data.activeEngagements}\n hint={`${data.openMatters} open matters`}\n icon={<BarChart3 className=\"h-4 w-4\" />}\n tone=\"warning\"\n />\n </div>\n\n {/* Practice-health meters — collection & utilization at a glance */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <SectionCard title=\"Collection rate\" description=\"Paid vs. total invoiced.\">\n <GaugeRow\n pct={collectionRate}\n tone=\"accent\"\n primary={fmtMoney(data.paidInvoiceTotal)}\n primaryLabel=\"Collected\"\n secondary={fmtMoney(data.outstandingInvoiceTotal)}\n secondaryLabel=\"Outstanding\"\n />\n </SectionCard>\n <SectionCard title=\"Utilization\" description=\"Billed vs. total logged hours.\">\n <GaugeRow\n pct={utilization}\n tone=\"info\"\n primary={fmtHours(data.billedHours)}\n primaryLabel=\"Billed\"\n secondary={fmtHours(data.unbilledHours)}\n secondaryLabel=\"Unbilled\"\n />\n </SectionCard>\n </div>\n\n <SectionCard title=\"Revenue by month\" description=\"Invoiced totals across recent months.\">\n <RevenueChart data={data.revenueByMonth} />\n </SectionCard>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <SectionCard title=\"Engagements\" description=\"By status\">\n <Distribution items={data.engagementsByStatus} />\n </SectionCard>\n <SectionCard title=\"Matters\" description=\"By status\">\n <Distribution items={data.mattersByStatus} />\n </SectionCard>\n <SectionCard title=\"Invoices\" description=\"By status\">\n <Distribution items={data.invoicesByStatus} />\n </SectionCard>\n </div>\n\n <SectionCard title=\"Time & billing\" description=\"Billable capacity across the practice.\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <HoursBar\n label=\"Billed hours\"\n value={data.billedHours}\n total={totalHours}\n tone=\"accent\"\n />\n <HoursBar\n label=\"Unbilled hours\"\n value={data.unbilledHours}\n total={totalHours}\n tone=\"warning\"\n />\n </div>\n </SectionCard>\n\n <div className=\"grid grid-cols-2 gap-3 lg:grid-cols-4\">\n <MetricCard\n label=\"Realization\"\n value={`${Math.round(data.realizationPct)}%`}\n hint={`${fmtMoney(data.billedValue)} of ${fmtMoney(data.workedValue)} worked`}\n icon={<Scale className=\"h-4 w-4\" />}\n tone=\"accent\"\n />\n <MetricCard\n label=\"Write-offs\"\n value={fmtMoney(data.writeOffTotal)}\n hint=\"Total written off\"\n icon={<AlertTriangle className=\"h-4 w-4\" />}\n tone=\"warning\"\n />\n </div>\n\n <SectionCard\n title=\"Aged receivables\"\n description=\"Outstanding invoice value by age — the margin-leak early warning.\"\n >\n <div className=\"grid grid-cols-2 gap-3 lg:grid-cols-4\">\n <AgingBucket label=\"Current\" bucket={data.agedReceivables.current} tone=\"success\" />\n <AgingBucket label=\"30 days\" bucket={data.agedReceivables.thirty} tone=\"info\" />\n <AgingBucket label=\"60 days\" bucket={data.agedReceivables.sixty} tone=\"warning\" />\n <AgingBucket label=\"90+ days\" bucket={data.agedReceivables.ninetyPlus} tone=\"danger\" />\n </div>\n </SectionCard>\n\n <SectionCard title=\"Write-offs\" description=\"Append-only audit of written-off amounts.\">\n {writeOffs.isLoading ? (\n <LoadingBlock label=\"Loading write-offs…\" />\n ) : !writeOffs.data?.length ? (\n <Empty title=\"No write-offs\" description=\"Nothing has been written off yet.\" />\n ) : (\n <TableWrap>\n <thead>\n <tr>\n <Th>Date</Th>\n <Th>Reason</Th>\n <Th className=\"text-right\">Amount</Th>\n </tr>\n </thead>\n <tbody>\n {writeOffs.data.map((w) => (\n <tr key={w.id}>\n <Td>{fmtDate(w.createdAt)}</Td>\n <Td>{w.reason}</Td>\n <Td className=\"text-right\">{fmtMoney(w.amount)}</Td>\n </tr>\n ))}\n </tbody>\n </TableWrap>\n )}\n </SectionCard>\n </div>\n );\n};\n\nconst AgingBucket: FC<{\n label: string;\n bucket: { amount: number; count: number };\n tone: 'success' | 'info' | 'warning' | 'danger';\n}> = ({ label, bucket, tone }) => (\n <MetricCard\n label={label}\n value={fmtMoney(bucket.amount)}\n hint={`${bucket.count} invoice${bucket.count === 1 ? '' : 's'}`}\n tone={tone}\n />\n);\n\n// ---------------------------------------------------------------------------\n// Radial gauge with a supporting two-number legend\n// ---------------------------------------------------------------------------\nconst GaugeRow: FC<{\n pct: number;\n tone: 'accent' | 'info';\n primary: string;\n primaryLabel: string;\n secondary: string;\n secondaryLabel: string;\n}> = ({ pct, tone, primary, primaryLabel, secondary, secondaryLabel }) => (\n <div className=\"flex items-center gap-5\">\n <Gauge pct={pct} tone={tone} />\n <div className=\"flex flex-1 flex-col gap-2.5\">\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"text-xs font-medium uppercase tracking-wider text-text-tertiary\">\n {primaryLabel}\n </span>\n <span className=\"text-sm font-semibold tabular-nums text-text-primary\">{primary}</span>\n </div>\n <div className=\"h-px bg-border-seam\" />\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"text-xs font-medium uppercase tracking-wider text-text-tertiary\">\n {secondaryLabel}\n </span>\n <span className=\"text-sm font-semibold tabular-nums text-text-secondary\">{secondary}</span>\n </div>\n </div>\n </div>\n);\n\nconst Gauge: FC<{ pct: number; tone: 'accent' | 'info' }> = ({ pct, tone }) => {\n const clamped = Math.max(0, Math.min(100, pct));\n const r = 34;\n const c = 2 * Math.PI * r;\n const dash = (clamped / 100) * c;\n return (\n <div\n className={cn(\n 'relative h-24 w-24 shrink-0',\n tone === 'accent' ? 'text-accent' : 'text-text-link'\n )}\n >\n <svg viewBox=\"0 0 80 80\" className=\"h-full w-full -rotate-90\">\n <circle\n cx=\"40\"\n cy=\"40\"\n r={r}\n fill=\"none\"\n strokeWidth=\"8\"\n className=\"text-bg-sunken\"\n stroke=\"currentColor\"\n />\n <circle\n cx=\"40\"\n cy=\"40\"\n r={r}\n fill=\"none\"\n strokeWidth=\"8\"\n strokeLinecap=\"round\"\n stroke=\"currentColor\"\n strokeDasharray={`${dash} ${c}`}\n className=\"transition-[stroke-dasharray] duration-700\"\n />\n </svg>\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <span className=\"text-lg font-bold tabular-nums text-text-primary\">{clamped}%</span>\n </div>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Revenue chart — gradient columns with value labels + month axis\n// ---------------------------------------------------------------------------\nconst RevenueChart: FC<{ data: { month: string; amount: number }[] }> = ({ data }) => {\n if (!data.length) return <p className=\"text-sm text-text-tertiary\">No invoiced revenue yet.</p>;\n const peak = Math.max(...data.map((d) => d.amount), 1);\n return (\n <div className=\"overflow-x-auto\">\n <div className=\"min-w-[520px]\">\n <div className=\"flex h-60 items-end gap-2 border-b border-border-seam\">\n {data.map((d) => (\n <div\n key={d.month}\n className=\"group flex h-full flex-1 flex-col items-center justify-end gap-1.5\"\n >\n <span className=\"whitespace-nowrap text-[10px] font-semibold tabular-nums text-text-secondary\">\n {fmtMoney(d.amount)}\n </span>\n <div\n className=\"w-full max-w-[3rem] rounded-t-lg bg-gradient-to-t from-accent/50 to-accent shadow-sm transition-all duration-200 group-hover:-translate-y-1 group-hover:shadow-md\"\n style={{ height: `${Math.max((d.amount / peak) * 90, 2)}%` }}\n title={fmtMoney(d.amount)}\n />\n </div>\n ))}\n </div>\n <div className=\"mt-2 flex gap-2\">\n {data.map((d) => (\n <span key={d.month} className=\"flex-1 text-center text-xs text-text-tertiary\">\n {monthShort(d.month)}\n </span>\n ))}\n </div>\n </div>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Distribution — tone-colored gradient bars with count + share\n// ---------------------------------------------------------------------------\nconst Distribution: FC<{ items: { status: string; count: number }[] }> = ({ items }) => {\n if (!items.length) return <p className=\"text-sm text-text-tertiary\">No records yet.</p>;\n const max = Math.max(...items.map((i) => i.count), 1);\n const total = items.reduce((s, i) => s + i.count, 0) || 1;\n return (\n <ul className=\"space-y-3.5\">\n {items.map((i) => (\n <li\n key={i.status}\n className=\"grid grid-cols-[7.5rem_minmax(0,1fr)_auto] items-center gap-3\"\n >\n <StatusPill status={i.status} className=\"justify-self-start\" />\n <div className=\"h-2.5 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className={cn(\n 'h-full rounded-full bg-gradient-to-r transition-all duration-500',\n barGradientFor(i.status)\n )}\n style={{ width: `${Math.max((i.count / max) * 100, 4)}%` }}\n />\n </div>\n <span className=\"flex items-baseline justify-end gap-1.5 tabular-nums\">\n <span className=\"text-sm font-semibold text-text-primary\">{i.count}</span>\n <span className=\"text-xs text-text-tertiary\">\n {Math.round((i.count / total) * 100)}%\n </span>\n </span>\n </li>\n ))}\n </ul>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Hours bar — big gradient progress meter (billed vs unbilled)\n// ---------------------------------------------------------------------------\nconst HoursBar: FC<{ label: string; value: number; total: number; tone: 'accent' | 'warning' }> = ({\n label,\n value,\n total,\n tone,\n}) => {\n const pct = total > 0 ? Math.round((value / total) * 100) : 0;\n const grad =\n tone === 'accent'\n ? 'from-accent/60 to-accent'\n : 'from-status-warning-text/60 to-status-warning-text';\n return (\n <div className=\"rounded-xl border border-border-seam bg-bg-canvas p-4\">\n <div className=\"mb-2.5 flex items-center justify-between gap-3\">\n <span className=\"text-sm font-medium text-text-secondary\">{label}</span>\n <span className=\"text-sm font-semibold tabular-nums text-text-primary\">\n {fmtHours(value)} · {pct}%\n </span>\n </div>\n <div className=\"h-3 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className={cn('h-full rounded-full bg-gradient-to-r transition-all duration-700', grad)}\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Chart helpers (token-driven; self-contained to this page)\n// ---------------------------------------------------------------------------\ntype BarTone = 'success' | 'warning' | 'danger' | 'info' | 'accent' | 'neutral';\n\nconst BAR_GRADIENT: Record<BarTone, string> = {\n success: 'from-status-success-text/55 to-status-success-text',\n warning: 'from-status-warning-text/55 to-status-warning-text',\n danger: 'from-status-error-text/55 to-status-error-text',\n info: 'from-text-link/55 to-text-link',\n accent: 'from-accent/55 to-accent',\n neutral: 'from-text-tertiary/30 to-text-tertiary/70',\n};\n\n/** Mirror of the shared StatusPill tone mapping, used to tint chart bars. */\nconst STATUS_BAR_TONE: Record<string, BarTone> = {\n ACTIVE: 'success',\n DELIVERED: 'success',\n INVOICED: 'success',\n PAID: 'success',\n APPROVED: 'success',\n PASSED: 'success',\n CLEAR: 'success',\n COMPLETED: 'success',\n FINAL: 'success',\n PROSPECT: 'info',\n PROPOSAL: 'info',\n MEDIUM: 'info',\n INTERNAL_REVIEW: 'info',\n SUBMITTED: 'info',\n SENT: 'info',\n IN_REVIEW: 'info',\n REVIEW: 'warning',\n ON_HOLD: 'warning',\n HIGH: 'warning',\n CLIENT_REVIEW: 'warning',\n PENDING: 'warning',\n POTENTIAL_CONFLICT: 'warning',\n CANCELLED: 'danger',\n CRITICAL: 'danger',\n REJECTED: 'danger',\n OVERDUE: 'danger',\n FAILED: 'danger',\n CONFLICT: 'danger',\n ACCEPTED: 'accent',\n IN_PROGRESS: 'accent',\n OPEN: 'accent',\n BILLED: 'accent',\n INACTIVE: 'neutral',\n ARCHIVED: 'neutral',\n CLOSED: 'neutral',\n LOW: 'neutral',\n DRAFT: 'neutral',\n WAIVED: 'neutral',\n};\n\nfunction barGradientFor(status: string): string {\n return BAR_GRADIENT[STATUS_BAR_TONE[status] ?? 'accent'];\n}\n\n/** \"2026-07\" → \"Jul\" (falls back to the raw value if unparseable). */\nfunction monthShort(m: string): string {\n const [y, mo] = m.split('-').map(Number);\n if (!y || !mo) return m;\n const d = new Date(y, mo - 1, 1);\n return Number.isNaN(d.getTime()) ? m : d.toLocaleDateString(undefined, { month: 'short' });\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InvoicesPage.d.ts","sourceRoot":"","sources":["../../../src/proserviceworld/pages/InvoicesPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"InvoicesPage.d.ts","sourceRoot":"","sources":["../../../src/proserviceworld/pages/InvoicesPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAmDhC,eAAO,MAAM,YAAY,EAAE,EAsP1B,CAAC"}
|