@burdenoff/microfe-proserviceworld 2026.626.2 → 2026.706.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.
Files changed (75) hide show
  1. package/dist/index.js +2 -3
  2. package/dist/node_modules/clsx/dist/clsx.js +18 -0
  3. package/dist/node_modules/clsx/dist/clsx.js.map +1 -0
  4. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +1716 -0
  5. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -0
  6. package/dist/proserviceworld/ProServiceWorldRoot.js +92 -136
  7. package/dist/proserviceworld/ProServiceWorldRoot.js.map +1 -1
  8. package/dist/proserviceworld/ProServiceWorldRoutes.js +18 -26
  9. package/dist/proserviceworld/ProServiceWorldRoutes.js.map +1 -1
  10. package/dist/proserviceworld/components/ui.js +273 -0
  11. package/dist/proserviceworld/components/ui.js.map +1 -0
  12. package/dist/proserviceworld/hooks/useFiles.js +45 -0
  13. package/dist/proserviceworld/hooks/useFiles.js.map +1 -0
  14. package/dist/proserviceworld/hooks/useProServiceWorldApi.js +416 -0
  15. package/dist/proserviceworld/hooks/useProServiceWorldApi.js.map +1 -0
  16. package/dist/proserviceworld/index.js +0 -1
  17. package/dist/proserviceworld/pages/AnalyticsPage.js +163 -475
  18. package/dist/proserviceworld/pages/AnalyticsPage.js.map +1 -1
  19. package/dist/proserviceworld/pages/ClientDetailPage.js +238 -0
  20. package/dist/proserviceworld/pages/ClientDetailPage.js.map +1 -0
  21. package/dist/proserviceworld/pages/ClientsPage.js +295 -0
  22. package/dist/proserviceworld/pages/ClientsPage.js.map +1 -0
  23. package/dist/proserviceworld/pages/CompliancePage.js +307 -0
  24. package/dist/proserviceworld/pages/CompliancePage.js.map +1 -0
  25. package/dist/proserviceworld/pages/ConflictsPage.js +234 -0
  26. package/dist/proserviceworld/pages/ConflictsPage.js.map +1 -0
  27. package/dist/proserviceworld/pages/DashboardPage.js +179 -632
  28. package/dist/proserviceworld/pages/DashboardPage.js.map +1 -1
  29. package/dist/proserviceworld/pages/DocumentsPage.js +235 -539
  30. package/dist/proserviceworld/pages/DocumentsPage.js.map +1 -1
  31. package/dist/proserviceworld/pages/EngagementDetailPage.js +444 -0
  32. package/dist/proserviceworld/pages/EngagementDetailPage.js.map +1 -0
  33. package/dist/proserviceworld/pages/EngagementsPage.js +256 -0
  34. package/dist/proserviceworld/pages/EngagementsPage.js.map +1 -0
  35. package/dist/proserviceworld/pages/InvoicesPage.js +303 -0
  36. package/dist/proserviceworld/pages/InvoicesPage.js.map +1 -0
  37. package/dist/proserviceworld/pages/MattersPage.js +275 -0
  38. package/dist/proserviceworld/pages/MattersPage.js.map +1 -0
  39. package/dist/proserviceworld/pages/PracticeAreasPage.js +176 -0
  40. package/dist/proserviceworld/pages/PracticeAreasPage.js.map +1 -0
  41. package/dist/proserviceworld/pages/TimePage.js +310 -0
  42. package/dist/proserviceworld/pages/TimePage.js.map +1 -0
  43. package/dist/proserviceworld/utils/cn.js +10 -0
  44. package/dist/proserviceworld/utils/cn.js.map +1 -0
  45. package/dist/proserviceworld/utils/nav.js +11 -0
  46. package/dist/proserviceworld/utils/nav.js.map +1 -0
  47. package/package.json +3 -2
  48. package/dist/proserviceworld/pages/AdminReportsPage.js +0 -579
  49. package/dist/proserviceworld/pages/AdminReportsPage.js.map +0 -1
  50. package/dist/proserviceworld/pages/AdminUsersPage.js +0 -468
  51. package/dist/proserviceworld/pages/AdminUsersPage.js.map +0 -1
  52. package/dist/proserviceworld/pages/BookingsPage.js +0 -446
  53. package/dist/proserviceworld/pages/BookingsPage.js.map +0 -1
  54. package/dist/proserviceworld/pages/MessagesPage.js +0 -238
  55. package/dist/proserviceworld/pages/MessagesPage.js.map +0 -1
  56. package/dist/proserviceworld/pages/PaymentsPage.js +0 -537
  57. package/dist/proserviceworld/pages/PaymentsPage.js.map +0 -1
  58. package/dist/proserviceworld/pages/ProjectsPage.js +0 -627
  59. package/dist/proserviceworld/pages/ProjectsPage.js.map +0 -1
  60. package/dist/proserviceworld/pages/ProviderDetailPage.js +0 -538
  61. package/dist/proserviceworld/pages/ProviderDetailPage.js.map +0 -1
  62. package/dist/proserviceworld/pages/ProvidersPage.js +0 -347
  63. package/dist/proserviceworld/pages/ProvidersPage.js.map +0 -1
  64. package/dist/proserviceworld/pages/ReviewsPage.js +0 -356
  65. package/dist/proserviceworld/pages/ReviewsPage.js.map +0 -1
  66. package/dist/proserviceworld/pages/ServiceDetailsPage.js +0 -631
  67. package/dist/proserviceworld/pages/ServiceDetailsPage.js.map +0 -1
  68. package/dist/proserviceworld/pages/ServicesPage.js +0 -344
  69. package/dist/proserviceworld/pages/ServicesPage.js.map +0 -1
  70. package/dist/proserviceworld/pages/VerificationPage.js +0 -370
  71. package/dist/proserviceworld/pages/VerificationPage.js.map +0 -1
  72. package/dist/proserviceworld/store/proserviceworldStore.js +0 -2490
  73. package/dist/proserviceworld/store/proserviceworldStore.js.map +0 -1
  74. package/dist/proserviceworld/utils/i18n.js +0 -15
  75. package/dist/proserviceworld/utils/i18n.js.map +0 -1
