@burdenoff/microfe-proserviceworld 2026.706.1 → 2026.706.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/proserviceworld/components/DiscussionThread.js +76 -0
- package/dist/proserviceworld/components/DiscussionThread.js.map +1 -0
- package/dist/proserviceworld/hooks/useEntityComments.js +56 -0
- package/dist/proserviceworld/hooks/useEntityComments.js.map +1 -0
- package/dist/proserviceworld/pages/ClientDetailPage.js +81 -76
- package/dist/proserviceworld/pages/ClientDetailPage.js.map +1 -1
- package/dist/proserviceworld/pages/EngagementDetailPage.js +119 -114
- package/dist/proserviceworld/pages/EngagementDetailPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useProServiceWorld as e } from "../providers/ProServiceWorldProvider.js";
|
|
2
|
+
import { Banner as t, Empty as n, LoadingBlock as r, SectionCard as i, TextArea as a, fmtDate as o } from "./ui.js";
|
|
3
|
+
import { useAddEntityComment as s, useEntityComments as c } from "../hooks/useEntityComments.js";
|
|
4
|
+
import { useState as l } from "react";
|
|
5
|
+
import { MessageSquare as u, Send as d } from "lucide-react";
|
|
6
|
+
import { jsx as f, jsxs as p } from "react/jsx-runtime";
|
|
7
|
+
import { Button as m } from "@burdenoff/fe-libs/ui";
|
|
8
|
+
//#region src/proserviceworld/components/DiscussionThread.tsx
|
|
9
|
+
var h = ({ entityType: h, entityId: g, title: _ = "Discussion" }) => {
|
|
10
|
+
let { currentUser: v } = e(), { data: y, isLoading: b, error: x } = c(h, g), S = s(h, g), [C, w] = l(""), [T, E] = l(null), D = () => {
|
|
11
|
+
E(null);
|
|
12
|
+
let e = C.trim();
|
|
13
|
+
e && S.mutate(e, {
|
|
14
|
+
onSuccess: () => w(""),
|
|
15
|
+
onError: (e) => E(e instanceof Error ? e.message : "Failed to post.")
|
|
16
|
+
});
|
|
17
|
+
}, O = (e) => v && e === v.id ? "You" : "Team member";
|
|
18
|
+
return /* @__PURE__ */ f(i, {
|
|
19
|
+
title: _,
|
|
20
|
+
description: "Notes & discussion — shared across the platform via Conversations.",
|
|
21
|
+
children: /* @__PURE__ */ p("div", {
|
|
22
|
+
className: "space-y-3",
|
|
23
|
+
children: [
|
|
24
|
+
T && /* @__PURE__ */ f(t, {
|
|
25
|
+
tone: "danger",
|
|
26
|
+
onClose: () => E(null),
|
|
27
|
+
children: T
|
|
28
|
+
}),
|
|
29
|
+
/* @__PURE__ */ p("div", {
|
|
30
|
+
className: "flex items-start gap-2",
|
|
31
|
+
children: [/* @__PURE__ */ f(a, {
|
|
32
|
+
value: C,
|
|
33
|
+
onChange: (e) => w(e.target.value),
|
|
34
|
+
placeholder: "Add a note…",
|
|
35
|
+
className: "min-h-[44px] flex-1",
|
|
36
|
+
onKeyDown: (e) => {
|
|
37
|
+
(e.metaKey || e.ctrlKey) && e.key === "Enter" && D();
|
|
38
|
+
}
|
|
39
|
+
}), /* @__PURE__ */ f(m, {
|
|
40
|
+
onClick: D,
|
|
41
|
+
disabled: S.isPending || !C.trim(),
|
|
42
|
+
"aria-label": "Post note",
|
|
43
|
+
children: /* @__PURE__ */ f(d, { className: "h-4 w-4" })
|
|
44
|
+
})]
|
|
45
|
+
}),
|
|
46
|
+
b ? /* @__PURE__ */ f(r, { label: "Loading discussion…" }) : x ? /* @__PURE__ */ f("p", {
|
|
47
|
+
className: "text-sm text-status-error-text",
|
|
48
|
+
children: "Couldn't load discussion."
|
|
49
|
+
}) : !y || y.items.length === 0 ? /* @__PURE__ */ f(n, {
|
|
50
|
+
title: "No notes yet",
|
|
51
|
+
description: "Start the discussion for this record.",
|
|
52
|
+
icon: /* @__PURE__ */ f(u, { className: "h-7 w-7" })
|
|
53
|
+
}) : /* @__PURE__ */ f("ul", {
|
|
54
|
+
className: "space-y-2.5",
|
|
55
|
+
children: y.items.map((e) => /* @__PURE__ */ p("li", {
|
|
56
|
+
className: "rounded-lg border border-border-seam bg-bg-canvas p-3",
|
|
57
|
+
children: [/* @__PURE__ */ p("div", {
|
|
58
|
+
className: "mb-1 flex items-center justify-between text-xs text-text-tertiary",
|
|
59
|
+
children: [/* @__PURE__ */ f("span", {
|
|
60
|
+
className: "font-medium text-text-secondary",
|
|
61
|
+
children: O(e.authorId)
|
|
62
|
+
}), /* @__PURE__ */ f("span", { children: o(e.createdAt) })]
|
|
63
|
+
}), /* @__PURE__ */ f("p", {
|
|
64
|
+
className: "whitespace-pre-wrap text-sm text-text-primary",
|
|
65
|
+
children: e.content?.text ?? ""
|
|
66
|
+
})]
|
|
67
|
+
}, e.id))
|
|
68
|
+
})
|
|
69
|
+
]
|
|
70
|
+
})
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
//#endregion
|
|
74
|
+
export { h as DiscussionThread };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=DiscussionThread.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DiscussionThread.js","names":[],"sources":["../../../src/proserviceworld/components/DiscussionThread.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { MessageSquare, Send } from 'lucide-react';\nimport { useEntityComments, useAddEntityComment } from '../hooks/useEntityComments';\nimport { useProServiceWorld } from '../providers/ProServiceWorldProvider';\nimport { SectionCard, TextArea, Banner, LoadingBlock, Empty, fmtDate } from './ui';\n\n/**\n * Discussion / notes for a Psw entity, backed entirely by wspace-conversations\n * (createEntityComment / getEntityComments) — no product-owned comments table.\n */\nexport const DiscussionThread: FC<{ entityType: string; entityId: string; title?: string }> = ({\n entityType,\n entityId,\n title = 'Discussion',\n}) => {\n const { currentUser } = useProServiceWorld();\n const { data, isLoading, error } = useEntityComments(entityType, entityId);\n const add = useAddEntityComment(entityType, entityId);\n const [text, setText] = useState('');\n const [err, setErr] = useState<string | null>(null);\n\n const submit = () => {\n setErr(null);\n const body = text.trim();\n if (!body) return;\n add.mutate(body, { onSuccess: () => setText(''), onError: (e) => setErr(e instanceof Error ? e.message : 'Failed to post.') });\n };\n\n const authorLabel = (authorId: string) =>\n currentUser && authorId === currentUser.id ? 'You' : 'Team member';\n\n return (\n <SectionCard title={title} description=\"Notes & discussion — shared across the platform via Conversations.\">\n <div className=\"space-y-3\">\n {err && <Banner tone=\"danger\" onClose={() => setErr(null)}>{err}</Banner>}\n <div className=\"flex items-start gap-2\">\n <TextArea\n value={text}\n onChange={(e) => setText(e.target.value)}\n placeholder=\"Add a note…\"\n className=\"min-h-[44px] flex-1\"\n onKeyDown={(e) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') submit();\n }}\n />\n <Button onClick={submit} disabled={add.isPending || !text.trim()} aria-label=\"Post note\">\n <Send className=\"h-4 w-4\" />\n </Button>\n </div>\n\n {isLoading ? (\n <LoadingBlock label=\"Loading discussion…\" />\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">Couldn't load discussion.</p>\n ) : !data || data.items.length === 0 ? (\n <Empty title=\"No notes yet\" description=\"Start the discussion for this record.\" icon={<MessageSquare className=\"h-7 w-7\" />} />\n ) : (\n <ul className=\"space-y-2.5\">\n {data.items.map((c) => (\n <li key={c.id} className=\"rounded-lg border border-border-seam bg-bg-canvas p-3\">\n <div className=\"mb-1 flex items-center justify-between text-xs text-text-tertiary\">\n <span className=\"font-medium text-text-secondary\">{authorLabel(c.authorId)}</span>\n <span>{fmtDate(c.createdAt)}</span>\n </div>\n <p className=\"whitespace-pre-wrap text-sm text-text-primary\">{c.content?.text ?? ''}</p>\n </li>\n ))}\n </ul>\n )}\n </div>\n </SectionCard>\n );\n};\n"],"mappings":";;;;;;;;AAYA,IAAa,KAAkF,EAC7F,eACA,aACA,WAAQ,mBACJ;CACJ,IAAM,EAAE,mBAAgB,GAAoB,EACtC,EAAE,SAAM,cAAW,aAAU,EAAkB,GAAY,EAAS,EACpE,IAAM,EAAoB,GAAY,EAAS,EAC/C,CAAC,GAAM,KAAW,EAAS,GAAG,EAC9B,CAAC,GAAK,KAAU,EAAwB,KAAK,EAE7C,UAAe;AACnB,IAAO,KAAK;EACZ,IAAM,IAAO,EAAK,MAAM;AACnB,OACL,EAAI,OAAO,GAAM;GAAE,iBAAiB,EAAQ,GAAG;GAAE,UAAU,MAAM,EAAO,aAAa,QAAQ,EAAE,UAAU,kBAAkB;GAAE,CAAC;IAG1H,KAAe,MACnB,KAAe,MAAa,EAAY,KAAK,QAAQ;AAEvD,QACE,kBAAC,GAAD;EAAoB;EAAO,aAAY;YACrC,kBAAC,OAAD;GAAK,WAAU;aAAf;IACG,KAAO,kBAAC,GAAD;KAAQ,MAAK;KAAS,eAAe,EAAO,KAAK;eAAG;KAAa,CAAA;IACzE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MACE,OAAO;MACP,WAAW,MAAM,EAAQ,EAAE,OAAO,MAAM;MACxC,aAAY;MACZ,WAAU;MACV,YAAY,MAAM;AAChB,QAAK,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,WAAS,GAAQ;;MAE7D,CAAA,EACF,kBAAC,GAAD;MAAQ,SAAS;MAAQ,UAAU,EAAI,aAAa,CAAC,EAAK,MAAM;MAAE,cAAW;gBAC3E,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;MACrB,CAAA,CACL;;IAEL,IACC,kBAAC,GAAD,EAAc,OAAM,uBAAwB,CAAA,GAC1C,IACF,kBAAC,KAAD;KAAG,WAAU;eAAiC;KAA6B,CAAA,GACzE,CAAC,KAAQ,EAAK,MAAM,WAAW,IACjC,kBAAC,GAAD;KAAO,OAAM;KAAe,aAAY;KAAwC,MAAM,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;KAAI,CAAA,GAE/H,kBAAC,MAAD;KAAI,WAAU;eACX,EAAK,MAAM,KAAK,MACf,kBAAC,MAAD;MAAe,WAAU;gBAAzB,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAmC,EAAY,EAAE,SAAS;QAAQ,CAAA,EAClF,kBAAC,QAAD,EAAA,UAAO,EAAQ,EAAE,UAAU,EAAQ,CAAA,CAC/B;UACN,kBAAC,KAAD;OAAG,WAAU;iBAAiD,EAAE,SAAS,QAAQ;OAAO,CAAA,CACrF;QANI,EAAE,GAMN,CACL;KACC,CAAA;IAEH;;EACM,CAAA"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useMutation as e, useQuery as t, useQueryClient as n } from "@tanstack/react-query";
|
|
2
|
+
import { graphqlFetch as r } from "@burdenoff/fe-libs/shared/graphql";
|
|
3
|
+
import { clearGraphqlFetchResponseCache as i } from "@burdenoff/fe-libs/shared/graphql/fetch";
|
|
4
|
+
//#region src/proserviceworld/hooks/useEntityComments.ts
|
|
5
|
+
async function a(e, t) {
|
|
6
|
+
let n = await r({
|
|
7
|
+
gateway: "workspace",
|
|
8
|
+
query: e,
|
|
9
|
+
variables: t,
|
|
10
|
+
workspaceToken: !0
|
|
11
|
+
});
|
|
12
|
+
if (n.errors?.length) throw Error(n.errors[0]?.message ?? "Conversations request failed");
|
|
13
|
+
if (!n.data) throw Error("Conversations response did not include data");
|
|
14
|
+
return e.trimStart().startsWith("mutation") && i(), n.data;
|
|
15
|
+
}
|
|
16
|
+
var o = "id authorId content { text } createdAt", s = "proserviceworld-comments";
|
|
17
|
+
function c(e, n) {
|
|
18
|
+
return t({
|
|
19
|
+
queryKey: [
|
|
20
|
+
s,
|
|
21
|
+
e,
|
|
22
|
+
n
|
|
23
|
+
],
|
|
24
|
+
enabled: !!n,
|
|
25
|
+
queryFn: async () => (await a(`query PswGetComments($input: GetCommentsInput!) {
|
|
26
|
+
getEntityComments(input: $input) { items { ${o} } total }
|
|
27
|
+
}`, { input: {
|
|
28
|
+
entityType: e,
|
|
29
|
+
entityId: n,
|
|
30
|
+
take: 50,
|
|
31
|
+
sortBy: "createdAt",
|
|
32
|
+
sortOrder: "desc"
|
|
33
|
+
} })).getEntityComments
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function l(t, r) {
|
|
37
|
+
let i = n();
|
|
38
|
+
return e({
|
|
39
|
+
mutationFn: (e) => a(`mutation PswAddComment($input: CreateEntityCommentInput!) {
|
|
40
|
+
createEntityComment(input: $input) { ${o} }
|
|
41
|
+
}`, { input: {
|
|
42
|
+
entityType: t,
|
|
43
|
+
entityId: r,
|
|
44
|
+
content: { text: e }
|
|
45
|
+
} }),
|
|
46
|
+
onSuccess: () => i.invalidateQueries({ queryKey: [
|
|
47
|
+
s,
|
|
48
|
+
t,
|
|
49
|
+
r
|
|
50
|
+
] })
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { l as useAddEntityComment, c as useEntityComments };
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=useEntityComments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEntityComments.js","names":[],"sources":["../../../src/proserviceworld/hooks/useEntityComments.ts"],"sourcesContent":["/**\n * Entity discussion via wspace-conversations-svc (NO duplication).\n * @module proserviceworld/hooks\n *\n * ProServiceWorld does NOT own a comments table. Discussion/notes on a client,\n * engagement or matter reuse the shared `wspace-conversations` entity-comment\n * API (createEntityComment / getEntityComments) — the same service that powers\n * comments everywhere on the platform — via the MFE's proven graphqlFetch\n * transport. `entityType` namespaces threads to this product\n * (\"proserviceworld-<entity>\"); `entityId` is the Psw row id.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { clearGraphqlFetchResponseCache } from '@burdenoff/fe-libs/shared/graphql/fetch';\n\nasync function convQuery<TData>(query: string, variables: Record<string, unknown>): Promise<TData> {\n const response = await graphqlFetch<TData>({ gateway: 'workspace', query, variables, workspaceToken: true });\n if (response.errors?.length) throw new Error(response.errors[0]?.message ?? 'Conversations request failed');\n if (!response.data) throw new Error('Conversations response did not include data');\n if (query.trimStart().startsWith('mutation')) clearGraphqlFetchResponseCache();\n return response.data;\n}\n\nexport interface EntityComment {\n id: string;\n authorId: string;\n content: { text: string | null } | null;\n createdAt: string;\n}\n\nconst COMMENT_F = 'id authorId content { text } createdAt';\nconst QK = 'proserviceworld-comments' as const;\n\n/** List the discussion thread for a Psw entity (most recent first). */\nexport function useEntityComments(entityType: string, entityId: string | null) {\n return useQuery({\n queryKey: [QK, entityType, entityId],\n enabled: Boolean(entityId),\n queryFn: async () => {\n const data = await convQuery<{ getEntityComments: { items: EntityComment[]; total: number } }>(\n `query PswGetComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) { items { ${COMMENT_F} } total }\n }`,\n { input: { entityType, entityId, take: 50, sortBy: 'createdAt', sortOrder: 'desc' } }\n );\n return data.getEntityComments;\n },\n });\n}\n\n/** Post a plain-text note onto a Psw entity's shared discussion thread. */\nexport function useAddEntityComment(entityType: string, entityId: string) {\n const qc = useQueryClient();\n return useMutation({\n mutationFn: (text: string) =>\n convQuery(\n `mutation PswAddComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) { ${COMMENT_F} }\n }`,\n { input: { entityType, entityId, content: { text } } }\n ),\n onSuccess: () => qc.invalidateQueries({ queryKey: [QK, entityType, entityId] }),\n });\n}\n"],"mappings":";;;;AAiBA,eAAe,EAAiB,GAAe,GAAoD;CACjG,IAAM,IAAW,MAAM,EAAoB;EAAE,SAAS;EAAa;EAAO;EAAW,gBAAgB;EAAM,CAAC;AAC5G,KAAI,EAAS,QAAQ,OAAQ,OAAU,MAAM,EAAS,OAAO,IAAI,WAAW,+BAA+B;AAC3G,KAAI,CAAC,EAAS,KAAM,OAAU,MAAM,8CAA8C;AAElF,QADI,EAAM,WAAW,CAAC,WAAW,WAAW,IAAE,GAAgC,EACvE,EAAS;;AAUlB,IAAM,IAAY,0CACZ,IAAK;AAGX,SAAgB,EAAkB,GAAoB,GAAyB;AAC7E,QAAO,EAAS;EACd,UAAU;GAAC;GAAI;GAAY;GAAS;EACpC,SAAS,EAAQ;EACjB,SAAS,aACM,MAAM,EACjB;uDAC+C,EAAU;YAEzD,EAAE,OAAO;GAAE;GAAY;GAAU,MAAM;GAAI,QAAQ;GAAa,WAAW;GAAQ,EAAE,CACtF,EACW;EAEf,CAAC;;AAIJ,SAAgB,EAAoB,GAAoB,GAAkB;CACxE,IAAM,IAAK,GAAgB;AAC3B,QAAO,EAAY;EACjB,aAAa,MACX,EACE;iDACyC,EAAU;YAEnD,EAAE,OAAO;GAAE;GAAY;GAAU,SAAS,EAAE,SAAM;GAAE,EAAE,CACvD;EACH,iBAAiB,EAAG,kBAAkB,EAAE,UAAU;GAAC;GAAI;GAAY;GAAS,EAAE,CAAC;EAChF,CAAC"}
|
|
@@ -1,190 +1,195 @@
|
|
|
1
1
|
import { useClient as e, useConflictChecks as t, useCreateConflictCheck as n } from "../hooks/useProServiceWorldApi.js";
|
|
2
2
|
import { useNav as r } from "../utils/nav.js";
|
|
3
3
|
import { Banner as i, Empty as a, ErrorState as o, Field as s, LoadingBlock as c, Modal as l, PageHeader as u, SectionCard as d, StatusPill as f, TextArea as p, TextInput as m, fmtDate as h, fmtMoney as g } from "../components/ui.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
4
|
+
import { DiscussionThread as _ } from "../components/DiscussionThread.js";
|
|
5
|
+
import { useState as v } from "react";
|
|
6
|
+
import { ArrowLeft as y, Briefcase as b, FolderKanban as x, GitBranch as S, Mail as C, MapPin as w, Phone as T, Plus as E } from "lucide-react";
|
|
7
|
+
import { useParams as D } from "react-router-dom";
|
|
8
|
+
import { Fragment as O, jsx as k, jsxs as A } from "react/jsx-runtime";
|
|
9
|
+
import { Button as j } from "@burdenoff/fe-libs/ui";
|
|
9
10
|
//#region src/proserviceworld/pages/ClientDetailPage.tsx
|
|
10
|
-
var
|
|
11
|
-
let { id: n = "" } =
|
|
12
|
-
return l ? /* @__PURE__ */
|
|
11
|
+
var M = () => {
|
|
12
|
+
let { id: n = "" } = D(), i = r(), { data: s, isLoading: l, error: p, refetch: m } = e(n), O = t({ clientId: n }), [M, I] = v(!1);
|
|
13
|
+
return l ? /* @__PURE__ */ k(c, {}) : p ? /* @__PURE__ */ k(o, {
|
|
13
14
|
error: p,
|
|
14
15
|
onRetry: () => void m()
|
|
15
|
-
}) : s ? /* @__PURE__ */
|
|
16
|
+
}) : s ? /* @__PURE__ */ A("div", {
|
|
16
17
|
className: "space-y-6",
|
|
17
18
|
children: [
|
|
18
|
-
/* @__PURE__ */
|
|
19
|
+
/* @__PURE__ */ A("button", {
|
|
19
20
|
type: "button",
|
|
20
21
|
onClick: () => i("clients"),
|
|
21
22
|
className: "flex items-center gap-1.5 text-sm text-text-secondary hover:text-accent",
|
|
22
|
-
children: [/* @__PURE__ */
|
|
23
|
+
children: [/* @__PURE__ */ k(y, { className: "h-4 w-4" }), " Back to clients"]
|
|
23
24
|
}),
|
|
24
|
-
/* @__PURE__ */
|
|
25
|
+
/* @__PURE__ */ k(u, {
|
|
25
26
|
title: s.name,
|
|
26
27
|
subtitle: [s.type === "ORGANIZATION" ? "Organization" : "Individual", s.industry].filter(Boolean).join(" · "),
|
|
27
|
-
actions: /* @__PURE__ */
|
|
28
|
+
actions: /* @__PURE__ */ k(f, { status: s.status })
|
|
28
29
|
}),
|
|
29
|
-
/* @__PURE__ */
|
|
30
|
+
/* @__PURE__ */ A("div", {
|
|
30
31
|
className: "grid grid-cols-1 gap-4 lg:grid-cols-3",
|
|
31
|
-
children: [/* @__PURE__ */
|
|
32
|
+
children: [/* @__PURE__ */ k(d, {
|
|
32
33
|
title: "Contact",
|
|
33
34
|
className: "lg:col-span-1",
|
|
34
|
-
children: /* @__PURE__ */
|
|
35
|
+
children: /* @__PURE__ */ A("dl", {
|
|
35
36
|
className: "space-y-2 text-sm",
|
|
36
37
|
children: [
|
|
37
|
-
/* @__PURE__ */
|
|
38
|
+
/* @__PURE__ */ k(N, {
|
|
38
39
|
label: "Contact",
|
|
39
40
|
value: s.contactName
|
|
40
41
|
}),
|
|
41
|
-
s.contactEmail && /* @__PURE__ */
|
|
42
|
-
icon: /* @__PURE__ */
|
|
42
|
+
s.contactEmail && /* @__PURE__ */ k(P, {
|
|
43
|
+
icon: /* @__PURE__ */ k(C, { className: "h-3.5 w-3.5" }),
|
|
43
44
|
value: s.contactEmail
|
|
44
45
|
}),
|
|
45
|
-
s.contactPhone && /* @__PURE__ */
|
|
46
|
-
icon: /* @__PURE__ */
|
|
46
|
+
s.contactPhone && /* @__PURE__ */ k(P, {
|
|
47
|
+
icon: /* @__PURE__ */ k(T, { className: "h-3.5 w-3.5" }),
|
|
47
48
|
value: s.contactPhone
|
|
48
49
|
}),
|
|
49
|
-
s.address && /* @__PURE__ */
|
|
50
|
-
icon: /* @__PURE__ */
|
|
50
|
+
s.address && /* @__PURE__ */ k(P, {
|
|
51
|
+
icon: /* @__PURE__ */ k(w, { className: "h-3.5 w-3.5" }),
|
|
51
52
|
value: s.address
|
|
52
53
|
}),
|
|
53
|
-
s.notes && /* @__PURE__ */
|
|
54
|
+
s.notes && /* @__PURE__ */ k("p", {
|
|
54
55
|
className: "pt-2 text-text-secondary",
|
|
55
56
|
children: s.notes
|
|
56
57
|
})
|
|
57
58
|
]
|
|
58
59
|
})
|
|
59
|
-
}), /* @__PURE__ */
|
|
60
|
+
}), /* @__PURE__ */ k(d, {
|
|
60
61
|
title: "Conflict checks",
|
|
61
62
|
className: "lg:col-span-2",
|
|
62
|
-
action: /* @__PURE__ */
|
|
63
|
+
action: /* @__PURE__ */ A(j, {
|
|
63
64
|
size: "sm",
|
|
64
65
|
variant: "outline",
|
|
65
|
-
onClick: () =>
|
|
66
|
-
children: [/* @__PURE__ */
|
|
66
|
+
onClick: () => I(!0),
|
|
67
|
+
children: [/* @__PURE__ */ k(S, { className: "mr-1.5 h-4 w-4" }), " Run check"]
|
|
67
68
|
}),
|
|
68
|
-
children:
|
|
69
|
+
children: O.isLoading ? /* @__PURE__ */ k(c, { label: "Loading…" }) : !O.data || O.data.length === 0 ? /* @__PURE__ */ k(a, {
|
|
69
70
|
title: "No conflict checks",
|
|
70
71
|
description: "Run a conflict check before accepting new work for this client."
|
|
71
|
-
}) : /* @__PURE__ */
|
|
72
|
+
}) : /* @__PURE__ */ k("ul", {
|
|
72
73
|
className: "divide-y divide-border-seam",
|
|
73
|
-
children:
|
|
74
|
+
children: O.data.map((e) => /* @__PURE__ */ A("li", {
|
|
74
75
|
className: "flex items-center justify-between py-2.5",
|
|
75
|
-
children: [/* @__PURE__ */
|
|
76
|
+
children: [/* @__PURE__ */ A("div", { children: [/* @__PURE__ */ k("div", {
|
|
76
77
|
className: "text-sm text-text-primary",
|
|
77
78
|
children: e.searchedParties.join(", ") || "Conflict search"
|
|
78
|
-
}), /* @__PURE__ */
|
|
79
|
+
}), /* @__PURE__ */ A("div", {
|
|
79
80
|
className: "text-xs text-text-tertiary",
|
|
80
81
|
children: [h(e.createdAt), e.findings ? ` · ${e.findings}` : ""]
|
|
81
|
-
})] }), /* @__PURE__ */
|
|
82
|
+
})] }), /* @__PURE__ */ k(f, { status: e.status })]
|
|
82
83
|
}, e.id))
|
|
83
84
|
})
|
|
84
85
|
})]
|
|
85
86
|
}),
|
|
86
|
-
/* @__PURE__ */
|
|
87
|
+
/* @__PURE__ */ k(d, {
|
|
87
88
|
title: "Engagements",
|
|
88
|
-
action: /* @__PURE__ */
|
|
89
|
+
action: /* @__PURE__ */ A(j, {
|
|
89
90
|
size: "sm",
|
|
90
91
|
variant: "outline",
|
|
91
92
|
onClick: () => i("engagements"),
|
|
92
|
-
children: [/* @__PURE__ */
|
|
93
|
+
children: [/* @__PURE__ */ k(E, { className: "mr-1.5 h-4 w-4" }), "Manage"]
|
|
93
94
|
}),
|
|
94
|
-
children: s.engagements.length === 0 ? /* @__PURE__ */
|
|
95
|
+
children: s.engagements.length === 0 ? /* @__PURE__ */ k(a, {
|
|
95
96
|
title: "No engagements",
|
|
96
97
|
description: "Scope the first engagement for this client.",
|
|
97
|
-
icon: /* @__PURE__ */
|
|
98
|
-
}) : /* @__PURE__ */
|
|
98
|
+
icon: /* @__PURE__ */ k(b, { className: "h-7 w-7" })
|
|
99
|
+
}) : /* @__PURE__ */ k("ul", {
|
|
99
100
|
className: "divide-y divide-border-seam",
|
|
100
|
-
children: s.engagements.map((e) => /* @__PURE__ */
|
|
101
|
+
children: s.engagements.map((e) => /* @__PURE__ */ k("li", { children: /* @__PURE__ */ A("button", {
|
|
101
102
|
type: "button",
|
|
102
103
|
onClick: () => i(`engagements/${e.id}`),
|
|
103
104
|
className: "flex w-full items-center justify-between py-2.5 text-left hover:text-accent",
|
|
104
|
-
children: [/* @__PURE__ */
|
|
105
|
+
children: [/* @__PURE__ */ A("div", { children: [/* @__PURE__ */ k("div", {
|
|
105
106
|
className: "text-sm font-medium text-text-primary",
|
|
106
107
|
children: e.name
|
|
107
|
-
}), /* @__PURE__ */
|
|
108
|
+
}), /* @__PURE__ */ A("div", {
|
|
108
109
|
className: "text-xs text-text-tertiary",
|
|
109
110
|
children: [
|
|
110
111
|
g(e.feeAmount, e.currency),
|
|
111
112
|
" · ",
|
|
112
113
|
e.feeArrangement.toLowerCase()
|
|
113
114
|
]
|
|
114
|
-
})] }), /* @__PURE__ */
|
|
115
|
+
})] }), /* @__PURE__ */ k(f, { status: e.status })]
|
|
115
116
|
}) }, e.id))
|
|
116
117
|
})
|
|
117
118
|
}),
|
|
118
|
-
/* @__PURE__ */
|
|
119
|
+
/* @__PURE__ */ k(d, {
|
|
119
120
|
title: "Matters",
|
|
120
|
-
action: /* @__PURE__ */
|
|
121
|
+
action: /* @__PURE__ */ A(j, {
|
|
121
122
|
size: "sm",
|
|
122
123
|
variant: "outline",
|
|
123
124
|
onClick: () => i("matters"),
|
|
124
|
-
children: [/* @__PURE__ */
|
|
125
|
+
children: [/* @__PURE__ */ k(E, { className: "mr-1.5 h-4 w-4" }), "Manage"]
|
|
125
126
|
}),
|
|
126
|
-
children: s.matters.length === 0 ? /* @__PURE__ */
|
|
127
|
+
children: s.matters.length === 0 ? /* @__PURE__ */ k(a, {
|
|
127
128
|
title: "No matters",
|
|
128
129
|
description: "Open a matter to track regulated work for this client.",
|
|
129
|
-
icon: /* @__PURE__ */
|
|
130
|
-
}) : /* @__PURE__ */
|
|
130
|
+
icon: /* @__PURE__ */ k(x, { className: "h-7 w-7" })
|
|
131
|
+
}) : /* @__PURE__ */ k("ul", {
|
|
131
132
|
className: "divide-y divide-border-seam",
|
|
132
|
-
children: s.matters.map((e) => /* @__PURE__ */
|
|
133
|
+
children: s.matters.map((e) => /* @__PURE__ */ A("li", {
|
|
133
134
|
className: "flex items-center justify-between py-2.5",
|
|
134
|
-
children: [/* @__PURE__ */
|
|
135
|
+
children: [/* @__PURE__ */ A("div", { children: [/* @__PURE__ */ k("div", {
|
|
135
136
|
className: "text-sm font-medium text-text-primary",
|
|
136
137
|
children: e.title
|
|
137
|
-
}), /* @__PURE__ */
|
|
138
|
+
}), /* @__PURE__ */ A("div", {
|
|
138
139
|
className: "text-xs text-text-tertiary",
|
|
139
140
|
children: [
|
|
140
141
|
e.reference || "No reference",
|
|
141
142
|
" · Due ",
|
|
142
143
|
h(e.dueDate)
|
|
143
144
|
]
|
|
144
|
-
})] }), /* @__PURE__ */
|
|
145
|
+
})] }), /* @__PURE__ */ A("div", {
|
|
145
146
|
className: "flex items-center gap-2",
|
|
146
|
-
children: [/* @__PURE__ */
|
|
147
|
+
children: [/* @__PURE__ */ k(f, { status: e.priority }), /* @__PURE__ */ k(f, { status: e.status })]
|
|
147
148
|
})]
|
|
148
149
|
}, e.id))
|
|
149
150
|
})
|
|
150
151
|
}),
|
|
151
|
-
|
|
152
|
+
/* @__PURE__ */ k(_, {
|
|
153
|
+
entityType: "proserviceworld-client",
|
|
154
|
+
entityId: n
|
|
155
|
+
}),
|
|
156
|
+
M && /* @__PURE__ */ k(F, {
|
|
152
157
|
clientId: n,
|
|
153
|
-
onClose: () =>
|
|
154
|
-
onDone: () => void
|
|
158
|
+
onClose: () => I(!1),
|
|
159
|
+
onDone: () => void O.refetch()
|
|
155
160
|
})
|
|
156
161
|
]
|
|
157
|
-
}) : /* @__PURE__ */
|
|
162
|
+
}) : /* @__PURE__ */ k(a, {
|
|
158
163
|
title: "Client not found",
|
|
159
164
|
description: "It may have been archived or belongs to another workspace."
|
|
160
165
|
});
|
|
161
|
-
},
|
|
166
|
+
}, N = ({ label: e, value: t }) => t ? /* @__PURE__ */ A("div", {
|
|
162
167
|
className: "flex justify-between gap-4",
|
|
163
|
-
children: [/* @__PURE__ */
|
|
168
|
+
children: [/* @__PURE__ */ k("dt", {
|
|
164
169
|
className: "text-text-tertiary",
|
|
165
170
|
children: e
|
|
166
|
-
}), /* @__PURE__ */
|
|
171
|
+
}), /* @__PURE__ */ k("dd", {
|
|
167
172
|
className: "text-right text-text-primary",
|
|
168
173
|
children: t
|
|
169
174
|
})]
|
|
170
|
-
}) : null,
|
|
175
|
+
}) : null, P = ({ icon: e, value: t }) => /* @__PURE__ */ A("div", {
|
|
171
176
|
className: "flex items-center gap-2 text-text-secondary",
|
|
172
|
-
children: [/* @__PURE__ */
|
|
177
|
+
children: [/* @__PURE__ */ k("span", {
|
|
173
178
|
className: "text-text-tertiary",
|
|
174
179
|
children: e
|
|
175
180
|
}), t]
|
|
176
|
-
}),
|
|
177
|
-
let a = n(), [o, c] =
|
|
178
|
-
return /* @__PURE__ */
|
|
181
|
+
}), F = ({ clientId: e, onClose: t, onDone: r }) => {
|
|
182
|
+
let a = n(), [o, c] = v(""), [u, d] = v(""), [f, h] = v(null);
|
|
183
|
+
return /* @__PURE__ */ k(l, {
|
|
179
184
|
open: !0,
|
|
180
185
|
onClose: t,
|
|
181
186
|
title: "Run conflict check",
|
|
182
|
-
footer: /* @__PURE__ */
|
|
187
|
+
footer: /* @__PURE__ */ A(O, { children: [/* @__PURE__ */ k(j, {
|
|
183
188
|
variant: "outline",
|
|
184
189
|
onClick: t,
|
|
185
190
|
disabled: a.isPending,
|
|
186
191
|
children: "Cancel"
|
|
187
|
-
}), /* @__PURE__ */
|
|
192
|
+
}), /* @__PURE__ */ k(j, {
|
|
188
193
|
onClick: () => {
|
|
189
194
|
h(null);
|
|
190
195
|
let n = o.split(",").map((e) => e.trim()).filter(Boolean);
|
|
@@ -203,26 +208,26 @@ var j = () => {
|
|
|
203
208
|
disabled: a.isPending,
|
|
204
209
|
children: a.isPending ? "Running…" : "Record check"
|
|
205
210
|
})] }),
|
|
206
|
-
children: /* @__PURE__ */
|
|
211
|
+
children: /* @__PURE__ */ A("div", {
|
|
207
212
|
className: "space-y-3",
|
|
208
213
|
children: [
|
|
209
|
-
f && /* @__PURE__ */
|
|
214
|
+
f && /* @__PURE__ */ k(i, {
|
|
210
215
|
tone: "danger",
|
|
211
216
|
children: f
|
|
212
217
|
}),
|
|
213
|
-
/* @__PURE__ */
|
|
218
|
+
/* @__PURE__ */ k(s, {
|
|
214
219
|
label: "Parties searched",
|
|
215
220
|
required: !0,
|
|
216
221
|
hint: "Comma-separated — counterparties, related entities, principals.",
|
|
217
|
-
children: /* @__PURE__ */
|
|
222
|
+
children: /* @__PURE__ */ k(m, {
|
|
218
223
|
value: o,
|
|
219
224
|
onChange: (e) => c(e.target.value),
|
|
220
225
|
placeholder: "Beta Corp, John Roe, Gamma LLP"
|
|
221
226
|
})
|
|
222
227
|
}),
|
|
223
|
-
/* @__PURE__ */
|
|
228
|
+
/* @__PURE__ */ k(s, {
|
|
224
229
|
label: "Findings",
|
|
225
|
-
children: /* @__PURE__ */
|
|
230
|
+
children: /* @__PURE__ */ k(p, {
|
|
226
231
|
value: u,
|
|
227
232
|
onChange: (e) => d(e.target.value),
|
|
228
233
|
placeholder: "Notes on any potential conflicts…"
|
|
@@ -233,6 +238,6 @@ var j = () => {
|
|
|
233
238
|
});
|
|
234
239
|
};
|
|
235
240
|
//#endregion
|
|
236
|
-
export {
|
|
241
|
+
export { M as ClientDetailPage };
|
|
237
242
|
|
|
238
243
|
//# sourceMappingURL=ClientDetailPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientDetailPage.js","names":[],"sources":["../../../src/proserviceworld/pages/ClientDetailPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport {\n ArrowLeft,\n Briefcase,\n FolderKanban,\n GitBranch,\n Mail,\n Phone,\n MapPin,\n Plus,\n} from 'lucide-react';\nimport {\n useClient,\n useConflictChecks,\n useCreateConflictCheck,\n type PswEngagement,\n type PswMatter,\n} from '../hooks/useProServiceWorldApi';\nimport { useNav } from '../utils/nav';\nimport {\n PageHeader,\n SectionCard,\n LoadingBlock,\n ErrorState,\n Empty,\n StatusPill,\n Modal,\n Field,\n TextInput,\n TextArea,\n Banner,\n fmtDate,\n fmtMoney,\n} from '../components/ui';\n\nexport const ClientDetailPage: FC = () => {\n const { id = '' } = useParams();\n const nav = useNav();\n const { data: client, isLoading, error, refetch } = useClient(id);\n const conflicts = useConflictChecks({ clientId: id });\n const [runConflict, setRunConflict] = useState(false);\n\n if (isLoading) return <LoadingBlock />;\n if (error) return <ErrorState error={error} onRetry={() => void refetch()} />;\n if (!client)\n return (\n <Empty\n title=\"Client not found\"\n description=\"It may have been archived or belongs to another workspace.\"\n />\n );\n\n return (\n <div className=\"space-y-6\">\n <button\n type=\"button\"\n onClick={() => nav('clients')}\n className=\"flex items-center gap-1.5 text-sm text-text-secondary hover:text-accent\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back to clients\n </button>\n\n <PageHeader\n title={client.name}\n subtitle={[client.type === 'ORGANIZATION' ? 'Organization' : 'Individual', client.industry]\n .filter(Boolean)\n .join(' · ')}\n actions={<StatusPill status={client.status} />}\n />\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <SectionCard title=\"Contact\" className=\"lg:col-span-1\">\n <dl className=\"space-y-2 text-sm\">\n <Row label=\"Contact\" value={client.contactName} />\n {client.contactEmail && (\n <IconRow icon={<Mail className=\"h-3.5 w-3.5\" />} value={client.contactEmail} />\n )}\n {client.contactPhone && (\n <IconRow icon={<Phone className=\"h-3.5 w-3.5\" />} value={client.contactPhone} />\n )}\n {client.address && (\n <IconRow icon={<MapPin className=\"h-3.5 w-3.5\" />} value={client.address} />\n )}\n {client.notes && <p className=\"pt-2 text-text-secondary\">{client.notes}</p>}\n </dl>\n </SectionCard>\n\n <SectionCard\n title=\"Conflict checks\"\n className=\"lg:col-span-2\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => setRunConflict(true)}>\n <GitBranch className=\"mr-1.5 h-4 w-4\" /> Run check\n </Button>\n }\n >\n {conflicts.isLoading ? (\n <LoadingBlock label=\"Loading…\" />\n ) : !conflicts.data || conflicts.data.length === 0 ? (\n <Empty\n title=\"No conflict checks\"\n description=\"Run a conflict check before accepting new work for this client.\"\n />\n ) : (\n <ul className=\"divide-y divide-border-seam\">\n {conflicts.data.map((c) => (\n <li key={c.id} className=\"flex items-center justify-between py-2.5\">\n <div>\n <div className=\"text-sm text-text-primary\">\n {c.searchedParties.join(', ') || 'Conflict search'}\n </div>\n <div className=\"text-xs text-text-tertiary\">\n {fmtDate(c.createdAt)}\n {c.findings ? ` · ${c.findings}` : ''}\n </div>\n </div>\n <StatusPill status={c.status} />\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n </div>\n\n <SectionCard\n title=\"Engagements\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => nav('engagements')}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n Manage\n </Button>\n }\n >\n {client.engagements.length === 0 ? (\n <Empty\n title=\"No engagements\"\n description=\"Scope the first engagement for this client.\"\n icon={<Briefcase className=\"h-7 w-7\" />}\n />\n ) : (\n <ul className=\"divide-y divide-border-seam\">\n {client.engagements.map((e: PswEngagement) => (\n <li key={e.id}>\n <button\n type=\"button\"\n onClick={() => nav(`engagements/${e.id}`)}\n className=\"flex w-full items-center justify-between py-2.5 text-left hover:text-accent\"\n >\n <div>\n <div className=\"text-sm font-medium text-text-primary\">{e.name}</div>\n <div className=\"text-xs text-text-tertiary\">\n {fmtMoney(e.feeAmount, e.currency)} · {e.feeArrangement.toLowerCase()}\n </div>\n </div>\n <StatusPill status={e.status} />\n </button>\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n\n <SectionCard\n title=\"Matters\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => nav('matters')}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n Manage\n </Button>\n }\n >\n {client.matters.length === 0 ? (\n <Empty\n title=\"No matters\"\n description=\"Open a matter to track regulated work for this client.\"\n icon={<FolderKanban className=\"h-7 w-7\" />}\n />\n ) : (\n <ul className=\"divide-y divide-border-seam\">\n {client.matters.map((m: PswMatter) => (\n <li key={m.id} className=\"flex items-center justify-between py-2.5\">\n <div>\n <div className=\"text-sm font-medium text-text-primary\">{m.title}</div>\n <div className=\"text-xs text-text-tertiary\">\n {m.reference || 'No reference'} · Due {fmtDate(m.dueDate)}\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <StatusPill status={m.priority} />\n <StatusPill status={m.status} />\n </div>\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n\n {runConflict && (\n <ConflictModal\n clientId={id}\n onClose={() => setRunConflict(false)}\n onDone={() => void conflicts.refetch()}\n />\n )}\n </div>\n );\n};\n\nconst Row: FC<{ label: string; value: string | null }> = ({ label, value }) =>\n value ? (\n <div className=\"flex justify-between gap-4\">\n <dt className=\"text-text-tertiary\">{label}</dt>\n <dd className=\"text-right text-text-primary\">{value}</dd>\n </div>\n ) : null;\n\nconst IconRow: FC<{ icon: React.ReactNode; value: string }> = ({ icon, value }) => (\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <span className=\"text-text-tertiary\">{icon}</span>\n {value}\n </div>\n);\n\nconst ConflictModal: FC<{ clientId: string; onClose: () => void; onDone: () => void }> = ({\n clientId,\n onClose,\n onDone,\n}) => {\n const create = useCreateConflictCheck();\n const [parties, setParties] = useState('');\n const [findings, setFindings] = useState('');\n const [err, setErr] = useState<string | null>(null);\n\n const submit = () => {\n setErr(null);\n const searchedParties = parties\n .split(',')\n .map((p) => p.trim())\n .filter(Boolean);\n if (searchedParties.length === 0) return setErr('Add at least one party to search.');\n create.mutate(\n { clientId, searchedParties, findings: findings.trim() || null },\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=\"Run conflict check\"\n footer={\n <>\n <Button variant=\"outline\" onClick={onClose} disabled={create.isPending}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={create.isPending}>\n {create.isPending ? 'Running…' : 'Record check'}\n </Button>\n </>\n }\n >\n <div className=\"space-y-3\">\n {err && <Banner tone=\"danger\">{err}</Banner>}\n <Field\n label=\"Parties searched\"\n required\n hint=\"Comma-separated — counterparties, related entities, principals.\"\n >\n <TextInput\n value={parties}\n onChange={(e) => setParties(e.target.value)}\n placeholder=\"Beta Corp, John Roe, Gamma LLP\"\n />\n </Field>\n <Field label=\"Findings\">\n <TextArea\n value={findings}\n onChange={(e) => setFindings(e.target.value)}\n placeholder=\"Notes on any potential conflicts…\"\n />\n </Field>\n </div>\n </Modal>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ClientDetailPage.js","names":[],"sources":["../../../src/proserviceworld/pages/ClientDetailPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport {\n ArrowLeft,\n Briefcase,\n FolderKanban,\n GitBranch,\n Mail,\n Phone,\n MapPin,\n Plus,\n} from 'lucide-react';\nimport {\n useClient,\n useConflictChecks,\n useCreateConflictCheck,\n type PswEngagement,\n type PswMatter,\n} from '../hooks/useProServiceWorldApi';\nimport { useNav } from '../utils/nav';\nimport { DiscussionThread } from '../components/DiscussionThread';\nimport {\n PageHeader,\n SectionCard,\n LoadingBlock,\n ErrorState,\n Empty,\n StatusPill,\n Modal,\n Field,\n TextInput,\n TextArea,\n Banner,\n fmtDate,\n fmtMoney,\n} from '../components/ui';\n\nexport const ClientDetailPage: FC = () => {\n const { id = '' } = useParams();\n const nav = useNav();\n const { data: client, isLoading, error, refetch } = useClient(id);\n const conflicts = useConflictChecks({ clientId: id });\n const [runConflict, setRunConflict] = useState(false);\n\n if (isLoading) return <LoadingBlock />;\n if (error) return <ErrorState error={error} onRetry={() => void refetch()} />;\n if (!client)\n return (\n <Empty\n title=\"Client not found\"\n description=\"It may have been archived or belongs to another workspace.\"\n />\n );\n\n return (\n <div className=\"space-y-6\">\n <button\n type=\"button\"\n onClick={() => nav('clients')}\n className=\"flex items-center gap-1.5 text-sm text-text-secondary hover:text-accent\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back to clients\n </button>\n\n <PageHeader\n title={client.name}\n subtitle={[client.type === 'ORGANIZATION' ? 'Organization' : 'Individual', client.industry]\n .filter(Boolean)\n .join(' · ')}\n actions={<StatusPill status={client.status} />}\n />\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <SectionCard title=\"Contact\" className=\"lg:col-span-1\">\n <dl className=\"space-y-2 text-sm\">\n <Row label=\"Contact\" value={client.contactName} />\n {client.contactEmail && (\n <IconRow icon={<Mail className=\"h-3.5 w-3.5\" />} value={client.contactEmail} />\n )}\n {client.contactPhone && (\n <IconRow icon={<Phone className=\"h-3.5 w-3.5\" />} value={client.contactPhone} />\n )}\n {client.address && (\n <IconRow icon={<MapPin className=\"h-3.5 w-3.5\" />} value={client.address} />\n )}\n {client.notes && <p className=\"pt-2 text-text-secondary\">{client.notes}</p>}\n </dl>\n </SectionCard>\n\n <SectionCard\n title=\"Conflict checks\"\n className=\"lg:col-span-2\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => setRunConflict(true)}>\n <GitBranch className=\"mr-1.5 h-4 w-4\" /> Run check\n </Button>\n }\n >\n {conflicts.isLoading ? (\n <LoadingBlock label=\"Loading…\" />\n ) : !conflicts.data || conflicts.data.length === 0 ? (\n <Empty\n title=\"No conflict checks\"\n description=\"Run a conflict check before accepting new work for this client.\"\n />\n ) : (\n <ul className=\"divide-y divide-border-seam\">\n {conflicts.data.map((c) => (\n <li key={c.id} className=\"flex items-center justify-between py-2.5\">\n <div>\n <div className=\"text-sm text-text-primary\">\n {c.searchedParties.join(', ') || 'Conflict search'}\n </div>\n <div className=\"text-xs text-text-tertiary\">\n {fmtDate(c.createdAt)}\n {c.findings ? ` · ${c.findings}` : ''}\n </div>\n </div>\n <StatusPill status={c.status} />\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n </div>\n\n <SectionCard\n title=\"Engagements\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => nav('engagements')}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n Manage\n </Button>\n }\n >\n {client.engagements.length === 0 ? (\n <Empty\n title=\"No engagements\"\n description=\"Scope the first engagement for this client.\"\n icon={<Briefcase className=\"h-7 w-7\" />}\n />\n ) : (\n <ul className=\"divide-y divide-border-seam\">\n {client.engagements.map((e: PswEngagement) => (\n <li key={e.id}>\n <button\n type=\"button\"\n onClick={() => nav(`engagements/${e.id}`)}\n className=\"flex w-full items-center justify-between py-2.5 text-left hover:text-accent\"\n >\n <div>\n <div className=\"text-sm font-medium text-text-primary\">{e.name}</div>\n <div className=\"text-xs text-text-tertiary\">\n {fmtMoney(e.feeAmount, e.currency)} · {e.feeArrangement.toLowerCase()}\n </div>\n </div>\n <StatusPill status={e.status} />\n </button>\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n\n <SectionCard\n title=\"Matters\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => nav('matters')}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n Manage\n </Button>\n }\n >\n {client.matters.length === 0 ? (\n <Empty\n title=\"No matters\"\n description=\"Open a matter to track regulated work for this client.\"\n icon={<FolderKanban className=\"h-7 w-7\" />}\n />\n ) : (\n <ul className=\"divide-y divide-border-seam\">\n {client.matters.map((m: PswMatter) => (\n <li key={m.id} className=\"flex items-center justify-between py-2.5\">\n <div>\n <div className=\"text-sm font-medium text-text-primary\">{m.title}</div>\n <div className=\"text-xs text-text-tertiary\">\n {m.reference || 'No reference'} · Due {fmtDate(m.dueDate)}\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <StatusPill status={m.priority} />\n <StatusPill status={m.status} />\n </div>\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n\n <DiscussionThread entityType=\"proserviceworld-client\" entityId={id} />\n\n {runConflict && (\n <ConflictModal\n clientId={id}\n onClose={() => setRunConflict(false)}\n onDone={() => void conflicts.refetch()}\n />\n )}\n </div>\n );\n};\n\nconst Row: FC<{ label: string; value: string | null }> = ({ label, value }) =>\n value ? (\n <div className=\"flex justify-between gap-4\">\n <dt className=\"text-text-tertiary\">{label}</dt>\n <dd className=\"text-right text-text-primary\">{value}</dd>\n </div>\n ) : null;\n\nconst IconRow: FC<{ icon: React.ReactNode; value: string }> = ({ icon, value }) => (\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <span className=\"text-text-tertiary\">{icon}</span>\n {value}\n </div>\n);\n\nconst ConflictModal: FC<{ clientId: string; onClose: () => void; onDone: () => void }> = ({\n clientId,\n onClose,\n onDone,\n}) => {\n const create = useCreateConflictCheck();\n const [parties, setParties] = useState('');\n const [findings, setFindings] = useState('');\n const [err, setErr] = useState<string | null>(null);\n\n const submit = () => {\n setErr(null);\n const searchedParties = parties\n .split(',')\n .map((p) => p.trim())\n .filter(Boolean);\n if (searchedParties.length === 0) return setErr('Add at least one party to search.');\n create.mutate(\n { clientId, searchedParties, findings: findings.trim() || null },\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=\"Run conflict check\"\n footer={\n <>\n <Button variant=\"outline\" onClick={onClose} disabled={create.isPending}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={create.isPending}>\n {create.isPending ? 'Running…' : 'Record check'}\n </Button>\n </>\n }\n >\n <div className=\"space-y-3\">\n {err && <Banner tone=\"danger\">{err}</Banner>}\n <Field\n label=\"Parties searched\"\n required\n hint=\"Comma-separated — counterparties, related entities, principals.\"\n >\n <TextInput\n value={parties}\n onChange={(e) => setParties(e.target.value)}\n placeholder=\"Beta Corp, John Roe, Gamma LLP\"\n />\n </Field>\n <Field label=\"Findings\">\n <TextArea\n value={findings}\n onChange={(e) => setFindings(e.target.value)}\n placeholder=\"Notes on any potential conflicts…\"\n />\n </Field>\n </div>\n </Modal>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { useCompleteMilestone as e, useCreateDeliverable as t, useCreateMilestone as n, useEngagement as r, useTimeEntries as i, useTransitionDeliverable as a, useTransitionEngagement as o } from "../hooks/useProServiceWorldApi.js";
|
|
2
2
|
import { useNav as s } from "../utils/nav.js";
|
|
3
3
|
import { Banner as c, Empty as l, ErrorState as u, Field as d, LoadingBlock as f, MetricCard as p, Modal as m, NativeSelect as h, PageHeader as g, SectionCard as _, StatusPill as v, TextArea as y, TextInput as b, fmtDate as x, fmtMinutesAsHours as S, fmtMoney as C, humanize as w } from "../components/ui.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
4
|
+
import { DiscussionThread as T } from "../components/DiscussionThread.js";
|
|
5
|
+
import { useState as E } from "react";
|
|
6
|
+
import { ArrowLeft as D, CheckCircle2 as O, Clock as k, FileText as A, Flag as j, Plus as M } from "lucide-react";
|
|
7
|
+
import { useParams as N } from "react-router-dom";
|
|
8
|
+
import { Fragment as P, jsx as F, jsxs as I } from "react/jsx-runtime";
|
|
9
|
+
import { Button as L } from "@burdenoff/fe-libs/ui";
|
|
9
10
|
//#region src/proserviceworld/pages/EngagementDetailPage.tsx
|
|
10
|
-
var
|
|
11
|
+
var R = {
|
|
11
12
|
PROPOSAL: ["ACCEPTED", "CANCELLED"],
|
|
12
13
|
ACCEPTED: ["IN_PROGRESS", "CANCELLED"],
|
|
13
14
|
IN_PROGRESS: ["REVIEW", "CANCELLED"],
|
|
@@ -20,69 +21,69 @@ var L = {
|
|
|
20
21
|
INVOICED: ["CLOSED"],
|
|
21
22
|
CLOSED: [],
|
|
22
23
|
CANCELLED: []
|
|
23
|
-
},
|
|
24
|
+
}, z = [
|
|
24
25
|
"DRAFT",
|
|
25
26
|
"INTERNAL_REVIEW",
|
|
26
27
|
"CLIENT_REVIEW",
|
|
27
28
|
"ACCEPTED",
|
|
28
29
|
"REJECTED"
|
|
29
|
-
],
|
|
30
|
-
let { id: e = "" } =
|
|
31
|
-
if (a) return /* @__PURE__ */
|
|
32
|
-
if (d) return /* @__PURE__ */
|
|
30
|
+
], B = () => {
|
|
31
|
+
let { id: e = "" } = N(), t = s(), { data: n, isLoading: a, error: d, refetch: m } = r(e), h = i({ engagementId: e }), y = o(), [b, O] = E(!1), [P, z] = E(!1), [B, G] = E(null);
|
|
32
|
+
if (a) return /* @__PURE__ */ F(f, {});
|
|
33
|
+
if (d) return /* @__PURE__ */ F(u, {
|
|
33
34
|
error: d,
|
|
34
35
|
onRetry: () => void m()
|
|
35
36
|
});
|
|
36
|
-
if (!n) return /* @__PURE__ */
|
|
37
|
-
let
|
|
38
|
-
return /* @__PURE__ */
|
|
37
|
+
if (!n) return /* @__PURE__ */ F(l, { title: "Engagement not found" });
|
|
38
|
+
let K = R[n.status] ?? [], q = (h.data?.items ?? []).reduce((e, t) => e + t.minutes, 0);
|
|
39
|
+
return /* @__PURE__ */ I("div", {
|
|
39
40
|
className: "space-y-6",
|
|
40
41
|
children: [
|
|
41
|
-
/* @__PURE__ */
|
|
42
|
+
/* @__PURE__ */ I("button", {
|
|
42
43
|
type: "button",
|
|
43
44
|
onClick: () => t("engagements"),
|
|
44
45
|
className: "flex items-center gap-1.5 text-sm text-text-secondary hover:text-accent",
|
|
45
|
-
children: [/* @__PURE__ */
|
|
46
|
+
children: [/* @__PURE__ */ F(D, { className: "h-4 w-4" }), " Back to engagements"]
|
|
46
47
|
}),
|
|
47
|
-
/* @__PURE__ */
|
|
48
|
+
/* @__PURE__ */ F(g, {
|
|
48
49
|
title: n.name,
|
|
49
50
|
subtitle: n.client ? `${n.client.name} · ${w(n.feeArrangement)}` : w(n.feeArrangement),
|
|
50
|
-
actions: /* @__PURE__ */
|
|
51
|
+
actions: /* @__PURE__ */ F(v, { status: n.status })
|
|
51
52
|
}),
|
|
52
|
-
|
|
53
|
-
tone:
|
|
54
|
-
onClose: () =>
|
|
55
|
-
children:
|
|
53
|
+
B && /* @__PURE__ */ F(c, {
|
|
54
|
+
tone: B.tone,
|
|
55
|
+
onClose: () => G(null),
|
|
56
|
+
children: B.text
|
|
56
57
|
}),
|
|
57
|
-
/* @__PURE__ */
|
|
58
|
+
/* @__PURE__ */ I("div", {
|
|
58
59
|
className: "grid grid-cols-2 gap-3 lg:grid-cols-4",
|
|
59
60
|
children: [
|
|
60
|
-
/* @__PURE__ */
|
|
61
|
+
/* @__PURE__ */ F(p, {
|
|
61
62
|
label: "Fee",
|
|
62
63
|
value: C(n.feeAmount, n.currency),
|
|
63
64
|
hint: w(n.feeArrangement)
|
|
64
65
|
}),
|
|
65
|
-
/* @__PURE__ */
|
|
66
|
+
/* @__PURE__ */ F(p, {
|
|
66
67
|
label: "Deliverables",
|
|
67
68
|
value: n.deliverables.length
|
|
68
69
|
}),
|
|
69
|
-
/* @__PURE__ */
|
|
70
|
+
/* @__PURE__ */ F(p, {
|
|
70
71
|
label: "Milestones",
|
|
71
72
|
value: n.milestones.length
|
|
72
73
|
}),
|
|
73
|
-
/* @__PURE__ */
|
|
74
|
+
/* @__PURE__ */ F(p, {
|
|
74
75
|
label: "Time logged",
|
|
75
|
-
value: S(
|
|
76
|
+
value: S(q),
|
|
76
77
|
hint: `${h.data?.totalCount ?? 0} entries`
|
|
77
78
|
})
|
|
78
79
|
]
|
|
79
80
|
}),
|
|
80
|
-
|
|
81
|
+
K.length > 0 && /* @__PURE__ */ F(_, {
|
|
81
82
|
title: "Advance lifecycle",
|
|
82
83
|
description: `Current status: ${w(n.status)}`,
|
|
83
|
-
children: /* @__PURE__ */
|
|
84
|
+
children: /* @__PURE__ */ F("div", {
|
|
84
85
|
className: "flex flex-wrap gap-2",
|
|
85
|
-
children:
|
|
86
|
+
children: K.map((e) => /* @__PURE__ */ F(L, {
|
|
86
87
|
size: "sm",
|
|
87
88
|
variant: e === "CANCELLED" ? "outline" : "default",
|
|
88
89
|
disabled: y.isPending,
|
|
@@ -90,11 +91,11 @@ var L = {
|
|
|
90
91
|
id: n.id,
|
|
91
92
|
status: e
|
|
92
93
|
}, {
|
|
93
|
-
onSuccess: () =>
|
|
94
|
+
onSuccess: () => G({
|
|
94
95
|
tone: "success",
|
|
95
96
|
text: `Moved to ${w(e)}.`
|
|
96
97
|
}),
|
|
97
|
-
onError: (e) =>
|
|
98
|
+
onError: (e) => G({
|
|
98
99
|
tone: "danger",
|
|
99
100
|
text: e instanceof Error ? e.message : "Transition failed."
|
|
100
101
|
})
|
|
@@ -103,40 +104,40 @@ var L = {
|
|
|
103
104
|
}, e))
|
|
104
105
|
})
|
|
105
106
|
}),
|
|
106
|
-
/* @__PURE__ */
|
|
107
|
+
/* @__PURE__ */ I("div", {
|
|
107
108
|
className: "grid grid-cols-1 gap-4 lg:grid-cols-2",
|
|
108
|
-
children: [/* @__PURE__ */
|
|
109
|
+
children: [/* @__PURE__ */ F(_, {
|
|
109
110
|
title: "Deliverables",
|
|
110
|
-
action: /* @__PURE__ */
|
|
111
|
+
action: /* @__PURE__ */ I(L, {
|
|
111
112
|
size: "sm",
|
|
112
113
|
variant: "outline",
|
|
113
|
-
onClick: () =>
|
|
114
|
-
children: [/* @__PURE__ */
|
|
114
|
+
onClick: () => O(!0),
|
|
115
|
+
children: [/* @__PURE__ */ F(M, { className: "mr-1.5 h-4 w-4" }), "Add"]
|
|
115
116
|
}),
|
|
116
|
-
children: n.deliverables.length === 0 ? /* @__PURE__ */
|
|
117
|
+
children: n.deliverables.length === 0 ? /* @__PURE__ */ F(l, {
|
|
117
118
|
title: "No deliverables",
|
|
118
119
|
description: "Track the tangible outputs of this engagement.",
|
|
119
|
-
icon: /* @__PURE__ */
|
|
120
|
-
}) : /* @__PURE__ */
|
|
120
|
+
icon: /* @__PURE__ */ F(A, { className: "h-7 w-7" })
|
|
121
|
+
}) : /* @__PURE__ */ F("ul", {
|
|
121
122
|
className: "space-y-2",
|
|
122
|
-
children: n.deliverables.map((e) => /* @__PURE__ */
|
|
123
|
+
children: n.deliverables.map((e) => /* @__PURE__ */ F("li", {
|
|
123
124
|
className: "rounded-lg border border-border-seam p-3",
|
|
124
|
-
children: /* @__PURE__ */
|
|
125
|
+
children: /* @__PURE__ */ I("div", {
|
|
125
126
|
className: "flex items-start justify-between gap-2",
|
|
126
|
-
children: [/* @__PURE__ */
|
|
127
|
+
children: [/* @__PURE__ */ I("div", { children: [/* @__PURE__ */ I("div", {
|
|
127
128
|
className: "text-sm font-medium text-text-primary",
|
|
128
129
|
children: [
|
|
129
130
|
e.name,
|
|
130
131
|
" ",
|
|
131
|
-
/* @__PURE__ */
|
|
132
|
+
/* @__PURE__ */ I("span", {
|
|
132
133
|
className: "text-xs text-text-tertiary",
|
|
133
134
|
children: ["v", e.version]
|
|
134
135
|
})
|
|
135
136
|
]
|
|
136
|
-
}), e.dueDate && /* @__PURE__ */
|
|
137
|
+
}), e.dueDate && /* @__PURE__ */ I("div", {
|
|
137
138
|
className: "text-xs text-text-tertiary",
|
|
138
139
|
children: ["Due ", x(e.dueDate)]
|
|
139
|
-
})] }), /* @__PURE__ */
|
|
140
|
+
})] }), /* @__PURE__ */ F(V, {
|
|
140
141
|
id: e.id,
|
|
141
142
|
engagementId: n.id,
|
|
142
143
|
status: e.status,
|
|
@@ -145,35 +146,35 @@ var L = {
|
|
|
145
146
|
})
|
|
146
147
|
}, e.id))
|
|
147
148
|
})
|
|
148
|
-
}), /* @__PURE__ */
|
|
149
|
+
}), /* @__PURE__ */ F(_, {
|
|
149
150
|
title: "Milestones",
|
|
150
|
-
action: /* @__PURE__ */
|
|
151
|
+
action: /* @__PURE__ */ I(L, {
|
|
151
152
|
size: "sm",
|
|
152
153
|
variant: "outline",
|
|
153
|
-
onClick: () =>
|
|
154
|
-
children: [/* @__PURE__ */
|
|
154
|
+
onClick: () => z(!0),
|
|
155
|
+
children: [/* @__PURE__ */ F(M, { className: "mr-1.5 h-4 w-4" }), "Add"]
|
|
155
156
|
}),
|
|
156
|
-
children: n.milestones.length === 0 ? /* @__PURE__ */
|
|
157
|
+
children: n.milestones.length === 0 ? /* @__PURE__ */ F(l, {
|
|
157
158
|
title: "No milestones",
|
|
158
159
|
description: "Add checkpoints — some can trigger billing.",
|
|
159
|
-
icon: /* @__PURE__ */
|
|
160
|
-
}) : /* @__PURE__ */
|
|
160
|
+
icon: /* @__PURE__ */ F(j, { className: "h-7 w-7" })
|
|
161
|
+
}) : /* @__PURE__ */ F("ul", {
|
|
161
162
|
className: "space-y-2",
|
|
162
|
-
children: n.milestones.map((e) => /* @__PURE__ */
|
|
163
|
+
children: n.milestones.map((e) => /* @__PURE__ */ I("li", {
|
|
163
164
|
className: "flex items-center justify-between rounded-lg border border-border-seam p-3",
|
|
164
|
-
children: [/* @__PURE__ */
|
|
165
|
+
children: [/* @__PURE__ */ I("div", { children: [/* @__PURE__ */ F("div", {
|
|
165
166
|
className: "text-sm font-medium text-text-primary",
|
|
166
167
|
children: e.name
|
|
167
|
-
}), /* @__PURE__ */
|
|
168
|
+
}), /* @__PURE__ */ I("div", {
|
|
168
169
|
className: "text-xs text-text-tertiary",
|
|
169
170
|
children: [
|
|
170
171
|
e.triggersInvoice ? `Bills ${C(e.triggerAmount, n.currency)} · ` : "",
|
|
171
172
|
"Due ",
|
|
172
173
|
x(e.dueDate)
|
|
173
174
|
]
|
|
174
|
-
})] }), /* @__PURE__ */
|
|
175
|
+
})] }), /* @__PURE__ */ I("div", {
|
|
175
176
|
className: "flex items-center gap-2",
|
|
176
|
-
children: [/* @__PURE__ */
|
|
177
|
+
children: [/* @__PURE__ */ F(v, { status: e.status }), e.status !== "COMPLETED" && e.status !== "CANCELLED" && /* @__PURE__ */ F(H, {
|
|
177
178
|
id: e.id,
|
|
178
179
|
engagementId: n.id,
|
|
179
180
|
onDone: () => void m()
|
|
@@ -183,53 +184,57 @@ var L = {
|
|
|
183
184
|
})
|
|
184
185
|
})]
|
|
185
186
|
}),
|
|
186
|
-
/* @__PURE__ */
|
|
187
|
+
/* @__PURE__ */ F(_, {
|
|
187
188
|
title: "Time logged",
|
|
188
|
-
action: /* @__PURE__ */
|
|
189
|
+
action: /* @__PURE__ */ I(L, {
|
|
189
190
|
size: "sm",
|
|
190
191
|
variant: "outline",
|
|
191
192
|
onClick: () => t("time"),
|
|
192
|
-
children: [/* @__PURE__ */
|
|
193
|
+
children: [/* @__PURE__ */ F(k, { className: "mr-1.5 h-4 w-4" }), "Log time"]
|
|
193
194
|
}),
|
|
194
|
-
children: h.isLoading ? /* @__PURE__ */
|
|
195
|
+
children: h.isLoading ? /* @__PURE__ */ F(f, { label: "Loading…" }) : !h.data || h.data.items.length === 0 ? /* @__PURE__ */ F(l, {
|
|
195
196
|
title: "No time logged",
|
|
196
197
|
description: "Time logged against this engagement will appear here."
|
|
197
|
-
}) : /* @__PURE__ */
|
|
198
|
+
}) : /* @__PURE__ */ F("ul", {
|
|
198
199
|
className: "divide-y divide-border-seam",
|
|
199
|
-
children: h.data.items.slice(0, 8).map((e) => /* @__PURE__ */
|
|
200
|
+
children: h.data.items.slice(0, 8).map((e) => /* @__PURE__ */ I("li", {
|
|
200
201
|
className: "flex items-center justify-between py-2 text-sm",
|
|
201
|
-
children: [/* @__PURE__ */
|
|
202
|
+
children: [/* @__PURE__ */ I("span", {
|
|
202
203
|
className: "text-text-secondary",
|
|
203
204
|
children: [
|
|
204
205
|
x(e.date),
|
|
205
206
|
" · ",
|
|
206
207
|
e.narrative || "Time entry"
|
|
207
208
|
]
|
|
208
|
-
}), /* @__PURE__ */
|
|
209
|
+
}), /* @__PURE__ */ I("span", {
|
|
209
210
|
className: "flex items-center gap-2",
|
|
210
|
-
children: [/* @__PURE__ */
|
|
211
|
+
children: [/* @__PURE__ */ F("span", {
|
|
211
212
|
className: "text-text-primary",
|
|
212
213
|
children: S(e.minutes)
|
|
213
|
-
}), /* @__PURE__ */
|
|
214
|
+
}), /* @__PURE__ */ F(v, { status: e.status })]
|
|
214
215
|
})]
|
|
215
216
|
}, e.id))
|
|
216
217
|
})
|
|
217
218
|
}),
|
|
218
|
-
|
|
219
|
+
/* @__PURE__ */ F(T, {
|
|
220
|
+
entityType: "proserviceworld-engagement",
|
|
221
|
+
entityId: n.id
|
|
222
|
+
}),
|
|
223
|
+
b && /* @__PURE__ */ F(U, {
|
|
219
224
|
engagementId: n.id,
|
|
220
|
-
onClose: () =>
|
|
225
|
+
onClose: () => O(!1),
|
|
221
226
|
onDone: () => {
|
|
222
|
-
m(),
|
|
227
|
+
m(), G({
|
|
223
228
|
tone: "success",
|
|
224
229
|
text: "Deliverable added."
|
|
225
230
|
});
|
|
226
231
|
}
|
|
227
232
|
}),
|
|
228
|
-
|
|
233
|
+
P && /* @__PURE__ */ F(W, {
|
|
229
234
|
engagementId: n.id,
|
|
230
|
-
onClose: () =>
|
|
235
|
+
onClose: () => z(!1),
|
|
231
236
|
onDone: () => {
|
|
232
|
-
m(),
|
|
237
|
+
m(), G({
|
|
233
238
|
tone: "success",
|
|
234
239
|
text: "Milestone added."
|
|
235
240
|
});
|
|
@@ -237,9 +242,9 @@ var L = {
|
|
|
237
242
|
})
|
|
238
243
|
]
|
|
239
244
|
});
|
|
240
|
-
},
|
|
245
|
+
}, V = ({ id: e, engagementId: t, status: n, onDone: r }) => {
|
|
241
246
|
let i = a();
|
|
242
|
-
return /* @__PURE__ */
|
|
247
|
+
return /* @__PURE__ */ F(h, {
|
|
243
248
|
value: n,
|
|
244
249
|
disabled: i.isPending,
|
|
245
250
|
onChange: (n) => i.mutate({
|
|
@@ -248,14 +253,14 @@ var L = {
|
|
|
248
253
|
engagementId: t
|
|
249
254
|
}, { onSuccess: r }),
|
|
250
255
|
className: "w-40",
|
|
251
|
-
children:
|
|
256
|
+
children: z.map((e) => /* @__PURE__ */ F("option", {
|
|
252
257
|
value: e,
|
|
253
258
|
children: w(e)
|
|
254
259
|
}, e))
|
|
255
260
|
});
|
|
256
|
-
},
|
|
261
|
+
}, H = ({ id: t, engagementId: n, onDone: r }) => {
|
|
257
262
|
let i = e();
|
|
258
|
-
return /* @__PURE__ */
|
|
263
|
+
return /* @__PURE__ */ F(L, {
|
|
259
264
|
variant: "ghost",
|
|
260
265
|
size: "sm",
|
|
261
266
|
"aria-label": "Complete milestone",
|
|
@@ -264,28 +269,28 @@ var L = {
|
|
|
264
269
|
id: t,
|
|
265
270
|
engagementId: n
|
|
266
271
|
}, { onSuccess: r }),
|
|
267
|
-
children: /* @__PURE__ */
|
|
272
|
+
children: /* @__PURE__ */ F(O, { className: "h-4 w-4" })
|
|
268
273
|
});
|
|
269
|
-
},
|
|
270
|
-
let i = t(), [a, o] =
|
|
274
|
+
}, U = ({ engagementId: e, onClose: n, onDone: r }) => {
|
|
275
|
+
let i = t(), [a, o] = E({
|
|
271
276
|
name: "",
|
|
272
277
|
description: "",
|
|
273
278
|
dueDate: "",
|
|
274
279
|
acceptanceCriteria: ""
|
|
275
|
-
}), [s, l] =
|
|
280
|
+
}), [s, l] = E(null), u = (e) => (t) => o((n) => ({
|
|
276
281
|
...n,
|
|
277
282
|
[e]: t.target.value
|
|
278
283
|
}));
|
|
279
|
-
return /* @__PURE__ */
|
|
284
|
+
return /* @__PURE__ */ F(m, {
|
|
280
285
|
open: !0,
|
|
281
286
|
onClose: n,
|
|
282
287
|
title: "Add deliverable",
|
|
283
|
-
footer: /* @__PURE__ */
|
|
288
|
+
footer: /* @__PURE__ */ I(P, { children: [/* @__PURE__ */ F(L, {
|
|
284
289
|
variant: "outline",
|
|
285
290
|
onClick: n,
|
|
286
291
|
disabled: i.isPending,
|
|
287
292
|
children: "Cancel"
|
|
288
|
-
}), /* @__PURE__ */
|
|
293
|
+
}), /* @__PURE__ */ F(L, {
|
|
289
294
|
onClick: () => {
|
|
290
295
|
if (l(null), !a.name.trim()) return l("Name is required.");
|
|
291
296
|
i.mutate({
|
|
@@ -304,40 +309,40 @@ var L = {
|
|
|
304
309
|
disabled: i.isPending,
|
|
305
310
|
children: i.isPending ? "Adding…" : "Add"
|
|
306
311
|
})] }),
|
|
307
|
-
children: /* @__PURE__ */
|
|
312
|
+
children: /* @__PURE__ */ I("div", {
|
|
308
313
|
className: "space-y-3",
|
|
309
314
|
children: [
|
|
310
|
-
s && /* @__PURE__ */
|
|
315
|
+
s && /* @__PURE__ */ F(c, {
|
|
311
316
|
tone: "danger",
|
|
312
317
|
children: s
|
|
313
318
|
}),
|
|
314
|
-
/* @__PURE__ */
|
|
319
|
+
/* @__PURE__ */ F(d, {
|
|
315
320
|
label: "Name",
|
|
316
321
|
required: !0,
|
|
317
|
-
children: /* @__PURE__ */
|
|
322
|
+
children: /* @__PURE__ */ F(b, {
|
|
318
323
|
value: a.name,
|
|
319
324
|
onChange: u("name"),
|
|
320
325
|
placeholder: "Draft audit report"
|
|
321
326
|
})
|
|
322
327
|
}),
|
|
323
|
-
/* @__PURE__ */
|
|
328
|
+
/* @__PURE__ */ F(d, {
|
|
324
329
|
label: "Due date",
|
|
325
|
-
children: /* @__PURE__ */
|
|
330
|
+
children: /* @__PURE__ */ F(b, {
|
|
326
331
|
type: "date",
|
|
327
332
|
value: a.dueDate,
|
|
328
333
|
onChange: u("dueDate")
|
|
329
334
|
})
|
|
330
335
|
}),
|
|
331
|
-
/* @__PURE__ */
|
|
336
|
+
/* @__PURE__ */ F(d, {
|
|
332
337
|
label: "Acceptance criteria",
|
|
333
|
-
children: /* @__PURE__ */
|
|
338
|
+
children: /* @__PURE__ */ F(y, {
|
|
334
339
|
value: a.acceptanceCriteria,
|
|
335
340
|
onChange: u("acceptanceCriteria")
|
|
336
341
|
})
|
|
337
342
|
}),
|
|
338
|
-
/* @__PURE__ */
|
|
343
|
+
/* @__PURE__ */ F(d, {
|
|
339
344
|
label: "Description",
|
|
340
|
-
children: /* @__PURE__ */
|
|
345
|
+
children: /* @__PURE__ */ F(y, {
|
|
341
346
|
value: a.description,
|
|
342
347
|
onChange: u("description")
|
|
343
348
|
})
|
|
@@ -345,23 +350,23 @@ var L = {
|
|
|
345
350
|
]
|
|
346
351
|
})
|
|
347
352
|
});
|
|
348
|
-
},
|
|
349
|
-
let i = n(), [a, o] =
|
|
353
|
+
}, W = ({ engagementId: e, onClose: t, onDone: r }) => {
|
|
354
|
+
let i = n(), [a, o] = E({
|
|
350
355
|
name: "",
|
|
351
356
|
dueDate: "",
|
|
352
357
|
triggerAmount: "",
|
|
353
358
|
triggersInvoice: !1
|
|
354
|
-
}), [s, l] =
|
|
355
|
-
return /* @__PURE__ */
|
|
359
|
+
}), [s, l] = E(null);
|
|
360
|
+
return /* @__PURE__ */ F(m, {
|
|
356
361
|
open: !0,
|
|
357
362
|
onClose: t,
|
|
358
363
|
title: "Add milestone",
|
|
359
|
-
footer: /* @__PURE__ */
|
|
364
|
+
footer: /* @__PURE__ */ I(P, { children: [/* @__PURE__ */ F(L, {
|
|
360
365
|
variant: "outline",
|
|
361
366
|
onClick: t,
|
|
362
367
|
disabled: i.isPending,
|
|
363
368
|
children: "Cancel"
|
|
364
|
-
}), /* @__PURE__ */
|
|
369
|
+
}), /* @__PURE__ */ F(L, {
|
|
365
370
|
onClick: () => {
|
|
366
371
|
if (l(null), !a.name.trim()) return l("Name is required.");
|
|
367
372
|
i.mutate({
|
|
@@ -380,17 +385,17 @@ var L = {
|
|
|
380
385
|
disabled: i.isPending,
|
|
381
386
|
children: i.isPending ? "Adding…" : "Add"
|
|
382
387
|
})] }),
|
|
383
|
-
children: /* @__PURE__ */
|
|
388
|
+
children: /* @__PURE__ */ I("div", {
|
|
384
389
|
className: "space-y-3",
|
|
385
390
|
children: [
|
|
386
|
-
s && /* @__PURE__ */
|
|
391
|
+
s && /* @__PURE__ */ F(c, {
|
|
387
392
|
tone: "danger",
|
|
388
393
|
children: s
|
|
389
394
|
}),
|
|
390
|
-
/* @__PURE__ */
|
|
395
|
+
/* @__PURE__ */ F(d, {
|
|
391
396
|
label: "Name",
|
|
392
397
|
required: !0,
|
|
393
|
-
children: /* @__PURE__ */
|
|
398
|
+
children: /* @__PURE__ */ F(b, {
|
|
394
399
|
value: a.name,
|
|
395
400
|
onChange: (e) => o((t) => ({
|
|
396
401
|
...t,
|
|
@@ -399,9 +404,9 @@ var L = {
|
|
|
399
404
|
placeholder: "Phase 1 sign-off"
|
|
400
405
|
})
|
|
401
406
|
}),
|
|
402
|
-
/* @__PURE__ */
|
|
407
|
+
/* @__PURE__ */ F(d, {
|
|
403
408
|
label: "Due date",
|
|
404
|
-
children: /* @__PURE__ */
|
|
409
|
+
children: /* @__PURE__ */ F(b, {
|
|
405
410
|
type: "date",
|
|
406
411
|
value: a.dueDate,
|
|
407
412
|
onChange: (e) => o((t) => ({
|
|
@@ -410,10 +415,10 @@ var L = {
|
|
|
410
415
|
}))
|
|
411
416
|
})
|
|
412
417
|
}),
|
|
413
|
-
/* @__PURE__ */
|
|
418
|
+
/* @__PURE__ */ F(d, {
|
|
414
419
|
label: "Trigger amount",
|
|
415
420
|
hint: "Amount billed when this milestone completes (if it triggers an invoice).",
|
|
416
|
-
children: /* @__PURE__ */
|
|
421
|
+
children: /* @__PURE__ */ F(b, {
|
|
417
422
|
type: "number",
|
|
418
423
|
min: "0",
|
|
419
424
|
value: a.triggerAmount,
|
|
@@ -423,9 +428,9 @@ var L = {
|
|
|
423
428
|
}))
|
|
424
429
|
})
|
|
425
430
|
}),
|
|
426
|
-
/* @__PURE__ */
|
|
431
|
+
/* @__PURE__ */ I("label", {
|
|
427
432
|
className: "flex items-center gap-2 text-sm text-text-secondary",
|
|
428
|
-
children: [/* @__PURE__ */
|
|
433
|
+
children: [/* @__PURE__ */ F("input", {
|
|
429
434
|
type: "checkbox",
|
|
430
435
|
checked: a.triggersInvoice,
|
|
431
436
|
onChange: (e) => o((t) => ({
|
|
@@ -439,6 +444,6 @@ var L = {
|
|
|
439
444
|
});
|
|
440
445
|
};
|
|
441
446
|
//#endregion
|
|
442
|
-
export {
|
|
447
|
+
export { B as EngagementDetailPage };
|
|
443
448
|
|
|
444
449
|
//# sourceMappingURL=EngagementDetailPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EngagementDetailPage.js","names":[],"sources":["../../../src/proserviceworld/pages/EngagementDetailPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { ArrowLeft, Plus, CheckCircle2, FileText, Flag, Clock } from 'lucide-react';\nimport {\n useEngagement,\n useTransitionEngagement,\n useCreateDeliverable,\n useTransitionDeliverable,\n useCreateMilestone,\n useCompleteMilestone,\n useTimeEntries,\n type PswEngagementStatus,\n type PswDeliverableStatus,\n} from '../hooks/useProServiceWorldApi';\nimport { useNav } from '../utils/nav';\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 fmtDate,\n fmtMoney,\n fmtMinutesAsHours,\n humanize,\n} from '../components/ui';\n\n// Mirrors the svc ENGAGEMENT_TRANSITIONS state machine so we only offer valid moves.\nconst NEXT: Record<PswEngagementStatus, PswEngagementStatus[]> = {\n PROPOSAL: ['ACCEPTED', 'CANCELLED'],\n ACCEPTED: ['IN_PROGRESS', 'CANCELLED'],\n IN_PROGRESS: ['REVIEW', 'CANCELLED'],\n REVIEW: ['DELIVERED', 'IN_PROGRESS', 'CANCELLED'],\n DELIVERED: ['INVOICED', 'CANCELLED'],\n INVOICED: ['CLOSED'],\n CLOSED: [],\n CANCELLED: [],\n};\nconst DELIVERABLE_STATUSES: PswDeliverableStatus[] = [\n 'DRAFT',\n 'INTERNAL_REVIEW',\n 'CLIENT_REVIEW',\n 'ACCEPTED',\n 'REJECTED',\n];\n\nexport const EngagementDetailPage: FC = () => {\n const { id = '' } = useParams();\n const nav = useNav();\n const { data: e, isLoading, error, refetch } = useEngagement(id);\n const time = useTimeEntries({ engagementId: id });\n const transition = useTransitionEngagement();\n const [addDeliverable, setAddDeliverable] = useState(false);\n const [addMilestone, setAddMilestone] = useState(false);\n const [banner, setBanner] = useState<{ tone: 'success' | 'danger'; text: string } | null>(null);\n\n if (isLoading) return <LoadingBlock />;\n if (error) return <ErrorState error={error} onRetry={() => void refetch()} />;\n if (!e) return <Empty title=\"Engagement not found\" />;\n\n const nextStates = NEXT[e.status] ?? [];\n const loggedMinutes = (time.data?.items ?? []).reduce((sum, t) => sum + t.minutes, 0);\n\n return (\n <div className=\"space-y-6\">\n <button\n type=\"button\"\n onClick={() => nav('engagements')}\n className=\"flex items-center gap-1.5 text-sm text-text-secondary hover:text-accent\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back to engagements\n </button>\n\n <PageHeader\n title={e.name}\n subtitle={\n e.client ? `${e.client.name} · ${humanize(e.feeArrangement)}` : humanize(e.feeArrangement)\n }\n actions={<StatusPill status={e.status} />}\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 lg:grid-cols-4\">\n <MetricCard\n label=\"Fee\"\n value={fmtMoney(e.feeAmount, e.currency)}\n hint={humanize(e.feeArrangement)}\n />\n <MetricCard label=\"Deliverables\" value={e.deliverables.length} />\n <MetricCard label=\"Milestones\" value={e.milestones.length} />\n <MetricCard\n label=\"Time logged\"\n value={fmtMinutesAsHours(loggedMinutes)}\n hint={`${time.data?.totalCount ?? 0} entries`}\n />\n </div>\n\n {nextStates.length > 0 && (\n <SectionCard\n title=\"Advance lifecycle\"\n description={`Current status: ${humanize(e.status)}`}\n >\n <div className=\"flex flex-wrap gap-2\">\n {nextStates.map((s) => (\n <Button\n key={s}\n size=\"sm\"\n variant={s === 'CANCELLED' ? 'outline' : 'default'}\n disabled={transition.isPending}\n onClick={() =>\n transition.mutate(\n { id: e.id, status: s },\n {\n onSuccess: () =>\n setBanner({ tone: 'success', text: `Moved to ${humanize(s)}.` }),\n onError: (err) =>\n setBanner({\n tone: 'danger',\n text: err instanceof Error ? err.message : 'Transition failed.',\n }),\n }\n )\n }\n >\n {humanize(s)}\n </Button>\n ))}\n </div>\n </SectionCard>\n )}\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <SectionCard\n title=\"Deliverables\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => setAddDeliverable(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n Add\n </Button>\n }\n >\n {e.deliverables.length === 0 ? (\n <Empty\n title=\"No deliverables\"\n description=\"Track the tangible outputs of this engagement.\"\n icon={<FileText className=\"h-7 w-7\" />}\n />\n ) : (\n <ul className=\"space-y-2\">\n {e.deliverables.map((d) => (\n <li key={d.id} className=\"rounded-lg border border-border-seam p-3\">\n <div className=\"flex items-start justify-between gap-2\">\n <div>\n <div className=\"text-sm font-medium text-text-primary\">\n {d.name} <span className=\"text-xs text-text-tertiary\">v{d.version}</span>\n </div>\n {d.dueDate && (\n <div className=\"text-xs text-text-tertiary\">Due {fmtDate(d.dueDate)}</div>\n )}\n </div>\n <DeliverableStatusSelect\n id={d.id}\n engagementId={e.id}\n status={d.status}\n onDone={() => void refetch()}\n />\n </div>\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n\n <SectionCard\n title=\"Milestones\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => setAddMilestone(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n Add\n </Button>\n }\n >\n {e.milestones.length === 0 ? (\n <Empty\n title=\"No milestones\"\n description=\"Add checkpoints — some can trigger billing.\"\n icon={<Flag className=\"h-7 w-7\" />}\n />\n ) : (\n <ul className=\"space-y-2\">\n {e.milestones.map((m) => (\n <li\n key={m.id}\n className=\"flex items-center justify-between rounded-lg border border-border-seam p-3\"\n >\n <div>\n <div className=\"text-sm font-medium text-text-primary\">{m.name}</div>\n <div className=\"text-xs text-text-tertiary\">\n {m.triggersInvoice ? `Bills ${fmtMoney(m.triggerAmount, e.currency)} · ` : ''}\n Due {fmtDate(m.dueDate)}\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <StatusPill status={m.status} />\n {m.status !== 'COMPLETED' && m.status !== 'CANCELLED' && (\n <CompleteMilestoneButton\n id={m.id}\n engagementId={e.id}\n onDone={() => void refetch()}\n />\n )}\n </div>\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n </div>\n\n <SectionCard\n title=\"Time logged\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => nav('time')}>\n <Clock className=\"mr-1.5 h-4 w-4\" />\n Log time\n </Button>\n }\n >\n {time.isLoading ? (\n <LoadingBlock label=\"Loading…\" />\n ) : !time.data || time.data.items.length === 0 ? (\n <Empty\n title=\"No time logged\"\n description=\"Time logged against this engagement will appear here.\"\n />\n ) : (\n <ul className=\"divide-y divide-border-seam\">\n {time.data.items.slice(0, 8).map((t) => (\n <li key={t.id} className=\"flex items-center justify-between py-2 text-sm\">\n <span className=\"text-text-secondary\">\n {fmtDate(t.date)} · {t.narrative || 'Time entry'}\n </span>\n <span className=\"flex items-center gap-2\">\n <span className=\"text-text-primary\">{fmtMinutesAsHours(t.minutes)}</span>\n <StatusPill status={t.status} />\n </span>\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n\n {addDeliverable && (\n <DeliverableModal\n engagementId={e.id}\n onClose={() => setAddDeliverable(false)}\n onDone={() => {\n void refetch();\n setBanner({ tone: 'success', text: 'Deliverable added.' });\n }}\n />\n )}\n {addMilestone && (\n <MilestoneModal\n engagementId={e.id}\n onClose={() => setAddMilestone(false)}\n onDone={() => {\n void refetch();\n setBanner({ tone: 'success', text: 'Milestone added.' });\n }}\n />\n )}\n </div>\n );\n};\n\nconst DeliverableStatusSelect: FC<{\n id: string;\n engagementId: string;\n status: PswDeliverableStatus;\n onDone: () => void;\n}> = ({ id, engagementId, status, onDone }) => {\n const transition = useTransitionDeliverable();\n return (\n <NativeSelect\n value={status}\n disabled={transition.isPending}\n onChange={(ev) =>\n transition.mutate(\n { id, status: ev.target.value as PswDeliverableStatus, engagementId },\n { onSuccess: onDone }\n )\n }\n className=\"w-40\"\n >\n {DELIVERABLE_STATUSES.map((s) => (\n <option key={s} value={s}>\n {humanize(s)}\n </option>\n ))}\n </NativeSelect>\n );\n};\n\nconst CompleteMilestoneButton: FC<{ id: string; engagementId: string; onDone: () => void }> = ({\n id,\n engagementId,\n onDone,\n}) => {\n const complete = useCompleteMilestone();\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n aria-label=\"Complete milestone\"\n disabled={complete.isPending}\n onClick={() => complete.mutate({ id, engagementId }, { onSuccess: onDone })}\n >\n <CheckCircle2 className=\"h-4 w-4\" />\n </Button>\n );\n};\n\nconst DeliverableModal: FC<{ engagementId: string; onClose: () => void; onDone: () => void }> = ({\n engagementId,\n onClose,\n onDone,\n}) => {\n const create = useCreateDeliverable();\n const [form, setForm] = useState({\n name: '',\n description: '',\n dueDate: '',\n acceptanceCriteria: '',\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 const submit = () => {\n setErr(null);\n if (!form.name.trim()) return setErr('Name is required.');\n create.mutate(\n {\n engagementId,\n name: form.name.trim(),\n description: form.description.trim() || null,\n acceptanceCriteria: form.acceptanceCriteria.trim() || null,\n dueDate: form.dueDate ? new Date(form.dueDate).toISOString() : null,\n },\n {\n onSuccess: () => {\n onDone();\n onClose();\n },\n onError: (e) => setErr(e instanceof Error ? e.message : 'Failed.'),\n }\n );\n };\n return (\n <Modal\n open\n onClose={onClose}\n title=\"Add deliverable\"\n footer={\n <>\n <Button variant=\"outline\" onClick={onClose} disabled={create.isPending}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={create.isPending}>\n {create.isPending ? 'Adding…' : 'Add'}\n </Button>\n </>\n }\n >\n <div className=\"space-y-3\">\n {err && <Banner tone=\"danger\">{err}</Banner>}\n <Field label=\"Name\" required>\n <TextInput value={form.name} onChange={set('name')} placeholder=\"Draft audit report\" />\n </Field>\n <Field label=\"Due date\">\n <TextInput type=\"date\" value={form.dueDate} onChange={set('dueDate')} />\n </Field>\n <Field label=\"Acceptance criteria\">\n <TextArea value={form.acceptanceCriteria} onChange={set('acceptanceCriteria')} />\n </Field>\n <Field label=\"Description\">\n <TextArea value={form.description} onChange={set('description')} />\n </Field>\n </div>\n </Modal>\n );\n};\n\nconst MilestoneModal: FC<{ engagementId: string; onClose: () => void; onDone: () => void }> = ({\n engagementId,\n onClose,\n onDone,\n}) => {\n const create = useCreateMilestone();\n const [form, setForm] = useState({\n name: '',\n dueDate: '',\n triggerAmount: '',\n triggersInvoice: false,\n });\n const [err, setErr] = useState<string | null>(null);\n const submit = () => {\n setErr(null);\n if (!form.name.trim()) return setErr('Name is required.');\n create.mutate(\n {\n engagementId,\n name: form.name.trim(),\n dueDate: form.dueDate ? new Date(form.dueDate).toISOString() : null,\n triggerAmount: form.triggerAmount ? Number(form.triggerAmount) : 0,\n triggersInvoice: form.triggersInvoice,\n },\n {\n onSuccess: () => {\n onDone();\n onClose();\n },\n onError: (e) => setErr(e instanceof Error ? e.message : 'Failed.'),\n }\n );\n };\n return (\n <Modal\n open\n onClose={onClose}\n title=\"Add milestone\"\n footer={\n <>\n <Button variant=\"outline\" onClick={onClose} disabled={create.isPending}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={create.isPending}>\n {create.isPending ? 'Adding…' : 'Add'}\n </Button>\n </>\n }\n >\n <div className=\"space-y-3\">\n {err && <Banner tone=\"danger\">{err}</Banner>}\n <Field label=\"Name\" required>\n <TextInput\n value={form.name}\n onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}\n placeholder=\"Phase 1 sign-off\"\n />\n </Field>\n <Field label=\"Due date\">\n <TextInput\n type=\"date\"\n value={form.dueDate}\n onChange={(e) => setForm((f) => ({ ...f, dueDate: e.target.value }))}\n />\n </Field>\n <Field\n label=\"Trigger amount\"\n hint=\"Amount billed when this milestone completes (if it triggers an invoice).\"\n >\n <TextInput\n type=\"number\"\n min=\"0\"\n value={form.triggerAmount}\n onChange={(e) => setForm((f) => ({ ...f, triggerAmount: e.target.value }))}\n />\n </Field>\n <label className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <input\n type=\"checkbox\"\n checked={form.triggersInvoice}\n onChange={(e) => setForm((f) => ({ ...f, triggersInvoice: e.target.checked }))}\n />\n Completing this milestone triggers an invoice\n </label>\n </div>\n </Modal>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EngagementDetailPage.js","names":[],"sources":["../../../src/proserviceworld/pages/EngagementDetailPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { ArrowLeft, Plus, CheckCircle2, FileText, Flag, Clock } from 'lucide-react';\nimport {\n useEngagement,\n useTransitionEngagement,\n useCreateDeliverable,\n useTransitionDeliverable,\n useCreateMilestone,\n useCompleteMilestone,\n useTimeEntries,\n type PswEngagementStatus,\n type PswDeliverableStatus,\n} from '../hooks/useProServiceWorldApi';\nimport { useNav } from '../utils/nav';\nimport { DiscussionThread } from '../components/DiscussionThread';\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 fmtDate,\n fmtMoney,\n fmtMinutesAsHours,\n humanize,\n} from '../components/ui';\n\n// Mirrors the svc ENGAGEMENT_TRANSITIONS state machine so we only offer valid moves.\nconst NEXT: Record<PswEngagementStatus, PswEngagementStatus[]> = {\n PROPOSAL: ['ACCEPTED', 'CANCELLED'],\n ACCEPTED: ['IN_PROGRESS', 'CANCELLED'],\n IN_PROGRESS: ['REVIEW', 'CANCELLED'],\n REVIEW: ['DELIVERED', 'IN_PROGRESS', 'CANCELLED'],\n DELIVERED: ['INVOICED', 'CANCELLED'],\n INVOICED: ['CLOSED'],\n CLOSED: [],\n CANCELLED: [],\n};\nconst DELIVERABLE_STATUSES: PswDeliverableStatus[] = [\n 'DRAFT',\n 'INTERNAL_REVIEW',\n 'CLIENT_REVIEW',\n 'ACCEPTED',\n 'REJECTED',\n];\n\nexport const EngagementDetailPage: FC = () => {\n const { id = '' } = useParams();\n const nav = useNav();\n const { data: e, isLoading, error, refetch } = useEngagement(id);\n const time = useTimeEntries({ engagementId: id });\n const transition = useTransitionEngagement();\n const [addDeliverable, setAddDeliverable] = useState(false);\n const [addMilestone, setAddMilestone] = useState(false);\n const [banner, setBanner] = useState<{ tone: 'success' | 'danger'; text: string } | null>(null);\n\n if (isLoading) return <LoadingBlock />;\n if (error) return <ErrorState error={error} onRetry={() => void refetch()} />;\n if (!e) return <Empty title=\"Engagement not found\" />;\n\n const nextStates = NEXT[e.status] ?? [];\n const loggedMinutes = (time.data?.items ?? []).reduce((sum, t) => sum + t.minutes, 0);\n\n return (\n <div className=\"space-y-6\">\n <button\n type=\"button\"\n onClick={() => nav('engagements')}\n className=\"flex items-center gap-1.5 text-sm text-text-secondary hover:text-accent\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back to engagements\n </button>\n\n <PageHeader\n title={e.name}\n subtitle={\n e.client ? `${e.client.name} · ${humanize(e.feeArrangement)}` : humanize(e.feeArrangement)\n }\n actions={<StatusPill status={e.status} />}\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 lg:grid-cols-4\">\n <MetricCard\n label=\"Fee\"\n value={fmtMoney(e.feeAmount, e.currency)}\n hint={humanize(e.feeArrangement)}\n />\n <MetricCard label=\"Deliverables\" value={e.deliverables.length} />\n <MetricCard label=\"Milestones\" value={e.milestones.length} />\n <MetricCard\n label=\"Time logged\"\n value={fmtMinutesAsHours(loggedMinutes)}\n hint={`${time.data?.totalCount ?? 0} entries`}\n />\n </div>\n\n {nextStates.length > 0 && (\n <SectionCard\n title=\"Advance lifecycle\"\n description={`Current status: ${humanize(e.status)}`}\n >\n <div className=\"flex flex-wrap gap-2\">\n {nextStates.map((s) => (\n <Button\n key={s}\n size=\"sm\"\n variant={s === 'CANCELLED' ? 'outline' : 'default'}\n disabled={transition.isPending}\n onClick={() =>\n transition.mutate(\n { id: e.id, status: s },\n {\n onSuccess: () =>\n setBanner({ tone: 'success', text: `Moved to ${humanize(s)}.` }),\n onError: (err) =>\n setBanner({\n tone: 'danger',\n text: err instanceof Error ? err.message : 'Transition failed.',\n }),\n }\n )\n }\n >\n {humanize(s)}\n </Button>\n ))}\n </div>\n </SectionCard>\n )}\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <SectionCard\n title=\"Deliverables\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => setAddDeliverable(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n Add\n </Button>\n }\n >\n {e.deliverables.length === 0 ? (\n <Empty\n title=\"No deliverables\"\n description=\"Track the tangible outputs of this engagement.\"\n icon={<FileText className=\"h-7 w-7\" />}\n />\n ) : (\n <ul className=\"space-y-2\">\n {e.deliverables.map((d) => (\n <li key={d.id} className=\"rounded-lg border border-border-seam p-3\">\n <div className=\"flex items-start justify-between gap-2\">\n <div>\n <div className=\"text-sm font-medium text-text-primary\">\n {d.name} <span className=\"text-xs text-text-tertiary\">v{d.version}</span>\n </div>\n {d.dueDate && (\n <div className=\"text-xs text-text-tertiary\">Due {fmtDate(d.dueDate)}</div>\n )}\n </div>\n <DeliverableStatusSelect\n id={d.id}\n engagementId={e.id}\n status={d.status}\n onDone={() => void refetch()}\n />\n </div>\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n\n <SectionCard\n title=\"Milestones\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => setAddMilestone(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n Add\n </Button>\n }\n >\n {e.milestones.length === 0 ? (\n <Empty\n title=\"No milestones\"\n description=\"Add checkpoints — some can trigger billing.\"\n icon={<Flag className=\"h-7 w-7\" />}\n />\n ) : (\n <ul className=\"space-y-2\">\n {e.milestones.map((m) => (\n <li\n key={m.id}\n className=\"flex items-center justify-between rounded-lg border border-border-seam p-3\"\n >\n <div>\n <div className=\"text-sm font-medium text-text-primary\">{m.name}</div>\n <div className=\"text-xs text-text-tertiary\">\n {m.triggersInvoice ? `Bills ${fmtMoney(m.triggerAmount, e.currency)} · ` : ''}\n Due {fmtDate(m.dueDate)}\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <StatusPill status={m.status} />\n {m.status !== 'COMPLETED' && m.status !== 'CANCELLED' && (\n <CompleteMilestoneButton\n id={m.id}\n engagementId={e.id}\n onDone={() => void refetch()}\n />\n )}\n </div>\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n </div>\n\n <SectionCard\n title=\"Time logged\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => nav('time')}>\n <Clock className=\"mr-1.5 h-4 w-4\" />\n Log time\n </Button>\n }\n >\n {time.isLoading ? (\n <LoadingBlock label=\"Loading…\" />\n ) : !time.data || time.data.items.length === 0 ? (\n <Empty\n title=\"No time logged\"\n description=\"Time logged against this engagement will appear here.\"\n />\n ) : (\n <ul className=\"divide-y divide-border-seam\">\n {time.data.items.slice(0, 8).map((t) => (\n <li key={t.id} className=\"flex items-center justify-between py-2 text-sm\">\n <span className=\"text-text-secondary\">\n {fmtDate(t.date)} · {t.narrative || 'Time entry'}\n </span>\n <span className=\"flex items-center gap-2\">\n <span className=\"text-text-primary\">{fmtMinutesAsHours(t.minutes)}</span>\n <StatusPill status={t.status} />\n </span>\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n\n <DiscussionThread entityType=\"proserviceworld-engagement\" entityId={e.id} />\n\n {addDeliverable && (\n <DeliverableModal\n engagementId={e.id}\n onClose={() => setAddDeliverable(false)}\n onDone={() => {\n void refetch();\n setBanner({ tone: 'success', text: 'Deliverable added.' });\n }}\n />\n )}\n {addMilestone && (\n <MilestoneModal\n engagementId={e.id}\n onClose={() => setAddMilestone(false)}\n onDone={() => {\n void refetch();\n setBanner({ tone: 'success', text: 'Milestone added.' });\n }}\n />\n )}\n </div>\n );\n};\n\nconst DeliverableStatusSelect: FC<{\n id: string;\n engagementId: string;\n status: PswDeliverableStatus;\n onDone: () => void;\n}> = ({ id, engagementId, status, onDone }) => {\n const transition = useTransitionDeliverable();\n return (\n <NativeSelect\n value={status}\n disabled={transition.isPending}\n onChange={(ev) =>\n transition.mutate(\n { id, status: ev.target.value as PswDeliverableStatus, engagementId },\n { onSuccess: onDone }\n )\n }\n className=\"w-40\"\n >\n {DELIVERABLE_STATUSES.map((s) => (\n <option key={s} value={s}>\n {humanize(s)}\n </option>\n ))}\n </NativeSelect>\n );\n};\n\nconst CompleteMilestoneButton: FC<{ id: string; engagementId: string; onDone: () => void }> = ({\n id,\n engagementId,\n onDone,\n}) => {\n const complete = useCompleteMilestone();\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n aria-label=\"Complete milestone\"\n disabled={complete.isPending}\n onClick={() => complete.mutate({ id, engagementId }, { onSuccess: onDone })}\n >\n <CheckCircle2 className=\"h-4 w-4\" />\n </Button>\n );\n};\n\nconst DeliverableModal: FC<{ engagementId: string; onClose: () => void; onDone: () => void }> = ({\n engagementId,\n onClose,\n onDone,\n}) => {\n const create = useCreateDeliverable();\n const [form, setForm] = useState({\n name: '',\n description: '',\n dueDate: '',\n acceptanceCriteria: '',\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 const submit = () => {\n setErr(null);\n if (!form.name.trim()) return setErr('Name is required.');\n create.mutate(\n {\n engagementId,\n name: form.name.trim(),\n description: form.description.trim() || null,\n acceptanceCriteria: form.acceptanceCriteria.trim() || null,\n dueDate: form.dueDate ? new Date(form.dueDate).toISOString() : null,\n },\n {\n onSuccess: () => {\n onDone();\n onClose();\n },\n onError: (e) => setErr(e instanceof Error ? e.message : 'Failed.'),\n }\n );\n };\n return (\n <Modal\n open\n onClose={onClose}\n title=\"Add deliverable\"\n footer={\n <>\n <Button variant=\"outline\" onClick={onClose} disabled={create.isPending}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={create.isPending}>\n {create.isPending ? 'Adding…' : 'Add'}\n </Button>\n </>\n }\n >\n <div className=\"space-y-3\">\n {err && <Banner tone=\"danger\">{err}</Banner>}\n <Field label=\"Name\" required>\n <TextInput value={form.name} onChange={set('name')} placeholder=\"Draft audit report\" />\n </Field>\n <Field label=\"Due date\">\n <TextInput type=\"date\" value={form.dueDate} onChange={set('dueDate')} />\n </Field>\n <Field label=\"Acceptance criteria\">\n <TextArea value={form.acceptanceCriteria} onChange={set('acceptanceCriteria')} />\n </Field>\n <Field label=\"Description\">\n <TextArea value={form.description} onChange={set('description')} />\n </Field>\n </div>\n </Modal>\n );\n};\n\nconst MilestoneModal: FC<{ engagementId: string; onClose: () => void; onDone: () => void }> = ({\n engagementId,\n onClose,\n onDone,\n}) => {\n const create = useCreateMilestone();\n const [form, setForm] = useState({\n name: '',\n dueDate: '',\n triggerAmount: '',\n triggersInvoice: false,\n });\n const [err, setErr] = useState<string | null>(null);\n const submit = () => {\n setErr(null);\n if (!form.name.trim()) return setErr('Name is required.');\n create.mutate(\n {\n engagementId,\n name: form.name.trim(),\n dueDate: form.dueDate ? new Date(form.dueDate).toISOString() : null,\n triggerAmount: form.triggerAmount ? Number(form.triggerAmount) : 0,\n triggersInvoice: form.triggersInvoice,\n },\n {\n onSuccess: () => {\n onDone();\n onClose();\n },\n onError: (e) => setErr(e instanceof Error ? e.message : 'Failed.'),\n }\n );\n };\n return (\n <Modal\n open\n onClose={onClose}\n title=\"Add milestone\"\n footer={\n <>\n <Button variant=\"outline\" onClick={onClose} disabled={create.isPending}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={create.isPending}>\n {create.isPending ? 'Adding…' : 'Add'}\n </Button>\n </>\n }\n >\n <div className=\"space-y-3\">\n {err && <Banner tone=\"danger\">{err}</Banner>}\n <Field label=\"Name\" required>\n <TextInput\n value={form.name}\n onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}\n placeholder=\"Phase 1 sign-off\"\n />\n </Field>\n <Field label=\"Due date\">\n <TextInput\n type=\"date\"\n value={form.dueDate}\n onChange={(e) => setForm((f) => ({ ...f, dueDate: e.target.value }))}\n />\n </Field>\n <Field\n label=\"Trigger amount\"\n hint=\"Amount billed when this milestone completes (if it triggers an invoice).\"\n >\n <TextInput\n type=\"number\"\n min=\"0\"\n value={form.triggerAmount}\n onChange={(e) => setForm((f) => ({ ...f, triggerAmount: e.target.value }))}\n />\n </Field>\n <label className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <input\n type=\"checkbox\"\n checked={form.triggersInvoice}\n onChange={(e) => setForm((f) => ({ ...f, triggersInvoice: e.target.checked }))}\n />\n Completing this milestone triggers an invoice\n </label>\n </div>\n </Modal>\n );\n};\n"],"mappings":"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"}
|