@burdenoff/microfe-proserviceworld 2026.1026.2 → 2026.1031.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/proserviceworld/components/CredentialDirectory.js +44 -43
- package/dist/proserviceworld/components/CredentialDirectory.js.map +1 -1
- package/dist/proserviceworld/components/CredentialReminderPanel.js +5 -4
- package/dist/proserviceworld/components/CredentialReminderPanel.js.map +1 -1
- package/dist/proserviceworld/components/ui.d.ts +7 -3
- package/dist/proserviceworld/components/ui.d.ts.map +1 -1
- package/dist/proserviceworld/components/ui.js +135 -121
- package/dist/proserviceworld/components/ui.js.map +1 -1
- package/dist/proserviceworld/hooks/useProServiceWorldApi.d.ts +28 -0
- package/dist/proserviceworld/hooks/useProServiceWorldApi.d.ts.map +1 -1
- package/dist/proserviceworld/hooks/useProServiceWorldApi.js +95 -73
- package/dist/proserviceworld/hooks/useProServiceWorldApi.js.map +1 -1
- package/dist/proserviceworld/hooks/useTr.d.ts +49 -8
- package/dist/proserviceworld/hooks/useTr.d.ts.map +1 -1
- package/dist/proserviceworld/hooks/useTr.js +45 -21
- package/dist/proserviceworld/hooks/useTr.js.map +1 -1
- package/dist/proserviceworld/i18n/catalog/analytics.d.ts +5 -0
- package/dist/proserviceworld/i18n/catalog/analytics.d.ts.map +1 -0
- package/dist/proserviceworld/i18n/catalog/analytics.js +271 -0
- package/dist/proserviceworld/i18n/catalog/analytics.js.map +1 -0
- package/dist/proserviceworld/i18n/catalog/common.d.ts +9 -0
- package/dist/proserviceworld/i18n/catalog/common.d.ts.map +1 -0
- package/dist/proserviceworld/i18n/catalog/common.js +163 -0
- package/dist/proserviceworld/i18n/catalog/common.js.map +1 -0
- package/dist/proserviceworld/i18n/catalog/compliance.d.ts +5 -0
- package/dist/proserviceworld/i18n/catalog/compliance.d.ts.map +1 -0
- package/dist/proserviceworld/i18n/catalog/compliance.js +157 -0
- package/dist/proserviceworld/i18n/catalog/compliance.js.map +1 -0
- package/dist/proserviceworld/i18n/catalog/conflicts.d.ts +5 -0
- package/dist/proserviceworld/i18n/catalog/conflicts.d.ts.map +1 -0
- package/dist/proserviceworld/i18n/catalog/conflicts.js +145 -0
- package/dist/proserviceworld/i18n/catalog/conflicts.js.map +1 -0
- package/dist/proserviceworld/i18n/catalog/documents.d.ts +5 -0
- package/dist/proserviceworld/i18n/catalog/documents.d.ts.map +1 -0
- package/dist/proserviceworld/i18n/catalog/documents.js +157 -0
- package/dist/proserviceworld/i18n/catalog/documents.js.map +1 -0
- package/dist/proserviceworld/i18n/catalog/enums.d.ts +10 -0
- package/dist/proserviceworld/i18n/catalog/enums.d.ts.map +1 -0
- package/dist/proserviceworld/i18n/catalog/enums.js +481 -0
- package/dist/proserviceworld/i18n/catalog/enums.js.map +1 -0
- package/dist/proserviceworld/i18n/catalog/index.d.ts +9 -0
- package/dist/proserviceworld/i18n/catalog/index.d.ts.map +1 -0
- package/dist/proserviceworld/i18n/catalog/index.js +25 -0
- package/dist/proserviceworld/i18n/catalog/index.js.map +1 -0
- package/dist/proserviceworld/i18n/catalog/invoices.d.ts +5 -0
- package/dist/proserviceworld/i18n/catalog/invoices.d.ts.map +1 -0
- package/dist/proserviceworld/i18n/catalog/invoices.js +247 -0
- package/dist/proserviceworld/i18n/catalog/invoices.js.map +1 -0
- package/dist/proserviceworld/i18n/catalog/practiceAreas.d.ts +5 -0
- package/dist/proserviceworld/i18n/catalog/practiceAreas.d.ts.map +1 -0
- package/dist/proserviceworld/i18n/catalog/practiceAreas.js +97 -0
- package/dist/proserviceworld/i18n/catalog/practiceAreas.js.map +1 -0
- package/dist/proserviceworld/i18n/catalog/time.d.ts +5 -0
- package/dist/proserviceworld/i18n/catalog/time.d.ts.map +1 -0
- package/dist/proserviceworld/i18n/catalog/time.js +301 -0
- package/dist/proserviceworld/i18n/catalog/time.js.map +1 -0
- package/dist/proserviceworld/i18n/index.d.ts +10 -9
- package/dist/proserviceworld/i18n/index.d.ts.map +1 -1
- package/dist/proserviceworld/i18n/index.js +16 -12
- package/dist/proserviceworld/i18n/index.js.map +1 -1
- package/dist/proserviceworld/i18n/types.d.ts +13 -0
- package/dist/proserviceworld/i18n/types.d.ts.map +1 -0
- package/dist/proserviceworld/i18n/types.js +18 -0
- package/dist/proserviceworld/i18n/types.js.map +1 -0
- package/dist/proserviceworld/pages/AnalyticsPage.d.ts.map +1 -1
- package/dist/proserviceworld/pages/AnalyticsPage.js +236 -152
- package/dist/proserviceworld/pages/AnalyticsPage.js.map +1 -1
- package/dist/proserviceworld/pages/ClientDetailPage.js +99 -98
- package/dist/proserviceworld/pages/ClientDetailPage.js.map +1 -1
- package/dist/proserviceworld/pages/ClientsPage.d.ts.map +1 -1
- package/dist/proserviceworld/pages/ClientsPage.js +181 -181
- package/dist/proserviceworld/pages/ClientsPage.js.map +1 -1
- package/dist/proserviceworld/pages/CompliancePage.d.ts.map +1 -1
- package/dist/proserviceworld/pages/CompliancePage.js +210 -209
- package/dist/proserviceworld/pages/CompliancePage.js.map +1 -1
- package/dist/proserviceworld/pages/ConflictsPage.d.ts.map +1 -1
- package/dist/proserviceworld/pages/ConflictsPage.js +139 -138
- package/dist/proserviceworld/pages/ConflictsPage.js.map +1 -1
- package/dist/proserviceworld/pages/DocumentsPage.d.ts.map +1 -1
- package/dist/proserviceworld/pages/DocumentsPage.js +150 -149
- package/dist/proserviceworld/pages/DocumentsPage.js.map +1 -1
- package/dist/proserviceworld/pages/EngagementDetailPage.js +105 -104
- package/dist/proserviceworld/pages/EngagementDetailPage.js.map +1 -1
- package/dist/proserviceworld/pages/EngagementsPage.d.ts.map +1 -1
- package/dist/proserviceworld/pages/EngagementsPage.js +137 -137
- package/dist/proserviceworld/pages/EngagementsPage.js.map +1 -1
- package/dist/proserviceworld/pages/InvoicesPage.d.ts.map +1 -1
- package/dist/proserviceworld/pages/InvoicesPage.js +275 -258
- package/dist/proserviceworld/pages/InvoicesPage.js.map +1 -1
- package/dist/proserviceworld/pages/MattersPage.d.ts.map +1 -1
- package/dist/proserviceworld/pages/MattersPage.js +166 -166
- package/dist/proserviceworld/pages/MattersPage.js.map +1 -1
- package/dist/proserviceworld/pages/PracticeAreasPage.d.ts.map +1 -1
- package/dist/proserviceworld/pages/PracticeAreasPage.js +100 -99
- package/dist/proserviceworld/pages/PracticeAreasPage.js.map +1 -1
- package/dist/proserviceworld/pages/PractitionerCredentialsPage.js +30 -29
- package/dist/proserviceworld/pages/PractitionerCredentialsPage.js.map +1 -1
- package/dist/proserviceworld/pages/TimePage.d.ts.map +1 -1
- package/dist/proserviceworld/pages/TimePage.js +343 -350
- package/dist/proserviceworld/pages/TimePage.js.map +1 -1
- package/dist/proserviceworld/utils/humanize.d.ts +3 -0
- package/dist/proserviceworld/utils/humanize.d.ts.map +1 -0
- package/dist/proserviceworld/utils/humanize.js +6 -0
- package/dist/proserviceworld/utils/humanize.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../src/proserviceworld/pages/AnalyticsPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { BarChart3, TrendingUp, Clock, Wallet } from 'lucide-react';\nimport { useDashboardStats } from '../hooks/useProServiceWorldApi';\nimport { cn } from '../utils/cn';\nimport {\n PageHeader,\n SectionCard,\n MetricCard,\n LoadingBlock,\n ErrorState,\n StatusPill,\n fmtMoney,\n fmtHours,\n} from '../components/ui';\n\nexport const AnalyticsPage: FC = () => {\n const { data, isLoading, error, refetch } = useDashboardStats();\n if (isLoading) return <LoadingBlock label=\"Crunching numbers…\" />;\n if (error) return <ErrorState error={error} onRetry={() => void refetch()} />;\n if (!data) return null;\n\n const totalHours = data.unbilledHours + data.billedHours;\n const utilization = totalHours > 0 ? Math.round((data.billedHours / totalHours) * 100) : 0;\n const billableBase = data.paidInvoiceTotal + data.outstandingInvoiceTotal;\n const collectionRate =\n billableBase > 0 ? Math.round((data.paidInvoiceTotal / billableBase) * 100) : 0;\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Analytics\"\n subtitle=\"Real, live metrics across the practice — no estimates, straight from your records.\"\n icon={<BarChart3 className=\"h-6 w-6\" />}\n />\n\n <div className=\"grid grid-cols-2 gap-3 lg:grid-cols-4\">\n <MetricCard\n label=\"Collected revenue\"\n value={fmtMoney(data.paidInvoiceTotal)}\n hint={`${fmtMoney(billableBase)} invoiced`}\n icon={<Wallet className=\"h-4 w-4\" />}\n tone=\"success\"\n />\n <MetricCard\n label=\"Collection rate\"\n value={`${collectionRate}%`}\n hint={`${fmtMoney(data.outstandingInvoiceTotal)} outstanding`}\n icon={<TrendingUp className=\"h-4 w-4\" />}\n tone=\"accent\"\n />\n <MetricCard\n label=\"Utilization\"\n value={`${utilization}%`}\n hint={`${fmtHours(data.billedHours)} of ${fmtHours(totalHours)}`}\n icon={<Clock className=\"h-4 w-4\" />}\n tone=\"info\"\n />\n <MetricCard\n label=\"Active engagements\"\n value={data.activeEngagements}\n hint={`${data.openMatters} open matters`}\n icon={<BarChart3 className=\"h-4 w-4\" />}\n tone=\"warning\"\n />\n </div>\n\n {/* Practice-health meters — collection & utilization at a glance */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <SectionCard title=\"Collection rate\" description=\"Paid vs. total invoiced.\">\n <GaugeRow\n pct={collectionRate}\n tone=\"accent\"\n primary={fmtMoney(data.paidInvoiceTotal)}\n primaryLabel=\"Collected\"\n secondary={fmtMoney(data.outstandingInvoiceTotal)}\n secondaryLabel=\"Outstanding\"\n />\n </SectionCard>\n <SectionCard title=\"Utilization\" description=\"Billed vs. total logged hours.\">\n <GaugeRow\n pct={utilization}\n tone=\"info\"\n primary={fmtHours(data.billedHours)}\n primaryLabel=\"Billed\"\n secondary={fmtHours(data.unbilledHours)}\n secondaryLabel=\"Unbilled\"\n />\n </SectionCard>\n </div>\n\n <SectionCard title=\"Revenue by month\" description=\"Invoiced totals across recent months.\">\n <RevenueChart data={data.revenueByMonth} />\n </SectionCard>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <SectionCard title=\"Engagements\" description=\"By status\">\n <Distribution items={data.engagementsByStatus} />\n </SectionCard>\n <SectionCard title=\"Matters\" description=\"By status\">\n <Distribution items={data.mattersByStatus} />\n </SectionCard>\n <SectionCard title=\"Invoices\" description=\"By status\">\n <Distribution items={data.invoicesByStatus} />\n </SectionCard>\n </div>\n\n <SectionCard title=\"Time & billing\" description=\"Billable capacity across the practice.\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <HoursBar\n label=\"Billed hours\"\n value={data.billedHours}\n total={totalHours}\n tone=\"accent\"\n />\n <HoursBar\n label=\"Unbilled hours\"\n value={data.unbilledHours}\n total={totalHours}\n tone=\"warning\"\n />\n </div>\n </SectionCard>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Radial gauge with a supporting two-number legend\n// ---------------------------------------------------------------------------\nconst GaugeRow: FC<{\n pct: number;\n tone: 'accent' | 'info';\n primary: string;\n primaryLabel: string;\n secondary: string;\n secondaryLabel: string;\n}> = ({ pct, tone, primary, primaryLabel, secondary, secondaryLabel }) => (\n <div className=\"flex items-center gap-5\">\n <Gauge pct={pct} tone={tone} />\n <div className=\"flex flex-1 flex-col gap-2.5\">\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"text-xs font-medium uppercase tracking-wider text-text-tertiary\">\n {primaryLabel}\n </span>\n <span className=\"text-sm font-semibold tabular-nums text-text-primary\">{primary}</span>\n </div>\n <div className=\"h-px bg-border-seam\" />\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"text-xs font-medium uppercase tracking-wider text-text-tertiary\">\n {secondaryLabel}\n </span>\n <span className=\"text-sm font-semibold tabular-nums text-text-secondary\">{secondary}</span>\n </div>\n </div>\n </div>\n);\n\nconst Gauge: FC<{ pct: number; tone: 'accent' | 'info' }> = ({ pct, tone }) => {\n const clamped = Math.max(0, Math.min(100, pct));\n const r = 34;\n const c = 2 * Math.PI * r;\n const dash = (clamped / 100) * c;\n return (\n <div\n className={cn(\n 'relative h-24 w-24 shrink-0',\n tone === 'accent' ? 'text-accent' : 'text-text-link'\n )}\n >\n <svg viewBox=\"0 0 80 80\" className=\"h-full w-full -rotate-90\">\n <circle\n cx=\"40\"\n cy=\"40\"\n r={r}\n fill=\"none\"\n strokeWidth=\"8\"\n className=\"text-bg-sunken\"\n stroke=\"currentColor\"\n />\n <circle\n cx=\"40\"\n cy=\"40\"\n r={r}\n fill=\"none\"\n strokeWidth=\"8\"\n strokeLinecap=\"round\"\n stroke=\"currentColor\"\n strokeDasharray={`${dash} ${c}`}\n className=\"transition-[stroke-dasharray] duration-700\"\n />\n </svg>\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <span className=\"text-lg font-bold tabular-nums text-text-primary\">{clamped}%</span>\n </div>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Revenue chart — gradient columns with value labels + month axis\n// ---------------------------------------------------------------------------\nconst RevenueChart: FC<{ data: { month: string; amount: number }[] }> = ({ data }) => {\n if (!data.length) return <p className=\"text-sm text-text-tertiary\">No invoiced revenue yet.</p>;\n const peak = Math.max(...data.map((d) => d.amount), 1);\n return (\n <div className=\"overflow-x-auto\">\n <div className=\"min-w-[520px]\">\n <div className=\"flex h-60 items-end gap-2 border-b border-border-seam\">\n {data.map((d) => (\n <div\n key={d.month}\n className=\"group flex h-full flex-1 flex-col items-center justify-end gap-1.5\"\n >\n <span className=\"whitespace-nowrap text-[10px] font-semibold tabular-nums text-text-secondary\">\n {fmtMoney(d.amount)}\n </span>\n <div\n className=\"w-full max-w-[3rem] rounded-t-lg bg-gradient-to-t from-accent/50 to-accent shadow-sm transition-all duration-200 group-hover:-translate-y-1 group-hover:shadow-md\"\n style={{ height: `${Math.max((d.amount / peak) * 90, 2)}%` }}\n title={fmtMoney(d.amount)}\n />\n </div>\n ))}\n </div>\n <div className=\"mt-2 flex gap-2\">\n {data.map((d) => (\n <span key={d.month} className=\"flex-1 text-center text-xs text-text-tertiary\">\n {monthShort(d.month)}\n </span>\n ))}\n </div>\n </div>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Distribution — tone-colored gradient bars with count + share\n// ---------------------------------------------------------------------------\nconst Distribution: FC<{ items: { status: string; count: number }[] }> = ({ items }) => {\n if (!items.length) return <p className=\"text-sm text-text-tertiary\">No records yet.</p>;\n const max = Math.max(...items.map((i) => i.count), 1);\n const total = items.reduce((s, i) => s + i.count, 0) || 1;\n return (\n <ul className=\"space-y-3.5\">\n {items.map((i) => (\n <li\n key={i.status}\n className=\"grid grid-cols-[7.5rem_minmax(0,1fr)_auto] items-center gap-3\"\n >\n <StatusPill status={i.status} className=\"justify-self-start\" />\n <div className=\"h-2.5 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className={cn(\n 'h-full rounded-full bg-gradient-to-r transition-all duration-500',\n barGradientFor(i.status)\n )}\n style={{ width: `${Math.max((i.count / max) * 100, 4)}%` }}\n />\n </div>\n <span className=\"flex items-baseline justify-end gap-1.5 tabular-nums\">\n <span className=\"text-sm font-semibold text-text-primary\">{i.count}</span>\n <span className=\"text-xs text-text-tertiary\">\n {Math.round((i.count / total) * 100)}%\n </span>\n </span>\n </li>\n ))}\n </ul>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Hours bar — big gradient progress meter (billed vs unbilled)\n// ---------------------------------------------------------------------------\nconst HoursBar: FC<{ label: string; value: number; total: number; tone: 'accent' | 'warning' }> = ({\n label,\n value,\n total,\n tone,\n}) => {\n const pct = total > 0 ? Math.round((value / total) * 100) : 0;\n const grad =\n tone === 'accent'\n ? 'from-accent/60 to-accent'\n : 'from-status-warning-text/60 to-status-warning-text';\n return (\n <div className=\"rounded-xl border border-border-seam bg-bg-canvas p-4\">\n <div className=\"mb-2.5 flex items-center justify-between gap-3\">\n <span className=\"text-sm font-medium text-text-secondary\">{label}</span>\n <span className=\"text-sm font-semibold tabular-nums text-text-primary\">\n {fmtHours(value)} · {pct}%\n </span>\n </div>\n <div className=\"h-3 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className={cn('h-full rounded-full bg-gradient-to-r transition-all duration-700', grad)}\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Chart helpers (token-driven; self-contained to this page)\n// ---------------------------------------------------------------------------\ntype BarTone = 'success' | 'warning' | 'danger' | 'info' | 'accent' | 'neutral';\n\nconst BAR_GRADIENT: Record<BarTone, string> = {\n success: 'from-status-success-text/55 to-status-success-text',\n warning: 'from-status-warning-text/55 to-status-warning-text',\n danger: 'from-status-error-text/55 to-status-error-text',\n info: 'from-text-link/55 to-text-link',\n accent: 'from-accent/55 to-accent',\n neutral: 'from-text-tertiary/30 to-text-tertiary/70',\n};\n\n/** Mirror of the shared StatusPill tone mapping, used to tint chart bars. */\nconst STATUS_BAR_TONE: Record<string, BarTone> = {\n ACTIVE: 'success',\n DELIVERED: 'success',\n INVOICED: 'success',\n PAID: 'success',\n APPROVED: 'success',\n PASSED: 'success',\n CLEAR: 'success',\n COMPLETED: 'success',\n FINAL: 'success',\n PROSPECT: 'info',\n PROPOSAL: 'info',\n MEDIUM: 'info',\n INTERNAL_REVIEW: 'info',\n SUBMITTED: 'info',\n SENT: 'info',\n IN_REVIEW: 'info',\n REVIEW: 'warning',\n ON_HOLD: 'warning',\n HIGH: 'warning',\n CLIENT_REVIEW: 'warning',\n PENDING: 'warning',\n POTENTIAL_CONFLICT: 'warning',\n CANCELLED: 'danger',\n CRITICAL: 'danger',\n REJECTED: 'danger',\n OVERDUE: 'danger',\n FAILED: 'danger',\n CONFLICT: 'danger',\n ACCEPTED: 'accent',\n IN_PROGRESS: 'accent',\n OPEN: 'accent',\n BILLED: 'accent',\n INACTIVE: 'neutral',\n ARCHIVED: 'neutral',\n CLOSED: 'neutral',\n LOW: 'neutral',\n DRAFT: 'neutral',\n WAIVED: 'neutral',\n};\n\nfunction barGradientFor(status: string): string {\n return BAR_GRADIENT[STATUS_BAR_TONE[status] ?? 'accent'];\n}\n\n/** \"2026-07\" → \"Jul\" (falls back to the raw value if unparseable). */\nfunction monthShort(m: string): string {\n const [y, mo] = m.split('-').map(Number);\n if (!y || !mo) return m;\n const d = new Date(y, mo - 1, 1);\n return Number.isNaN(d.getTime()) ? m : d.toLocaleDateString(undefined, { month: 'short' });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../src/proserviceworld/pages/AnalyticsPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { BarChart3, TrendingUp, Clock, Wallet, Scale, AlertTriangle } from 'lucide-react';\nimport { useDashboardStats, useWriteOffs } from '../hooks/useProServiceWorldApi';\nimport { cn } from '../utils/cn';\nimport { plural, useTr } from '../hooks/useTr';\nimport {\n PageHeader,\n SectionCard,\n MetricCard,\n LoadingBlock,\n ErrorState,\n StatusPill,\n Empty,\n TableWrap,\n Th,\n Td,\n fmtMoney,\n fmtHours,\n fmtDate,\n} from '../components/ui';\n\nexport const AnalyticsPage: FC = () => {\n const tr = useTr();\n const { data, isLoading, error, refetch } = useDashboardStats();\n const writeOffs = useWriteOffs({ pagination: { limit: 10 } });\n if (isLoading) return <LoadingBlock label={tr('psw.analytics.loading', 'Crunching numbers…')} />;\n if (error) return <ErrorState error={error} onRetry={() => void refetch()} />;\n if (!data) return null;\n\n const totalHours = data.unbilledHours + data.billedHours;\n const utilization = totalHours > 0 ? Math.round((data.billedHours / totalHours) * 100) : 0;\n const billableBase = data.paidInvoiceTotal + data.outstandingInvoiceTotal;\n const collectionRate =\n billableBase > 0 ? Math.round((data.paidInvoiceTotal / billableBase) * 100) : 0;\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title={tr('psw.analytics.title', 'Analytics')}\n subtitle={tr(\n 'psw.analytics.subtitle',\n 'Real, live metrics across the practice — no estimates, straight from your records.'\n )}\n icon={<BarChart3 className=\"h-6 w-6\" />}\n />\n\n <div className=\"grid grid-cols-2 gap-3 lg:grid-cols-4\">\n <MetricCard\n label={tr('psw.analytics.collectedRevenue', 'Collected revenue')}\n value={fmtMoney(data.paidInvoiceTotal)}\n hint={tr('psw.analytics.invoicedHint', '{{amount}} invoiced', {\n amount: fmtMoney(billableBase),\n })}\n icon={<Wallet className=\"h-4 w-4\" />}\n tone=\"success\"\n />\n <MetricCard\n label={tr('psw.analytics.collectionRate', 'Collection rate')}\n value={`${collectionRate}%`}\n hint={tr('psw.analytics.outstandingHint', '{{amount}} outstanding', {\n amount: fmtMoney(data.outstandingInvoiceTotal),\n })}\n icon={<TrendingUp className=\"h-4 w-4\" />}\n tone=\"accent\"\n />\n <MetricCard\n label={tr('psw.analytics.utilization', 'Utilization')}\n value={`${utilization}%`}\n hint={tr('psw.analytics.hoursOfTotal', '{{billed}} of {{total}}', {\n billed: fmtHours(data.billedHours),\n total: fmtHours(totalHours),\n })}\n icon={<Clock className=\"h-4 w-4\" />}\n tone=\"info\"\n />\n <MetricCard\n label={tr('psw.analytics.activeEngagements', 'Active engagements')}\n value={data.activeEngagements}\n hint={tr('psw.analytics.openMattersHint', '{{count}} open matters', {\n count: data.openMatters,\n })}\n icon={<BarChart3 className=\"h-4 w-4\" />}\n tone=\"warning\"\n />\n </div>\n\n {/* Practice-health meters — collection & utilization at a glance */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <SectionCard\n title={tr('psw.analytics.collectionRate', 'Collection rate')}\n description={tr('psw.analytics.collectionRateDescription', 'Paid vs. total invoiced.')}\n >\n <GaugeRow\n pct={collectionRate}\n tone=\"accent\"\n primary={fmtMoney(data.paidInvoiceTotal)}\n primaryLabel={tr('psw.analytics.collected', 'Collected')}\n secondary={fmtMoney(data.outstandingInvoiceTotal)}\n secondaryLabel={tr('psw.analytics.outstanding', 'Outstanding')}\n />\n </SectionCard>\n <SectionCard\n title={tr('psw.analytics.utilization', 'Utilization')}\n description={tr('psw.analytics.utilizationDescription', 'Billed vs. total logged hours.')}\n >\n <GaugeRow\n pct={utilization}\n tone=\"info\"\n primary={fmtHours(data.billedHours)}\n primaryLabel={tr('psw.analytics.billed', 'Billed')}\n secondary={fmtHours(data.unbilledHours)}\n secondaryLabel={tr('psw.analytics.unbilled', 'Unbilled')}\n />\n </SectionCard>\n </div>\n\n <SectionCard\n title={tr('psw.analytics.revenueByMonth', 'Revenue by month')}\n description={tr(\n 'psw.analytics.revenueByMonthDescription',\n 'Invoiced totals across recent months.'\n )}\n >\n <RevenueChart data={data.revenueByMonth} />\n </SectionCard>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <SectionCard\n title={tr('psw.analytics.engagements', 'Engagements')}\n description={tr('psw.analytics.byStatus', 'By status')}\n >\n <Distribution items={data.engagementsByStatus} />\n </SectionCard>\n <SectionCard\n title={tr('psw.analytics.matters', 'Matters')}\n description={tr('psw.analytics.byStatus', 'By status')}\n >\n <Distribution items={data.mattersByStatus} />\n </SectionCard>\n <SectionCard\n title={tr('psw.analytics.invoices', 'Invoices')}\n description={tr('psw.analytics.byStatus', 'By status')}\n >\n <Distribution items={data.invoicesByStatus} />\n </SectionCard>\n </div>\n\n <SectionCard\n title={tr('psw.analytics.timeBilling', 'Time & billing')}\n description={tr(\n 'psw.analytics.timeBillingDescription',\n 'Billable capacity across the practice.'\n )}\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <HoursBar\n label={tr('psw.analytics.billedHours', 'Billed hours')}\n value={data.billedHours}\n total={totalHours}\n tone=\"accent\"\n />\n <HoursBar\n label={tr('psw.analytics.unbilledHours', 'Unbilled hours')}\n value={data.unbilledHours}\n total={totalHours}\n tone=\"warning\"\n />\n </div>\n </SectionCard>\n\n <div className=\"grid grid-cols-2 gap-3 lg:grid-cols-4\">\n <MetricCard\n label={tr('psw.analytics.realization', 'Realization')}\n value={`${Math.round(data.realizationPct)}%`}\n hint={tr('psw.analytics.realizationHint', '{{billed}} of {{worked}} worked', {\n billed: fmtMoney(data.billedValue),\n worked: fmtMoney(data.workedValue),\n })}\n icon={<Scale className=\"h-4 w-4\" />}\n tone=\"accent\"\n />\n <MetricCard\n label={tr('psw.analytics.writeOffs', 'Write-offs')}\n value={fmtMoney(data.writeOffTotal)}\n hint={tr('psw.analytics.writeOffsHint', 'Total written off')}\n icon={<AlertTriangle className=\"h-4 w-4\" />}\n tone=\"warning\"\n />\n </div>\n\n <SectionCard\n title={tr('psw.analytics.agedReceivables', 'Aged receivables')}\n description={tr(\n 'psw.analytics.agedReceivablesDescription',\n 'Outstanding invoice value by age — the margin-leak early warning.'\n )}\n >\n <div className=\"grid grid-cols-2 gap-3 lg:grid-cols-4\">\n <AgingBucket\n label={tr('psw.analytics.agingCurrent', 'Current')}\n bucket={data.agedReceivables.current}\n tone=\"success\"\n />\n <AgingBucket\n label={tr('psw.analytics.aging30', '30 days')}\n bucket={data.agedReceivables.thirty}\n tone=\"info\"\n />\n <AgingBucket\n label={tr('psw.analytics.aging60', '60 days')}\n bucket={data.agedReceivables.sixty}\n tone=\"warning\"\n />\n <AgingBucket\n label={tr('psw.analytics.aging90', '90+ days')}\n bucket={data.agedReceivables.ninetyPlus}\n tone=\"danger\"\n />\n </div>\n </SectionCard>\n\n <SectionCard\n title={tr('psw.analytics.writeOffs', 'Write-offs')}\n description={tr(\n 'psw.analytics.writeOffsDescription',\n 'Append-only audit of written-off amounts.'\n )}\n >\n {writeOffs.isLoading ? (\n <LoadingBlock label={tr('psw.analytics.writeOffsLoading', 'Loading write-offs…')} />\n ) : !writeOffs.data?.length ? (\n <Empty\n title={tr('psw.analytics.writeOffsEmptyTitle', 'No write-offs')}\n description={tr(\n 'psw.analytics.writeOffsEmptyDescription',\n 'Nothing has been written off yet.'\n )}\n />\n ) : (\n <TableWrap>\n <thead>\n <tr>\n <Th>{tr('psw.common.date', 'Date')}</Th>\n <Th>{tr('psw.analytics.reason', 'Reason')}</Th>\n <Th className=\"text-end\">{tr('psw.common.amount', 'Amount')}</Th>\n </tr>\n </thead>\n <tbody>\n {writeOffs.data.map((w) => (\n <tr key={w.id}>\n <Td>{fmtDate(w.createdAt)}</Td>\n <Td>{w.reason}</Td>\n <Td className=\"text-end\">{fmtMoney(w.amount)}</Td>\n </tr>\n ))}\n </tbody>\n </TableWrap>\n )}\n </SectionCard>\n </div>\n );\n};\n\nconst AgingBucket: FC<{\n label: string;\n bucket: { amount: number; count: number };\n tone: 'success' | 'info' | 'warning' | 'danger';\n}> = ({ label, bucket, tone }) => {\n const tr = useTr();\n return (\n <MetricCard\n label={label}\n value={fmtMoney(bucket.amount)}\n hint={plural(\n tr,\n 'psw.invoices.count',\n bucket.count,\n '{{count}} invoice',\n '{{count}} invoices'\n )}\n tone={tone}\n />\n );\n};\n\n// ---------------------------------------------------------------------------\n// Radial gauge with a supporting two-number legend\n// ---------------------------------------------------------------------------\nconst GaugeRow: FC<{\n pct: number;\n tone: 'accent' | 'info';\n primary: string;\n primaryLabel: string;\n secondary: string;\n secondaryLabel: string;\n}> = ({ pct, tone, primary, primaryLabel, secondary, secondaryLabel }) => (\n <div className=\"flex items-center gap-5\">\n <Gauge pct={pct} tone={tone} />\n <div className=\"flex flex-1 flex-col gap-2.5\">\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"text-xs font-medium uppercase tracking-wider text-text-tertiary\">\n {primaryLabel}\n </span>\n <span className=\"text-sm font-semibold tabular-nums text-text-primary\">{primary}</span>\n </div>\n <div className=\"h-px bg-border-seam\" />\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"text-xs font-medium uppercase tracking-wider text-text-tertiary\">\n {secondaryLabel}\n </span>\n <span className=\"text-sm font-semibold tabular-nums text-text-secondary\">{secondary}</span>\n </div>\n </div>\n </div>\n);\n\nconst Gauge: FC<{ pct: number; tone: 'accent' | 'info' }> = ({ pct, tone }) => {\n const clamped = Math.max(0, Math.min(100, pct));\n const r = 34;\n const c = 2 * Math.PI * r;\n const dash = (clamped / 100) * c;\n return (\n <div\n className={cn(\n 'relative h-24 w-24 shrink-0',\n tone === 'accent' ? 'text-accent' : 'text-text-link'\n )}\n >\n <svg viewBox=\"0 0 80 80\" className=\"h-full w-full -rotate-90\">\n <circle\n cx=\"40\"\n cy=\"40\"\n r={r}\n fill=\"none\"\n strokeWidth=\"8\"\n className=\"text-bg-sunken\"\n stroke=\"currentColor\"\n />\n <circle\n cx=\"40\"\n cy=\"40\"\n r={r}\n fill=\"none\"\n strokeWidth=\"8\"\n strokeLinecap=\"round\"\n stroke=\"currentColor\"\n strokeDasharray={`${dash} ${c}`}\n className=\"transition-[stroke-dasharray] duration-700\"\n />\n </svg>\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <span className=\"text-lg font-bold tabular-nums text-text-primary\">{clamped}%</span>\n </div>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Revenue chart — gradient columns with value labels + month axis\n// ---------------------------------------------------------------------------\nconst RevenueChart: FC<{ data: { month: string; amount: number }[] }> = ({ data }) => {\n const tr = useTr();\n if (!data.length)\n return (\n <p className=\"text-sm text-text-tertiary\">\n {tr('psw.dashboard.noRevenue', 'No invoiced revenue yet.')}\n </p>\n );\n const peak = Math.max(...data.map((d) => d.amount), 1);\n return (\n <div className=\"overflow-x-auto\">\n <div className=\"min-w-[520px]\">\n <div className=\"flex h-60 items-end gap-2 border-b border-border-seam\">\n {data.map((d) => (\n <div\n key={d.month}\n className=\"group flex h-full flex-1 flex-col items-center justify-end gap-1.5\"\n >\n <span className=\"whitespace-nowrap text-[10px] font-semibold tabular-nums text-text-secondary\">\n {fmtMoney(d.amount)}\n </span>\n <div\n className=\"w-full max-w-[3rem] rounded-t-lg bg-gradient-to-t from-accent/50 to-accent shadow-sm transition-all duration-200 group-hover:-translate-y-1 group-hover:shadow-md\"\n style={{ height: `${Math.max((d.amount / peak) * 90, 2)}%` }}\n title={fmtMoney(d.amount)}\n />\n </div>\n ))}\n </div>\n <div className=\"mt-2 flex gap-2\">\n {data.map((d) => (\n <span key={d.month} className=\"flex-1 text-center text-xs text-text-tertiary\">\n {monthShort(d.month)}\n </span>\n ))}\n </div>\n </div>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Distribution — tone-colored gradient bars with count + share\n// ---------------------------------------------------------------------------\nconst Distribution: FC<{ items: { status: string; count: number }[] }> = ({ items }) => {\n const tr = useTr();\n if (!items.length)\n return (\n <p className=\"text-sm text-text-tertiary\">\n {tr('psw.dashboard.noRecords', 'No records yet.')}\n </p>\n );\n const max = Math.max(...items.map((i) => i.count), 1);\n const total = items.reduce((s, i) => s + i.count, 0) || 1;\n return (\n <ul className=\"space-y-3.5\">\n {items.map((i) => (\n <li\n key={i.status}\n className=\"grid grid-cols-[7.5rem_minmax(0,1fr)_auto] items-center gap-3\"\n >\n <StatusPill status={i.status} className=\"justify-self-start\" />\n <div className=\"h-2.5 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className={cn(\n 'h-full rounded-full bg-gradient-to-r transition-all duration-500',\n barGradientFor(i.status)\n )}\n style={{ width: `${Math.max((i.count / max) * 100, 4)}%` }}\n />\n </div>\n <span className=\"flex items-baseline justify-end gap-1.5 tabular-nums\">\n <span className=\"text-sm font-semibold text-text-primary\">{i.count}</span>\n <span className=\"text-xs text-text-tertiary\">\n {Math.round((i.count / total) * 100)}%\n </span>\n </span>\n </li>\n ))}\n </ul>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Hours bar — big gradient progress meter (billed vs unbilled)\n// ---------------------------------------------------------------------------\nconst HoursBar: FC<{ label: string; value: number; total: number; tone: 'accent' | 'warning' }> = ({\n label,\n value,\n total,\n tone,\n}) => {\n const pct = total > 0 ? Math.round((value / total) * 100) : 0;\n const grad =\n tone === 'accent'\n ? 'from-accent/60 to-accent'\n : 'from-status-warning-text/60 to-status-warning-text';\n return (\n <div className=\"rounded-xl border border-border-seam bg-bg-canvas p-4\">\n <div className=\"mb-2.5 flex items-center justify-between gap-3\">\n <span className=\"text-sm font-medium text-text-secondary\">{label}</span>\n <span className=\"text-sm font-semibold tabular-nums text-text-primary\">\n {fmtHours(value)} · {pct}%\n </span>\n </div>\n <div className=\"h-3 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className={cn('h-full rounded-full bg-gradient-to-r transition-all duration-700', grad)}\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Chart helpers (token-driven; self-contained to this page)\n// ---------------------------------------------------------------------------\ntype BarTone = 'success' | 'warning' | 'danger' | 'info' | 'accent' | 'neutral';\n\nconst BAR_GRADIENT: Record<BarTone, string> = {\n success: 'from-status-success-text/55 to-status-success-text',\n warning: 'from-status-warning-text/55 to-status-warning-text',\n danger: 'from-status-error-text/55 to-status-error-text',\n info: 'from-text-link/55 to-text-link',\n accent: 'from-accent/55 to-accent',\n neutral: 'from-text-tertiary/30 to-text-tertiary/70',\n};\n\n/** Mirror of the shared StatusPill tone mapping, used to tint chart bars. */\nconst STATUS_BAR_TONE: Record<string, BarTone> = {\n ACTIVE: 'success',\n DELIVERED: 'success',\n INVOICED: 'success',\n PAID: 'success',\n APPROVED: 'success',\n PASSED: 'success',\n CLEAR: 'success',\n COMPLETED: 'success',\n FINAL: 'success',\n PROSPECT: 'info',\n PROPOSAL: 'info',\n MEDIUM: 'info',\n INTERNAL_REVIEW: 'info',\n SUBMITTED: 'info',\n SENT: 'info',\n IN_REVIEW: 'info',\n REVIEW: 'warning',\n ON_HOLD: 'warning',\n HIGH: 'warning',\n CLIENT_REVIEW: 'warning',\n PENDING: 'warning',\n POTENTIAL_CONFLICT: 'warning',\n CANCELLED: 'danger',\n CRITICAL: 'danger',\n REJECTED: 'danger',\n OVERDUE: 'danger',\n FAILED: 'danger',\n CONFLICT: 'danger',\n ACCEPTED: 'accent',\n IN_PROGRESS: 'accent',\n OPEN: 'accent',\n BILLED: 'accent',\n INACTIVE: 'neutral',\n ARCHIVED: 'neutral',\n CLOSED: 'neutral',\n LOW: 'neutral',\n DRAFT: 'neutral',\n WAIVED: 'neutral',\n};\n\nfunction barGradientFor(status: string): string {\n return BAR_GRADIENT[STATUS_BAR_TONE[status] ?? 'accent'];\n}\n\n/** \"2026-07\" → \"Jul\" (falls back to the raw value if unparseable). */\nfunction monthShort(m: string): string {\n const [y, mo] = m.split('-').map(Number);\n if (!y || !mo) return m;\n const d = new Date(y, mo - 1, 1);\n return Number.isNaN(d.getTime()) ? m : d.toLocaleDateString(undefined, { month: 'short' });\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useClient as e, useClientTeamAssignments as t, useConflictChecks as n, useCreateConflictCheck as r } from "../hooks/useProServiceWorldApi.js";
|
|
2
2
|
import { useNav as i } from "../utils/nav.js";
|
|
3
3
|
import { cn as a } from "../utils/cn.js";
|
|
4
|
-
import {
|
|
4
|
+
import { humanize as o } from "../utils/humanize.js";
|
|
5
|
+
import { Banner as s, Empty as c, ErrorState as l, Field as u, LoadingBlock as d, Modal as f, PageHeader as p, SectionCard as m, StatusPill as h, TableWrap as g, Td as _, TextArea as v, TextInput as y, Th as b, fmtDate as x, fmtMoney as S } from "../components/ui.js";
|
|
5
6
|
import { useActorNames as C } from "../hooks/useActorNames.js";
|
|
6
7
|
import { DiscussionThread as w } from "../components/DiscussionThread.js";
|
|
7
8
|
import { useMemo as T, useState as E } from "react";
|
|
@@ -11,30 +12,30 @@ import { Fragment as B, jsx as V, jsxs as H } from "react/jsx-runtime";
|
|
|
11
12
|
import { Button as U } from "@burdenoff/fe-libs/ui";
|
|
12
13
|
//#region src/proserviceworld/pages/ClientDetailPage.tsx
|
|
13
14
|
var W = (e) => e.split(/\s+/).filter(Boolean).slice(0, 2).map((e) => e[0]?.toUpperCase() ?? "").join("") || "?", G = () => {
|
|
14
|
-
let { id: r = "" } = z(), a = i(), { data:
|
|
15
|
+
let { id: r = "" } = z(), a = i(), { data: s, isLoading: u, error: f, refetch: v } = e(r), y = n({ clientId: r }), [R, B] = E(!1), G = T(() => [...(s?.engagements ?? []).map((e) => ({
|
|
15
16
|
type: "engagement",
|
|
16
17
|
id: e.id,
|
|
17
18
|
name: e.name
|
|
18
|
-
})), ...(
|
|
19
|
+
})), ...(s?.matters ?? []).map((e) => ({
|
|
19
20
|
type: "matter",
|
|
20
21
|
id: e.id,
|
|
21
22
|
name: e.reference || e.title
|
|
22
|
-
}))], [
|
|
23
|
-
|
|
24
|
-
...(
|
|
25
|
-
...(
|
|
23
|
+
}))], [s]), K = t(G), X = C([
|
|
24
|
+
s?.relationshipOwnerId,
|
|
25
|
+
...(s?.engagements ?? []).map((e) => e.responsiblePartnerId),
|
|
26
|
+
...(s?.matters ?? []).map((e) => e.leadPractitionerId),
|
|
26
27
|
...(K.data ?? []).map((e) => e.practitionerId)
|
|
27
28
|
]);
|
|
28
|
-
if (
|
|
29
|
-
if (
|
|
30
|
-
error:
|
|
31
|
-
onRetry: () => void
|
|
29
|
+
if (u) return /* @__PURE__ */ V(d, {});
|
|
30
|
+
if (f) return /* @__PURE__ */ V(l, {
|
|
31
|
+
error: f,
|
|
32
|
+
onRetry: () => void v()
|
|
32
33
|
});
|
|
33
|
-
if (!
|
|
34
|
+
if (!s) return /* @__PURE__ */ V(c, {
|
|
34
35
|
title: "Client not found",
|
|
35
36
|
description: "It may have been archived or belongs to another workspace."
|
|
36
37
|
});
|
|
37
|
-
let Z =
|
|
38
|
+
let Z = s.contactName || s.name;
|
|
38
39
|
return /* @__PURE__ */ H("div", {
|
|
39
40
|
className: "space-y-6",
|
|
40
41
|
children: [
|
|
@@ -44,21 +45,21 @@ var W = (e) => e.split(/\s+/).filter(Boolean).slice(0, 2).map((e) => e[0]?.toUpp
|
|
|
44
45
|
className: "flex items-center gap-1.5 text-sm text-text-secondary transition-colors hover:text-accent",
|
|
45
46
|
children: [/* @__PURE__ */ V(D, { className: "h-4 w-4" }), " Back to clients"]
|
|
46
47
|
}),
|
|
47
|
-
/* @__PURE__ */ V(
|
|
48
|
-
title:
|
|
49
|
-
subtitle: [
|
|
50
|
-
actions: /* @__PURE__ */ V(
|
|
48
|
+
/* @__PURE__ */ V(p, {
|
|
49
|
+
title: s.name,
|
|
50
|
+
subtitle: [s.type === "ORGANIZATION" ? "Organization" : "Individual", s.industry].filter(Boolean).join(" · "),
|
|
51
|
+
actions: /* @__PURE__ */ V(h, { status: s.status })
|
|
51
52
|
}),
|
|
52
|
-
|
|
53
|
+
s.tags.length > 0 && /* @__PURE__ */ V("div", {
|
|
53
54
|
className: "flex flex-wrap gap-2",
|
|
54
|
-
children:
|
|
55
|
+
children: s.tags.map((e) => /* @__PURE__ */ V("span", {
|
|
55
56
|
className: "rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary",
|
|
56
57
|
children: e
|
|
57
58
|
}, e))
|
|
58
59
|
}),
|
|
59
60
|
/* @__PURE__ */ H("div", {
|
|
60
61
|
className: "grid grid-cols-1 gap-4 lg:grid-cols-3",
|
|
61
|
-
children: [/* @__PURE__ */ V(
|
|
62
|
+
children: [/* @__PURE__ */ V(m, {
|
|
62
63
|
title: "Contact",
|
|
63
64
|
className: "lg:col-span-1",
|
|
64
65
|
children: /* @__PURE__ */ H("div", {
|
|
@@ -72,44 +73,44 @@ var W = (e) => e.split(/\s+/).filter(Boolean).slice(0, 2).map((e) => e[0]?.toUpp
|
|
|
72
73
|
className: "min-w-0",
|
|
73
74
|
children: [/* @__PURE__ */ V("div", {
|
|
74
75
|
className: "truncate text-sm font-semibold text-text-primary",
|
|
75
|
-
children:
|
|
76
|
+
children: s.contactName || s.name
|
|
76
77
|
}), /* @__PURE__ */ V("div", {
|
|
77
78
|
className: "truncate text-xs text-text-tertiary",
|
|
78
|
-
children:
|
|
79
|
+
children: s.contactName ? "Primary contact" : "Client record"
|
|
79
80
|
})]
|
|
80
81
|
})]
|
|
81
82
|
}), /* @__PURE__ */ H("dl", {
|
|
82
83
|
className: "space-y-2.5 text-sm",
|
|
83
84
|
children: [
|
|
84
|
-
|
|
85
|
+
s.contactEmail && /* @__PURE__ */ V(J, {
|
|
85
86
|
icon: /* @__PURE__ */ V(M, { className: "h-4 w-4" }),
|
|
86
|
-
value:
|
|
87
|
+
value: s.contactEmail
|
|
87
88
|
}),
|
|
88
|
-
|
|
89
|
+
s.contactPhone && /* @__PURE__ */ V(J, {
|
|
89
90
|
icon: /* @__PURE__ */ V(P, { className: "h-4 w-4" }),
|
|
90
|
-
value:
|
|
91
|
+
value: s.contactPhone
|
|
91
92
|
}),
|
|
92
|
-
|
|
93
|
+
s.address && /* @__PURE__ */ V(J, {
|
|
93
94
|
icon: /* @__PURE__ */ V(N, { className: "h-4 w-4" }),
|
|
94
|
-
value:
|
|
95
|
+
value: s.address
|
|
95
96
|
}),
|
|
96
|
-
|
|
97
|
+
s.notes && /* @__PURE__ */ V(J, {
|
|
97
98
|
icon: /* @__PURE__ */ V(I, { className: "h-4 w-4" }),
|
|
98
|
-
value:
|
|
99
|
+
value: s.notes,
|
|
99
100
|
multiline: !0
|
|
100
101
|
}),
|
|
101
|
-
|
|
102
|
+
s.relationshipOwnerId && /* @__PURE__ */ V(J, {
|
|
102
103
|
icon: /* @__PURE__ */ V(L, { className: "h-4 w-4" }),
|
|
103
|
-
value: `${X.nameOf(
|
|
104
|
+
value: `${X.nameOf(s.relationshipOwnerId)} · relationship owner`
|
|
104
105
|
}),
|
|
105
|
-
!
|
|
106
|
+
!s.contactEmail && !s.contactPhone && !s.address && !s.notes && !s.relationshipOwnerId && /* @__PURE__ */ V("p", {
|
|
106
107
|
className: "text-sm text-text-tertiary",
|
|
107
108
|
children: "No contact details recorded yet."
|
|
108
109
|
})
|
|
109
110
|
]
|
|
110
111
|
})]
|
|
111
112
|
})
|
|
112
|
-
}), /* @__PURE__ */ V(
|
|
113
|
+
}), /* @__PURE__ */ V(m, {
|
|
113
114
|
title: "Conflict checks",
|
|
114
115
|
className: "lg:col-span-2",
|
|
115
116
|
action: /* @__PURE__ */ H(U, {
|
|
@@ -118,34 +119,34 @@ var W = (e) => e.split(/\s+/).filter(Boolean).slice(0, 2).map((e) => e[0]?.toUpp
|
|
|
118
119
|
onClick: () => B(!0),
|
|
119
120
|
children: [/* @__PURE__ */ V(j, { className: "mr-1.5 h-4 w-4" }), " Run check"]
|
|
120
121
|
}),
|
|
121
|
-
children:
|
|
122
|
+
children: y.isLoading ? /* @__PURE__ */ V(d, { label: "Loading…" }) : !y.data || y.data.items.length === 0 ? /* @__PURE__ */ V(c, {
|
|
122
123
|
title: "No conflict checks",
|
|
123
124
|
description: "Run a conflict check before accepting new work for this client.",
|
|
124
125
|
icon: /* @__PURE__ */ V(j, { className: "h-7 w-7" })
|
|
125
|
-
}) : /* @__PURE__ */ H(
|
|
126
|
-
/* @__PURE__ */ V(
|
|
127
|
-
/* @__PURE__ */ V(
|
|
128
|
-
/* @__PURE__ */ V(
|
|
126
|
+
}) : /* @__PURE__ */ H(g, { children: [/* @__PURE__ */ V("thead", { children: /* @__PURE__ */ H("tr", { children: [
|
|
127
|
+
/* @__PURE__ */ V(b, { children: "Parties searched" }),
|
|
128
|
+
/* @__PURE__ */ V(b, { children: "Date" }),
|
|
129
|
+
/* @__PURE__ */ V(b, {
|
|
129
130
|
className: "text-right",
|
|
130
131
|
children: "Status"
|
|
131
132
|
})
|
|
132
|
-
] }) }), /* @__PURE__ */ V("tbody", { children:
|
|
133
|
+
] }) }), /* @__PURE__ */ V("tbody", { children: y.data.items.map((e) => /* @__PURE__ */ H("tr", {
|
|
133
134
|
className: "transition-colors hover:bg-bg-sunken/60",
|
|
134
135
|
children: [
|
|
135
|
-
/* @__PURE__ */ H(
|
|
136
|
+
/* @__PURE__ */ H(_, { children: [/* @__PURE__ */ V("div", {
|
|
136
137
|
className: "font-medium text-text-primary",
|
|
137
138
|
children: e.searchedParties.join(", ") || "Conflict search"
|
|
138
139
|
}), e.findings ? /* @__PURE__ */ V("div", {
|
|
139
140
|
className: "text-xs text-text-tertiary",
|
|
140
141
|
children: e.findings
|
|
141
142
|
}) : null] }),
|
|
142
|
-
/* @__PURE__ */ V(
|
|
143
|
+
/* @__PURE__ */ V(_, {
|
|
143
144
|
className: "whitespace-nowrap",
|
|
144
|
-
children:
|
|
145
|
+
children: x(e.createdAt)
|
|
145
146
|
}),
|
|
146
|
-
/* @__PURE__ */ V(
|
|
147
|
+
/* @__PURE__ */ V(_, {
|
|
147
148
|
className: "text-right",
|
|
148
|
-
children: /* @__PURE__ */ V(
|
|
149
|
+
children: /* @__PURE__ */ V(h, { status: e.status })
|
|
149
150
|
})
|
|
150
151
|
]
|
|
151
152
|
}, e.id)) })] })
|
|
@@ -158,111 +159,111 @@ var W = (e) => e.split(/\s+/).filter(Boolean).slice(0, 2).map((e) => e[0]?.toUpp
|
|
|
158
159
|
hasScopes: G.length > 0,
|
|
159
160
|
actors: X
|
|
160
161
|
}),
|
|
161
|
-
/* @__PURE__ */ V(
|
|
162
|
+
/* @__PURE__ */ V(m, {
|
|
162
163
|
title: "Engagements",
|
|
163
|
-
description: `${
|
|
164
|
+
description: `${s.engagements.length} scoped for this client`,
|
|
164
165
|
action: /* @__PURE__ */ H(U, {
|
|
165
166
|
size: "sm",
|
|
166
167
|
variant: "outline",
|
|
167
168
|
onClick: () => a("engagements"),
|
|
168
169
|
children: [/* @__PURE__ */ V(F, { className: "mr-1.5 h-4 w-4" }), "Manage"]
|
|
169
170
|
}),
|
|
170
|
-
children:
|
|
171
|
+
children: s.engagements.length === 0 ? /* @__PURE__ */ V(c, {
|
|
171
172
|
title: "No engagements",
|
|
172
173
|
description: "Scope the first engagement for this client.",
|
|
173
174
|
icon: /* @__PURE__ */ V(O, { className: "h-7 w-7" })
|
|
174
|
-
}) : /* @__PURE__ */ H(
|
|
175
|
-
/* @__PURE__ */ V(
|
|
176
|
-
/* @__PURE__ */ V(
|
|
177
|
-
/* @__PURE__ */ V(
|
|
178
|
-
/* @__PURE__ */ V(
|
|
179
|
-
/* @__PURE__ */ V(
|
|
175
|
+
}) : /* @__PURE__ */ H(g, { children: [/* @__PURE__ */ V("thead", { children: /* @__PURE__ */ H("tr", { children: [
|
|
176
|
+
/* @__PURE__ */ V(b, { children: "Engagement" }),
|
|
177
|
+
/* @__PURE__ */ V(b, { children: "Partner" }),
|
|
178
|
+
/* @__PURE__ */ V(b, { children: "Fee" }),
|
|
179
|
+
/* @__PURE__ */ V(b, { children: "Started" }),
|
|
180
|
+
/* @__PURE__ */ V(b, {
|
|
180
181
|
className: "text-right",
|
|
181
182
|
children: "Status"
|
|
182
183
|
})
|
|
183
|
-
] }) }), /* @__PURE__ */ V("tbody", { children:
|
|
184
|
+
] }) }), /* @__PURE__ */ V("tbody", { children: s.engagements.map((e) => /* @__PURE__ */ H("tr", {
|
|
184
185
|
className: "group cursor-pointer transition-colors hover:bg-bg-sunken/60",
|
|
185
186
|
onClick: () => a(`engagements/${e.id}`),
|
|
186
187
|
children: [
|
|
187
|
-
/* @__PURE__ */ V(
|
|
188
|
+
/* @__PURE__ */ V(_, { children: /* @__PURE__ */ H("div", {
|
|
188
189
|
className: "flex items-center gap-1 font-medium text-text-primary transition-colors group-hover:text-accent",
|
|
189
190
|
children: [/* @__PURE__ */ V("span", { children: e.name }), /* @__PURE__ */ V(k, { className: "h-3.5 w-3.5 shrink-0 text-text-tertiary opacity-0 transition-opacity group-hover:opacity-100" })]
|
|
190
191
|
}) }),
|
|
191
|
-
/* @__PURE__ */ V(
|
|
192
|
+
/* @__PURE__ */ V(_, {
|
|
192
193
|
className: "whitespace-nowrap",
|
|
193
194
|
children: e.responsiblePartnerId ? X.nameOf(e.responsiblePartnerId) : /* @__PURE__ */ V("span", {
|
|
194
195
|
className: "text-text-tertiary",
|
|
195
196
|
children: "Unassigned"
|
|
196
197
|
})
|
|
197
198
|
}),
|
|
198
|
-
/* @__PURE__ */ H(
|
|
199
|
+
/* @__PURE__ */ H(_, {
|
|
199
200
|
className: "whitespace-nowrap",
|
|
200
201
|
children: [
|
|
201
|
-
|
|
202
|
+
S(e.feeAmount, e.currency),
|
|
202
203
|
" ",
|
|
203
204
|
/* @__PURE__ */ H("span", {
|
|
204
205
|
className: "text-xs text-text-tertiary",
|
|
205
|
-
children: ["/ ",
|
|
206
|
+
children: ["/ ", o(e.feeArrangement)]
|
|
206
207
|
})
|
|
207
208
|
]
|
|
208
209
|
}),
|
|
209
|
-
/* @__PURE__ */ V(
|
|
210
|
+
/* @__PURE__ */ V(_, {
|
|
210
211
|
className: "whitespace-nowrap",
|
|
211
|
-
children:
|
|
212
|
+
children: x(e.startDate)
|
|
212
213
|
}),
|
|
213
|
-
/* @__PURE__ */ V(
|
|
214
|
+
/* @__PURE__ */ V(_, {
|
|
214
215
|
className: "text-right",
|
|
215
|
-
children: /* @__PURE__ */ V(
|
|
216
|
+
children: /* @__PURE__ */ V(h, { status: e.status })
|
|
216
217
|
})
|
|
217
218
|
]
|
|
218
219
|
}, e.id)) })] })
|
|
219
220
|
}),
|
|
220
|
-
/* @__PURE__ */ V(
|
|
221
|
+
/* @__PURE__ */ V(m, {
|
|
221
222
|
title: "Matters",
|
|
222
|
-
description: `${
|
|
223
|
+
description: `${s.matters.length} tracked for this client`,
|
|
223
224
|
action: /* @__PURE__ */ H(U, {
|
|
224
225
|
size: "sm",
|
|
225
226
|
variant: "outline",
|
|
226
227
|
onClick: () => a("matters"),
|
|
227
228
|
children: [/* @__PURE__ */ V(F, { className: "mr-1.5 h-4 w-4" }), "Manage"]
|
|
228
229
|
}),
|
|
229
|
-
children:
|
|
230
|
+
children: s.matters.length === 0 ? /* @__PURE__ */ V(c, {
|
|
230
231
|
title: "No matters",
|
|
231
232
|
description: "Open a matter to track regulated work for this client.",
|
|
232
233
|
icon: /* @__PURE__ */ V(A, { className: "h-7 w-7" })
|
|
233
|
-
}) : /* @__PURE__ */ H(
|
|
234
|
-
/* @__PURE__ */ V(
|
|
235
|
-
/* @__PURE__ */ V(
|
|
236
|
-
/* @__PURE__ */ V(
|
|
237
|
-
/* @__PURE__ */ V(
|
|
238
|
-
/* @__PURE__ */ V(
|
|
239
|
-
/* @__PURE__ */ V(
|
|
234
|
+
}) : /* @__PURE__ */ H(g, { children: [/* @__PURE__ */ V("thead", { children: /* @__PURE__ */ H("tr", { children: [
|
|
235
|
+
/* @__PURE__ */ V(b, { children: "Reference" }),
|
|
236
|
+
/* @__PURE__ */ V(b, { children: "Title" }),
|
|
237
|
+
/* @__PURE__ */ V(b, { children: "Lead" }),
|
|
238
|
+
/* @__PURE__ */ V(b, { children: "Due" }),
|
|
239
|
+
/* @__PURE__ */ V(b, { children: "Priority" }),
|
|
240
|
+
/* @__PURE__ */ V(b, {
|
|
240
241
|
className: "text-right",
|
|
241
242
|
children: "Status"
|
|
242
243
|
})
|
|
243
|
-
] }) }), /* @__PURE__ */ V("tbody", { children:
|
|
244
|
+
] }) }), /* @__PURE__ */ V("tbody", { children: s.matters.map((e) => /* @__PURE__ */ H("tr", {
|
|
244
245
|
className: "transition-colors hover:bg-bg-sunken/60",
|
|
245
246
|
children: [
|
|
246
|
-
/* @__PURE__ */ V(
|
|
247
|
+
/* @__PURE__ */ V(_, {
|
|
247
248
|
className: "whitespace-nowrap font-medium text-text-primary",
|
|
248
249
|
children: e.reference || "—"
|
|
249
250
|
}),
|
|
250
|
-
/* @__PURE__ */ V(
|
|
251
|
-
/* @__PURE__ */ V(
|
|
251
|
+
/* @__PURE__ */ V(_, { children: e.title }),
|
|
252
|
+
/* @__PURE__ */ V(_, {
|
|
252
253
|
className: "whitespace-nowrap",
|
|
253
254
|
children: e.leadPractitionerId ? X.nameOf(e.leadPractitionerId) : /* @__PURE__ */ V("span", {
|
|
254
255
|
className: "text-text-tertiary",
|
|
255
256
|
children: "Unassigned"
|
|
256
257
|
})
|
|
257
258
|
}),
|
|
258
|
-
/* @__PURE__ */ V(
|
|
259
|
+
/* @__PURE__ */ V(_, {
|
|
259
260
|
className: "whitespace-nowrap",
|
|
260
|
-
children:
|
|
261
|
+
children: x(e.dueDate)
|
|
261
262
|
}),
|
|
262
|
-
/* @__PURE__ */ V(
|
|
263
|
-
/* @__PURE__ */ V(
|
|
263
|
+
/* @__PURE__ */ V(_, { children: /* @__PURE__ */ V(h, { status: e.priority }) }),
|
|
264
|
+
/* @__PURE__ */ V(_, {
|
|
264
265
|
className: "text-right",
|
|
265
|
-
children: /* @__PURE__ */ V(
|
|
266
|
+
children: /* @__PURE__ */ V(h, { status: e.status })
|
|
266
267
|
})
|
|
267
268
|
]
|
|
268
269
|
}, e.id)) })] })
|
|
@@ -274,7 +275,7 @@ var W = (e) => e.split(/\s+/).filter(Boolean).slice(0, 2).map((e) => e[0]?.toUpp
|
|
|
274
275
|
R && /* @__PURE__ */ V(Y, {
|
|
275
276
|
clientId: r,
|
|
276
277
|
onClose: () => B(!1),
|
|
277
|
-
onDone: () => void
|
|
278
|
+
onDone: () => void y.refetch()
|
|
278
279
|
})
|
|
279
280
|
]
|
|
280
281
|
});
|
|
@@ -287,7 +288,7 @@ function K(e) {
|
|
|
287
288
|
roles: [],
|
|
288
289
|
isLead: !1,
|
|
289
290
|
scopes: []
|
|
290
|
-
}, r =
|
|
291
|
+
}, r = o(n.role);
|
|
291
292
|
e.roles.includes(r) || e.roles.push(r), e.isLead = e.isLead || n.isLead, e.scopes.some((e) => e.id === n.scopeId) || e.scopes.push({
|
|
292
293
|
id: n.scopeId,
|
|
293
294
|
name: n.scopeName,
|
|
@@ -298,10 +299,10 @@ function K(e) {
|
|
|
298
299
|
}
|
|
299
300
|
var q = ({ rows: e, loading: t, error: n, hasScopes: r, actors: i }) => {
|
|
300
301
|
let a = K(e);
|
|
301
|
-
return /* @__PURE__ */ V(
|
|
302
|
+
return /* @__PURE__ */ V(m, {
|
|
302
303
|
title: "Client team",
|
|
303
304
|
description: "Everyone staffed on this client's engagements and matters.",
|
|
304
|
-
children: t ? /* @__PURE__ */ V(
|
|
305
|
+
children: t ? /* @__PURE__ */ V(d, { label: "Loading…" }) : n ? /* @__PURE__ */ V(l, { error: n }) : r ? a.length === 0 ? /* @__PURE__ */ V(c, {
|
|
305
306
|
title: "No one assigned",
|
|
306
307
|
description: "Staff practitioners from an engagement's Team section to see them here.",
|
|
307
308
|
icon: /* @__PURE__ */ V(R, { className: "h-7 w-7" })
|
|
@@ -355,7 +356,7 @@ var q = ({ rows: e, loading: t, error: n, hasScopes: r, actors: i }) => {
|
|
|
355
356
|
]
|
|
356
357
|
}, e.practitionerId);
|
|
357
358
|
})
|
|
358
|
-
}) : /* @__PURE__ */ V(
|
|
359
|
+
}) : /* @__PURE__ */ V(c, {
|
|
359
360
|
title: "Nothing to staff yet",
|
|
360
361
|
description: "Scope an engagement or open a matter, then assign the team that will deliver it.",
|
|
361
362
|
icon: /* @__PURE__ */ V(R, { className: "h-7 w-7" })
|
|
@@ -371,8 +372,8 @@ var q = ({ rows: e, loading: t, error: n, hasScopes: r, actors: i }) => {
|
|
|
371
372
|
children: t
|
|
372
373
|
})]
|
|
373
374
|
}), Y = ({ clientId: e, onClose: t, onDone: n }) => {
|
|
374
|
-
let i = r(), [a,
|
|
375
|
-
return /* @__PURE__ */ V(
|
|
375
|
+
let i = r(), [a, o] = E(""), [c, l] = E(""), [d, p] = E(null);
|
|
376
|
+
return /* @__PURE__ */ V(f, {
|
|
376
377
|
open: !0,
|
|
377
378
|
onClose: t,
|
|
378
379
|
title: "Run conflict check",
|
|
@@ -403,25 +404,25 @@ var q = ({ rows: e, loading: t, error: n, hasScopes: r, actors: i }) => {
|
|
|
403
404
|
children: /* @__PURE__ */ H("div", {
|
|
404
405
|
className: "space-y-3",
|
|
405
406
|
children: [
|
|
406
|
-
|
|
407
|
+
d && /* @__PURE__ */ V(s, {
|
|
407
408
|
tone: "danger",
|
|
408
|
-
children:
|
|
409
|
+
children: d
|
|
409
410
|
}),
|
|
410
|
-
/* @__PURE__ */ V(
|
|
411
|
+
/* @__PURE__ */ V(u, {
|
|
411
412
|
label: "Parties searched",
|
|
412
413
|
required: !0,
|
|
413
414
|
hint: "Comma-separated — counterparties, related entities, principals.",
|
|
414
|
-
children: /* @__PURE__ */ V(
|
|
415
|
+
children: /* @__PURE__ */ V(y, {
|
|
415
416
|
value: a,
|
|
416
|
-
onChange: (e) =>
|
|
417
|
+
onChange: (e) => o(e.target.value),
|
|
417
418
|
placeholder: "Beta Corp, John Roe, Gamma LLP"
|
|
418
419
|
})
|
|
419
420
|
}),
|
|
420
|
-
/* @__PURE__ */ V(
|
|
421
|
+
/* @__PURE__ */ V(u, {
|
|
421
422
|
label: "Findings",
|
|
422
|
-
children: /* @__PURE__ */ V(
|
|
423
|
+
children: /* @__PURE__ */ V(v, {
|
|
423
424
|
value: c,
|
|
424
|
-
onChange: (e) =>
|
|
425
|
+
onChange: (e) => l(e.target.value),
|
|
425
426
|
placeholder: "Notes on any potential conflicts…"
|
|
426
427
|
})
|
|
427
428
|
})
|