@@ -0,0 +1,310 @@
1
+ import { useApproveTimeEntry as e, useEngagements as t, useLogTimeEntry as n, useMatters as r, useSubmitTimeEntry as i, useTimeEntries as a } from "../hooks/useProServiceWorldApi.js";
2
+ import { Banner as o, Empty as s, ErrorState as c, Field as l, LoadingBlock as u, MetricCard as d, Modal as f, NativeSelect as p, PageHeader as m, SectionCard as h, StatusPill as g, TableWrap as _, Td as v, TextArea as y, TextInput as b, Th as x, enumOptions as S, fmtDate as C, fmtMinutesAsHours as w } from "../components/ui.js";
3
+ import { useState as T } from "react";
4
+ import { CheckCircle2 as E, Clock as D, Plus as O, Send as k } from "lucide-react";
5
+ import { Fragment as A, jsx as j, jsxs as M } from "react/jsx-runtime";
6
+ import { Button as N } from "@burdenoff/fe-libs/ui";
7
+ //#region src/proserviceworld/pages/TimePage.tsx
8
+ var P = [
9
+ "DRAFT",
10
+ "SUBMITTED",
11
+ "APPROVED",
12
+ "REJECTED",
13
+ "BILLED"
14
+ ], F = () => {
15
+ let [t, n] = T(""), [r, l] = T(!1), [f, y] = T(null), { data: b, isLoading: A, error: F, refetch: L } = a({ status: t || void 0 }), R = i(), z = e(), B = (b?.items ?? []).reduce((e, t) => e + t.minutes, 0), V = (b?.items ?? []).filter((e) => e.billable).reduce((e, t) => e + t.minutes, 0), H = (e, t, n) => e.mutate(t, {
16
+ onSuccess: () => y({
17
+ tone: "success",
18
+ text: `Time entry ${n}.`
19
+ }),
20
+ onError: (e) => y({
21
+ tone: "danger",
22
+ text: e instanceof Error ? e.message : "Failed."
23
+ })
24
+ });
25
+ return /* @__PURE__ */ M("div", {
26
+ className: "space-y-6",
27
+ children: [
28
+ /* @__PURE__ */ j(m, {
29
+ title: "Time",
30
+ subtitle: "Capture expert effort against engagements and matters — the raw material of billing and utilization.",
31
+ icon: /* @__PURE__ */ j(D, { className: "h-6 w-6" }),
32
+ actions: /* @__PURE__ */ M(N, {
33
+ onClick: () => l(!0),
34
+ children: [/* @__PURE__ */ j(O, { className: "mr-1.5 h-4 w-4" }), "Log time"]
35
+ })
36
+ }),
37
+ f && /* @__PURE__ */ j(o, {
38
+ tone: f.tone,
39
+ onClose: () => y(null),
40
+ children: f.text
41
+ }),
42
+ /* @__PURE__ */ M("div", {
43
+ className: "grid grid-cols-2 gap-3 sm:grid-cols-4",
44
+ children: [
45
+ /* @__PURE__ */ j(d, {
46
+ label: "Entries",
47
+ value: b?.totalCount ?? 0
48
+ }),
49
+ /* @__PURE__ */ j(d, {
50
+ label: "Total hours",
51
+ value: w(B)
52
+ }),
53
+ /* @__PURE__ */ j(d, {
54
+ label: "Billable hours",
55
+ value: w(V),
56
+ tone: "accent"
57
+ }),
58
+ /* @__PURE__ */ j(d, {
59
+ label: "Non-billable",
60
+ value: w(B - V)
61
+ })
62
+ ]
63
+ }),
64
+ /* @__PURE__ */ j("div", {
65
+ className: "flex justify-end",
66
+ children: /* @__PURE__ */ M(p, {
67
+ value: t,
68
+ onChange: (e) => n(e.target.value),
69
+ className: "sm:w-52",
70
+ children: [/* @__PURE__ */ j("option", {
71
+ value: "",
72
+ children: "All statuses"
73
+ }), S(P)]
74
+ })
75
+ }),
76
+ A ? /* @__PURE__ */ j(u, {}) : F ? /* @__PURE__ */ j(c, {
77
+ error: F,
78
+ onRetry: () => void L()
79
+ }) : !b || b.items.length === 0 ? /* @__PURE__ */ j(s, {
80
+ title: "No time logged",
81
+ description: "Log your first time entry against an engagement or matter.",
82
+ icon: /* @__PURE__ */ j(D, { className: "h-8 w-8" }),
83
+ action: /* @__PURE__ */ M(N, {
84
+ onClick: () => l(!0),
85
+ children: [/* @__PURE__ */ j(O, { className: "mr-1.5 h-4 w-4" }), "Log time"]
86
+ })
87
+ }) : /* @__PURE__ */ j(h, {
88
+ title: "Time entries",
89
+ children: /* @__PURE__ */ M(_, { children: [/* @__PURE__ */ j("thead", { children: /* @__PURE__ */ M("tr", { children: [
90
+ /* @__PURE__ */ j(x, { children: "Date" }),
91
+ /* @__PURE__ */ j(x, { children: "Narrative" }),
92
+ /* @__PURE__ */ j(x, { children: "Hours" }),
93
+ /* @__PURE__ */ j(x, { children: "Billable" }),
94
+ /* @__PURE__ */ j(x, { children: "Status" }),
95
+ /* @__PURE__ */ j(x, {
96
+ className: "text-right",
97
+ children: "Actions"
98
+ })
99
+ ] }) }), /* @__PURE__ */ j("tbody", { children: b.items.map((e) => /* @__PURE__ */ M("tr", {
100
+ className: "hover:bg-bg-sunken/50",
101
+ children: [
102
+ /* @__PURE__ */ j(v, { children: C(e.date) }),
103
+ /* @__PURE__ */ j(v, {
104
+ className: "max-w-xs truncate text-text-primary",
105
+ children: e.narrative || "—"
106
+ }),
107
+ /* @__PURE__ */ j(v, { children: w(e.minutes) }),
108
+ /* @__PURE__ */ j(v, { children: e.billable ? "Yes" : "No" }),
109
+ /* @__PURE__ */ j(v, { children: /* @__PURE__ */ j(g, { status: e.status }) }),
110
+ /* @__PURE__ */ j(v, {
111
+ className: "text-right",
112
+ children: /* @__PURE__ */ M("div", {
113
+ className: "flex justify-end gap-1",
114
+ children: [e.status === "DRAFT" && /* @__PURE__ */ M(N, {
115
+ variant: "ghost",
116
+ size: "sm",
117
+ disabled: R.isPending,
118
+ onClick: () => H(R, e.id, "submitted"),
119
+ children: [/* @__PURE__ */ j(k, { className: "mr-1 h-3.5 w-3.5" }), "Submit"]
120
+ }), e.status === "SUBMITTED" && /* @__PURE__ */ M(N, {
121
+ variant: "ghost",
122
+ size: "sm",
123
+ disabled: z.isPending,
124
+ onClick: () => H(z, e.id, "approved"),
125
+ children: [/* @__PURE__ */ j(E, { className: "mr-1 h-3.5 w-3.5" }), "Approve"]
126
+ })]
127
+ })
128
+ })
129
+ ]
130
+ }, e.id)) })] })
131
+ }),
132
+ r && /* @__PURE__ */ j(I, {
133
+ onClose: () => l(!1),
134
+ onDone: () => y({
135
+ tone: "success",
136
+ text: "Time logged."
137
+ })
138
+ })
139
+ ]
140
+ });
141
+ }, I = ({ onClose: e, onDone: i }) => {
142
+ let a = n(), s = t({ pagination: { limit: 100 } }), c = r({ pagination: { limit: 100 } }), [u, d] = T({
143
+ against: "engagement",
144
+ engagementId: "",
145
+ matterId: "",
146
+ date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
147
+ hours: "",
148
+ minutes: "",
149
+ rate: "",
150
+ billable: !0,
151
+ narrative: ""
152
+ }), [m, h] = T(null), g = (e) => (t) => d((n) => ({
153
+ ...n,
154
+ [e]: t.target.value
155
+ }));
156
+ return /* @__PURE__ */ j(f, {
157
+ open: !0,
158
+ onClose: e,
159
+ title: "Log time",
160
+ footer: /* @__PURE__ */ M(A, { children: [/* @__PURE__ */ j(N, {
161
+ variant: "outline",
162
+ onClick: e,
163
+ disabled: a.isPending,
164
+ children: "Cancel"
165
+ }), /* @__PURE__ */ j(N, {
166
+ onClick: () => {
167
+ h(null);
168
+ let t = (Number(u.hours) || 0) * 60 + (Number(u.minutes) || 0);
169
+ if (t <= 0) return h("Enter the time spent.");
170
+ let n = u.against === "engagement" ? { engagementId: u.engagementId } : { matterId: u.matterId };
171
+ if (u.against === "engagement" && !u.engagementId) return h("Select an engagement.");
172
+ if (u.against === "matter" && !u.matterId) return h("Select a matter.");
173
+ a.mutate({
174
+ ...n,
175
+ date: new Date(u.date).toISOString(),
176
+ minutes: t,
177
+ rate: u.rate ? Number(u.rate) : 0,
178
+ billable: u.billable,
179
+ narrative: u.narrative.trim() || null
180
+ }, {
181
+ onSuccess: () => {
182
+ i(), e();
183
+ },
184
+ onError: (e) => h(e instanceof Error ? e.message : "Failed.")
185
+ });
186
+ },
187
+ disabled: a.isPending,
188
+ children: a.isPending ? "Logging…" : "Log time"
189
+ })] }),
190
+ children: /* @__PURE__ */ M("div", {
191
+ className: "space-y-3",
192
+ children: [
193
+ m && /* @__PURE__ */ j(o, {
194
+ tone: "danger",
195
+ children: m
196
+ }),
197
+ /* @__PURE__ */ j(l, {
198
+ label: "Against",
199
+ children: /* @__PURE__ */ M(p, {
200
+ value: u.against,
201
+ onChange: g("against"),
202
+ children: [/* @__PURE__ */ j("option", {
203
+ value: "engagement",
204
+ children: "Engagement"
205
+ }), /* @__PURE__ */ j("option", {
206
+ value: "matter",
207
+ children: "Matter"
208
+ })]
209
+ })
210
+ }),
211
+ u.against === "engagement" ? /* @__PURE__ */ j(l, {
212
+ label: "Engagement",
213
+ required: !0,
214
+ children: /* @__PURE__ */ M(p, {
215
+ value: u.engagementId,
216
+ onChange: g("engagementId"),
217
+ children: [/* @__PURE__ */ j("option", {
218
+ value: "",
219
+ children: "Select an engagement…"
220
+ }), s.data?.items.map((e) => /* @__PURE__ */ j("option", {
221
+ value: e.id,
222
+ children: e.name
223
+ }, e.id))]
224
+ })
225
+ }) : /* @__PURE__ */ j(l, {
226
+ label: "Matter",
227
+ required: !0,
228
+ children: /* @__PURE__ */ M(p, {
229
+ value: u.matterId,
230
+ onChange: g("matterId"),
231
+ children: [/* @__PURE__ */ j("option", {
232
+ value: "",
233
+ children: "Select a matter…"
234
+ }), c.data?.items.map((e) => /* @__PURE__ */ j("option", {
235
+ value: e.id,
236
+ children: e.title
237
+ }, e.id))]
238
+ })
239
+ }),
240
+ /* @__PURE__ */ M("div", {
241
+ className: "grid grid-cols-3 gap-3",
242
+ children: [
243
+ /* @__PURE__ */ j(l, {
244
+ label: "Date",
245
+ children: /* @__PURE__ */ j(b, {
246
+ type: "date",
247
+ value: u.date,
248
+ onChange: g("date")
249
+ })
250
+ }),
251
+ /* @__PURE__ */ j(l, {
252
+ label: "Hours",
253
+ children: /* @__PURE__ */ j(b, {
254
+ type: "number",
255
+ min: "0",
256
+ value: u.hours,
257
+ onChange: g("hours"),
258
+ placeholder: "2"
259
+ })
260
+ }),
261
+ /* @__PURE__ */ j(l, {
262
+ label: "Minutes",
263
+ children: /* @__PURE__ */ j(b, {
264
+ type: "number",
265
+ min: "0",
266
+ max: "59",
267
+ value: u.minutes,
268
+ onChange: g("minutes"),
269
+ placeholder: "30"
270
+ })
271
+ })
272
+ ]
273
+ }),
274
+ /* @__PURE__ */ j(l, {
275
+ label: "Rate (per hour)",
276
+ children: /* @__PURE__ */ j(b, {
277
+ type: "number",
278
+ min: "0",
279
+ value: u.rate,
280
+ onChange: g("rate"),
281
+ placeholder: "450"
282
+ })
283
+ }),
284
+ /* @__PURE__ */ M("label", {
285
+ className: "flex items-center gap-2 text-sm text-text-secondary",
286
+ children: [/* @__PURE__ */ j("input", {
287
+ type: "checkbox",
288
+ checked: u.billable,
289
+ onChange: (e) => d((t) => ({
290
+ ...t,
291
+ billable: e.target.checked
292
+ }))
293
+ }), "Billable"]
294
+ }),
295
+ /* @__PURE__ */ j(l, {
296
+ label: "Narrative",
297
+ children: /* @__PURE__ */ j(y, {
298
+ value: u.narrative,
299
+ onChange: g("narrative"),
300
+ placeholder: "Reviewed working papers and drafted findings memo…"
301
+ })
302
+ })
303
+ ]
304
+ })
305
+ });
306
+ };
307
+ //#endregion
308
+ export { F as TimePage };
309
+
310
+ //# sourceMappingURL=TimePage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimePage.js","names":[],"sources":["../../../src/proserviceworld/pages/TimePage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Plus, Clock, Send, CheckCircle2 } from 'lucide-react';\nimport {\n useTimeEntries,\n useLogTimeEntry,\n useSubmitTimeEntry,\n useApproveTimeEntry,\n useEngagements,\n useMatters,\n type PswTimeEntryStatus,\n} from '../hooks/useProServiceWorldApi';\nimport {\n PageHeader,\n SectionCard,\n LoadingBlock,\n ErrorState,\n Empty,\n StatusPill,\n Modal,\n Field,\n TextInput,\n TextArea,\n NativeSelect,\n Banner,\n MetricCard,\n TableWrap,\n Th,\n Td,\n enumOptions,\n fmtDate,\n fmtMinutesAsHours,\n} from '../components/ui';\n\nconst STATUSES: PswTimeEntryStatus[] = ['DRAFT', 'SUBMITTED', 'APPROVED', 'REJECTED', 'BILLED'];\n\nexport const TimePage: FC = () => {\n const [statusFilter, setStatusFilter] = useState<PswTimeEntryStatus | ''>('');\n const [logging, setLogging] = useState(false);\n const [banner, setBanner] = useState<{ tone: 'success' | 'danger'; text: string } | null>(null);\n const { data, isLoading, error, refetch } = useTimeEntries({ status: statusFilter || undefined });\n const submit = useSubmitTimeEntry();\n const approve = useApproveTimeEntry();\n\n const totalMinutes = (data?.items ?? []).reduce((s, t) => s + t.minutes, 0);\n const billableMinutes = (data?.items ?? [])\n .filter((t) => t.billable)\n .reduce((s, t) => s + t.minutes, 0);\n\n const act = (\n fn: {\n mutate: (id: string, opts: { onSuccess: () => void; onError: (e: unknown) => void }) => void;\n },\n id: string,\n verb: string\n ) =>\n fn.mutate(id, {\n onSuccess: () => setBanner({ tone: 'success', text: `Time entry ${verb}.` }),\n onError: (e) =>\n setBanner({ tone: 'danger', text: e instanceof Error ? e.message : 'Failed.' }),\n });\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Time\"\n subtitle=\"Capture expert effort against engagements and matters — the raw material of billing and utilization.\"\n icon={<Clock className=\"h-6 w-6\" />}\n actions={\n <Button onClick={() => setLogging(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n Log time\n </Button>\n }\n />\n\n {banner && (\n <Banner tone={banner.tone} onClose={() => setBanner(null)}>\n {banner.text}\n </Banner>\n )}\n\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <MetricCard label=\"Entries\" value={data?.totalCount ?? 0} />\n <MetricCard label=\"Total hours\" value={fmtMinutesAsHours(totalMinutes)} />\n <MetricCard\n label=\"Billable hours\"\n value={fmtMinutesAsHours(billableMinutes)}\n tone=\"accent\"\n />\n <MetricCard\n label=\"Non-billable\"\n value={fmtMinutesAsHours(totalMinutes - billableMinutes)}\n />\n </div>\n\n <div className=\"flex justify-end\">\n <NativeSelect\n value={statusFilter}\n onChange={(e) => setStatusFilter(e.target.value as PswTimeEntryStatus | '')}\n className=\"sm:w-52\"\n >\n <option value=\"\">All statuses</option>\n {enumOptions(STATUSES)}\n </NativeSelect>\n </div>\n\n {isLoading ? (\n <LoadingBlock />\n ) : error ? (\n <ErrorState error={error} onRetry={() => void refetch()} />\n ) : !data || data.items.length === 0 ? (\n <Empty\n title=\"No time logged\"\n description=\"Log your first time entry against an engagement or matter.\"\n icon={<Clock className=\"h-8 w-8\" />}\n action={\n <Button onClick={() => setLogging(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n Log time\n </Button>\n }\n />\n ) : (\n <SectionCard title=\"Time entries\">\n <TableWrap>\n <thead>\n <tr>\n <Th>Date</Th>\n <Th>Narrative</Th>\n <Th>Hours</Th>\n <Th>Billable</Th>\n <Th>Status</Th>\n <Th className=\"text-right\">Actions</Th>\n </tr>\n </thead>\n <tbody>\n {data.items.map((t) => (\n <tr key={t.id} className=\"hover:bg-bg-sunken/50\">\n <Td>{fmtDate(t.date)}</Td>\n <Td className=\"max-w-xs truncate text-text-primary\">{t.narrative || '—'}</Td>\n <Td>{fmtMinutesAsHours(t.minutes)}</Td>\n <Td>{t.billable ? 'Yes' : 'No'}</Td>\n <Td>\n <StatusPill status={t.status} />\n </Td>\n <Td className=\"text-right\">\n <div className=\"flex justify-end gap-1\">\n {t.status === 'DRAFT' && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n disabled={submit.isPending}\n onClick={() => act(submit, t.id, 'submitted')}\n >\n <Send className=\"mr-1 h-3.5 w-3.5\" />\n Submit\n </Button>\n )}\n {t.status === 'SUBMITTED' && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n disabled={approve.isPending}\n onClick={() => act(approve, t.id, 'approved')}\n >\n <CheckCircle2 className=\"mr-1 h-3.5 w-3.5\" />\n Approve\n </Button>\n )}\n </div>\n </Td>\n </tr>\n ))}\n </tbody>\n </TableWrap>\n </SectionCard>\n )}\n\n {logging && (\n <LogTimeModal\n onClose={() => setLogging(false)}\n onDone={() => setBanner({ tone: 'success', text: 'Time logged.' })}\n />\n )}\n </div>\n );\n};\n\nconst LogTimeModal: FC<{ onClose: () => void; onDone: () => void }> = ({ onClose, onDone }) => {\n const log = useLogTimeEntry();\n const engagements = useEngagements({ pagination: { limit: 100 } });\n const matters = useMatters({ pagination: { limit: 100 } });\n const [form, setForm] = useState({\n against: 'engagement',\n engagementId: '',\n matterId: '',\n date: new Date().toISOString().slice(0, 10),\n hours: '',\n minutes: '',\n rate: '',\n billable: true,\n narrative: '',\n });\n const [err, setErr] = useState<string | null>(null);\n const set = (k: keyof typeof form) => (e: { target: { value: string } }) =>\n setForm((f) => ({ ...f, [k]: e.target.value }));\n\n const submit = () => {\n setErr(null);\n const totalMinutes = (Number(form.hours) || 0) * 60 + (Number(form.minutes) || 0);\n if (totalMinutes <= 0) return setErr('Enter the time spent.');\n const ref =\n form.against === 'engagement'\n ? { engagementId: form.engagementId }\n : { matterId: form.matterId };\n if (form.against === 'engagement' && !form.engagementId) return setErr('Select an engagement.');\n if (form.against === 'matter' && !form.matterId) return setErr('Select a matter.');\n log.mutate(\n {\n ...ref,\n date: new Date(form.date).toISOString(),\n minutes: totalMinutes,\n rate: form.rate ? Number(form.rate) : 0,\n billable: form.billable,\n narrative: form.narrative.trim() || null,\n },\n {\n onSuccess: () => {\n onDone();\n onClose();\n },\n onError: (e) => setErr(e instanceof Error ? e.message : 'Failed.'),\n }\n );\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"Log time\"\n footer={\n <>\n <Button variant=\"outline\" onClick={onClose} disabled={log.isPending}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={log.isPending}>\n {log.isPending ? 'Logging…' : 'Log time'}\n </Button>\n </>\n }\n >\n <div className=\"space-y-3\">\n {err && <Banner tone=\"danger\">{err}</Banner>}\n <Field label=\"Against\">\n <NativeSelect value={form.against} onChange={set('against')}>\n <option value=\"engagement\">Engagement</option>\n <option value=\"matter\">Matter</option>\n </NativeSelect>\n </Field>\n {form.against === 'engagement' ? (\n <Field label=\"Engagement\" required>\n <NativeSelect value={form.engagementId} onChange={set('engagementId')}>\n <option value=\"\">Select an engagement…</option>\n {engagements.data?.items.map((e) => (\n <option key={e.id} value={e.id}>\n {e.name}\n </option>\n ))}\n </NativeSelect>\n </Field>\n ) : (\n <Field label=\"Matter\" required>\n <NativeSelect value={form.matterId} onChange={set('matterId')}>\n <option value=\"\">Select a matter…</option>\n {matters.data?.items.map((m) => (\n <option key={m.id} value={m.id}>\n {m.title}\n </option>\n ))}\n </NativeSelect>\n </Field>\n )}\n <div className=\"grid grid-cols-3 gap-3\">\n <Field label=\"Date\">\n <TextInput type=\"date\" value={form.date} onChange={set('date')} />\n </Field>\n <Field label=\"Hours\">\n <TextInput\n type=\"number\"\n min=\"0\"\n value={form.hours}\n onChange={set('hours')}\n placeholder=\"2\"\n />\n </Field>\n <Field label=\"Minutes\">\n <TextInput\n type=\"number\"\n min=\"0\"\n max=\"59\"\n value={form.minutes}\n onChange={set('minutes')}\n placeholder=\"30\"\n />\n </Field>\n </div>\n <Field label=\"Rate (per hour)\">\n <TextInput\n type=\"number\"\n min=\"0\"\n value={form.rate}\n onChange={set('rate')}\n placeholder=\"450\"\n />\n </Field>\n <label className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <input\n type=\"checkbox\"\n checked={form.billable}\n onChange={(e) => setForm((f) => ({ ...f, billable: e.target.checked }))}\n />\n Billable\n </label>\n <Field label=\"Narrative\">\n <TextArea\n value={form.narrative}\n onChange={set('narrative')}\n placeholder=\"Reviewed working papers and drafted findings memo…\"\n />\n </Field>\n </div>\n </Modal>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import e from "../../node_modules/clsx/dist/clsx.js";
2
+ import { twMerge as t } from "../../node_modules/tailwind-merge/dist/bundle-mjs.js";
3
+ //#region src/proserviceworld/utils/cn.ts
4
+ function n(...n) {
5
+ return t(e(n));
6
+ }
7
+ //#endregion
8
+ export { n as cn };
9
+
10
+ //# sourceMappingURL=cn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cn.js","names":[],"sources":["../../../src/proserviceworld/utils/cn.ts"],"sourcesContent":["import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":";;;AAGA,SAAgB,EAAG,GAAG,GAAsB;AAC1C,QAAO,EAAQ,EAAK,EAAO,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { useProServiceWorld as e } from "../providers/ProServiceWorldProvider.js";
2
+ import { useNavigate as t } from "react-router-dom";
3
+ //#region src/proserviceworld/utils/nav.ts
4
+ function n() {
5
+ let n = t(), { basePath: r = "" } = e();
6
+ return (e) => n(`${r}/${e}`.replace(/\/{2,}/g, "/"));
7
+ }
8
+ //#endregion
9
+ export { n as useNav };
10
+
11
+ //# sourceMappingURL=nav.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nav.js","names":[],"sources":["../../../src/proserviceworld/utils/nav.ts"],"sourcesContent":["import { useNavigate } from 'react-router-dom';\nimport { useProServiceWorld } from '../providers/ProServiceWorldProvider';\n\n/**\n * Navigate to a path relative to the MFE's basePath. Works both mounted in a\n * host shell (basePath set, shell BrowserRouter) and standalone (MemoryRouter).\n */\nexport function useNav(): (relative: string) => void {\n const navigate = useNavigate();\n const { basePath = '' } = useProServiceWorld();\n return (relative: string) => navigate(`${basePath}/${relative}`.replace(/\\/{2,}/g, '/'));\n}\n"],"mappings":";;;AAOA,SAAgB,IAAqC;CACnD,IAAM,IAAW,GAAa,EACxB,EAAE,cAAW,OAAO,GAAoB;AAC9C,SAAQ,MAAqB,EAAS,GAAG,EAAS,GAAG,IAAW,QAAQ,WAAW,IAAI,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-proserviceworld",
3
- "version": "2026.626.2",
3
+ "version": "2026.706.1",
4
4
  "description": "Burdenoff ProServiceWorld Microfrontend",
5
5
  "type": "module",
6
6
  "files": [
@@ -24,9 +24,10 @@
24
24
  "dev": "vite",
25
25
  "build": "tsc && vite build",
26
26
  "lint": "eslint .",
27
+ "lint:sanity": "if [ \"${CI:-}\" = \"true\" ]; then bun run lint; else bun run lint -- --cache --cache-location .eslintcache; fi",
27
28
  "format": "prettier --write \"src/**/*.{ts,tsx}\"",
28
29
  "type:check": "tsc --noEmit",
29
- "sanity": "bun run codegen && bun run lint && bun run format && bun run type:check && bun run build",
30
+ "sanity": "bun run codegen && bun run lint:sanity && bun run format && bun run type:check && bun run build",
30
31
  "schema:pull": "graphql-codegen --project schema-wspace && graphql-codegen --project schema-global",
31
32
  "codegen": "graphql-codegen --project wspace && graphql-codegen --project global",
32
33
  "codegen:all": "bun run schema:pull && bun run codegen",