@burdenoff/microfe-brainyrich 2026.705.1 → 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.
- package/dist/brainyrich/BrainyRichRoot.js +13 -13
- package/dist/brainyrich/components/ActorName.js +38 -0
- package/dist/brainyrich/components/ActorName.js.map +1 -0
- package/dist/brainyrich/components/ConfirmDialog.js +25 -0
- package/dist/brainyrich/components/ConfirmDialog.js.map +1 -0
- package/dist/brainyrich/components/LoadMore.js +24 -0
- package/dist/brainyrich/components/LoadMore.js.map +1 -0
- package/dist/brainyrich/components/index.js +5 -0
- package/dist/brainyrich/components/notify.js +38 -0
- package/dist/brainyrich/components/notify.js.map +1 -0
- package/dist/brainyrich/components/pickers.js +232 -0
- package/dist/brainyrich/components/pickers.js.map +1 -0
- package/dist/brainyrich/components/reuse/CommentThread.js +208 -0
- package/dist/brainyrich/components/reuse/CommentThread.js.map +1 -0
- package/dist/brainyrich/components/reuse/DocumentsPanel.js +237 -0
- package/dist/brainyrich/components/reuse/DocumentsPanel.js.map +1 -0
- package/dist/brainyrich/components/reuse/TagField.js +206 -0
- package/dist/brainyrich/components/reuse/TagField.js.map +1 -0
- package/dist/brainyrich/components/reuse/transport.js +24 -0
- package/dist/brainyrich/components/reuse/transport.js.map +1 -0
- package/dist/brainyrich/hooks/useBrainyRichApi.js +644 -372
- package/dist/brainyrich/hooks/useBrainyRichApi.js.map +1 -1
- package/dist/brainyrich/pages/AccountsPage.js +346 -293
- package/dist/brainyrich/pages/AccountsPage.js.map +1 -1
- package/dist/brainyrich/pages/AiAssistantPage.js +339 -320
- package/dist/brainyrich/pages/AiAssistantPage.js.map +1 -1
- package/dist/brainyrich/pages/AnalyticsPage.js +178 -164
- package/dist/brainyrich/pages/AnalyticsPage.js.map +1 -1
- package/dist/brainyrich/pages/DashboardPage.js +195 -169
- package/dist/brainyrich/pages/DashboardPage.js.map +1 -1
- package/dist/brainyrich/pages/FamilyPage.js +247 -198
- package/dist/brainyrich/pages/FamilyPage.js.map +1 -1
- package/dist/brainyrich/pages/GoalsPage.js +478 -442
- package/dist/brainyrich/pages/GoalsPage.js.map +1 -1
- package/dist/brainyrich/pages/InvestmentsPage.js.map +1 -1
- package/dist/brainyrich/pages/PortfoliosPage.js +438 -396
- package/dist/brainyrich/pages/PortfoliosPage.js.map +1 -1
- package/dist/brainyrich/pages/ReportsPage.js +246 -213
- package/dist/brainyrich/pages/ReportsPage.js.map +1 -1
- package/dist/brainyrich/pages/RiskPage.js +321 -173
- package/dist/brainyrich/pages/RiskPage.js.map +1 -1
- package/dist/brainyrich/pages/TaxPage.js +258 -225
- package/dist/brainyrich/pages/TaxPage.js.map +1 -1
- package/dist/brainyrich/pages/_shared.js.map +1 -1
- package/dist/node_modules/sonner/dist/index.js +163 -0
- package/dist/node_modules/sonner/dist/index.js.map +1 -0
- package/package.json +3 -2
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { useBrainyRich as e } from "../../providers/BrainyRichProvider.js";
|
|
2
|
+
import { formatDate as t } from "../../pages/_shared.js";
|
|
3
|
+
import { wspaceFetch as n, wspaceMutate as r } from "./transport.js";
|
|
4
|
+
import { useState as i } from "react";
|
|
5
|
+
import { MessagesSquare as a, Send as o, Trash2 as s } from "lucide-react";
|
|
6
|
+
import { useMutation as c, useQuery as l, useQueryClient as u } from "@tanstack/react-query";
|
|
7
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
8
|
+
import { Button as p, Skeleton as m, Textarea as h } from "@burdenoff/fe-libs/ui";
|
|
9
|
+
//#region src/brainyrich/components/reuse/CommentThread.tsx
|
|
10
|
+
var g = "\n id authorId authorType content { text markdown } createdAt edited status\n", _ = (e, t) => [
|
|
11
|
+
"brainyrich",
|
|
12
|
+
"entity-comments",
|
|
13
|
+
e,
|
|
14
|
+
t
|
|
15
|
+
], v = (t, r) => {
|
|
16
|
+
let { apiGatewayUrl: i } = e();
|
|
17
|
+
return l({
|
|
18
|
+
queryKey: _(t, r ?? ""),
|
|
19
|
+
enabled: !!r,
|
|
20
|
+
queryFn: async () => (await n(`query BraGetEntityComments($input: GetCommentsInput!) {
|
|
21
|
+
getEntityComments(input: $input) {
|
|
22
|
+
items { ${g} }
|
|
23
|
+
total
|
|
24
|
+
}
|
|
25
|
+
}`, { input: {
|
|
26
|
+
entityType: t,
|
|
27
|
+
entityId: r,
|
|
28
|
+
take: 50,
|
|
29
|
+
sortBy: "createdAt",
|
|
30
|
+
sortOrder: "desc"
|
|
31
|
+
} }, i)).getEntityComments
|
|
32
|
+
});
|
|
33
|
+
}, y = (t, n) => {
|
|
34
|
+
let { apiGatewayUrl: i } = e(), a = u();
|
|
35
|
+
return c({
|
|
36
|
+
mutationFn: async (e) => {
|
|
37
|
+
let a = e.trim();
|
|
38
|
+
if (!a) throw Error("A comment is required.");
|
|
39
|
+
return (await r("mutation BraCreateEntityComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) { id }\n }", { input: {
|
|
40
|
+
entityType: t,
|
|
41
|
+
entityId: n,
|
|
42
|
+
content: { text: a }
|
|
43
|
+
} }, i)).createEntityComment;
|
|
44
|
+
},
|
|
45
|
+
onSuccess: () => a.invalidateQueries({ queryKey: _(t, n) })
|
|
46
|
+
});
|
|
47
|
+
}, b = (t, n) => {
|
|
48
|
+
let { apiGatewayUrl: i } = e(), a = u();
|
|
49
|
+
return c({
|
|
50
|
+
mutationFn: async (e) => (await r("mutation BraDeleteEntityComment($input: DeleteCommentInput!) {\n deleteEntityComment(input: $input)\n }", { input: {
|
|
51
|
+
id: e,
|
|
52
|
+
soft: !0
|
|
53
|
+
} }, i)).deleteEntityComment,
|
|
54
|
+
onSuccess: () => a.invalidateQueries({ queryKey: _(t, n) })
|
|
55
|
+
});
|
|
56
|
+
}, x = (e, t) => t ? "You" : e.slice(0, 2).toUpperCase();
|
|
57
|
+
function S({ entityType: n, entityId: r, heading: c = "Discussion", hideHeading: l = !1 }) {
|
|
58
|
+
let { currentUser: u } = e(), [g, _] = i(""), S = v(n, r), C = y(n, r), w = b(n, r), T = (S.data?.items ?? []).filter((e) => e.status !== "DELETED"), E = u?.name || [u?.firstName, u?.lastName].filter(Boolean).join(" ") || u?.email || "You", D = async () => {
|
|
59
|
+
!g.trim() || C.isPending || (await C.mutateAsync(g), _(""));
|
|
60
|
+
};
|
|
61
|
+
return /* @__PURE__ */ f("div", {
|
|
62
|
+
className: "space-y-3",
|
|
63
|
+
children: [
|
|
64
|
+
!l && /* @__PURE__ */ f("h3", {
|
|
65
|
+
className: "flex items-center gap-2 text-sm font-semibold text-text-primary",
|
|
66
|
+
children: [
|
|
67
|
+
/* @__PURE__ */ d(a, {
|
|
68
|
+
className: "size-4",
|
|
69
|
+
"aria-hidden": "true"
|
|
70
|
+
}),
|
|
71
|
+
c,
|
|
72
|
+
S.data ? /* @__PURE__ */ f("span", {
|
|
73
|
+
className: "text-xs font-normal text-text-secondary",
|
|
74
|
+
children: [
|
|
75
|
+
"(",
|
|
76
|
+
T.length,
|
|
77
|
+
")"
|
|
78
|
+
]
|
|
79
|
+
}) : null
|
|
80
|
+
]
|
|
81
|
+
}),
|
|
82
|
+
/* @__PURE__ */ f("div", {
|
|
83
|
+
className: "space-y-2",
|
|
84
|
+
children: [
|
|
85
|
+
/* @__PURE__ */ d("label", {
|
|
86
|
+
htmlFor: `comment-${r}`,
|
|
87
|
+
className: "sr-only",
|
|
88
|
+
children: "Add a comment"
|
|
89
|
+
}),
|
|
90
|
+
/* @__PURE__ */ d(h, {
|
|
91
|
+
id: `comment-${r}`,
|
|
92
|
+
value: g,
|
|
93
|
+
onChange: (e) => _(e.target.value),
|
|
94
|
+
onKeyDown: (e) => {
|
|
95
|
+
(e.metaKey || e.ctrlKey) && e.key === "Enter" && (e.preventDefault(), D());
|
|
96
|
+
},
|
|
97
|
+
placeholder: "Add a note or rationale…",
|
|
98
|
+
rows: 3,
|
|
99
|
+
disabled: C.isPending
|
|
100
|
+
}),
|
|
101
|
+
/* @__PURE__ */ f("div", {
|
|
102
|
+
className: "flex items-center justify-between",
|
|
103
|
+
children: [/* @__PURE__ */ d("span", {
|
|
104
|
+
className: "text-xs text-text-secondary",
|
|
105
|
+
children: "⌘/Ctrl + Enter to post"
|
|
106
|
+
}), /* @__PURE__ */ f(p, {
|
|
107
|
+
type: "button",
|
|
108
|
+
size: "sm",
|
|
109
|
+
onClick: D,
|
|
110
|
+
disabled: !g.trim() || C.isPending,
|
|
111
|
+
children: [/* @__PURE__ */ d(o, {
|
|
112
|
+
className: "size-4",
|
|
113
|
+
"aria-hidden": "true"
|
|
114
|
+
}), C.isPending ? "Posting…" : "Post"]
|
|
115
|
+
})]
|
|
116
|
+
}),
|
|
117
|
+
C.isError && /* @__PURE__ */ d("p", {
|
|
118
|
+
className: "text-sm text-status-error-text",
|
|
119
|
+
children: "Could not post your comment. Please try again."
|
|
120
|
+
})
|
|
121
|
+
]
|
|
122
|
+
}),
|
|
123
|
+
S.isLoading ? /* @__PURE__ */ f("div", {
|
|
124
|
+
className: "space-y-3",
|
|
125
|
+
"aria-busy": "true",
|
|
126
|
+
children: [/* @__PURE__ */ d(m, { className: "h-16 w-full rounded-md" }), /* @__PURE__ */ d(m, { className: "h-16 w-full rounded-md" })]
|
|
127
|
+
}) : S.isError ? /* @__PURE__ */ f("p", {
|
|
128
|
+
className: "text-sm text-status-error-text",
|
|
129
|
+
children: [
|
|
130
|
+
"Could not load the discussion.",
|
|
131
|
+
" ",
|
|
132
|
+
/* @__PURE__ */ d("button", {
|
|
133
|
+
type: "button",
|
|
134
|
+
className: "underline underline-offset-2",
|
|
135
|
+
onClick: () => S.refetch(),
|
|
136
|
+
children: "Retry"
|
|
137
|
+
})
|
|
138
|
+
]
|
|
139
|
+
}) : T.length === 0 ? /* @__PURE__ */ f("div", {
|
|
140
|
+
className: "rounded-lg border border-dashed border-border-subtle bg-bg-surface px-4 py-8 text-center",
|
|
141
|
+
children: [
|
|
142
|
+
/* @__PURE__ */ d(a, {
|
|
143
|
+
className: "mx-auto mb-2 size-6 text-text-secondary",
|
|
144
|
+
"aria-hidden": "true"
|
|
145
|
+
}),
|
|
146
|
+
/* @__PURE__ */ d("p", {
|
|
147
|
+
className: "text-sm text-text-primary",
|
|
148
|
+
children: "No comments yet"
|
|
149
|
+
}),
|
|
150
|
+
/* @__PURE__ */ d("p", {
|
|
151
|
+
className: "mt-0.5 text-xs text-text-secondary",
|
|
152
|
+
children: "Start the discussion with a note or rationale."
|
|
153
|
+
})
|
|
154
|
+
]
|
|
155
|
+
}) : /* @__PURE__ */ d("ul", {
|
|
156
|
+
className: "space-y-3",
|
|
157
|
+
children: T.map((e) => {
|
|
158
|
+
let n = !!u?.id && e.authorId === u?.id, r = e.content.text ?? e.content.markdown ?? "";
|
|
159
|
+
return /* @__PURE__ */ f("li", {
|
|
160
|
+
className: "flex gap-3",
|
|
161
|
+
children: [/* @__PURE__ */ d("div", {
|
|
162
|
+
className: "flex size-8 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-xs font-medium text-text-secondary",
|
|
163
|
+
"aria-hidden": "true",
|
|
164
|
+
children: x(e.authorId, n)
|
|
165
|
+
}), /* @__PURE__ */ f("div", {
|
|
166
|
+
className: "min-w-0 flex-1 rounded-lg border border-border-subtle bg-bg-surface p-3",
|
|
167
|
+
children: [/* @__PURE__ */ f("div", {
|
|
168
|
+
className: "mb-1 flex items-center gap-2",
|
|
169
|
+
children: [
|
|
170
|
+
/* @__PURE__ */ d("span", {
|
|
171
|
+
className: "text-sm font-medium text-text-primary",
|
|
172
|
+
children: n ? E : "Team member"
|
|
173
|
+
}),
|
|
174
|
+
/* @__PURE__ */ d("span", {
|
|
175
|
+
className: "text-xs text-text-secondary",
|
|
176
|
+
children: t(e.createdAt)
|
|
177
|
+
}),
|
|
178
|
+
e.edited ? /* @__PURE__ */ d("span", {
|
|
179
|
+
className: "text-xs text-text-secondary",
|
|
180
|
+
children: "(edited)"
|
|
181
|
+
}) : null,
|
|
182
|
+
n && /* @__PURE__ */ d("button", {
|
|
183
|
+
type: "button",
|
|
184
|
+
"aria-label": "Delete comment",
|
|
185
|
+
className: "ml-auto rounded p-1 text-text-secondary hover:bg-bg-sunken hover:text-status-error-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-border-focus disabled:opacity-50",
|
|
186
|
+
disabled: w.isPending,
|
|
187
|
+
onClick: () => w.mutate(e.id),
|
|
188
|
+
children: /* @__PURE__ */ d(s, {
|
|
189
|
+
className: "size-3.5",
|
|
190
|
+
"aria-hidden": "true"
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
]
|
|
194
|
+
}), /* @__PURE__ */ d("p", {
|
|
195
|
+
className: "whitespace-pre-wrap break-words text-sm text-text-primary",
|
|
196
|
+
children: r
|
|
197
|
+
})]
|
|
198
|
+
})]
|
|
199
|
+
}, e.id);
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
]
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
206
|
+
export { S as CommentThread };
|
|
207
|
+
|
|
208
|
+
//# sourceMappingURL=CommentThread.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CommentThread.js","names":[],"sources":["../../../../src/brainyrich/components/reuse/CommentThread.tsx"],"sourcesContent":["/**\n * CommentThread — REUSE of `wspace-conversations-svc` EntityComment as a real\n * comment thread.\n * @module brainyrich/components/reuse/CommentThread\n *\n * Replaces the single-field notes / rationale fork on Households / Portfolios /\n * RebalanceProposals with a threaded, multi-author comment log keyed by\n * `(entityType, entityId)`.\n *\n * Why not the fe-libs `contextConversationWidget`? That folder ships a kit of\n * sub-components + Apollo hooks (subscriptions, `sendContextMessage`, a\n * conversationId-resolution flow) but exports NO mountable root widget and no\n * single mount hook — it is not wrappable as `<CommentThread>` without importing\n * its internals. Per the task's fallback, we build a minimal thread directly on\n * the platform EntityComment API, which is purpose-built for exactly this\n * \"comments on an arbitrary entity\" case:\n * - `getEntityComments(input)` → list (newest first)\n * - `createEntityComment(input)` → post\n * - `deleteEntityComment(input)` → remove own comment (soft)\n *\n * All ops route through the workspace supergraph the MFE already uses.\n * Presentational + self-contained: owns its hooks, reads gateway base URL +\n * current user from `useBrainyRich()`, uses fe-libs/ui + semantic tokens.\n */\n\nimport { useState, type KeyboardEvent } from 'react';\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { MessagesSquare, Send, Trash2 } from 'lucide-react';\nimport { Button, Skeleton, Textarea } from '@burdenoff/fe-libs/ui';\n\nimport { useBrainyRich } from '../../providers/BrainyRichProvider';\nimport { formatDate } from '../../pages/_shared';\nimport { wspaceFetch, wspaceMutate } from './transport';\n\ninterface EntityComment {\n id: string;\n authorId: string;\n authorType: string;\n content: { text: string | null; markdown: string | null };\n createdAt: string;\n edited: boolean;\n status: string;\n}\n\nconst COMMENT_FIELDS = `\n id authorId authorType content { text markdown } createdAt edited status\n`;\n\nconst commentsKey = (entityType: string, entityId: string) =>\n ['brainyrich', 'entity-comments', entityType, entityId] as const;\n\nconst useEntityComments = (entityType: string, entityId: string | undefined) => {\n const { apiGatewayUrl } = useBrainyRich();\n return useQuery({\n queryKey: commentsKey(entityType, entityId ?? ''),\n enabled: Boolean(entityId),\n queryFn: async () => {\n const data = await wspaceFetch<{ getEntityComments: { items: EntityComment[]; total: number } }>(\n `query BraGetEntityComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) {\n items { ${COMMENT_FIELDS} }\n total\n }\n }`,\n {\n input: { entityType, entityId, take: 50, sortBy: 'createdAt', sortOrder: 'desc' },\n },\n apiGatewayUrl\n );\n return data.getEntityComments;\n },\n });\n};\n\nconst useCreateComment = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useBrainyRich();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (text: string) => {\n const trimmed = text.trim();\n if (!trimmed) throw new Error('A comment is required.');\n const data = await wspaceMutate<{ createEntityComment: { id: string } }>(\n `mutation BraCreateEntityComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) { id }\n }`,\n { input: { entityType, entityId, content: { text: trimmed } } },\n apiGatewayUrl\n );\n return data.createEntityComment;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: commentsKey(entityType, entityId) }),\n });\n};\n\nconst useDeleteComment = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useBrainyRich();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const data = await wspaceMutate<{ deleteEntityComment: boolean }>(\n `mutation BraDeleteEntityComment($input: DeleteCommentInput!) {\n deleteEntityComment(input: $input)\n }`,\n { input: { id, soft: true } },\n apiGatewayUrl\n );\n return data.deleteEntityComment;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: commentsKey(entityType, entityId) }),\n });\n};\n\nconst authorInitials = (authorId: string, isSelf: boolean): string => {\n if (isSelf) return 'You';\n return authorId.slice(0, 2).toUpperCase();\n};\n\nexport interface CommentThreadProps {\n /** Polymorphic entity type, e.g. \"brainyrich-household\". */\n entityType: string;\n /** The bra entity's id (an ID scalar). */\n entityId: string;\n /** Optional heading; hide with `hideHeading`. */\n heading?: string;\n hideHeading?: boolean;\n}\n\nexport function CommentThread({\n entityType,\n entityId,\n heading = 'Discussion',\n hideHeading = false,\n}: CommentThreadProps) {\n const { currentUser } = useBrainyRich();\n const [draft, setDraft] = useState('');\n const comments = useEntityComments(entityType, entityId);\n const create = useCreateComment(entityType, entityId);\n const remove = useDeleteComment(entityType, entityId);\n\n const items = (comments.data?.items ?? []).filter((c) => c.status !== 'DELETED');\n const selfName =\n currentUser?.name ||\n [currentUser?.firstName, currentUser?.lastName].filter(Boolean).join(' ') ||\n currentUser?.email ||\n 'You';\n\n const submit = async () => {\n if (!draft.trim() || create.isPending) return;\n await create.mutateAsync(draft);\n setDraft('');\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault();\n void submit();\n }\n };\n\n return (\n <div className=\"space-y-3\">\n {!hideHeading && (\n <h3 className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <MessagesSquare className=\"size-4\" aria-hidden=\"true\" />\n {heading}\n {comments.data ? (\n <span className=\"text-xs font-normal text-text-secondary\">({items.length})</span>\n ) : null}\n </h3>\n )}\n\n {/* Composer */}\n <div className=\"space-y-2\">\n <label htmlFor={`comment-${entityId}`} className=\"sr-only\">\n Add a comment\n </label>\n <Textarea\n id={`comment-${entityId}`}\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n onKeyDown={onKeyDown}\n placeholder=\"Add a note or rationale…\"\n rows={3}\n disabled={create.isPending}\n />\n <div className=\"flex items-center justify-between\">\n <span className=\"text-xs text-text-secondary\">⌘/Ctrl + Enter to post</span>\n <Button type=\"button\" size=\"sm\" onClick={submit} disabled={!draft.trim() || create.isPending}>\n <Send className=\"size-4\" aria-hidden=\"true\" />\n {create.isPending ? 'Posting…' : 'Post'}\n </Button>\n </div>\n {create.isError && (\n <p className=\"text-sm text-status-error-text\">Could not post your comment. Please try again.</p>\n )}\n </div>\n\n {/* Thread */}\n {comments.isLoading ? (\n <div className=\"space-y-3\" aria-busy=\"true\">\n <Skeleton className=\"h-16 w-full rounded-md\" />\n <Skeleton className=\"h-16 w-full rounded-md\" />\n </div>\n ) : comments.isError ? (\n <p className=\"text-sm text-status-error-text\">\n Could not load the discussion.{' '}\n <button\n type=\"button\"\n className=\"underline underline-offset-2\"\n onClick={() => comments.refetch()}\n >\n Retry\n </button>\n </p>\n ) : items.length === 0 ? (\n <div className=\"rounded-lg border border-dashed border-border-subtle bg-bg-surface px-4 py-8 text-center\">\n <MessagesSquare className=\"mx-auto mb-2 size-6 text-text-secondary\" aria-hidden=\"true\" />\n <p className=\"text-sm text-text-primary\">No comments yet</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Start the discussion with a note or rationale.\n </p>\n </div>\n ) : (\n <ul className=\"space-y-3\">\n {items.map((c) => {\n const isSelf = Boolean(currentUser?.id) && c.authorId === currentUser?.id;\n const body = c.content.text ?? c.content.markdown ?? '';\n return (\n <li key={c.id} className=\"flex gap-3\">\n <div\n className=\"flex size-8 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-xs font-medium text-text-secondary\"\n aria-hidden=\"true\"\n >\n {authorInitials(c.authorId, isSelf)}\n </div>\n <div className=\"min-w-0 flex-1 rounded-lg border border-border-subtle bg-bg-surface p-3\">\n <div className=\"mb-1 flex items-center gap-2\">\n <span className=\"text-sm font-medium text-text-primary\">\n {isSelf ? selfName : 'Team member'}\n </span>\n <span className=\"text-xs text-text-secondary\">{formatDate(c.createdAt)}</span>\n {c.edited ? <span className=\"text-xs text-text-secondary\">(edited)</span> : null}\n {isSelf && (\n <button\n type=\"button\"\n aria-label=\"Delete comment\"\n className=\"ml-auto rounded p-1 text-text-secondary hover:bg-bg-sunken hover:text-status-error-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-border-focus disabled:opacity-50\"\n disabled={remove.isPending}\n onClick={() => remove.mutate(c.id)}\n >\n <Trash2 className=\"size-3.5\" aria-hidden=\"true\" />\n </button>\n )}\n </div>\n <p className=\"whitespace-pre-wrap break-words text-sm text-text-primary\">{body}</p>\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { useBrainyRich as e } from "../../providers/BrainyRichProvider.js";
|
|
2
|
+
import { formatDate as t } from "../../pages/_shared.js";
|
|
3
|
+
import { wspaceFetch as n, wspaceMutate as r } from "./transport.js";
|
|
4
|
+
import { useCallback as i, useRef as a, useState as o } from "react";
|
|
5
|
+
import { Download as s, FileText as c, Upload as l } from "lucide-react";
|
|
6
|
+
import { useMutation as u, useQuery as d, useQueryClient as f } from "@tanstack/react-query";
|
|
7
|
+
import { jsx as p, jsxs as m } from "react/jsx-runtime";
|
|
8
|
+
import { Button as h, Card as g, CardContent as _, Skeleton as v } from "@burdenoff/fe-libs/ui";
|
|
9
|
+
//#region src/brainyrich/components/reuse/DocumentsPanel.tsx
|
|
10
|
+
var y = 21600, b = "id name extension mimeType size createdAt", x = (e, t) => `BrainyRich · ${e} · ${t}`, S = (e, t) => [
|
|
11
|
+
"brainyrich",
|
|
12
|
+
"entity-folder",
|
|
13
|
+
e,
|
|
14
|
+
t
|
|
15
|
+
], C = (e) => [
|
|
16
|
+
"brainyrich",
|
|
17
|
+
"folder-files",
|
|
18
|
+
e ?? "none"
|
|
19
|
+
], w = (t, r) => {
|
|
20
|
+
let { apiGatewayUrl: i } = e(), a = r ? x(t, r) : "";
|
|
21
|
+
return d({
|
|
22
|
+
queryKey: S(t, r ?? ""),
|
|
23
|
+
enabled: !!r,
|
|
24
|
+
queryFn: async () => (await n("query BraGetFolders($pagination: PaginationInput) {\n getFolders(pagination: $pagination) { folders { id name } }\n }", { pagination: {
|
|
25
|
+
limit: 200,
|
|
26
|
+
offset: 0
|
|
27
|
+
} }, i)).getFolders.folders.find((e) => e.name === a) ?? null
|
|
28
|
+
});
|
|
29
|
+
}, T = (t) => {
|
|
30
|
+
let { apiGatewayUrl: r } = e();
|
|
31
|
+
return d({
|
|
32
|
+
queryKey: C(t),
|
|
33
|
+
enabled: !!t,
|
|
34
|
+
queryFn: async () => (await n(`query BraGetFilesInFolder($folderId: ID, $pagination: PaginationInput) {
|
|
35
|
+
getFilesInFolder(folderId: $folderId, pagination: $pagination) {
|
|
36
|
+
files { ${b} }
|
|
37
|
+
}
|
|
38
|
+
}`, {
|
|
39
|
+
folderId: t,
|
|
40
|
+
pagination: {
|
|
41
|
+
limit: 100,
|
|
42
|
+
offset: 0
|
|
43
|
+
}
|
|
44
|
+
}, r)).getFilesInFolder.files
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
function E(e, t, n, r) {
|
|
48
|
+
return new Promise((i, a) => {
|
|
49
|
+
let o = new XMLHttpRequest();
|
|
50
|
+
o.open("PUT", e), o.setRequestHeader("Content-Type", n), o.upload.onprogress = (e) => {
|
|
51
|
+
e.lengthComputable && r(Math.round(e.loaded / e.total * 100));
|
|
52
|
+
}, o.onload = () => o.status >= 200 && o.status < 300 ? i() : a(/* @__PURE__ */ Error(`File storage rejected the upload (HTTP ${o.status}).`)), o.onerror = () => a(/* @__PURE__ */ Error("Network error while uploading the file.")), o.onabort = () => a(/* @__PURE__ */ Error("The upload was cancelled.")), o.send(t);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
var D = (t, i) => {
|
|
56
|
+
let { apiGatewayUrl: a } = e(), s = f(), [c, l] = o(0);
|
|
57
|
+
return {
|
|
58
|
+
...u({
|
|
59
|
+
mutationFn: async (e) => {
|
|
60
|
+
l(0);
|
|
61
|
+
let o = await n("query BraResolveFolder($pagination: PaginationInput) {\n getFolders(pagination: $pagination) { folders { id name } }\n }", { pagination: {
|
|
62
|
+
limit: 200,
|
|
63
|
+
offset: 0
|
|
64
|
+
} }, a).then((e) => e.getFolders.folders.find((e) => e.name === x(t, i)));
|
|
65
|
+
o ||= (await r("mutation BraCreateFolder($input: CreateFolderInput!) {\n createFolder(input: $input) { id name }\n }", { input: {
|
|
66
|
+
name: x(t, i),
|
|
67
|
+
description: `Documents attached to ${t} ${i}.`,
|
|
68
|
+
visibility: "WORKSPACE"
|
|
69
|
+
} }, a)).createFolder;
|
|
70
|
+
let s = o.id, c = e.name.lastIndexOf("."), u = c > -1 ? e.name.slice(c + 1) : "", d = e.type || "application/octet-stream", f = String(e.size), p = await r("mutation BraInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) { uploadUrl session { id } }\n }", { input: {
|
|
71
|
+
fileName: e.name,
|
|
72
|
+
extension: u,
|
|
73
|
+
mimeType: d,
|
|
74
|
+
fileSize: f,
|
|
75
|
+
folderId: s,
|
|
76
|
+
visibility: "WORKSPACE"
|
|
77
|
+
} }, a), { uploadUrl: m } = p.initiateUpload, h = p.initiateUpload.session.id;
|
|
78
|
+
if (!m) throw Error("The file service did not return an upload URL.");
|
|
79
|
+
return await E(m, e, d, l), await r("mutation BraCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) { fileId }\n }", { input: {
|
|
80
|
+
sessionId: h,
|
|
81
|
+
fileSize: f,
|
|
82
|
+
folderId: s
|
|
83
|
+
} }, a), l(100), s;
|
|
84
|
+
},
|
|
85
|
+
onSuccess: (e) => {
|
|
86
|
+
s.invalidateQueries({ queryKey: S(t, i) }), s.invalidateQueries({ queryKey: C(e) });
|
|
87
|
+
}
|
|
88
|
+
}),
|
|
89
|
+
progress: c
|
|
90
|
+
};
|
|
91
|
+
}, O = () => {
|
|
92
|
+
let { apiGatewayUrl: t } = e();
|
|
93
|
+
return u({ mutationFn: async (e) => (await n("query BraGetFileDownloadUrl($id: ID!, $expiresIn: Int) {\n getFileDownloadUrl(id: $id, expiresIn: $expiresIn)\n }", {
|
|
94
|
+
id: e,
|
|
95
|
+
expiresIn: y
|
|
96
|
+
}, t)).getFileDownloadUrl });
|
|
97
|
+
}, k = (e) => {
|
|
98
|
+
let t = Number(e);
|
|
99
|
+
return !Number.isFinite(t) || t <= 0 ? "—" : t < 1024 ? `${t} B` : t < 1024 * 1024 ? `${(t / 1024).toFixed(0)} KB` : `${(t / (1024 * 1024)).toFixed(1)} MB`;
|
|
100
|
+
};
|
|
101
|
+
function A({ entityType: e, entityId: n, heading: r = "Documents", hideHeading: o = !1 }) {
|
|
102
|
+
let u = a(null), d = w(e, n), f = T(d.data?.id), y = D(e, n), b = O(), x = i((e) => {
|
|
103
|
+
let t = e.target.files?.[0];
|
|
104
|
+
t && y.mutate(t), e.target.value = "";
|
|
105
|
+
}, [y]), S = async (e) => {
|
|
106
|
+
let t = await b.mutateAsync(e);
|
|
107
|
+
t && window.open(t, "_blank", "noopener,noreferrer");
|
|
108
|
+
}, C = d.isLoading || !!d.data && f.isLoading, E = f.data ?? [];
|
|
109
|
+
return /* @__PURE__ */ m("div", {
|
|
110
|
+
className: "space-y-3",
|
|
111
|
+
children: [
|
|
112
|
+
/* @__PURE__ */ m("div", {
|
|
113
|
+
className: "flex items-center justify-between",
|
|
114
|
+
children: [!o && /* @__PURE__ */ m("h3", {
|
|
115
|
+
className: "flex items-center gap-2 text-sm font-semibold text-text-primary",
|
|
116
|
+
children: [/* @__PURE__ */ p(c, {
|
|
117
|
+
className: "size-4",
|
|
118
|
+
"aria-hidden": "true"
|
|
119
|
+
}), r]
|
|
120
|
+
}), /* @__PURE__ */ m("div", {
|
|
121
|
+
className: "ml-auto",
|
|
122
|
+
children: [/* @__PURE__ */ p("input", {
|
|
123
|
+
ref: u,
|
|
124
|
+
type: "file",
|
|
125
|
+
className: "sr-only",
|
|
126
|
+
"aria-hidden": "true",
|
|
127
|
+
tabIndex: -1,
|
|
128
|
+
onChange: x
|
|
129
|
+
}), /* @__PURE__ */ m(h, {
|
|
130
|
+
type: "button",
|
|
131
|
+
size: "sm",
|
|
132
|
+
variant: "outline",
|
|
133
|
+
onClick: () => u.current?.click(),
|
|
134
|
+
disabled: y.isPending,
|
|
135
|
+
children: [/* @__PURE__ */ p(l, {
|
|
136
|
+
className: "size-4",
|
|
137
|
+
"aria-hidden": "true"
|
|
138
|
+
}), y.isPending ? `Uploading ${y.progress}%` : "Upload"]
|
|
139
|
+
})]
|
|
140
|
+
})]
|
|
141
|
+
}),
|
|
142
|
+
y.isPending && /* @__PURE__ */ p("div", {
|
|
143
|
+
className: "h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken",
|
|
144
|
+
role: "progressbar",
|
|
145
|
+
"aria-label": "Upload progress",
|
|
146
|
+
"aria-valuenow": y.progress,
|
|
147
|
+
"aria-valuemin": 0,
|
|
148
|
+
"aria-valuemax": 100,
|
|
149
|
+
children: /* @__PURE__ */ p("div", {
|
|
150
|
+
className: "h-full rounded-full bg-primary motion-safe:transition-[width] motion-safe:duration-200",
|
|
151
|
+
style: { width: `${y.progress}%` }
|
|
152
|
+
})
|
|
153
|
+
}),
|
|
154
|
+
C ? /* @__PURE__ */ m("div", {
|
|
155
|
+
className: "space-y-2",
|
|
156
|
+
"aria-busy": "true",
|
|
157
|
+
children: [/* @__PURE__ */ p(v, { className: "h-12 w-full rounded-md" }), /* @__PURE__ */ p(v, { className: "h-12 w-full rounded-md" })]
|
|
158
|
+
}) : f.isError ? /* @__PURE__ */ m("p", {
|
|
159
|
+
className: "text-sm text-status-error-text",
|
|
160
|
+
children: [
|
|
161
|
+
"Could not load documents.",
|
|
162
|
+
" ",
|
|
163
|
+
/* @__PURE__ */ p("button", {
|
|
164
|
+
type: "button",
|
|
165
|
+
className: "underline underline-offset-2",
|
|
166
|
+
onClick: () => f.refetch(),
|
|
167
|
+
children: "Retry"
|
|
168
|
+
})
|
|
169
|
+
]
|
|
170
|
+
}) : E.length === 0 ? /* @__PURE__ */ m("div", {
|
|
171
|
+
className: "rounded-lg border border-dashed border-border-subtle bg-bg-surface px-4 py-8 text-center",
|
|
172
|
+
children: [
|
|
173
|
+
/* @__PURE__ */ p(c, {
|
|
174
|
+
className: "mx-auto mb-2 size-6 text-text-secondary",
|
|
175
|
+
"aria-hidden": "true"
|
|
176
|
+
}),
|
|
177
|
+
/* @__PURE__ */ p("p", {
|
|
178
|
+
className: "text-sm text-text-primary",
|
|
179
|
+
children: "No documents yet"
|
|
180
|
+
}),
|
|
181
|
+
/* @__PURE__ */ p("p", {
|
|
182
|
+
className: "mt-0.5 text-xs text-text-secondary",
|
|
183
|
+
children: "Upload statements, KYC or tax documents for this record."
|
|
184
|
+
})
|
|
185
|
+
]
|
|
186
|
+
}) : /* @__PURE__ */ p("ul", {
|
|
187
|
+
className: "space-y-2",
|
|
188
|
+
children: E.map((e) => /* @__PURE__ */ p("li", { children: /* @__PURE__ */ p(g, { children: /* @__PURE__ */ m(_, {
|
|
189
|
+
className: "flex items-center gap-3 p-3",
|
|
190
|
+
children: [
|
|
191
|
+
/* @__PURE__ */ p("div", {
|
|
192
|
+
className: "flex size-9 shrink-0 items-center justify-center rounded-md bg-bg-sunken text-text-secondary",
|
|
193
|
+
children: /* @__PURE__ */ p(c, {
|
|
194
|
+
className: "size-4",
|
|
195
|
+
"aria-hidden": "true"
|
|
196
|
+
})
|
|
197
|
+
}),
|
|
198
|
+
/* @__PURE__ */ m("div", {
|
|
199
|
+
className: "min-w-0 flex-1",
|
|
200
|
+
children: [/* @__PURE__ */ p("p", {
|
|
201
|
+
className: "truncate text-sm font-medium text-text-primary",
|
|
202
|
+
children: e.name
|
|
203
|
+
}), /* @__PURE__ */ m("p", {
|
|
204
|
+
className: "text-xs text-text-secondary",
|
|
205
|
+
children: [
|
|
206
|
+
k(e.size),
|
|
207
|
+
" · ",
|
|
208
|
+
t(e.createdAt)
|
|
209
|
+
]
|
|
210
|
+
})]
|
|
211
|
+
}),
|
|
212
|
+
/* @__PURE__ */ p(h, {
|
|
213
|
+
type: "button",
|
|
214
|
+
size: "sm",
|
|
215
|
+
variant: "ghost",
|
|
216
|
+
"aria-label": `Download ${e.name}`,
|
|
217
|
+
disabled: b.isPending,
|
|
218
|
+
onClick: () => S(e.id),
|
|
219
|
+
children: /* @__PURE__ */ p(s, {
|
|
220
|
+
className: "size-4",
|
|
221
|
+
"aria-hidden": "true"
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
]
|
|
225
|
+
}) }) }, e.id))
|
|
226
|
+
}),
|
|
227
|
+
(y.isError || b.isError) && /* @__PURE__ */ p("p", {
|
|
228
|
+
className: "text-sm text-status-error-text",
|
|
229
|
+
children: y.error instanceof Error ? y.error.message : b.error instanceof Error ? "Could not open the document. Please try again." : "Something went wrong. Please try again."
|
|
230
|
+
})
|
|
231
|
+
]
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
export { A as DocumentsPanel };
|
|
236
|
+
|
|
237
|
+
//# sourceMappingURL=DocumentsPanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DocumentsPanel.js","names":[],"sources":["../../../../src/brainyrich/components/reuse/DocumentsPanel.tsx"],"sourcesContent":["/**\n * DocumentsPanel — REUSE of `wspace-files-svc` for statement / KYC / tax-doc\n * attachments on Accounts + Households.\n * @module brainyrich/components/reuse/DocumentsPanel\n *\n * A wealth product needs to attach documents (statements, KYC, tax packs) to\n * accounts and households. `wspace-files-svc` has no native entity-owned link on\n * `File`, so we scope documents to an entity via a dedicated, stably-named\n * FOLDER per `(entityType, entityId)` and list its contents. All ops route\n * through the workspace supergraph the MFE already uses.\n *\n * Flow:\n * - resolve folder → `getFolders(filter)` matched by name (created lazily on\n * first upload via `createFolder`); no side-effects on read.\n * - list → `getFilesInFolder(folderId, pagination)`\n * - upload (3-step) → `initiateUpload` → S3 `PUT` (byte-progress) → `completeUpload`\n * - download → `getFileDownloadUrl(id, expiresIn)` capped at 21600s to\n * avoid the >604800 fleet bug (S3 7-day cap → 500).\n *\n * Presentational + self-contained: owns its hooks, reads the gateway base URL\n * from `useBrainyRich()`, uses fe-libs/ui primitives + semantic tokens.\n */\n\nimport { useCallback, useRef, useState } from 'react';\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { Download, FileText, Upload } from 'lucide-react';\nimport { Button, Card, CardContent, Skeleton } from '@burdenoff/fe-libs/ui';\n\nimport { useBrainyRich } from '../../providers/BrainyRichProvider';\nimport { formatDate } from '../../pages/_shared';\nimport { wspaceFetch, wspaceMutate } from './transport';\n\n// Presigned-URL TTL: cap at 21600s (6h). >604800 (S3 7-day) → INTERNAL_SERVER_ERROR.\nconst DOWNLOAD_URL_TTL_SECONDS = 21600;\n\ninterface FilesFolder {\n id: string;\n name: string;\n}\n\ninterface FilesFile {\n id: string;\n name: string;\n extension: string;\n mimeType: string;\n size: string;\n createdAt: string;\n}\n\nconst FILE_FIELDS = 'id name extension mimeType size createdAt';\n\nconst folderName = (entityType: string, entityId: string) =>\n `BrainyRich · ${entityType} · ${entityId}`;\n\nconst folderKey = (entityType: string, entityId: string) =>\n ['brainyrich', 'entity-folder', entityType, entityId] as const;\n\nconst filesKey = (folderId: string | null | undefined) =>\n ['brainyrich', 'folder-files', folderId ?? 'none'] as const;\n\n// ============================================================================\n// Folder resolution (read-only — no lazy creation here)\n// ============================================================================\n\nconst useEntityFolder = (entityType: string, entityId: string | undefined) => {\n const { apiGatewayUrl } = useBrainyRich();\n const wanted = entityId ? folderName(entityType, entityId) : '';\n return useQuery({\n queryKey: folderKey(entityType, entityId ?? ''),\n enabled: Boolean(entityId),\n queryFn: async () => {\n const data = await wspaceFetch<{ getFolders: { folders: FilesFolder[] } }>(\n `query BraGetFolders($pagination: PaginationInput) {\n getFolders(pagination: $pagination) { folders { id name } }\n }`,\n { pagination: { limit: 200, offset: 0 } },\n apiGatewayUrl\n );\n return data.getFolders.folders.find((f) => f.name === wanted) ?? null;\n },\n });\n};\n\nconst useFolderFiles = (folderId: string | null | undefined) => {\n const { apiGatewayUrl } = useBrainyRich();\n return useQuery({\n queryKey: filesKey(folderId),\n enabled: Boolean(folderId),\n queryFn: async () => {\n const data = await wspaceFetch<{ getFilesInFolder: { files: FilesFile[] } }>(\n `query BraGetFilesInFolder($folderId: ID, $pagination: PaginationInput) {\n getFilesInFolder(folderId: $folderId, pagination: $pagination) {\n files { ${FILE_FIELDS} }\n }\n }`,\n { folderId, pagination: { limit: 100, offset: 0 } },\n apiGatewayUrl\n );\n return data.getFilesInFolder.files;\n },\n });\n};\n\n// ============================================================================\n// Upload (3-step platform flow, with lazy folder creation)\n// ============================================================================\n\nfunction putWithProgress(\n url: string,\n file: File,\n contentType: string,\n onProgress: (percent: number) => void\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open('PUT', url);\n xhr.setRequestHeader('Content-Type', contentType);\n xhr.upload.onprogress = (event) => {\n if (event.lengthComputable) {\n onProgress(Math.round((event.loaded / event.total) * 100));\n }\n };\n xhr.onload = () =>\n xhr.status >= 200 && xhr.status < 300\n ? resolve()\n : reject(new Error(`File storage rejected the upload (HTTP ${xhr.status}).`));\n xhr.onerror = () => reject(new Error('Network error while uploading the file.'));\n xhr.onabort = () => reject(new Error('The upload was cancelled.'));\n xhr.send(file);\n });\n}\n\nconst useDocumentUpload = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useBrainyRich();\n const qc = useQueryClient();\n const [progress, setProgress] = useState(0);\n\n const mutation = useMutation({\n mutationFn: async (file: File) => {\n setProgress(0);\n\n // 0. resolve or create the entity folder\n let folder = await wspaceFetch<{ getFolders: { folders: FilesFolder[] } }>(\n `query BraResolveFolder($pagination: PaginationInput) {\n getFolders(pagination: $pagination) { folders { id name } }\n }`,\n { pagination: { limit: 200, offset: 0 } },\n apiGatewayUrl\n ).then((d) => d.getFolders.folders.find((f) => f.name === folderName(entityType, entityId)));\n\n if (!folder) {\n const created = await wspaceMutate<{ createFolder: FilesFolder }>(\n `mutation BraCreateFolder($input: CreateFolderInput!) {\n createFolder(input: $input) { id name }\n }`,\n {\n input: {\n name: folderName(entityType, entityId),\n description: `Documents attached to ${entityType} ${entityId}.`,\n visibility: 'WORKSPACE',\n },\n },\n apiGatewayUrl\n );\n folder = created.createFolder;\n }\n const folderId = folder.id;\n\n const dotIndex = file.name.lastIndexOf('.');\n const extension = dotIndex > -1 ? file.name.slice(dotIndex + 1) : '';\n const mimeType = file.type || 'application/octet-stream';\n const fileSize = String(file.size);\n\n // 1. initiate\n const initiated = await wspaceMutate<{\n initiateUpload: { uploadUrl: string | null; session: { id: string } };\n }>(\n `mutation BraInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) { uploadUrl session { id } }\n }`,\n {\n input: { fileName: file.name, extension, mimeType, fileSize, folderId, visibility: 'WORKSPACE' },\n },\n apiGatewayUrl\n );\n const { uploadUrl } = initiated.initiateUpload;\n const sessionId = initiated.initiateUpload.session.id;\n if (!uploadUrl) throw new Error('The file service did not return an upload URL.');\n\n // 2. PUT the bytes\n await putWithProgress(uploadUrl, file, mimeType, setProgress);\n\n // 3. complete\n await wspaceMutate<{ completeUpload: { fileId: string } }>(\n `mutation BraCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) { fileId }\n }`,\n { input: { sessionId, fileSize, folderId } },\n apiGatewayUrl\n );\n setProgress(100);\n return folderId;\n },\n onSuccess: (folderId) => {\n qc.invalidateQueries({ queryKey: folderKey(entityType, entityId) });\n qc.invalidateQueries({ queryKey: filesKey(folderId) });\n },\n });\n\n return { ...mutation, progress };\n};\n\nconst useDownloadFile = () => {\n const { apiGatewayUrl } = useBrainyRich();\n return useMutation({\n mutationFn: async (id: string) => {\n const data = await wspaceFetch<{ getFileDownloadUrl: string }>(\n `query BraGetFileDownloadUrl($id: ID!, $expiresIn: Int) {\n getFileDownloadUrl(id: $id, expiresIn: $expiresIn)\n }`,\n { id, expiresIn: DOWNLOAD_URL_TTL_SECONDS },\n apiGatewayUrl\n );\n return data.getFileDownloadUrl;\n },\n });\n};\n\n// ============================================================================\n// Component\n// ============================================================================\n\nconst formatSize = (bytes: string): string => {\n const n = Number(bytes);\n if (!Number.isFinite(n) || n <= 0) return '—';\n if (n < 1024) return `${n} B`;\n if (n < 1024 * 1024) return `${(n / 1024).toFixed(0)} KB`;\n return `${(n / (1024 * 1024)).toFixed(1)} MB`;\n};\n\nexport interface DocumentsPanelProps {\n /** Polymorphic entity type, e.g. \"brainyrich-account\" | \"brainyrich-household\". */\n entityType: string;\n /** The bra entity's id (an ID scalar). */\n entityId: string;\n /** Optional heading; hide with `hideHeading`. */\n heading?: string;\n hideHeading?: boolean;\n}\n\nexport function DocumentsPanel({\n entityType,\n entityId,\n heading = 'Documents',\n hideHeading = false,\n}: DocumentsPanelProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n const folder = useEntityFolder(entityType, entityId);\n const files = useFolderFiles(folder.data?.id);\n const upload = useDocumentUpload(entityType, entityId);\n const download = useDownloadFile();\n\n const onPick = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const file = e.target.files?.[0];\n if (file) upload.mutate(file);\n e.target.value = '';\n },\n [upload]\n );\n\n const onDownload = async (id: string) => {\n const url = await download.mutateAsync(id);\n if (url) window.open(url, '_blank', 'noopener,noreferrer');\n };\n\n const loading = folder.isLoading || (Boolean(folder.data) && files.isLoading);\n const list = files.data ?? [];\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n {!hideHeading && (\n <h3 className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <FileText className=\"size-4\" aria-hidden=\"true\" />\n {heading}\n </h3>\n )}\n <div className=\"ml-auto\">\n <input\n ref={inputRef}\n type=\"file\"\n className=\"sr-only\"\n aria-hidden=\"true\"\n tabIndex={-1}\n onChange={onPick}\n />\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n onClick={() => inputRef.current?.click()}\n disabled={upload.isPending}\n >\n <Upload className=\"size-4\" aria-hidden=\"true\" />\n {upload.isPending ? `Uploading ${upload.progress}%` : 'Upload'}\n </Button>\n </div>\n </div>\n\n {upload.isPending && (\n <div\n className=\"h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\"\n role=\"progressbar\"\n aria-label=\"Upload progress\"\n aria-valuenow={upload.progress}\n aria-valuemin={0}\n aria-valuemax={100}\n >\n <div\n className=\"h-full rounded-full bg-primary motion-safe:transition-[width] motion-safe:duration-200\"\n style={{ width: `${upload.progress}%` }}\n />\n </div>\n )}\n\n {loading ? (\n <div className=\"space-y-2\" aria-busy=\"true\">\n <Skeleton className=\"h-12 w-full rounded-md\" />\n <Skeleton className=\"h-12 w-full rounded-md\" />\n </div>\n ) : files.isError ? (\n <p className=\"text-sm text-status-error-text\">\n Could not load documents.{' '}\n <button\n type=\"button\"\n className=\"underline underline-offset-2\"\n onClick={() => files.refetch()}\n >\n Retry\n </button>\n </p>\n ) : list.length === 0 ? (\n <div className=\"rounded-lg border border-dashed border-border-subtle bg-bg-surface px-4 py-8 text-center\">\n <FileText className=\"mx-auto mb-2 size-6 text-text-secondary\" aria-hidden=\"true\" />\n <p className=\"text-sm text-text-primary\">No documents yet</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Upload statements, KYC or tax documents for this record.\n </p>\n </div>\n ) : (\n <ul className=\"space-y-2\">\n {list.map((file) => (\n <li key={file.id}>\n <Card>\n <CardContent className=\"flex items-center gap-3 p-3\">\n <div className=\"flex size-9 shrink-0 items-center justify-center rounded-md bg-bg-sunken text-text-secondary\">\n <FileText className=\"size-4\" aria-hidden=\"true\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{file.name}</p>\n <p className=\"text-xs text-text-secondary\">\n {formatSize(file.size)} · {formatDate(file.createdAt)}\n </p>\n </div>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Download ${file.name}`}\n disabled={download.isPending}\n onClick={() => onDownload(file.id)}\n >\n <Download className=\"size-4\" aria-hidden=\"true\" />\n </Button>\n </CardContent>\n </Card>\n </li>\n ))}\n </ul>\n )}\n\n {(upload.isError || download.isError) && (\n <p className=\"text-sm text-status-error-text\">\n {upload.error instanceof Error\n ? upload.error.message\n : download.error instanceof Error\n ? 'Could not open the document. Please try again.'\n : 'Something went wrong. Please try again.'}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|