@burdenoff/microfe-proserviceworld 2026.827.1 → 2026.1009.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/index.js +4 -4
- package/dist/proserviceworld/components/ui.js +1 -1
- package/dist/proserviceworld/components/ui.js.map +1 -1
- package/dist/proserviceworld/hooks/useProServiceWorldApi.d.ts.map +1 -1
- package/dist/proserviceworld/hooks/useProServiceWorldApi.js.map +1 -1
- package/dist/proserviceworld/hooks/useTr.d.ts +19 -0
- package/dist/proserviceworld/hooks/useTr.d.ts.map +1 -0
- package/dist/proserviceworld/hooks/useTr.js +33 -0
- package/dist/proserviceworld/hooks/useTr.js.map +1 -0
- package/dist/proserviceworld/i18n/index.d.ts +1 -1
- package/dist/proserviceworld/i18n/index.d.ts.map +1 -1
- package/dist/proserviceworld/i18n/index.js +723 -18
- package/dist/proserviceworld/i18n/index.js.map +1 -1
- package/dist/proserviceworld/index.js +4 -4
- package/dist/proserviceworld/pages/ClientsPage.d.ts.map +1 -1
- package/dist/proserviceworld/pages/ClientsPage.js +166 -164
- package/dist/proserviceworld/pages/ClientsPage.js.map +1 -1
- package/dist/proserviceworld/pages/DashboardPage.d.ts.map +1 -1
- package/dist/proserviceworld/pages/DashboardPage.js +152 -142
- package/dist/proserviceworld/pages/DashboardPage.js.map +1 -1
- package/dist/proserviceworld/pages/EngagementsPage.d.ts.map +1 -1
- package/dist/proserviceworld/pages/EngagementsPage.js +135 -135
- package/dist/proserviceworld/pages/EngagementsPage.js.map +1 -1
- package/dist/proserviceworld/pages/MattersPage.d.ts.map +1 -1
- package/dist/proserviceworld/pages/MattersPage.js +204 -197
- package/dist/proserviceworld/pages/MattersPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientsPage.js","names":[],"sources":["../../../src/proserviceworld/pages/ClientsPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useMemo, useState } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Plus, Search, Users, Pencil, Archive, Building2, User, ChevronRight } from 'lucide-react';\nimport {\n useClients,\n useCreateClient,\n useUpdateClient,\n useArchiveClient,\n type PswClient,\n type PswClientStatus,\n} from '../hooks/useProServiceWorldApi';\nimport { useNav } from '../utils/nav';\nimport { cn } from '../utils/cn';\nimport { useAnalytics } from '../hooks/useAnalytics';\nimport { t } from '../i18n';\nimport { impactFeedback } from '../utils/native';\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 TableWrap,\n Th,\n Td,\n enumOptions,\n fmtDate,\n} from '../components/ui';\nimport { TagPicker } from '../components/TagPicker';\nimport { LoadMore } from '../components/LoadMore';\n\nconst CLIENT_TYPES = ['ORGANIZATION', 'INDIVIDUAL'] as const;\nconst CLIENT_STATUSES: PswClientStatus[] = ['PROSPECT', 'ACTIVE', 'INACTIVE', 'ARCHIVED'];\n\nconst initials = (name: string): string =>\n name\n .split(/\\s+/)\n .filter(Boolean)\n .slice(0, 2)\n .map((w) => w[0]?.toUpperCase() ?? '')\n .join('') || '?';\n\n/** Tinted initials avatar (semantic tokens only). */\nexport const Avatar: FC<{ name: string; className?: string }> = ({ name, className }) => (\n <span\n className={cn(\n 'flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent/10 text-xs font-semibold text-accent ring-1 ring-inset ring-accent/15',\n className\n )}\n aria-hidden\n >\n {initials(name)}\n </span>\n);\n\ninterface FormState {\n name: string;\n type: string;\n status: string;\n industry: string;\n contactName: string;\n contactEmail: string;\n contactPhone: string;\n notes: string;\n}\nconst emptyForm: FormState = {\n name: '',\n type: 'ORGANIZATION',\n status: 'PROSPECT',\n industry: '',\n contactName: '',\n contactEmail: '',\n contactPhone: '',\n notes: '',\n};\n\nconst PAGE_SIZE = 25;\n\nexport const ClientsPage: FC = () => {\n const nav = useNav();\n const { emitIntent, emitFact } = useAnalytics();\n const [search, setSearch] = useState('');\n const [statusFilter, setStatusFilter] = useState<PswClientStatus | ''>('');\n const [editing, setEditing] = useState<PswClient | null>(null);\n const [creating, setCreating] = useState(false);\n const [banner, setBanner] = useState<{ tone: 'success' | 'danger'; text: string } | null>(null);\n const [pagination, setPagination] = useState({ limit: PAGE_SIZE, offset: 0 });\n\n const { data, isLoading, error, refetch } = useClients({\n search: search.trim() || undefined,\n status: statusFilter || undefined,\n pagination,\n });\n const update = useUpdateClient();\n\n const allItems = useMemo(() => data?.items ?? [], [data]);\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title={t('psw.clients.title')}\n subtitle={t('psw.clients.subtitle')}\n icon={<Users className=\"h-6 w-6\" />}\n actions={\n <Button\n onClick={() => {\n void impactFeedback('light');\n emitIntent('psw.client.create_clicked');\n setCreating(true);\n }}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" /> {t('psw.clients.new')}\n </Button>\n }\n />\n\n {banner && (\n <Banner tone={banner.tone} onClose={() => setBanner(null)}>\n {banner.text}\n </Banner>\n )}\n\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n <div className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary\" />\n <TextInput\n placeholder={t('psw.clients.searchPlaceholder')}\n value={search}\n onChange={(e) => {\n setSearch(e.target.value);\n setPagination({ limit: PAGE_SIZE, offset: 0 });\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter' && search.trim()) {\n emitFact('psw.search.performed', {\n entityType: 'client',\n queryLength: search.trim().length,\n });\n }\n }}\n className=\"pl-9\"\n />\n </div>\n <NativeSelect\n value={statusFilter}\n onChange={(e) => {\n setStatusFilter(e.target.value as PswClientStatus | '');\n setPagination({ limit: PAGE_SIZE, offset: 0 });\n }}\n className=\"sm:w-48\"\n >\n <option value=\"\">{t('psw.clients.allStatuses')}</option>\n {enumOptions(CLIENT_STATUSES)}\n </NativeSelect>\n </div>\n\n {isLoading ? (\n <LoadingBlock />\n ) : error ? (\n <ErrorState error={error} onRetry={() => void refetch()} />\n ) : !data || data.items.length === 0 ? (\n <Empty\n title={t('psw.clients.emptyTitle')}\n description={t('psw.clients.emptyDescription')}\n icon={<Users className=\"h-8 w-8\" />}\n action={\n <Button onClick={() => setCreating(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> {t('psw.clients.new')}\n </Button>\n }\n />\n ) : (\n <SectionCard title={`${data.totalCount} client${data.totalCount === 1 ? '' : 's'}`}>\n {/*\n Phones get a tappable card list: the desktop table carries seven\n columns behind a horizontal scroll, which hides the status and the\n row actions on a narrow viewport. Same fields, same affordances.\n */}\n <ul className=\"space-y-2.5 sm:hidden\">\n {data.items.map((c) => (\n <li\n key={c.id}\n className=\"overflow-hidden rounded-xl border border-border-seam bg-bg-canvas\"\n >\n <button\n type=\"button\"\n onClick={() => {\n void impactFeedback('light');\n nav(`clients/${c.id}`);\n }}\n className=\"flex w-full items-center gap-3 p-3.5 text-left transition-colors active:bg-bg-sunken/60\"\n >\n <Avatar name={c.name} className=\"h-11 w-11 text-sm\" />\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-semibold text-text-primary\">\n {c.name}\n </span>\n <span className=\"mt-0.5 block truncate text-sm text-text-secondary\">\n {c.contactName || (c.type === 'ORGANIZATION' ? 'Organization' : 'Individual')}\n </span>\n <span className=\"mt-1.5 flex items-center gap-2\">\n <span className=\"truncate text-xs text-text-tertiary\">\n {c.industry || fmtDate(c.createdAt)}\n </span>\n <StatusPill status={c.status} />\n </span>\n </span>\n <ChevronRight className=\"h-4 w-4 shrink-0 text-text-tertiary\" />\n </button>\n <div className=\"flex items-center justify-between gap-2 border-t border-border-seam px-3.5 py-2\">\n <div className=\"min-w-0 flex-1\">\n <TagPicker\n entityType=\"PswClient\"\n entityId={c.id}\n tags={c.tags ?? []}\n onChange={async (tags) => {\n await update.mutateAsync({ id: c.id, input: { tags } });\n void refetch();\n }}\n maxTags={5}\n />\n </div>\n <div className=\"flex shrink-0 gap-1\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setEditing(c)}\n aria-label={`Edit ${c.name}`}\n >\n <Pencil className=\"h-4 w-4\" />\n </Button>\n {c.status !== 'ARCHIVED' && (\n <ArchiveButton\n id={c.id}\n onDone={(ok) =>\n setBanner(\n ok\n ? { tone: 'success', text: `${c.name} archived.` }\n : { tone: 'danger', text: 'Could not archive client.' }\n )\n }\n />\n )}\n </div>\n </div>\n </li>\n ))}\n </ul>\n\n <div className=\"hidden sm:block\">\n <TableWrap>\n <thead>\n <tr>\n <Th>{t('psw.common.name')}</Th>\n <Th>{t('psw.common.type')}</Th>\n <Th>Industry</Th>\n <Th>{t('psw.common.status')}</Th>\n <Th>{t('psw.common.added')}</Th>\n <Th>Tags</Th>\n <Th className=\"text-right\">{t('psw.common.actions')}</Th>\n </tr>\n </thead>\n <tbody>\n {data.items.map((c) => (\n <tr\n key={c.id}\n className=\"group cursor-pointer transition-colors hover:bg-bg-sunken/60\"\n onClick={() => nav(`clients/${c.id}`)}\n >\n <Td>\n <div className=\"flex items-center gap-3\">\n <Avatar name={c.name} />\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-1 font-medium text-text-primary transition-colors group-hover:text-accent\">\n <span className=\"truncate\">{c.name}</span>\n <ChevronRight className=\"h-3.5 w-3.5 shrink-0 text-text-tertiary opacity-0 transition-opacity group-hover:opacity-100\" />\n </div>\n {c.contactName ? (\n <div className=\"truncate text-xs text-text-tertiary\">\n {c.contactName}\n </div>\n ) : null}\n </div>\n </div>\n </Td>\n <Td>\n <span className=\"inline-flex items-center gap-1.5 text-text-secondary\">\n {c.type === 'ORGANIZATION' ? (\n <Building2 className=\"h-3.5 w-3.5 text-text-tertiary\" />\n ) : (\n <User className=\"h-3.5 w-3.5 text-text-tertiary\" />\n )}\n {c.type === 'ORGANIZATION' ? 'Organization' : 'Individual'}\n </span>\n </Td>\n <Td>{c.industry || '—'}</Td>\n <Td>\n <StatusPill status={c.status} />\n </Td>\n <Td className=\"whitespace-nowrap\">{fmtDate(c.createdAt)}</Td>\n <Td>\n <div onClick={(ev) => ev.stopPropagation()} role=\"presentation\">\n <TagPicker\n entityType=\"PswClient\"\n entityId={c.id}\n tags={c.tags ?? []}\n onChange={async (tags) => {\n await update.mutateAsync({ id: c.id, input: { tags } });\n void refetch();\n }}\n maxTags={5}\n />\n </div>\n </Td>\n <Td className=\"text-right\">\n <div\n className=\"flex justify-end gap-1\"\n onClick={(ev) => ev.stopPropagation()}\n role=\"presentation\"\n >\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setEditing(c)}\n aria-label=\"Edit\"\n >\n <Pencil className=\"h-4 w-4\" />\n </Button>\n {c.status !== 'ARCHIVED' && (\n <ArchiveButton\n id={c.id}\n onDone={(ok) =>\n setBanner(\n ok\n ? { tone: 'success', text: `${c.name} archived.` }\n : { tone: 'danger', text: 'Could not archive client.' }\n )\n }\n />\n )}\n </div>\n </Td>\n </tr>\n ))}\n </tbody>\n </TableWrap>\n </div>\n <LoadMore\n hasMore={data?.pageInfo?.hasMore ?? false}\n loading={isLoading}\n loadedCount={allItems.length}\n totalCount={data?.totalCount ?? allItems.length}\n onLoadMore={() =>\n setPagination((p) => ({ limit: PAGE_SIZE, offset: p.offset + PAGE_SIZE }))\n }\n />\n </SectionCard>\n )}\n\n {creating && (\n <ClientModal\n onClose={() => setCreating(false)}\n onSaved={(name) => setBanner({ tone: 'success', text: `${name} created.` })}\n />\n )}\n {editing && (\n <ClientModal\n client={editing}\n onClose={() => setEditing(null)}\n onSaved={(name) => setBanner({ tone: 'success', text: `${name} updated.` })}\n />\n )}\n </div>\n );\n};\n\nconst ArchiveButton: FC<{ id: string; onDone: (ok: boolean) => void }> = ({ id, onDone }) => {\n const archive = useArchiveClient();\n const { emitFact } = useAnalytics();\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n aria-label=\"Archive\"\n disabled={archive.isPending}\n onClick={() => {\n if (!window.confirm('Archive this client? Their engagements and matters remain.')) return;\n archive.mutate(id, {\n onSuccess: (r) => {\n emitFact('psw.client.archived', { clientId: id });\n onDone(Boolean(r));\n },\n onError: () => onDone(false),\n });\n }}\n >\n <Archive className=\"h-4 w-4\" />\n </Button>\n );\n};\n\nconst ClientModal: FC<{\n client?: PswClient;\n onClose: () => void;\n onSaved: (name: string) => void;\n}> = ({ client, onClose, onSaved }) => {\n const create = useCreateClient();\n const update = useUpdateClient();\n const { emitFact } = useAnalytics();\n const [form, setForm] = useState<FormState>(\n client\n ? {\n name: client.name,\n type: client.type,\n status: client.status,\n industry: client.industry ?? '',\n contactName: client.contactName ?? '',\n contactEmail: client.contactEmail ?? '',\n contactPhone: client.contactPhone ?? '',\n notes: client.notes ?? '',\n }\n : emptyForm\n );\n const [err, setErr] = useState<string | null>(null);\n const set = (k: keyof FormState) => (e: { target: { value: string } }) =>\n setForm((f) => ({ ...f, [k]: e.target.value }));\n const pending = create.isPending || update.isPending;\n\n const payload = useMemo(\n () => ({\n name: form.name.trim(),\n type: form.type,\n status: form.status,\n industry: form.industry.trim() || null,\n contactName: form.contactName.trim() || null,\n contactEmail: form.contactEmail.trim() || null,\n contactPhone: form.contactPhone.trim() || null,\n notes: form.notes.trim() || null,\n }),\n [form]\n );\n\n const submit = () => {\n setErr(null);\n if (!payload.name) return setErr('Client name is required.');\n const onError = (e: unknown) => setErr(e instanceof Error ? e.message : 'Save failed.');\n if (client) {\n update.mutate(\n { id: client.id, input: payload },\n {\n onSuccess: () => {\n emitFact('psw.client.updated', { clientId: client.id });\n onSaved(payload.name);\n onClose();\n },\n onError,\n }\n );\n } else {\n create.mutate(payload, {\n onSuccess: (created) => {\n emitFact('psw.client.created', { clientId: created.pswCreateClient.id });\n onSaved(payload.name);\n onClose();\n },\n onError,\n });\n }\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title={client ? 'Edit client' : 'New client'}\n footer={\n <>\n <Button variant=\"outline\" onClick={onClose} disabled={pending}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={pending}>\n {pending ? 'Saving…' : client ? 'Save changes' : 'Create client'}\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=\"Acme Holdings Ltd.\" />\n </Field>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"Type\">\n <NativeSelect value={form.type} onChange={set('type')}>\n {enumOptions(CLIENT_TYPES)}\n </NativeSelect>\n </Field>\n <Field label=\"Status\">\n <NativeSelect value={form.status} onChange={set('status')}>\n {enumOptions(CLIENT_STATUSES)}\n </NativeSelect>\n </Field>\n </div>\n <Field label=\"Industry\">\n <TextInput\n value={form.industry}\n onChange={set('industry')}\n placeholder=\"Financial services\"\n />\n </Field>\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n <Field label=\"Contact name\">\n <TextInput value={form.contactName} onChange={set('contactName')} />\n </Field>\n <Field label=\"Contact email\">\n <TextInput type=\"email\" value={form.contactEmail} onChange={set('contactEmail')} />\n </Field>\n </div>\n <Field label=\"Contact phone\">\n <TextInput value={form.contactPhone} onChange={set('contactPhone')} />\n </Field>\n <Field label=\"Notes\">\n <TextArea value={form.notes} onChange={set('notes')} />\n </Field>\n </div>\n </Modal>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ClientsPage.js","names":[],"sources":["../../../src/proserviceworld/pages/ClientsPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useMemo, useState } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Plus, Search, Users, Pencil, Archive, Building2, User, ChevronRight } from 'lucide-react';\nimport {\n useClients,\n useCreateClient,\n useUpdateClient,\n useArchiveClient,\n type PswClient,\n type PswClientStatus,\n} from '../hooks/useProServiceWorldApi';\nimport { useNav } from '../utils/nav';\nimport { cn } from '../utils/cn';\nimport { useAnalytics } from '../hooks/useAnalytics';\nimport { useTr } from '../hooks/useTr';\nimport { impactFeedback } from '../utils/native';\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 TableWrap,\n Th,\n Td,\n enumOptions,\n fmtDate,\n} from '../components/ui';\nimport { TagPicker } from '../components/TagPicker';\nimport { LoadMore } from '../components/LoadMore';\n\nconst CLIENT_TYPES = ['ORGANIZATION', 'INDIVIDUAL'] as const;\nconst CLIENT_STATUSES: PswClientStatus[] = ['PROSPECT', 'ACTIVE', 'INACTIVE', 'ARCHIVED'];\n\nconst initials = (name: string): string =>\n name\n .split(/\\s+/)\n .filter(Boolean)\n .slice(0, 2)\n .map((w) => w[0]?.toUpperCase() ?? '')\n .join('') || '?';\n\n/** Tinted initials avatar (semantic tokens only). */\nexport const Avatar: FC<{ name: string; className?: string }> = ({ name, className }) => (\n <span\n className={cn(\n 'flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent/10 text-xs font-semibold text-accent ring-1 ring-inset ring-accent/15',\n className\n )}\n aria-hidden\n >\n {initials(name)}\n </span>\n);\n\ninterface FormState {\n name: string;\n type: string;\n status: string;\n industry: string;\n contactName: string;\n contactEmail: string;\n contactPhone: string;\n notes: string;\n}\nconst emptyForm: FormState = {\n name: '',\n type: 'ORGANIZATION',\n status: 'PROSPECT',\n industry: '',\n contactName: '',\n contactEmail: '',\n contactPhone: '',\n notes: '',\n};\n\nconst PAGE_SIZE = 25;\n\nexport const ClientsPage: FC = () => {\n const nav = useNav();\n const tr = useTr();\n const { emitIntent, emitFact } = useAnalytics();\n const [search, setSearch] = useState('');\n const [statusFilter, setStatusFilter] = useState<PswClientStatus | ''>('');\n const [editing, setEditing] = useState<PswClient | null>(null);\n const [creating, setCreating] = useState(false);\n const [banner, setBanner] = useState<{ tone: 'success' | 'danger'; text: string } | null>(null);\n const [pagination, setPagination] = useState({ limit: PAGE_SIZE, offset: 0 });\n\n const { data, isLoading, error, refetch } = useClients({\n search: search.trim() || undefined,\n status: statusFilter || undefined,\n pagination,\n });\n const update = useUpdateClient();\n\n const allItems = useMemo(() => data?.items ?? [], [data]);\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title={tr('psw.clients.title', 'Clients')}\n subtitle={tr('psw.clients.subtitle', 'Every person or organization your firm serves.')}\n icon={<Users className=\"h-6 w-6\" />}\n actions={\n <Button\n onClick={() => {\n void impactFeedback('light');\n emitIntent('psw.client.create_clicked');\n setCreating(true);\n }}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" /> {tr('psw.clients.new', 'New client')}\n </Button>\n }\n />\n\n {banner && (\n <Banner tone={banner.tone} onClose={() => setBanner(null)}>\n {banner.text}\n </Banner>\n )}\n\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n <div className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary\" />\n <TextInput\n placeholder={tr('psw.clients.searchPlaceholder', 'Search clients…')}\n value={search}\n onChange={(e) => {\n setSearch(e.target.value);\n setPagination({ limit: PAGE_SIZE, offset: 0 });\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter' && search.trim()) {\n emitFact('psw.search.performed', {\n entityType: 'client',\n queryLength: search.trim().length,\n });\n }\n }}\n className=\"pl-9\"\n />\n </div>\n <NativeSelect\n value={statusFilter}\n onChange={(e) => {\n setStatusFilter(e.target.value as PswClientStatus | '');\n setPagination({ limit: PAGE_SIZE, offset: 0 });\n }}\n className=\"sm:w-48\"\n >\n <option value=\"\">{tr('psw.clients.allStatuses', 'All statuses')}</option>\n {enumOptions(CLIENT_STATUSES)}\n </NativeSelect>\n </div>\n\n {isLoading ? (\n <LoadingBlock />\n ) : error ? (\n <ErrorState error={error} onRetry={() => void refetch()} />\n ) : !data || data.items.length === 0 ? (\n <Empty\n title={tr('psw.clients.emptyTitle', 'No clients yet')}\n description={tr(\n 'psw.clients.emptyDescription',\n 'Add your first client to start scoping engagements and matters.'\n )}\n icon={<Users className=\"h-8 w-8\" />}\n action={\n <Button onClick={() => setCreating(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> {tr('psw.clients.new', 'New client')}\n </Button>\n }\n />\n ) : (\n <SectionCard\n title={\n data.totalCount === 1\n ? tr('psw.clients.count', '{{count}} client', { count: data.totalCount })\n : tr('psw.clients.countPlural', '{{count}} clients', { count: data.totalCount })\n }\n >\n {/*\n Phones get a tappable card list: the desktop table carries seven\n columns behind a horizontal scroll, which hides the status and the\n row actions on a narrow viewport. Same fields, same affordances.\n */}\n <ul className=\"space-y-2.5 sm:hidden\">\n {data.items.map((c) => (\n <li\n key={c.id}\n className=\"overflow-hidden rounded-xl border border-border-seam bg-bg-canvas\"\n >\n <button\n type=\"button\"\n onClick={() => {\n void impactFeedback('light');\n nav(`clients/${c.id}`);\n }}\n className=\"flex w-full items-center gap-3 p-3.5 text-left transition-colors active:bg-bg-sunken/60\"\n >\n <Avatar name={c.name} className=\"h-11 w-11 text-sm\" />\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-semibold text-text-primary\">\n {c.name}\n </span>\n <span className=\"mt-0.5 block truncate text-sm text-text-secondary\">\n {c.contactName ||\n (c.type === 'ORGANIZATION'\n ? tr('psw.clients.typeOrganization', 'Organization')\n : tr('psw.clients.typeIndividual', 'Individual'))}\n </span>\n <span className=\"mt-1.5 flex items-center gap-2\">\n <span className=\"truncate text-xs text-text-tertiary\">\n {c.industry || fmtDate(c.createdAt)}\n </span>\n <StatusPill status={c.status} />\n </span>\n </span>\n <ChevronRight className=\"h-4 w-4 shrink-0 text-text-tertiary\" />\n </button>\n <div className=\"flex items-center justify-between gap-2 border-t border-border-seam px-3.5 py-2\">\n <div className=\"min-w-0 flex-1\">\n <TagPicker\n entityType=\"PswClient\"\n entityId={c.id}\n tags={c.tags ?? []}\n onChange={async (tags) => {\n await update.mutateAsync({ id: c.id, input: { tags } });\n void refetch();\n }}\n maxTags={5}\n />\n </div>\n <div className=\"flex shrink-0 gap-1\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setEditing(c)}\n aria-label={tr('psw.common.edit', 'Edit')}\n >\n <Pencil className=\"h-4 w-4\" />\n </Button>\n {c.status !== 'ARCHIVED' && (\n <ArchiveButton\n id={c.id}\n name={c.name}\n onDone={(ok) =>\n setBanner(\n ok\n ? {\n tone: 'success',\n text: tr('psw.clients.archived', '{{name}} archived.', {\n name: c.name,\n }),\n }\n : {\n tone: 'danger',\n text: tr(\n 'psw.clients.archiveFailed',\n 'Could not archive client.'\n ),\n }\n )\n }\n />\n )}\n </div>\n </div>\n </li>\n ))}\n </ul>\n\n <div className=\"hidden sm:block\">\n <TableWrap>\n <thead>\n <tr>\n <Th>{tr('psw.common.name', 'Name')}</Th>\n <Th>{tr('psw.common.type', 'Type')}</Th>\n <Th>{tr('psw.clients.tableIndustry', 'Industry')}</Th>\n <Th>{tr('psw.common.status', 'Status')}</Th>\n <Th>{tr('psw.common.added', 'Added')}</Th>\n <Th>{tr('psw.clients.tableTags', 'Tags')}</Th>\n <Th className=\"text-right\">{tr('psw.common.actions', 'Actions')}</Th>\n </tr>\n </thead>\n <tbody>\n {data.items.map((c) => (\n <tr\n key={c.id}\n className=\"group cursor-pointer transition-colors hover:bg-bg-sunken/60\"\n onClick={() => nav(`clients/${c.id}`)}\n >\n <Td>\n <div className=\"flex items-center gap-3\">\n <Avatar name={c.name} />\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-1 font-medium text-text-primary transition-colors group-hover:text-accent\">\n <span className=\"truncate\">{c.name}</span>\n <ChevronRight className=\"h-3.5 w-3.5 shrink-0 text-text-tertiary opacity-0 transition-opacity group-hover:opacity-100\" />\n </div>\n {c.contactName ? (\n <div className=\"truncate text-xs text-text-tertiary\">\n {c.contactName}\n </div>\n ) : null}\n </div>\n </div>\n </Td>\n <Td>\n <span className=\"inline-flex items-center gap-1.5 text-text-secondary\">\n {c.type === 'ORGANIZATION' ? (\n <Building2 className=\"h-3.5 w-3.5 text-text-tertiary\" />\n ) : (\n <User className=\"h-3.5 w-3.5 text-text-tertiary\" />\n )}\n {c.type === 'ORGANIZATION'\n ? tr('psw.clients.typeOrganization', 'Organization')\n : tr('psw.clients.typeIndividual', 'Individual')}\n </span>\n </Td>\n <Td>{c.industry || tr('psw.common.dash', '—')}</Td>\n <Td>\n <StatusPill status={c.status} />\n </Td>\n <Td className=\"whitespace-nowrap\">{fmtDate(c.createdAt)}</Td>\n <Td>\n <div onClick={(ev) => ev.stopPropagation()} role=\"presentation\">\n <TagPicker\n entityType=\"PswClient\"\n entityId={c.id}\n tags={c.tags ?? []}\n onChange={async (tags) => {\n await update.mutateAsync({ id: c.id, input: { tags } });\n void refetch();\n }}\n maxTags={5}\n />\n </div>\n </Td>\n <Td className=\"text-right\">\n <div\n className=\"flex justify-end gap-1\"\n onClick={(ev) => ev.stopPropagation()}\n role=\"presentation\"\n >\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setEditing(c)}\n aria-label={tr('psw.common.edit', 'Edit')}\n >\n <Pencil className=\"h-4 w-4\" />\n </Button>\n {c.status !== 'ARCHIVED' && (\n <ArchiveButton\n id={c.id}\n name={c.name}\n onDone={(ok) =>\n setBanner(\n ok\n ? {\n tone: 'success',\n text: tr('psw.clients.archived', '{{name}} archived.', {\n name: c.name,\n }),\n }\n : {\n tone: 'danger',\n text: tr(\n 'psw.clients.archiveFailed',\n 'Could not archive client.'\n ),\n }\n )\n }\n />\n )}\n </div>\n </Td>\n </tr>\n ))}\n </tbody>\n </TableWrap>\n </div>\n <LoadMore\n hasMore={data?.pageInfo?.hasMore ?? false}\n loading={isLoading}\n loadedCount={allItems.length}\n totalCount={data?.totalCount ?? allItems.length}\n onLoadMore={() =>\n setPagination((p) => ({ limit: PAGE_SIZE, offset: p.offset + PAGE_SIZE }))\n }\n />\n </SectionCard>\n )}\n\n {creating && (\n <ClientModal\n onClose={() => setCreating(false)}\n onSaved={(name) =>\n setBanner({\n tone: 'success',\n text: tr('psw.clients.created', '{{name}} created.', { name }),\n })\n }\n />\n )}\n {editing && (\n <ClientModal\n client={editing}\n onClose={() => setEditing(null)}\n onSaved={(name) =>\n setBanner({\n tone: 'success',\n text: tr('psw.clients.updated', '{{name}} updated.', { name }),\n })\n }\n />\n )}\n </div>\n );\n};\n\nconst ArchiveButton: FC<{ id: string; name: string; onDone: (ok: boolean) => void }> = ({\n id,\n name,\n onDone,\n}) => {\n const archive = useArchiveClient();\n const tr = useTr();\n const { emitFact } = useAnalytics();\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n aria-label={tr('psw.common.archive', 'Archive')}\n disabled={archive.isPending}\n onClick={() => {\n if (\n !window.confirm(\n tr(\n 'psw.clients.archiveConfirm',\n 'Archive this client? Their engagements and matters remain.'\n )\n )\n )\n return;\n archive.mutate(id, {\n onSuccess: (r) => {\n emitFact('psw.client.archived', { clientId: id });\n onDone(Boolean(r));\n },\n onError: () => onDone(false),\n });\n }}\n >\n <Archive className=\"h-4 w-4\" />\n </Button>\n );\n};\n\nconst ClientModal: FC<{\n client?: PswClient;\n onClose: () => void;\n onSaved: (name: string) => void;\n}> = ({ client, onClose, onSaved }) => {\n const tr = useTr();\n const create = useCreateClient();\n const update = useUpdateClient();\n const { emitFact } = useAnalytics();\n const [form, setForm] = useState<FormState>(\n client\n ? {\n name: client.name,\n type: client.type,\n status: client.status,\n industry: client.industry ?? '',\n contactName: client.contactName ?? '',\n contactEmail: client.contactEmail ?? '',\n contactPhone: client.contactPhone ?? '',\n notes: client.notes ?? '',\n }\n : emptyForm\n );\n const [err, setErr] = useState<string | null>(null);\n const set = (k: keyof FormState) => (e: { target: { value: string } }) =>\n setForm((f) => ({ ...f, [k]: e.target.value }));\n const pending = create.isPending || update.isPending;\n\n const payload = useMemo(\n () => ({\n name: form.name.trim(),\n type: form.type,\n status: form.status,\n industry: form.industry.trim() || null,\n contactName: form.contactName.trim() || null,\n contactEmail: form.contactEmail.trim() || null,\n contactPhone: form.contactPhone.trim() || null,\n notes: form.notes.trim() || null,\n }),\n [form]\n );\n\n const submit = () => {\n setErr(null);\n if (!payload.name) return setErr(tr('psw.clients.nameRequired', 'Client name is required.'));\n const onError = (e: unknown) =>\n setErr(e instanceof Error ? e.message : tr('psw.clients.saveFailed', 'Save failed.'));\n if (client) {\n update.mutate(\n { id: client.id, input: payload },\n {\n onSuccess: () => {\n emitFact('psw.client.updated', { clientId: client.id });\n onSaved(payload.name);\n onClose();\n },\n onError,\n }\n );\n } else {\n create.mutate(payload, {\n onSuccess: (created) => {\n emitFact('psw.client.created', { clientId: created.pswCreateClient.id });\n onSaved(payload.name);\n onClose();\n },\n onError,\n });\n }\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title={\n client ? tr('psw.clients.edit', 'Edit client') : tr('psw.clients.newTitle', 'New client')\n }\n footer={\n <>\n <Button variant=\"outline\" onClick={onClose} disabled={pending}>\n {tr('psw.common.cancel', 'Cancel')}\n </Button>\n <Button onClick={submit} disabled={pending}>\n {pending\n ? tr('psw.common.saving', 'Saving…')\n : client\n ? tr('psw.common.save', 'Save changes')\n : tr('psw.common.create', 'Create client')}\n </Button>\n </>\n }\n >\n <div className=\"space-y-3\">\n {err && <Banner tone=\"danger\">{err}</Banner>}\n <Field label={tr('psw.common.name', 'Name')} required>\n <TextInput value={form.name} onChange={set('name')} placeholder=\"Acme Holdings Ltd.\" />\n </Field>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label={tr('psw.common.type', 'Type')}>\n <NativeSelect value={form.type} onChange={set('type')}>\n {enumOptions(CLIENT_TYPES)}\n </NativeSelect>\n </Field>\n <Field label={tr('psw.common.status', 'Status')}>\n <NativeSelect value={form.status} onChange={set('status')}>\n {enumOptions(CLIENT_STATUSES)}\n </NativeSelect>\n </Field>\n </div>\n <Field label={tr('psw.clients.industry', 'Industry')}>\n <TextInput\n value={form.industry}\n onChange={set('industry')}\n placeholder=\"Financial services\"\n />\n </Field>\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n <Field label={tr('psw.clients.contactName', 'Contact name')}>\n <TextInput value={form.contactName} onChange={set('contactName')} />\n </Field>\n <Field label={tr('psw.clients.contactEmail', 'Contact email')}>\n <TextInput type=\"email\" value={form.contactEmail} onChange={set('contactEmail')} />\n </Field>\n </div>\n <Field label={tr('psw.clients.contactPhone', 'Contact phone')}>\n <TextInput value={form.contactPhone} onChange={set('contactPhone')} />\n </Field>\n <Field label={tr('psw.clients.notes', 'Notes')}>\n <TextArea value={form.notes} onChange={set('notes')} />\n </Field>\n </div>\n </Modal>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardPage.d.ts","sourceRoot":"","sources":["../../../src/proserviceworld/pages/DashboardPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAa,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"DashboardPage.d.ts","sourceRoot":"","sources":["../../../src/proserviceworld/pages/DashboardPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAa,MAAM,OAAO,CAAC;AA6C3C,eAAO,MAAM,aAAa,EAAE,EAmI3B,CAAC"}
|
|
@@ -1,271 +1,281 @@
|
|
|
1
1
|
import { useDashboardStats as e } from "../hooks/useProServiceWorldApi.js";
|
|
2
2
|
import { useNav as t } from "../utils/nav.js";
|
|
3
3
|
import { cn as n } from "../utils/cn.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { useTr as r } from "../hooks/useTr.js";
|
|
5
|
+
import { ErrorState as i, LoadingBlock as a, MetricCard as o, PageHeader as s, SectionCard as c, StatusPill as l, fmtHours as u, fmtMoney as d } from "../components/ui.js";
|
|
6
|
+
import { ArrowRight as f, BarChart3 as p, Briefcase as m, Clock as h, FileText as g, FolderKanban as _, GitBranch as v, Network as y, Receipt as b, Scale as x, ShieldCheck as S, Users as C, Wallet as w } from "lucide-react";
|
|
7
|
+
import { jsx as T, jsxs as E } from "react/jsx-runtime";
|
|
7
8
|
//#region src/proserviceworld/pages/DashboardPage.tsx
|
|
8
|
-
var
|
|
9
|
+
var D = [
|
|
9
10
|
{
|
|
10
|
-
|
|
11
|
+
key: "clients",
|
|
11
12
|
to: "clients",
|
|
12
|
-
icon: /* @__PURE__ */
|
|
13
|
+
icon: /* @__PURE__ */ T(C, { className: "h-4 w-4" })
|
|
13
14
|
},
|
|
14
15
|
{
|
|
15
|
-
|
|
16
|
+
key: "engagements",
|
|
16
17
|
to: "engagements",
|
|
17
|
-
icon: /* @__PURE__ */
|
|
18
|
+
icon: /* @__PURE__ */ T(m, { className: "h-4 w-4" })
|
|
18
19
|
},
|
|
19
20
|
{
|
|
20
|
-
|
|
21
|
+
key: "matters",
|
|
21
22
|
to: "matters",
|
|
22
|
-
icon: /* @__PURE__ */
|
|
23
|
+
icon: /* @__PURE__ */ T(_, { className: "h-4 w-4" })
|
|
23
24
|
},
|
|
24
25
|
{
|
|
25
|
-
|
|
26
|
+
key: "time",
|
|
26
27
|
to: "time",
|
|
27
|
-
icon: /* @__PURE__ */
|
|
28
|
+
icon: /* @__PURE__ */ T(h, { className: "h-4 w-4" })
|
|
28
29
|
},
|
|
29
30
|
{
|
|
30
|
-
|
|
31
|
+
key: "invoices",
|
|
31
32
|
to: "invoices",
|
|
32
|
-
icon: /* @__PURE__ */
|
|
33
|
+
icon: /* @__PURE__ */ T(b, { className: "h-4 w-4" })
|
|
33
34
|
},
|
|
34
35
|
{
|
|
35
|
-
|
|
36
|
+
key: "trust",
|
|
36
37
|
to: "trust",
|
|
37
|
-
icon: /* @__PURE__ */ w
|
|
38
|
+
icon: /* @__PURE__ */ T(w, { className: "h-4 w-4" })
|
|
38
39
|
},
|
|
39
40
|
{
|
|
40
|
-
|
|
41
|
+
key: "compliance",
|
|
41
42
|
to: "compliance",
|
|
42
|
-
icon: /* @__PURE__ */
|
|
43
|
+
icon: /* @__PURE__ */ T(S, { className: "h-4 w-4" })
|
|
43
44
|
},
|
|
44
45
|
{
|
|
45
|
-
|
|
46
|
+
key: "conflicts",
|
|
46
47
|
to: "conflicts",
|
|
47
|
-
icon: /* @__PURE__ */
|
|
48
|
+
icon: /* @__PURE__ */ T(x, { className: "h-4 w-4" })
|
|
48
49
|
},
|
|
49
50
|
{
|
|
50
|
-
|
|
51
|
+
key: "documents",
|
|
51
52
|
to: "documents",
|
|
52
|
-
icon: /* @__PURE__ */
|
|
53
|
+
icon: /* @__PURE__ */ T(g, { className: "h-4 w-4" })
|
|
53
54
|
},
|
|
54
55
|
{
|
|
55
|
-
|
|
56
|
+
key: "practice-areas",
|
|
56
57
|
to: "practice-areas",
|
|
57
|
-
icon: /* @__PURE__ */
|
|
58
|
+
icon: /* @__PURE__ */ T(y, { className: "h-4 w-4" })
|
|
58
59
|
},
|
|
59
60
|
{
|
|
60
|
-
|
|
61
|
+
key: "analytics",
|
|
61
62
|
to: "analytics",
|
|
62
|
-
icon: /* @__PURE__ */
|
|
63
|
+
icon: /* @__PURE__ */ T(p, { className: "h-4 w-4" })
|
|
63
64
|
}
|
|
64
|
-
],
|
|
65
|
-
let
|
|
66
|
-
return
|
|
67
|
-
error:
|
|
68
|
-
onRetry: () => void
|
|
69
|
-
}) :
|
|
65
|
+
], O = () => {
|
|
66
|
+
let l = t(), p = r(), { data: g, isLoading: y, error: x, refetch: O } = e();
|
|
67
|
+
return y ? /* @__PURE__ */ T(a, { label: p("psw.dashboard.loading", "Loading practice overview…") }) : x ? /* @__PURE__ */ T(i, {
|
|
68
|
+
error: x,
|
|
69
|
+
onRetry: () => void O()
|
|
70
|
+
}) : g ? /* @__PURE__ */ E("div", {
|
|
70
71
|
className: "space-y-6",
|
|
71
72
|
children: [
|
|
72
|
-
/* @__PURE__ */
|
|
73
|
-
title: "Practice overview",
|
|
74
|
-
subtitle: "One governed view of clients, engagements, matters, time and billing across your firm."
|
|
73
|
+
/* @__PURE__ */ T(s, {
|
|
74
|
+
title: p("psw.dashboard.title", "Practice overview"),
|
|
75
|
+
subtitle: p("psw.dashboard.subtitle", "One governed view of clients, engagements, matters, time and billing across your firm.")
|
|
75
76
|
}),
|
|
76
|
-
/* @__PURE__ */
|
|
77
|
+
/* @__PURE__ */ E("div", {
|
|
77
78
|
className: "grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4",
|
|
78
79
|
children: [
|
|
79
|
-
/* @__PURE__ */
|
|
80
|
-
label: "Active clients",
|
|
81
|
-
value:
|
|
82
|
-
hint:
|
|
83
|
-
icon: /* @__PURE__ */
|
|
80
|
+
/* @__PURE__ */ T(o, {
|
|
81
|
+
label: p("psw.dashboard.activeClients", "Active clients"),
|
|
82
|
+
value: g.activeClients,
|
|
83
|
+
hint: p("psw.dashboard.totalClients", "{{count}} total", { count: g.totalClients }),
|
|
84
|
+
icon: /* @__PURE__ */ T(C, { className: "h-4 w-4" }),
|
|
84
85
|
tone: "success"
|
|
85
86
|
}),
|
|
86
|
-
/* @__PURE__ */
|
|
87
|
-
label: "Active engagements",
|
|
88
|
-
value:
|
|
89
|
-
icon: /* @__PURE__ */
|
|
87
|
+
/* @__PURE__ */ T(o, {
|
|
88
|
+
label: p("psw.dashboard.activeEngagements", "Active engagements"),
|
|
89
|
+
value: g.activeEngagements,
|
|
90
|
+
icon: /* @__PURE__ */ T(m, { className: "h-4 w-4" }),
|
|
90
91
|
tone: "accent"
|
|
91
92
|
}),
|
|
92
|
-
/* @__PURE__ */
|
|
93
|
-
label: "Open matters",
|
|
94
|
-
value:
|
|
95
|
-
icon: /* @__PURE__ */
|
|
93
|
+
/* @__PURE__ */ T(o, {
|
|
94
|
+
label: p("psw.dashboard.openMatters", "Open matters"),
|
|
95
|
+
value: g.openMatters,
|
|
96
|
+
icon: /* @__PURE__ */ T(_, { className: "h-4 w-4" }),
|
|
96
97
|
tone: "info"
|
|
97
98
|
}),
|
|
98
|
-
/* @__PURE__ */
|
|
99
|
-
label: "Unbilled hours",
|
|
100
|
-
value:
|
|
101
|
-
hint:
|
|
102
|
-
icon: /* @__PURE__ */
|
|
99
|
+
/* @__PURE__ */ T(o, {
|
|
100
|
+
label: p("psw.dashboard.unbilledHours", "Unbilled hours"),
|
|
101
|
+
value: u(g.unbilledHours),
|
|
102
|
+
hint: p("psw.dashboard.billedHours", "{{count}} billed", { count: u(g.billedHours) }),
|
|
103
|
+
icon: /* @__PURE__ */ T(h, { className: "h-4 w-4" }),
|
|
103
104
|
tone: "warning"
|
|
104
105
|
}),
|
|
105
|
-
/* @__PURE__ */
|
|
106
|
-
label: "Outstanding",
|
|
107
|
-
value:
|
|
108
|
-
hint: "Sent + overdue",
|
|
109
|
-
icon: /* @__PURE__ */
|
|
106
|
+
/* @__PURE__ */ T(o, {
|
|
107
|
+
label: p("psw.dashboard.outstanding", "Outstanding"),
|
|
108
|
+
value: d(g.outstandingInvoiceTotal),
|
|
109
|
+
hint: p("psw.dashboard.outstandingHint", "Sent + overdue"),
|
|
110
|
+
icon: /* @__PURE__ */ T(b, { className: "h-4 w-4" }),
|
|
110
111
|
tone: "warning"
|
|
111
112
|
}),
|
|
112
|
-
/* @__PURE__ */
|
|
113
|
-
label: "Collected",
|
|
114
|
-
value:
|
|
115
|
-
icon: /* @__PURE__ */
|
|
113
|
+
/* @__PURE__ */ T(o, {
|
|
114
|
+
label: p("psw.dashboard.collected", "Collected"),
|
|
115
|
+
value: d(g.paidInvoiceTotal),
|
|
116
|
+
icon: /* @__PURE__ */ T(b, { className: "h-4 w-4" }),
|
|
116
117
|
tone: "success"
|
|
117
118
|
}),
|
|
118
|
-
/* @__PURE__ */
|
|
119
|
-
label: "Trust held",
|
|
120
|
-
value:
|
|
121
|
-
hint:
|
|
122
|
-
icon: /* @__PURE__ */ w
|
|
119
|
+
/* @__PURE__ */ T(o, {
|
|
120
|
+
label: p("psw.dashboard.trustHeld", "Trust held"),
|
|
121
|
+
value: d(g.trustHeldTotal),
|
|
122
|
+
hint: g.activeTrustAccounts === 1 ? p("psw.dashboard.trustAccounts", "{{count}} active account", { count: g.activeTrustAccounts }) : p("psw.dashboard.trustAccountsPlural", "{{count}} active accounts", { count: g.activeTrustAccounts }),
|
|
123
|
+
icon: /* @__PURE__ */ T(w, { className: "h-4 w-4" }),
|
|
123
124
|
tone: "info"
|
|
124
125
|
}),
|
|
125
|
-
/* @__PURE__ */
|
|
126
|
-
label: "Compliance due",
|
|
127
|
-
value:
|
|
128
|
-
icon: /* @__PURE__ */
|
|
129
|
-
tone:
|
|
126
|
+
/* @__PURE__ */ T(o, {
|
|
127
|
+
label: p("psw.dashboard.complianceDue", "Compliance due"),
|
|
128
|
+
value: g.pendingComplianceChecks,
|
|
129
|
+
icon: /* @__PURE__ */ T(S, { className: "h-4 w-4" }),
|
|
130
|
+
tone: g.pendingComplianceChecks ? "warning" : "success"
|
|
130
131
|
}),
|
|
131
|
-
/* @__PURE__ */
|
|
132
|
-
label: "Open conflicts",
|
|
133
|
-
value:
|
|
134
|
-
icon: /* @__PURE__ */
|
|
135
|
-
tone:
|
|
132
|
+
/* @__PURE__ */ T(o, {
|
|
133
|
+
label: p("psw.dashboard.openConflicts", "Open conflicts"),
|
|
134
|
+
value: g.openConflictChecks,
|
|
135
|
+
icon: /* @__PURE__ */ T(v, { className: "h-4 w-4" }),
|
|
136
|
+
tone: g.openConflictChecks ? "danger" : "success"
|
|
136
137
|
})
|
|
137
138
|
]
|
|
138
139
|
}),
|
|
139
|
-
/* @__PURE__ */
|
|
140
|
+
/* @__PURE__ */ E("div", {
|
|
140
141
|
className: "grid grid-cols-1 gap-4 lg:grid-cols-3",
|
|
141
142
|
children: [
|
|
142
|
-
/* @__PURE__ */
|
|
143
|
-
title: "Engagements by status",
|
|
144
|
-
children: /* @__PURE__ */
|
|
143
|
+
/* @__PURE__ */ T(c, {
|
|
144
|
+
title: p("psw.dashboard.engagementsByStatus", "Engagements by status"),
|
|
145
|
+
children: /* @__PURE__ */ T(k, {
|
|
146
|
+
items: g.engagementsByStatus,
|
|
147
|
+
tr: p
|
|
148
|
+
})
|
|
145
149
|
}),
|
|
146
|
-
/* @__PURE__ */
|
|
147
|
-
title: "Matters by status",
|
|
148
|
-
children: /* @__PURE__ */
|
|
150
|
+
/* @__PURE__ */ T(c, {
|
|
151
|
+
title: p("psw.dashboard.mattersByStatus", "Matters by status"),
|
|
152
|
+
children: /* @__PURE__ */ T(k, {
|
|
153
|
+
items: g.mattersByStatus,
|
|
154
|
+
tr: p
|
|
155
|
+
})
|
|
149
156
|
}),
|
|
150
|
-
/* @__PURE__ */
|
|
151
|
-
title: "Revenue",
|
|
152
|
-
description: "Invoiced totals, recent months.",
|
|
153
|
-
children: /* @__PURE__ */
|
|
157
|
+
/* @__PURE__ */ T(c, {
|
|
158
|
+
title: p("psw.dashboard.revenue", "Revenue"),
|
|
159
|
+
description: p("psw.dashboard.revenueDescription", "Invoiced totals, recent months."),
|
|
160
|
+
children: /* @__PURE__ */ T(A, {
|
|
161
|
+
data: g.revenueByMonth,
|
|
162
|
+
tr: p
|
|
163
|
+
})
|
|
154
164
|
})
|
|
155
165
|
]
|
|
156
166
|
}),
|
|
157
|
-
/* @__PURE__ */
|
|
158
|
-
title: "Jump to",
|
|
159
|
-
children: /* @__PURE__ */
|
|
167
|
+
/* @__PURE__ */ T(c, {
|
|
168
|
+
title: p("psw.dashboard.jumpTo", "Jump to"),
|
|
169
|
+
children: /* @__PURE__ */ T("div", {
|
|
160
170
|
className: "grid grid-cols-1 gap-2.5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5",
|
|
161
|
-
children:
|
|
171
|
+
children: D.map((e) => /* @__PURE__ */ E("button", {
|
|
162
172
|
type: "button",
|
|
163
|
-
onClick: () =>
|
|
173
|
+
onClick: () => l(e.to),
|
|
164
174
|
className: n("group flex items-center gap-3 rounded-xl border border-border-seam bg-bg-canvas px-3.5 py-3 text-left", "transition hover:-translate-y-0.5 hover:border-accent hover:bg-bg-surface hover:shadow-sm"),
|
|
165
175
|
children: [
|
|
166
|
-
/* @__PURE__ */
|
|
176
|
+
/* @__PURE__ */ T("span", {
|
|
167
177
|
className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-bg-sunken text-accent transition group-hover:bg-accent/15",
|
|
168
178
|
children: e.icon
|
|
169
179
|
}),
|
|
170
|
-
/* @__PURE__ */
|
|
180
|
+
/* @__PURE__ */ T("span", {
|
|
171
181
|
className: "flex-1 truncate text-sm font-medium text-text-primary",
|
|
172
|
-
children: e.
|
|
182
|
+
children: p(`psw.${e.key}.title`, e.key)
|
|
173
183
|
}),
|
|
174
|
-
/* @__PURE__ */
|
|
184
|
+
/* @__PURE__ */ T(f, { className: "h-4 w-4 shrink-0 text-text-tertiary transition group-hover:translate-x-0.5 group-hover:text-accent" })
|
|
175
185
|
]
|
|
176
186
|
}, e.to))
|
|
177
187
|
})
|
|
178
188
|
})
|
|
179
189
|
]
|
|
180
190
|
}) : null;
|
|
181
|
-
},
|
|
182
|
-
if (!e.length) return /* @__PURE__ */
|
|
191
|
+
}, k = ({ items: e, tr: t }) => {
|
|
192
|
+
if (!e.length) return /* @__PURE__ */ T("p", {
|
|
183
193
|
className: "text-sm text-text-tertiary",
|
|
184
|
-
children: "No records yet."
|
|
194
|
+
children: t("psw.dashboard.noRecords", "No records yet.")
|
|
185
195
|
});
|
|
186
|
-
let
|
|
187
|
-
return /* @__PURE__ */
|
|
196
|
+
let r = Math.max(...e.map((e) => e.count), 1), i = e.reduce((e, t) => e + t.count, 0) || 1;
|
|
197
|
+
return /* @__PURE__ */ T("ul", {
|
|
188
198
|
className: "space-y-3.5",
|
|
189
|
-
children: e.map((e) => /* @__PURE__ */
|
|
199
|
+
children: e.map((e) => /* @__PURE__ */ E("li", {
|
|
190
200
|
className: "grid grid-cols-[7.5rem_minmax(0,1fr)_auto] items-center gap-3",
|
|
191
201
|
children: [
|
|
192
|
-
/* @__PURE__ */
|
|
202
|
+
/* @__PURE__ */ T(l, {
|
|
193
203
|
status: e.status,
|
|
194
204
|
className: "justify-self-start"
|
|
195
205
|
}),
|
|
196
|
-
/* @__PURE__ */
|
|
206
|
+
/* @__PURE__ */ T("div", {
|
|
197
207
|
className: "h-2.5 overflow-hidden rounded-full bg-bg-sunken",
|
|
198
|
-
children: /* @__PURE__ */
|
|
199
|
-
className: n("h-full rounded-full bg-gradient-to-r transition-all duration-500",
|
|
200
|
-
style: { width: `${Math.max(e.count /
|
|
208
|
+
children: /* @__PURE__ */ T("div", {
|
|
209
|
+
className: n("h-full rounded-full bg-gradient-to-r transition-all duration-500", N(e.status)),
|
|
210
|
+
style: { width: `${Math.max(e.count / r * 100, 4)}%` }
|
|
201
211
|
})
|
|
202
212
|
}),
|
|
203
|
-
/* @__PURE__ */
|
|
213
|
+
/* @__PURE__ */ E("span", {
|
|
204
214
|
className: "flex items-baseline justify-end gap-1.5 tabular-nums",
|
|
205
|
-
children: [/* @__PURE__ */
|
|
215
|
+
children: [/* @__PURE__ */ T("span", {
|
|
206
216
|
className: "text-sm font-semibold text-text-primary",
|
|
207
217
|
children: e.count
|
|
208
|
-
}), /* @__PURE__ */
|
|
218
|
+
}), /* @__PURE__ */ E("span", {
|
|
209
219
|
className: "text-xs text-text-tertiary",
|
|
210
|
-
children: [Math.round(e.count /
|
|
220
|
+
children: [Math.round(e.count / i * 100), "%"]
|
|
211
221
|
})]
|
|
212
222
|
})
|
|
213
223
|
]
|
|
214
224
|
}, e.status))
|
|
215
225
|
});
|
|
216
|
-
},
|
|
217
|
-
if (!e.length) return /* @__PURE__ */
|
|
226
|
+
}, A = ({ data: e, tr: t }) => {
|
|
227
|
+
if (!e.length) return /* @__PURE__ */ T("p", {
|
|
218
228
|
className: "text-sm text-text-tertiary",
|
|
219
|
-
children: "No invoiced revenue yet."
|
|
229
|
+
children: t("psw.dashboard.noRevenue", "No invoiced revenue yet.")
|
|
220
230
|
});
|
|
221
|
-
let
|
|
231
|
+
let r = P(Math.max(...e.map((e) => e.amount), 1)), i = [
|
|
222
232
|
1,
|
|
223
233
|
.75,
|
|
224
234
|
.5,
|
|
225
235
|
.25,
|
|
226
236
|
0
|
|
227
|
-
].map((e) =>
|
|
228
|
-
return /* @__PURE__ */
|
|
237
|
+
].map((e) => r * e);
|
|
238
|
+
return /* @__PURE__ */ T("div", {
|
|
229
239
|
className: "overflow-x-auto",
|
|
230
|
-
children: /* @__PURE__ */
|
|
240
|
+
children: /* @__PURE__ */ E("div", {
|
|
231
241
|
className: "min-w-[260px]",
|
|
232
|
-
children: [/* @__PURE__ */
|
|
242
|
+
children: [/* @__PURE__ */ E("div", {
|
|
233
243
|
className: "relative h-44",
|
|
234
|
-
children: [
|
|
244
|
+
children: [i.map((e, t) => /* @__PURE__ */ E("div", {
|
|
235
245
|
className: "absolute inset-x-0 flex items-center gap-2",
|
|
236
|
-
style: { top: `${t / (
|
|
237
|
-
children: [/* @__PURE__ */
|
|
246
|
+
style: { top: `${t / (i.length - 1) * 100}%` },
|
|
247
|
+
children: [/* @__PURE__ */ T("span", {
|
|
238
248
|
className: "w-11 shrink-0 text-right text-[10px] leading-none tabular-nums text-text-tertiary",
|
|
239
|
-
children:
|
|
240
|
-
}), /* @__PURE__ */
|
|
241
|
-
}, e)), /* @__PURE__ */
|
|
249
|
+
children: F(e)
|
|
250
|
+
}), /* @__PURE__ */ T("span", { className: n("h-px flex-1", t === i.length - 1 ? "bg-border-seam" : "bg-border-seam/50") })]
|
|
251
|
+
}, e)), /* @__PURE__ */ T("div", {
|
|
242
252
|
className: "absolute inset-0 flex items-end gap-1.5 pl-[3.25rem]",
|
|
243
|
-
children: e.map((e) => /* @__PURE__ */
|
|
253
|
+
children: e.map((e) => /* @__PURE__ */ T("div", {
|
|
244
254
|
className: "group flex h-full flex-1 flex-col items-center justify-end",
|
|
245
|
-
children: /* @__PURE__ */
|
|
255
|
+
children: /* @__PURE__ */ T("div", {
|
|
246
256
|
className: "w-full max-w-[2rem] rounded-t-md bg-gradient-to-t from-accent/55 to-accent shadow-sm transition-all duration-200 group-hover:-translate-y-0.5 group-hover:shadow",
|
|
247
|
-
style: { height: `${Math.max(e.amount /
|
|
248
|
-
title:
|
|
257
|
+
style: { height: `${Math.max(e.amount / r * 100, 1.5)}%` },
|
|
258
|
+
title: d(e.amount)
|
|
249
259
|
})
|
|
250
260
|
}, e.month))
|
|
251
261
|
})]
|
|
252
|
-
}), /* @__PURE__ */
|
|
262
|
+
}), /* @__PURE__ */ T("div", {
|
|
253
263
|
className: "mt-2 flex gap-1.5 pl-[3.25rem]",
|
|
254
|
-
children: e.map((e) => /* @__PURE__ */
|
|
264
|
+
children: e.map((e) => /* @__PURE__ */ T("span", {
|
|
255
265
|
className: "flex-1 text-center text-[10px] text-text-tertiary",
|
|
256
|
-
children:
|
|
266
|
+
children: I(e.month)
|
|
257
267
|
}, e.month))
|
|
258
268
|
})]
|
|
259
269
|
})
|
|
260
270
|
});
|
|
261
|
-
},
|
|
271
|
+
}, j = {
|
|
262
272
|
success: "from-status-success-text/55 to-status-success-text",
|
|
263
273
|
warning: "from-status-warning-text/55 to-status-warning-text",
|
|
264
274
|
danger: "from-status-error-text/55 to-status-error-text",
|
|
265
275
|
info: "from-text-link/55 to-text-link",
|
|
266
276
|
accent: "from-accent/55 to-accent",
|
|
267
277
|
neutral: "from-text-tertiary/30 to-text-tertiary/70"
|
|
268
|
-
},
|
|
278
|
+
}, M = {
|
|
269
279
|
ACTIVE: "success",
|
|
270
280
|
DELIVERED: "success",
|
|
271
281
|
INVOICED: "success",
|
|
@@ -305,24 +315,24 @@ var E = [
|
|
|
305
315
|
DRAFT: "neutral",
|
|
306
316
|
WAIVED: "neutral"
|
|
307
317
|
};
|
|
308
|
-
function M(e) {
|
|
309
|
-
return A[j[e] ?? "accent"];
|
|
310
|
-
}
|
|
311
318
|
function N(e) {
|
|
319
|
+
return j[M[e] ?? "accent"];
|
|
320
|
+
}
|
|
321
|
+
function P(e) {
|
|
312
322
|
if (e <= 0) return 1;
|
|
313
323
|
let t = 10 ** Math.floor(Math.log10(e)), n = e / t;
|
|
314
324
|
return (n <= 1 ? 1 : n <= 2 ? 2 : n <= 2.5 ? 2.5 : n <= 5 ? 5 : 10) * t;
|
|
315
325
|
}
|
|
316
|
-
function
|
|
326
|
+
function F(e) {
|
|
317
327
|
return e >= 1e6 ? `$${(e / 1e6).toFixed(e % 1e6 == 0 ? 0 : 1)}M` : e >= 1e3 ? `$${Math.round(e / 1e3)}k` : `$${Math.round(e)}`;
|
|
318
328
|
}
|
|
319
|
-
function
|
|
329
|
+
function I(e) {
|
|
320
330
|
let [t, n] = e.split("-").map(Number);
|
|
321
331
|
if (!t || !n) return e;
|
|
322
332
|
let r = new Date(t, n - 1, 1);
|
|
323
333
|
return Number.isNaN(r.getTime()) ? e : r.toLocaleDateString(void 0, { month: "short" });
|
|
324
334
|
}
|
|
325
335
|
//#endregion
|
|
326
|
-
export {
|
|
336
|
+
export { O as DashboardPage };
|
|
327
337
|
|
|
328
338
|
//# sourceMappingURL=DashboardPage.js.map
|