@burdenoff/microfe-proserviceworld 2026.706.2 → 2026.706.3
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/components/DiscussionThread.js +1 -1
- package/dist/proserviceworld/components/DiscussionThread.js.map +1 -1
- package/dist/proserviceworld/components/ui.js +18 -18
- package/dist/proserviceworld/components/ui.js.map +1 -1
- package/dist/proserviceworld/hooks/useEntityComments.js.map +1 -1
- package/dist/proserviceworld/pages/AnalyticsPage.js +277 -111
- package/dist/proserviceworld/pages/AnalyticsPage.js.map +1 -1
- package/dist/proserviceworld/pages/ClientDetailPage.js +219 -151
- package/dist/proserviceworld/pages/ClientDetailPage.js.map +1 -1
- package/dist/proserviceworld/pages/ClientsPage.js +148 -127
- package/dist/proserviceworld/pages/ClientsPage.js.map +1 -1
- package/dist/proserviceworld/pages/CompliancePage.js +140 -96
- package/dist/proserviceworld/pages/CompliancePage.js.map +1 -1
- package/dist/proserviceworld/pages/DashboardPage.js +221 -104
- package/dist/proserviceworld/pages/DashboardPage.js.map +1 -1
- package/dist/proserviceworld/pages/EngagementDetailPage.js +211 -181
- package/dist/proserviceworld/pages/EngagementDetailPage.js.map +1 -1
- package/dist/proserviceworld/pages/EngagementsPage.js +144 -106
- package/dist/proserviceworld/pages/EngagementsPage.js.map +1 -1
- package/dist/proserviceworld/pages/InvoicesPage.js +144 -129
- package/dist/proserviceworld/pages/InvoicesPage.js.map +1 -1
- package/dist/proserviceworld/pages/MattersPage.js +163 -122
- package/dist/proserviceworld/pages/MattersPage.js.map +1 -1
- package/dist/proserviceworld/pages/TimePage.js +112 -92
- package/dist/proserviceworld/pages/TimePage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,179 +1,345 @@
|
|
|
1
1
|
import { useDashboardStats as e } from "../hooks/useProServiceWorldApi.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { cn as t } from "../utils/cn.js";
|
|
3
|
+
import { ErrorState as n, LoadingBlock as r, MetricCard as i, PageHeader as a, SectionCard as o, StatusPill as s, fmtHours as c, fmtMoney as l } from "../components/ui.js";
|
|
4
|
+
import { BarChart3 as u, Clock as d, TrendingUp as f, Wallet as p } from "lucide-react";
|
|
5
|
+
import { jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
5
6
|
//#region src/proserviceworld/pages/AnalyticsPage.tsx
|
|
6
|
-
var
|
|
7
|
-
let { data:
|
|
8
|
-
if (
|
|
9
|
-
if (
|
|
10
|
-
error:
|
|
11
|
-
onRetry: () => void
|
|
7
|
+
var g = () => {
|
|
8
|
+
let { data: t, isLoading: s, error: g, refetch: v } = e();
|
|
9
|
+
if (s) return /* @__PURE__ */ m(r, { label: "Crunching numbers…" });
|
|
10
|
+
if (g) return /* @__PURE__ */ m(n, {
|
|
11
|
+
error: g,
|
|
12
|
+
onRetry: () => void v()
|
|
12
13
|
});
|
|
13
|
-
if (!
|
|
14
|
-
let
|
|
15
|
-
return /* @__PURE__ */
|
|
14
|
+
if (!t) return null;
|
|
15
|
+
let S = t.unbilledHours + t.billedHours, C = S > 0 ? Math.round(t.billedHours / S * 100) : 0, w = t.paidInvoiceTotal + t.outstandingInvoiceTotal, T = w > 0 ? Math.round(t.paidInvoiceTotal / w * 100) : 0;
|
|
16
|
+
return /* @__PURE__ */ h("div", {
|
|
16
17
|
className: "space-y-6",
|
|
17
18
|
children: [
|
|
18
|
-
/* @__PURE__ */
|
|
19
|
+
/* @__PURE__ */ m(a, {
|
|
19
20
|
title: "Analytics",
|
|
20
21
|
subtitle: "Real, live metrics across the practice — no estimates, straight from your records.",
|
|
21
|
-
icon: /* @__PURE__ */
|
|
22
|
+
icon: /* @__PURE__ */ m(u, { className: "h-6 w-6" })
|
|
22
23
|
}),
|
|
23
|
-
/* @__PURE__ */
|
|
24
|
+
/* @__PURE__ */ h("div", {
|
|
24
25
|
className: "grid grid-cols-2 gap-3 lg:grid-cols-4",
|
|
25
26
|
children: [
|
|
26
|
-
/* @__PURE__ */
|
|
27
|
+
/* @__PURE__ */ m(i, {
|
|
27
28
|
label: "Collected revenue",
|
|
28
|
-
value:
|
|
29
|
-
|
|
29
|
+
value: l(t.paidInvoiceTotal),
|
|
30
|
+
hint: `${l(w)} invoiced`,
|
|
31
|
+
icon: /* @__PURE__ */ m(p, { className: "h-4 w-4" }),
|
|
30
32
|
tone: "success"
|
|
31
33
|
}),
|
|
32
|
-
/* @__PURE__ */
|
|
34
|
+
/* @__PURE__ */ m(i, {
|
|
33
35
|
label: "Collection rate",
|
|
34
|
-
value: `${
|
|
35
|
-
hint: `${
|
|
36
|
-
icon: /* @__PURE__ */
|
|
36
|
+
value: `${T}%`,
|
|
37
|
+
hint: `${l(t.outstandingInvoiceTotal)} outstanding`,
|
|
38
|
+
icon: /* @__PURE__ */ m(f, { className: "h-4 w-4" }),
|
|
37
39
|
tone: "accent"
|
|
38
40
|
}),
|
|
39
|
-
/* @__PURE__ */
|
|
41
|
+
/* @__PURE__ */ m(i, {
|
|
40
42
|
label: "Utilization",
|
|
41
|
-
value: `${
|
|
42
|
-
hint: `${
|
|
43
|
-
icon: /* @__PURE__ */
|
|
43
|
+
value: `${C}%`,
|
|
44
|
+
hint: `${c(t.billedHours)} of ${c(S)}`,
|
|
45
|
+
icon: /* @__PURE__ */ m(d, { className: "h-4 w-4" }),
|
|
44
46
|
tone: "info"
|
|
45
47
|
}),
|
|
46
|
-
/* @__PURE__ */
|
|
48
|
+
/* @__PURE__ */ m(i, {
|
|
47
49
|
label: "Active engagements",
|
|
48
|
-
value:
|
|
49
|
-
|
|
50
|
+
value: t.activeEngagements,
|
|
51
|
+
hint: `${t.openMatters} open matters`,
|
|
52
|
+
icon: /* @__PURE__ */ m(u, { className: "h-4 w-4" }),
|
|
53
|
+
tone: "warning"
|
|
50
54
|
})
|
|
51
55
|
]
|
|
52
56
|
}),
|
|
53
|
-
/* @__PURE__ */
|
|
57
|
+
/* @__PURE__ */ h("div", {
|
|
58
|
+
className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
|
|
59
|
+
children: [/* @__PURE__ */ m(o, {
|
|
60
|
+
title: "Collection rate",
|
|
61
|
+
description: "Paid vs. total invoiced.",
|
|
62
|
+
children: /* @__PURE__ */ m(_, {
|
|
63
|
+
pct: T,
|
|
64
|
+
tone: "accent",
|
|
65
|
+
primary: l(t.paidInvoiceTotal),
|
|
66
|
+
primaryLabel: "Collected",
|
|
67
|
+
secondary: l(t.outstandingInvoiceTotal),
|
|
68
|
+
secondaryLabel: "Outstanding"
|
|
69
|
+
})
|
|
70
|
+
}), /* @__PURE__ */ m(o, {
|
|
71
|
+
title: "Utilization",
|
|
72
|
+
description: "Billed vs. total logged hours.",
|
|
73
|
+
children: /* @__PURE__ */ m(_, {
|
|
74
|
+
pct: C,
|
|
75
|
+
tone: "info",
|
|
76
|
+
primary: c(t.billedHours),
|
|
77
|
+
primaryLabel: "Billed",
|
|
78
|
+
secondary: c(t.unbilledHours),
|
|
79
|
+
secondaryLabel: "Unbilled"
|
|
80
|
+
})
|
|
81
|
+
})]
|
|
82
|
+
}),
|
|
83
|
+
/* @__PURE__ */ m(o, {
|
|
54
84
|
title: "Revenue by month",
|
|
55
85
|
description: "Invoiced totals across recent months.",
|
|
56
|
-
children: /* @__PURE__ */
|
|
86
|
+
children: /* @__PURE__ */ m(y, { data: t.revenueByMonth })
|
|
57
87
|
}),
|
|
58
|
-
/* @__PURE__ */
|
|
88
|
+
/* @__PURE__ */ h("div", {
|
|
59
89
|
className: "grid grid-cols-1 gap-4 lg:grid-cols-3",
|
|
60
90
|
children: [
|
|
61
|
-
/* @__PURE__ */
|
|
91
|
+
/* @__PURE__ */ m(o, {
|
|
62
92
|
title: "Engagements",
|
|
63
|
-
|
|
93
|
+
description: "By status",
|
|
94
|
+
children: /* @__PURE__ */ m(b, { items: t.engagementsByStatus })
|
|
64
95
|
}),
|
|
65
|
-
/* @__PURE__ */
|
|
96
|
+
/* @__PURE__ */ m(o, {
|
|
66
97
|
title: "Matters",
|
|
67
|
-
|
|
98
|
+
description: "By status",
|
|
99
|
+
children: /* @__PURE__ */ m(b, { items: t.mattersByStatus })
|
|
68
100
|
}),
|
|
69
|
-
/* @__PURE__ */
|
|
101
|
+
/* @__PURE__ */ m(o, {
|
|
70
102
|
title: "Invoices",
|
|
71
|
-
|
|
103
|
+
description: "By status",
|
|
104
|
+
children: /* @__PURE__ */ m(b, { items: t.invoicesByStatus })
|
|
72
105
|
})
|
|
73
106
|
]
|
|
74
107
|
}),
|
|
75
|
-
/* @__PURE__ */
|
|
108
|
+
/* @__PURE__ */ m(o, {
|
|
76
109
|
title: "Time & billing",
|
|
77
|
-
|
|
110
|
+
description: "Billable capacity across the practice.",
|
|
111
|
+
children: /* @__PURE__ */ h("div", {
|
|
78
112
|
className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
|
|
79
|
-
children: [/* @__PURE__ */
|
|
113
|
+
children: [/* @__PURE__ */ m(x, {
|
|
80
114
|
label: "Billed hours",
|
|
81
|
-
value:
|
|
82
|
-
total:
|
|
115
|
+
value: t.billedHours,
|
|
116
|
+
total: S,
|
|
83
117
|
tone: "accent"
|
|
84
|
-
}), /* @__PURE__ */
|
|
118
|
+
}), /* @__PURE__ */ m(x, {
|
|
85
119
|
label: "Unbilled hours",
|
|
86
|
-
value:
|
|
87
|
-
total:
|
|
120
|
+
value: t.unbilledHours,
|
|
121
|
+
total: S,
|
|
88
122
|
tone: "warning"
|
|
89
123
|
})]
|
|
90
124
|
})
|
|
91
125
|
})
|
|
92
126
|
]
|
|
93
127
|
});
|
|
94
|
-
},
|
|
95
|
-
|
|
128
|
+
}, _ = ({ pct: e, tone: t, primary: n, primaryLabel: r, secondary: i, secondaryLabel: a }) => /* @__PURE__ */ h("div", {
|
|
129
|
+
className: "flex items-center gap-5",
|
|
130
|
+
children: [/* @__PURE__ */ m(v, {
|
|
131
|
+
pct: e,
|
|
132
|
+
tone: t
|
|
133
|
+
}), /* @__PURE__ */ h("div", {
|
|
134
|
+
className: "flex flex-1 flex-col gap-2.5",
|
|
135
|
+
children: [
|
|
136
|
+
/* @__PURE__ */ h("div", {
|
|
137
|
+
className: "flex items-baseline justify-between gap-3",
|
|
138
|
+
children: [/* @__PURE__ */ m("span", {
|
|
139
|
+
className: "text-xs font-medium uppercase tracking-wider text-text-tertiary",
|
|
140
|
+
children: r
|
|
141
|
+
}), /* @__PURE__ */ m("span", {
|
|
142
|
+
className: "text-sm font-semibold tabular-nums text-text-primary",
|
|
143
|
+
children: n
|
|
144
|
+
})]
|
|
145
|
+
}),
|
|
146
|
+
/* @__PURE__ */ m("div", { className: "h-px bg-border-seam" }),
|
|
147
|
+
/* @__PURE__ */ h("div", {
|
|
148
|
+
className: "flex items-baseline justify-between gap-3",
|
|
149
|
+
children: [/* @__PURE__ */ m("span", {
|
|
150
|
+
className: "text-xs font-medium uppercase tracking-wider text-text-tertiary",
|
|
151
|
+
children: a
|
|
152
|
+
}), /* @__PURE__ */ m("span", {
|
|
153
|
+
className: "text-sm font-semibold tabular-nums text-text-secondary",
|
|
154
|
+
children: i
|
|
155
|
+
})]
|
|
156
|
+
})
|
|
157
|
+
]
|
|
158
|
+
})]
|
|
159
|
+
}), v = ({ pct: e, tone: n }) => {
|
|
160
|
+
let r = Math.max(0, Math.min(100, e)), i = 2 * Math.PI * 34, a = r / 100 * i;
|
|
161
|
+
return /* @__PURE__ */ h("div", {
|
|
162
|
+
className: t("relative h-24 w-24 shrink-0", n === "accent" ? "text-accent" : "text-text-link"),
|
|
163
|
+
children: [/* @__PURE__ */ h("svg", {
|
|
164
|
+
viewBox: "0 0 80 80",
|
|
165
|
+
className: "h-full w-full -rotate-90",
|
|
166
|
+
children: [/* @__PURE__ */ m("circle", {
|
|
167
|
+
cx: "40",
|
|
168
|
+
cy: "40",
|
|
169
|
+
r: 34,
|
|
170
|
+
fill: "none",
|
|
171
|
+
strokeWidth: "8",
|
|
172
|
+
className: "text-bg-sunken",
|
|
173
|
+
stroke: "currentColor"
|
|
174
|
+
}), /* @__PURE__ */ m("circle", {
|
|
175
|
+
cx: "40",
|
|
176
|
+
cy: "40",
|
|
177
|
+
r: 34,
|
|
178
|
+
fill: "none",
|
|
179
|
+
strokeWidth: "8",
|
|
180
|
+
strokeLinecap: "round",
|
|
181
|
+
stroke: "currentColor",
|
|
182
|
+
strokeDasharray: `${a} ${i}`,
|
|
183
|
+
className: "transition-[stroke-dasharray] duration-700"
|
|
184
|
+
})]
|
|
185
|
+
}), /* @__PURE__ */ m("div", {
|
|
186
|
+
className: "absolute inset-0 flex items-center justify-center",
|
|
187
|
+
children: /* @__PURE__ */ h("span", {
|
|
188
|
+
className: "text-lg font-bold tabular-nums text-text-primary",
|
|
189
|
+
children: [r, "%"]
|
|
190
|
+
})
|
|
191
|
+
})]
|
|
192
|
+
});
|
|
193
|
+
}, y = ({ data: e }) => {
|
|
194
|
+
if (!e.length) return /* @__PURE__ */ m("p", {
|
|
96
195
|
className: "text-sm text-text-tertiary",
|
|
97
196
|
children: "No invoiced revenue yet."
|
|
98
197
|
});
|
|
99
198
|
let t = Math.max(...e.map((e) => e.amount), 1);
|
|
100
|
-
return /* @__PURE__ */
|
|
101
|
-
className: "
|
|
102
|
-
children:
|
|
103
|
-
className: "
|
|
104
|
-
children: [
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
className: "w-full rounded-t bg-accent",
|
|
113
|
-
style: { height: `${Math.max(e.amount / t *
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
199
|
+
return /* @__PURE__ */ m("div", {
|
|
200
|
+
className: "overflow-x-auto",
|
|
201
|
+
children: /* @__PURE__ */ h("div", {
|
|
202
|
+
className: "min-w-[520px]",
|
|
203
|
+
children: [/* @__PURE__ */ m("div", {
|
|
204
|
+
className: "flex h-60 items-end gap-2 border-b border-border-seam",
|
|
205
|
+
children: e.map((e) => /* @__PURE__ */ h("div", {
|
|
206
|
+
className: "group flex h-full flex-1 flex-col items-center justify-end gap-1.5",
|
|
207
|
+
children: [/* @__PURE__ */ m("span", {
|
|
208
|
+
className: "whitespace-nowrap text-[10px] font-semibold tabular-nums text-text-secondary",
|
|
209
|
+
children: l(e.amount)
|
|
210
|
+
}), /* @__PURE__ */ m("div", {
|
|
211
|
+
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
|
+
style: { height: `${Math.max(e.amount / t * 90, 2)}%` },
|
|
213
|
+
title: l(e.amount)
|
|
214
|
+
})]
|
|
215
|
+
}, e.month))
|
|
216
|
+
}), /* @__PURE__ */ m("div", {
|
|
217
|
+
className: "mt-2 flex gap-2",
|
|
218
|
+
children: e.map((e) => /* @__PURE__ */ m("span", {
|
|
219
|
+
className: "flex-1 text-center text-xs text-text-tertiary",
|
|
220
|
+
children: T(e.month)
|
|
221
|
+
}, e.month))
|
|
222
|
+
})]
|
|
223
|
+
})
|
|
122
224
|
});
|
|
123
|
-
},
|
|
124
|
-
if (!e.length) return /* @__PURE__ */
|
|
225
|
+
}, b = ({ items: e }) => {
|
|
226
|
+
if (!e.length) return /* @__PURE__ */ m("p", {
|
|
125
227
|
className: "text-sm text-text-tertiary",
|
|
126
228
|
children: "No records yet."
|
|
127
229
|
});
|
|
128
|
-
let
|
|
129
|
-
return /* @__PURE__ */
|
|
130
|
-
className: "space-y-
|
|
131
|
-
children: e.map((e) => /* @__PURE__ */
|
|
132
|
-
className: "
|
|
230
|
+
let n = Math.max(...e.map((e) => e.count), 1), r = e.reduce((e, t) => e + t.count, 0) || 1;
|
|
231
|
+
return /* @__PURE__ */ m("ul", {
|
|
232
|
+
className: "space-y-3.5",
|
|
233
|
+
children: e.map((e) => /* @__PURE__ */ h("li", {
|
|
234
|
+
className: "grid grid-cols-[7.5rem_minmax(0,1fr)_auto] items-center gap-3",
|
|
133
235
|
children: [
|
|
134
|
-
/* @__PURE__ */
|
|
135
|
-
|
|
136
|
-
|
|
236
|
+
/* @__PURE__ */ m(s, {
|
|
237
|
+
status: e.status,
|
|
238
|
+
className: "justify-self-start"
|
|
137
239
|
}),
|
|
138
|
-
/* @__PURE__ */
|
|
139
|
-
className: "h-2
|
|
140
|
-
children: /* @__PURE__ */
|
|
141
|
-
className: "h-full rounded-full bg-
|
|
142
|
-
style: { width: `${e.count /
|
|
240
|
+
/* @__PURE__ */ m("div", {
|
|
241
|
+
className: "h-2.5 overflow-hidden rounded-full bg-bg-sunken",
|
|
242
|
+
children: /* @__PURE__ */ m("div", {
|
|
243
|
+
className: t("h-full rounded-full bg-gradient-to-r transition-all duration-500", w(e.status)),
|
|
244
|
+
style: { width: `${Math.max(e.count / n * 100, 4)}%` }
|
|
143
245
|
})
|
|
144
246
|
}),
|
|
145
|
-
/* @__PURE__ */
|
|
146
|
-
className: "
|
|
147
|
-
children:
|
|
247
|
+
/* @__PURE__ */ h("span", {
|
|
248
|
+
className: "flex items-baseline justify-end gap-1.5 tabular-nums",
|
|
249
|
+
children: [/* @__PURE__ */ m("span", {
|
|
250
|
+
className: "text-sm font-semibold text-text-primary",
|
|
251
|
+
children: e.count
|
|
252
|
+
}), /* @__PURE__ */ h("span", {
|
|
253
|
+
className: "text-xs text-text-tertiary",
|
|
254
|
+
children: [Math.round(e.count / r * 100), "%"]
|
|
255
|
+
})]
|
|
148
256
|
})
|
|
149
257
|
]
|
|
150
258
|
}, e.status))
|
|
151
259
|
});
|
|
152
|
-
},
|
|
153
|
-
let
|
|
154
|
-
return /* @__PURE__ */
|
|
155
|
-
className: "
|
|
156
|
-
children: [/* @__PURE__ */
|
|
157
|
-
className: "
|
|
158
|
-
children:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
260
|
+
}, x = ({ label: e, value: n, total: r, tone: i }) => {
|
|
261
|
+
let a = r > 0 ? Math.round(n / r * 100) : 0, o = i === "accent" ? "from-accent/60 to-accent" : "from-status-warning-text/60 to-status-warning-text";
|
|
262
|
+
return /* @__PURE__ */ h("div", {
|
|
263
|
+
className: "rounded-xl border border-border-seam bg-bg-canvas p-4",
|
|
264
|
+
children: [/* @__PURE__ */ h("div", {
|
|
265
|
+
className: "mb-2.5 flex items-center justify-between gap-3",
|
|
266
|
+
children: [/* @__PURE__ */ m("span", {
|
|
267
|
+
className: "text-sm font-medium text-text-secondary",
|
|
268
|
+
children: e
|
|
269
|
+
}), /* @__PURE__ */ h("span", {
|
|
270
|
+
className: "text-sm font-semibold tabular-nums text-text-primary",
|
|
271
|
+
children: [
|
|
272
|
+
c(n),
|
|
273
|
+
" · ",
|
|
274
|
+
a,
|
|
275
|
+
"%"
|
|
276
|
+
]
|
|
277
|
+
})]
|
|
278
|
+
}), /* @__PURE__ */ m("div", {
|
|
279
|
+
className: "h-3 overflow-hidden rounded-full bg-bg-sunken",
|
|
280
|
+
children: /* @__PURE__ */ m("div", {
|
|
281
|
+
className: t("h-full rounded-full bg-gradient-to-r transition-all duration-700", o),
|
|
282
|
+
style: { width: `${a}%` }
|
|
283
|
+
})
|
|
167
284
|
})]
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
285
|
+
});
|
|
286
|
+
}, S = {
|
|
287
|
+
success: "from-status-success-text/55 to-status-success-text",
|
|
288
|
+
warning: "from-status-warning-text/55 to-status-warning-text",
|
|
289
|
+
danger: "from-status-error-text/55 to-status-error-text",
|
|
290
|
+
info: "from-text-link/55 to-text-link",
|
|
291
|
+
accent: "from-accent/55 to-accent",
|
|
292
|
+
neutral: "from-text-tertiary/30 to-text-tertiary/70"
|
|
293
|
+
}, C = {
|
|
294
|
+
ACTIVE: "success",
|
|
295
|
+
DELIVERED: "success",
|
|
296
|
+
INVOICED: "success",
|
|
297
|
+
PAID: "success",
|
|
298
|
+
APPROVED: "success",
|
|
299
|
+
PASSED: "success",
|
|
300
|
+
CLEAR: "success",
|
|
301
|
+
COMPLETED: "success",
|
|
302
|
+
FINAL: "success",
|
|
303
|
+
PROSPECT: "info",
|
|
304
|
+
PROPOSAL: "info",
|
|
305
|
+
MEDIUM: "info",
|
|
306
|
+
INTERNAL_REVIEW: "info",
|
|
307
|
+
SUBMITTED: "info",
|
|
308
|
+
SENT: "info",
|
|
309
|
+
IN_REVIEW: "info",
|
|
310
|
+
REVIEW: "warning",
|
|
311
|
+
ON_HOLD: "warning",
|
|
312
|
+
HIGH: "warning",
|
|
313
|
+
CLIENT_REVIEW: "warning",
|
|
314
|
+
PENDING: "warning",
|
|
315
|
+
POTENTIAL_CONFLICT: "warning",
|
|
316
|
+
CANCELLED: "danger",
|
|
317
|
+
CRITICAL: "danger",
|
|
318
|
+
REJECTED: "danger",
|
|
319
|
+
OVERDUE: "danger",
|
|
320
|
+
FAILED: "danger",
|
|
321
|
+
CONFLICT: "danger",
|
|
322
|
+
ACCEPTED: "accent",
|
|
323
|
+
IN_PROGRESS: "accent",
|
|
324
|
+
OPEN: "accent",
|
|
325
|
+
BILLED: "accent",
|
|
326
|
+
INACTIVE: "neutral",
|
|
327
|
+
ARCHIVED: "neutral",
|
|
328
|
+
CLOSED: "neutral",
|
|
329
|
+
LOW: "neutral",
|
|
330
|
+
DRAFT: "neutral",
|
|
331
|
+
WAIVED: "neutral"
|
|
175
332
|
};
|
|
333
|
+
function w(e) {
|
|
334
|
+
return S[C[e] ?? "accent"];
|
|
335
|
+
}
|
|
336
|
+
function T(e) {
|
|
337
|
+
let [t, n] = e.split("-").map(Number);
|
|
338
|
+
if (!t || !n) return e;
|
|
339
|
+
let r = new Date(t, n - 1, 1);
|
|
340
|
+
return Number.isNaN(r.getTime()) ? e : r.toLocaleDateString(void 0, { month: "short" });
|
|
341
|
+
}
|
|
176
342
|
//#endregion
|
|
177
|
-
export {
|
|
343
|
+
export { g as AnalyticsPage };
|
|
178
344
|
|
|
179
345
|
//# 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 {\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 collectionRate =\n data.paidInvoiceTotal + data.outstandingInvoiceTotal > 0\n ? Math.round(\n (data.paidInvoiceTotal / (data.paidInvoiceTotal + data.outstandingInvoiceTotal)) * 100\n )\n : 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 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 icon={<BarChart3 className=\"h-4 w-4\" />}\n />\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\">\n <Distribution items={data.engagementsByStatus} />\n </SectionCard>\n <SectionCard title=\"Matters\">\n <Distribution items={data.mattersByStatus} />\n </SectionCard>\n <SectionCard title=\"Invoices\">\n <Distribution items={data.invoicesByStatus} />\n </SectionCard>\n </div>\n\n <SectionCard title=\"Time & billing\">\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\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 max = Math.max(...data.map((d) => d.amount), 1);\n return (\n <div className=\"flex h-52 items-end gap-2\">\n {data.map((d) => (\n <div key={d.month} className=\"flex flex-1 flex-col items-center gap-2\">\n <span className=\"text-[10px] text-text-tertiary\">{fmtMoney(d.amount)}</span>\n <div className=\"flex w-full flex-1 items-end\">\n <div\n className=\"w-full rounded-t bg-accent\"\n style={{ height: `${Math.max((d.amount / max) * 100, 2)}%` }}\n />\n </div>\n <span className=\"text-[10px] text-text-tertiary\">{d.month}</span>\n </div>\n ))}\n </div>\n );\n};\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 total = items.reduce((s, i) => s + i.count, 0) || 1;\n return (\n <ul className=\"space-y-2.5\">\n {items.map((i) => (\n <li key={i.status} className=\"flex items-center gap-3\">\n <div className=\"w-28 shrink-0\">\n <StatusPill status={i.status} />\n </div>\n <div className=\"h-2 flex-1 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-accent\"\n style={{ width: `${(i.count / total) * 100}%` }}\n />\n </div>\n <span className=\"w-8 text-right text-sm font-medium text-text-primary\">{i.count}</span>\n </li>\n ))}\n </ul>\n );\n};\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 return (\n <div>\n <div className=\"mb-1 flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">{label}</span>\n <span className=\"font-medium text-text-primary\">\n {fmtHours(value)} · {pct}%\n </span>\n </div>\n <div className=\"h-2.5 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className={\n tone === 'accent'\n ? 'h-full rounded-full bg-accent'\n : 'h-full rounded-full bg-status-warning-text'\n }\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n );\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 } 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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"}
|