@burdenoff/microfe-proserviceworld 2026.626.2 → 2026.706.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -3
- package/dist/node_modules/clsx/dist/clsx.js +18 -0
- package/dist/node_modules/clsx/dist/clsx.js.map +1 -0
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +1716 -0
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -0
- package/dist/proserviceworld/ProServiceWorldRoot.js +92 -136
- package/dist/proserviceworld/ProServiceWorldRoot.js.map +1 -1
- package/dist/proserviceworld/ProServiceWorldRoutes.js +18 -26
- package/dist/proserviceworld/ProServiceWorldRoutes.js.map +1 -1
- package/dist/proserviceworld/components/ui.js +273 -0
- package/dist/proserviceworld/components/ui.js.map +1 -0
- package/dist/proserviceworld/hooks/useFiles.js +45 -0
- package/dist/proserviceworld/hooks/useFiles.js.map +1 -0
- package/dist/proserviceworld/hooks/useProServiceWorldApi.js +416 -0
- package/dist/proserviceworld/hooks/useProServiceWorldApi.js.map +1 -0
- package/dist/proserviceworld/index.js +0 -1
- package/dist/proserviceworld/pages/AnalyticsPage.js +163 -475
- package/dist/proserviceworld/pages/AnalyticsPage.js.map +1 -1
- package/dist/proserviceworld/pages/ClientDetailPage.js +238 -0
- package/dist/proserviceworld/pages/ClientDetailPage.js.map +1 -0
- package/dist/proserviceworld/pages/ClientsPage.js +295 -0
- package/dist/proserviceworld/pages/ClientsPage.js.map +1 -0
- package/dist/proserviceworld/pages/CompliancePage.js +307 -0
- package/dist/proserviceworld/pages/CompliancePage.js.map +1 -0
- package/dist/proserviceworld/pages/ConflictsPage.js +234 -0
- package/dist/proserviceworld/pages/ConflictsPage.js.map +1 -0
- package/dist/proserviceworld/pages/DashboardPage.js +179 -632
- package/dist/proserviceworld/pages/DashboardPage.js.map +1 -1
- package/dist/proserviceworld/pages/DocumentsPage.js +235 -539
- package/dist/proserviceworld/pages/DocumentsPage.js.map +1 -1
- package/dist/proserviceworld/pages/EngagementDetailPage.js +444 -0
- package/dist/proserviceworld/pages/EngagementDetailPage.js.map +1 -0
- package/dist/proserviceworld/pages/EngagementsPage.js +256 -0
- package/dist/proserviceworld/pages/EngagementsPage.js.map +1 -0
- package/dist/proserviceworld/pages/InvoicesPage.js +303 -0
- package/dist/proserviceworld/pages/InvoicesPage.js.map +1 -0
- package/dist/proserviceworld/pages/MattersPage.js +275 -0
- package/dist/proserviceworld/pages/MattersPage.js.map +1 -0
- package/dist/proserviceworld/pages/PracticeAreasPage.js +176 -0
- package/dist/proserviceworld/pages/PracticeAreasPage.js.map +1 -0
- package/dist/proserviceworld/pages/TimePage.js +310 -0
- package/dist/proserviceworld/pages/TimePage.js.map +1 -0
- package/dist/proserviceworld/utils/cn.js +10 -0
- package/dist/proserviceworld/utils/cn.js.map +1 -0
- package/dist/proserviceworld/utils/nav.js +11 -0
- package/dist/proserviceworld/utils/nav.js.map +1 -0
- package/package.json +3 -2
- package/dist/proserviceworld/pages/AdminReportsPage.js +0 -579
- package/dist/proserviceworld/pages/AdminReportsPage.js.map +0 -1
- package/dist/proserviceworld/pages/AdminUsersPage.js +0 -468
- package/dist/proserviceworld/pages/AdminUsersPage.js.map +0 -1
- package/dist/proserviceworld/pages/BookingsPage.js +0 -446
- package/dist/proserviceworld/pages/BookingsPage.js.map +0 -1
- package/dist/proserviceworld/pages/MessagesPage.js +0 -238
- package/dist/proserviceworld/pages/MessagesPage.js.map +0 -1
- package/dist/proserviceworld/pages/PaymentsPage.js +0 -537
- package/dist/proserviceworld/pages/PaymentsPage.js.map +0 -1
- package/dist/proserviceworld/pages/ProjectsPage.js +0 -627
- package/dist/proserviceworld/pages/ProjectsPage.js.map +0 -1
- package/dist/proserviceworld/pages/ProviderDetailPage.js +0 -538
- package/dist/proserviceworld/pages/ProviderDetailPage.js.map +0 -1
- package/dist/proserviceworld/pages/ProvidersPage.js +0 -347
- package/dist/proserviceworld/pages/ProvidersPage.js.map +0 -1
- package/dist/proserviceworld/pages/ReviewsPage.js +0 -356
- package/dist/proserviceworld/pages/ReviewsPage.js.map +0 -1
- package/dist/proserviceworld/pages/ServiceDetailsPage.js +0 -631
- package/dist/proserviceworld/pages/ServiceDetailsPage.js.map +0 -1
- package/dist/proserviceworld/pages/ServicesPage.js +0 -344
- package/dist/proserviceworld/pages/ServicesPage.js.map +0 -1
- package/dist/proserviceworld/pages/VerificationPage.js +0 -370
- package/dist/proserviceworld/pages/VerificationPage.js.map +0 -1
- package/dist/proserviceworld/store/proserviceworldStore.js +0 -2490
- package/dist/proserviceworld/store/proserviceworldStore.js.map +0 -1
- package/dist/proserviceworld/utils/i18n.js +0 -15
- package/dist/proserviceworld/utils/i18n.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../src/proserviceworld/pages/AnalyticsPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n GlassCard,\n EmphasisPanel,\n AuroraBackground,\n GradientText,\n PagePurpose,\n NextSteps,\n} from '@burdenoff/fe-libs/ui';\nimport {\n TrendingUp,\n TrendingDown,\n Minus,\n DollarSign,\n Users,\n BarChart3,\n Activity,\n Globe,\n Calendar,\n ArrowUpRight,\n ArrowDownRight,\n Download,\n} from 'lucide-react';\nimport { useProServiceWorldStore } from '../store/proserviceworldStore';\n\ntype AnalyticsTab = 'overview' | 'revenue' | 'clients' | 'services' | 'performance';\n\nexport function AnalyticsPage() {\n const { analyticsMetrics, revenueData, clientData, servicePerformance, geographicData } =\n useProServiceWorldStore();\n const [activeTab, setActiveTab] = useState<AnalyticsTab>('overview');\n const [timeRange, setTimeRange] = useState('6m');\n\n const tabs: { key: AnalyticsTab; label: string; icon: typeof BarChart3 }[] = [\n { key: 'overview', label: 'Overview', icon: BarChart3 },\n { key: 'revenue', label: 'Revenue', icon: DollarSign },\n { key: 'clients', label: 'Clients', icon: Users },\n { key: 'services', label: 'Services', icon: Activity },\n { key: 'performance', label: 'Performance', icon: TrendingUp },\n ];\n\n const changeIcon = (type: string) => {\n if (type === 'positive') return <TrendingUp className=\"h-4 w-4 text-status-success-text\" />;\n if (type === 'negative') return <TrendingDown className=\"h-4 w-4 text-status-error-text\" />;\n return <Minus className=\"h-4 w-4 text-text-muted\" />;\n };\n\n const maxRevenue = Math.max(...revenueData.map((d) => d.value));\n const maxClients = Math.max(...clientData.map((d) => d.value));\n const maxServiceBookings = Math.max(...servicePerformance.map((d) => d.value));\n\n return (\n <div className=\"space-y-6 p-6 bg-bg-canvas min-h-screen\">\n {/* Hero header — the ONE aurora backdrop for this MFE */}\n <div className=\"relative overflow-hidden rounded-card -mx-2 px-2 pt-1\">\n <AuroraBackground intensity=\"subtle\" />\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold tracking-tight\">\n <GradientText>Analytics</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n Insights and performance metrics for your professional services.\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <div className=\"flex rounded border border-border-seam overflow-hidden\">\n {['1m', '3m', '6m', '1y'].map((range) => (\n <button\n key={range}\n onClick={() => setTimeRange(range)}\n aria-pressed={timeRange === range}\n className={`px-3 py-1 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus ${\n timeRange === range\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'text-text-secondary hover:bg-bg-sunken/50'\n }`}\n >\n {range.toUpperCase()}\n </button>\n ))}\n </div>\n <Button variant=\"outline\" size=\"sm\">\n <Download className=\"mr-2 h-4 w-4\" /> Export\n </Button>\n </div>\n </div>\n </div>\n\n <PagePurpose icon={<BarChart3 className=\"h-4 w-4\" />}>\n Understand how your professional services are performing. Track revenue and client growth\n over time, see which services and regions drive the most demand, and export the numbers —\n so you can double down on what works and plan where to grow next.\n </PagePurpose>\n\n <NextSteps\n storageKey=\"proserviceworld-analytics\"\n steps={[\n {\n id: 'revenue',\n label: 'Inspect revenue performance',\n description: 'Open the Revenue tab to see month-over-month growth.',\n icon: <DollarSign className=\"h-4 w-4\" />,\n onClick: () => setActiveTab('revenue'),\n },\n {\n id: 'services',\n label: 'Find your top services',\n description: 'See which services attract the most bookings.',\n icon: <Activity className=\"h-4 w-4\" />,\n onClick: () => setActiveTab('services'),\n },\n {\n id: 'clients',\n label: 'Track client acquisition',\n description: 'Review client growth and geographic reach.',\n icon: <Users className=\"h-4 w-4\" />,\n onClick: () => setActiveTab('clients'),\n },\n ]}\n />\n\n {/* Tabs */}\n <div className=\"flex flex-wrap gap-1 border-b border-border-seam\">\n {tabs.map(({ key, label, icon: Icon }) => (\n <button\n key={key}\n onClick={() => setActiveTab(key)}\n aria-pressed={activeTab === key}\n className={`flex items-center gap-2 border-b-2 px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus ${\n activeTab === key\n ? 'border-action-primary-bg text-text-primary'\n : 'border-transparent text-text-secondary hover:text-text-primary'\n }`}\n >\n <Icon className=\"h-4 w-4\" />\n {label}\n </button>\n ))}\n </div>\n\n {/* Metric Cards (shown on all tabs) */}\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-5\">\n {analyticsMetrics.map((metric, i) => (\n <GlassCard key={i} treatment={i === 0 ? 'glass' : 'metric'} glow={i === 0} className=\"border-border-seam\">\n <div className=\"flex items-center justify-between\">\n <p className=\"text-xs font-medium text-text-secondary\">{metric.label}</p>\n {changeIcon(metric.changeType)}\n </div>\n <p className=\"mt-1 text-2xl font-bold tabular-nums\">{metric.value}</p>\n <p\n className={`mt-0.5 text-xs ${\n metric.changeType === 'positive'\n ? 'text-status-success-text'\n : metric.changeType === 'negative'\n ? 'text-status-error-text'\n : 'text-text-muted'\n }`}\n >\n {metric.change} from last period\n </p>\n </GlassCard>\n ))}\n </div>\n\n {/* Overview Tab */}\n {activeTab === 'overview' && (\n <div className=\"grid gap-6 lg:grid-cols-2\">\n {/* Revenue chart — the single emphasised insight zone */}\n <EmphasisPanel>\n <div className=\"flex items-center justify-between mb-4\">\n <h2 className=\"text-base font-semibold tracking-tight\">Revenue Trend</h2>\n <Badge variant=\"secondary\">Last 6 months</Badge>\n </div>\n <div className=\"space-y-3\">\n {revenueData.map((point) => (\n <div key={point.name} className=\"flex items-center gap-3\">\n <span className=\"w-10 text-xs text-text-secondary\">{point.name}</span>\n <div className=\"relative flex-1 h-6 bg-bg-sunken rounded overflow-hidden\">\n {point.prev !== undefined && (\n <div\n className=\"absolute h-6 bg-text-muted/15 rounded\"\n style={{ width: `${(point.prev / maxRevenue) * 100}%` }}\n />\n )}\n <div\n className=\"relative h-6 bg-action-primary-bg rounded shadow-[var(--shadow-glow)]\"\n style={{ width: `${(point.value / maxRevenue) * 100}%` }}\n />\n </div>\n <span className=\"w-20 text-right text-xs font-medium tabular-nums\">\n ${(point.value / 1000).toFixed(1)}k\n </span>\n </div>\n ))}\n </div>\n </EmphasisPanel>\n\n {/* Client growth */}\n <Card className=\"border-border-seam\">\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <CardTitle className=\"text-base\">Client Growth</CardTitle>\n <Badge variant=\"secondary\">Monthly</Badge>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {clientData.map((point) => (\n <div key={point.name} className=\"flex items-center gap-3\">\n <span className=\"w-10 text-xs text-text-secondary\">{point.name}</span>\n <div className=\"flex-1 h-6 bg-bg-sunken rounded overflow-hidden\">\n <div\n className=\"h-6 bg-accent-blue rounded\"\n style={{ width: `${(point.value / maxClients) * 100}%` }}\n />\n </div>\n <span className=\"w-12 text-right text-xs font-medium tabular-nums\">\n {point.value}\n </span>\n </div>\n ))}\n </CardContent>\n </Card>\n </div>\n )}\n\n {/* Revenue Tab */}\n {activeTab === 'revenue' && (\n <div className=\"space-y-6\">\n <Card className=\"border-border-seam\">\n <CardHeader>\n <CardTitle className=\"text-base\">Revenue Breakdown</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-4\">\n {revenueData.map((point) => {\n const growth = point.prev\n ? (((point.value - point.prev) / point.prev) * 100).toFixed(1)\n : '0';\n const isPositive = point.prev ? point.value >= point.prev : true;\n return (\n <div\n key={point.name}\n className=\"flex items-center justify-between rounded-lg border border-border-seam p-4 transition-shadow hover:shadow-pop\"\n >\n <div className=\"flex items-center gap-4\">\n <div className=\"flex h-10 w-10 items-center justify-center rounded-lg bg-accent-soft\">\n <Calendar className=\"h-5 w-5 text-text-primary\" />\n </div>\n <div>\n <p className=\"font-medium\">{point.name} 2025</p>\n <p className=\"text-sm text-text-secondary tabular-nums\">\n Previous: ${((point.prev ?? 0) / 1000).toFixed(1)}k\n </p>\n </div>\n </div>\n <div className=\"text-right\">\n <p className=\"text-lg font-bold tabular-nums\">\n ${(point.value / 1000).toFixed(1)}k\n </p>\n <p\n className={`flex items-center justify-end gap-1 text-xs tabular-nums ${isPositive ? 'text-status-success-text' : 'text-status-error-text'}`}\n >\n {isPositive ? (\n <ArrowUpRight className=\"h-3 w-3\" />\n ) : (\n <ArrowDownRight className=\"h-3 w-3\" />\n )}\n {growth}%\n </p>\n </div>\n </div>\n );\n })}\n </div>\n </CardContent>\n </Card>\n </div>\n )}\n\n {/* Clients Tab */}\n {activeTab === 'clients' && (\n <div className=\"space-y-6\">\n <Card className=\"border-border-seam\">\n <CardHeader>\n <CardTitle className=\"text-base\">Client Acquisition</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {clientData.map((point, idx) => {\n const prev = idx > 0 ? (clientData[idx - 1]?.value ?? 0) : 0;\n const growth = prev > 0 ? (((point.value - prev) / prev) * 100).toFixed(0) : 'N/A';\n return (\n <div\n key={point.name}\n className=\"flex items-center gap-4 rounded-lg border border-border-seam p-3\"\n >\n <div className=\"flex-1\">\n <div className=\"flex items-center justify-between\">\n <span className=\"font-medium\">{point.name}</span>\n <span className=\"text-lg font-bold tabular-nums\">{point.value} clients</span>\n </div>\n <div className=\"mt-2 h-2 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-2 rounded-full bg-accent-blue\"\n style={{ width: `${(point.value / maxClients) * 100}%` }}\n />\n </div>\n </div>\n <Badge variant=\"secondary\" className=\"text-xs tabular-nums\">\n {typeof growth === 'string' ? growth : `+${growth}%`}\n </Badge>\n </div>\n );\n })}\n </CardContent>\n </Card>\n\n {/* Geographic data */}\n <Card className=\"border-border-seam\">\n <CardHeader>\n <div className=\"flex items-center gap-2\">\n <Globe className=\"h-5 w-5 text-text-secondary\" />\n <CardTitle className=\"text-base\">Geographic Distribution</CardTitle>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam text-left text-text-secondary\">\n <th className=\"p-3 font-medium\">Country</th>\n <th className=\"p-3 text-right font-medium\">Clients</th>\n <th className=\"p-3 text-right font-medium\">Revenue</th>\n <th className=\"p-3 font-medium\">Distribution</th>\n </tr>\n </thead>\n <tbody>\n {geographicData.map((geo) => {\n const totalClients = geographicData.reduce((s, g) => s + g.clients, 0);\n const pct = Math.round((geo.clients / totalClients) * 100);\n return (\n <tr\n key={geo.country}\n className=\"border-b border-border-seam transition-colors hover:bg-bg-sunken/50\"\n >\n <td className=\"p-3 font-medium\">{geo.country}</td>\n <td className=\"p-3 text-right tabular-nums\">{geo.clients}</td>\n <td className=\"p-3 text-right tabular-nums\">\n ${(geo.revenue / 1000).toFixed(0)}k\n </td>\n <td className=\"p-3\">\n <div className=\"flex items-center gap-2\">\n <div className=\"h-2 flex-1 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-2 rounded-full bg-action-primary-bg\"\n style={{ width: `${pct}%` }}\n />\n </div>\n <span className=\"w-10 text-right text-xs text-text-secondary tabular-nums\">\n {pct}%\n </span>\n </div>\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n </CardContent>\n </Card>\n </div>\n )}\n\n {/* Services Tab */}\n {activeTab === 'services' && (\n <Card className=\"border-border-seam\">\n <CardHeader>\n <CardTitle className=\"text-base\">Service Performance (Total Bookings)</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {[...servicePerformance]\n .sort((a, b) => b.value - a.value)\n .map((service, idx) => (\n <div key={service.name} className=\"flex items-center gap-4\">\n <span className=\"w-6 text-sm font-medium text-text-secondary tabular-nums\">\n #{idx + 1}\n </span>\n <div className=\"flex-1\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm font-medium\">{service.name}</span>\n <span className=\"text-sm font-bold tabular-nums\">{service.value}</span>\n </div>\n <div className=\"mt-1 h-3 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-3 rounded-full bg-action-primary-bg transition-all\"\n style={{ width: `${(service.value / maxServiceBookings) * 100}%` }}\n />\n </div>\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n )}\n\n {/* Performance Tab */}\n {activeTab === 'performance' && (\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <Card className=\"border-border-seam\">\n <CardHeader>\n <CardTitle className=\"text-base\">Monthly Revenue vs Previous</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {revenueData.map((point) => (\n <div key={point.name} className=\"space-y-1\">\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"font-medium\">{point.name}</span>\n <span className=\"text-text-secondary tabular-nums\">\n ${(point.value / 1000).toFixed(1)}k vs $\n {((point.prev ?? 0) / 1000).toFixed(1)}k\n </span>\n </div>\n <div className=\"flex gap-1\">\n <div\n className=\"h-4 rounded bg-action-primary-bg/30\"\n style={{ width: `${((point.prev ?? 0) / maxRevenue) * 100}%` }}\n />\n <div\n className=\"h-4 rounded bg-action-primary-bg\"\n style={{\n width: `${(point.value / maxRevenue) * 100 - ((point.prev ?? 0) / maxRevenue) * 100}%`,\n }}\n />\n </div>\n </div>\n ))}\n <div className=\"mt-4 flex items-center gap-4 text-xs\">\n <span className=\"flex items-center gap-1\">\n <div className=\"h-3 w-3 rounded bg-action-primary-bg/30\" /> Previous\n </span>\n <span className=\"flex items-center gap-1\">\n <div className=\"h-3 w-3 rounded bg-action-primary-bg\" /> Current\n </span>\n </div>\n </CardContent>\n </Card>\n\n <Card className=\"border-border-seam\">\n <CardHeader>\n <CardTitle className=\"text-base\">Top Performing Services</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {[...servicePerformance]\n .sort((a, b) => b.value - a.value)\n .slice(0, 5)\n .map((service, idx) => (\n <div\n key={service.name}\n className=\"flex items-center justify-between rounded-lg border border-border-seam p-3\"\n >\n <div className=\"flex items-center gap-3\">\n <div\n className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold tabular-nums ${\n idx === 0\n ? 'bg-status-warning-bg-subtle text-status-warning-text'\n : idx === 1\n ? 'bg-bg-sunken text-text-secondary'\n : idx === 2\n ? 'bg-accent-soft text-text-primary'\n : 'bg-bg-sunken text-text-secondary'\n }`}\n >\n {idx + 1}\n </div>\n <span className=\"font-medium text-sm\">{service.name}</span>\n </div>\n <Badge variant=\"secondary\">{service.value} bookings</Badge>\n </div>\n ))}\n </div>\n </CardContent>\n </Card>\n </div>\n )}\n </div>\n );\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 } from 'lucide-react';\nimport { useDashboardStats } from '../hooks/useProServiceWorldApi';\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 collectionRate =\n data.paidInvoiceTotal + data.outstandingInvoiceTotal > 0\n ? Math.round(\n (data.paidInvoiceTotal / (data.paidInvoiceTotal + data.outstandingInvoiceTotal)) * 100\n )\n : 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 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 icon={<BarChart3 className=\"h-4 w-4\" />}\n />\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\">\n <Distribution items={data.engagementsByStatus} />\n </SectionCard>\n <SectionCard title=\"Matters\">\n <Distribution items={data.mattersByStatus} />\n </SectionCard>\n <SectionCard title=\"Invoices\">\n <Distribution items={data.invoicesByStatus} />\n </SectionCard>\n </div>\n\n <SectionCard title=\"Time & billing\">\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\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 max = Math.max(...data.map((d) => d.amount), 1);\n return (\n <div className=\"flex h-52 items-end gap-2\">\n {data.map((d) => (\n <div key={d.month} className=\"flex flex-1 flex-col items-center gap-2\">\n <span className=\"text-[10px] text-text-tertiary\">{fmtMoney(d.amount)}</span>\n <div className=\"flex w-full flex-1 items-end\">\n <div\n className=\"w-full rounded-t bg-accent\"\n style={{ height: `${Math.max((d.amount / max) * 100, 2)}%` }}\n />\n </div>\n <span className=\"text-[10px] text-text-tertiary\">{d.month}</span>\n </div>\n ))}\n </div>\n );\n};\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 total = items.reduce((s, i) => s + i.count, 0) || 1;\n return (\n <ul className=\"space-y-2.5\">\n {items.map((i) => (\n <li key={i.status} className=\"flex items-center gap-3\">\n <div className=\"w-28 shrink-0\">\n <StatusPill status={i.status} />\n </div>\n <div className=\"h-2 flex-1 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-accent\"\n style={{ width: `${(i.count / total) * 100}%` }}\n />\n </div>\n <span className=\"w-8 text-right text-sm font-medium text-text-primary\">{i.count}</span>\n </li>\n ))}\n </ul>\n );\n};\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 return (\n <div>\n <div className=\"mb-1 flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">{label}</span>\n <span className=\"font-medium text-text-primary\">\n {fmtHours(value)} · {pct}%\n </span>\n </div>\n <div className=\"h-2.5 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className={\n tone === 'accent'\n ? 'h-full rounded-full bg-accent'\n : 'h-full rounded-full bg-status-warning-text'\n }\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { useClient as e, useConflictChecks as t, useCreateConflictCheck as n } from "../hooks/useProServiceWorldApi.js";
|
|
2
|
+
import { useNav as r } from "../utils/nav.js";
|
|
3
|
+
import { Banner as i, Empty as a, ErrorState as o, Field as s, LoadingBlock as c, Modal as l, PageHeader as u, SectionCard as d, StatusPill as f, TextArea as p, TextInput as m, fmtDate as h, fmtMoney as g } from "../components/ui.js";
|
|
4
|
+
import { useState as _ } from "react";
|
|
5
|
+
import { ArrowLeft as v, Briefcase as y, FolderKanban as b, GitBranch as x, Mail as S, MapPin as C, Phone as w, Plus as T } from "lucide-react";
|
|
6
|
+
import { useParams as E } from "react-router-dom";
|
|
7
|
+
import { Fragment as D, jsx as O, jsxs as k } from "react/jsx-runtime";
|
|
8
|
+
import { Button as A } from "@burdenoff/fe-libs/ui";
|
|
9
|
+
//#region src/proserviceworld/pages/ClientDetailPage.tsx
|
|
10
|
+
var j = () => {
|
|
11
|
+
let { id: n = "" } = E(), i = r(), { data: s, isLoading: l, error: p, refetch: m } = e(n), D = t({ clientId: n }), [j, F] = _(!1);
|
|
12
|
+
return l ? /* @__PURE__ */ O(c, {}) : p ? /* @__PURE__ */ O(o, {
|
|
13
|
+
error: p,
|
|
14
|
+
onRetry: () => void m()
|
|
15
|
+
}) : s ? /* @__PURE__ */ k("div", {
|
|
16
|
+
className: "space-y-6",
|
|
17
|
+
children: [
|
|
18
|
+
/* @__PURE__ */ k("button", {
|
|
19
|
+
type: "button",
|
|
20
|
+
onClick: () => i("clients"),
|
|
21
|
+
className: "flex items-center gap-1.5 text-sm text-text-secondary hover:text-accent",
|
|
22
|
+
children: [/* @__PURE__ */ O(v, { className: "h-4 w-4" }), " Back to clients"]
|
|
23
|
+
}),
|
|
24
|
+
/* @__PURE__ */ O(u, {
|
|
25
|
+
title: s.name,
|
|
26
|
+
subtitle: [s.type === "ORGANIZATION" ? "Organization" : "Individual", s.industry].filter(Boolean).join(" · "),
|
|
27
|
+
actions: /* @__PURE__ */ O(f, { status: s.status })
|
|
28
|
+
}),
|
|
29
|
+
/* @__PURE__ */ k("div", {
|
|
30
|
+
className: "grid grid-cols-1 gap-4 lg:grid-cols-3",
|
|
31
|
+
children: [/* @__PURE__ */ O(d, {
|
|
32
|
+
title: "Contact",
|
|
33
|
+
className: "lg:col-span-1",
|
|
34
|
+
children: /* @__PURE__ */ k("dl", {
|
|
35
|
+
className: "space-y-2 text-sm",
|
|
36
|
+
children: [
|
|
37
|
+
/* @__PURE__ */ O(M, {
|
|
38
|
+
label: "Contact",
|
|
39
|
+
value: s.contactName
|
|
40
|
+
}),
|
|
41
|
+
s.contactEmail && /* @__PURE__ */ O(N, {
|
|
42
|
+
icon: /* @__PURE__ */ O(S, { className: "h-3.5 w-3.5" }),
|
|
43
|
+
value: s.contactEmail
|
|
44
|
+
}),
|
|
45
|
+
s.contactPhone && /* @__PURE__ */ O(N, {
|
|
46
|
+
icon: /* @__PURE__ */ O(w, { className: "h-3.5 w-3.5" }),
|
|
47
|
+
value: s.contactPhone
|
|
48
|
+
}),
|
|
49
|
+
s.address && /* @__PURE__ */ O(N, {
|
|
50
|
+
icon: /* @__PURE__ */ O(C, { className: "h-3.5 w-3.5" }),
|
|
51
|
+
value: s.address
|
|
52
|
+
}),
|
|
53
|
+
s.notes && /* @__PURE__ */ O("p", {
|
|
54
|
+
className: "pt-2 text-text-secondary",
|
|
55
|
+
children: s.notes
|
|
56
|
+
})
|
|
57
|
+
]
|
|
58
|
+
})
|
|
59
|
+
}), /* @__PURE__ */ O(d, {
|
|
60
|
+
title: "Conflict checks",
|
|
61
|
+
className: "lg:col-span-2",
|
|
62
|
+
action: /* @__PURE__ */ k(A, {
|
|
63
|
+
size: "sm",
|
|
64
|
+
variant: "outline",
|
|
65
|
+
onClick: () => F(!0),
|
|
66
|
+
children: [/* @__PURE__ */ O(x, { className: "mr-1.5 h-4 w-4" }), " Run check"]
|
|
67
|
+
}),
|
|
68
|
+
children: D.isLoading ? /* @__PURE__ */ O(c, { label: "Loading…" }) : !D.data || D.data.length === 0 ? /* @__PURE__ */ O(a, {
|
|
69
|
+
title: "No conflict checks",
|
|
70
|
+
description: "Run a conflict check before accepting new work for this client."
|
|
71
|
+
}) : /* @__PURE__ */ O("ul", {
|
|
72
|
+
className: "divide-y divide-border-seam",
|
|
73
|
+
children: D.data.map((e) => /* @__PURE__ */ k("li", {
|
|
74
|
+
className: "flex items-center justify-between py-2.5",
|
|
75
|
+
children: [/* @__PURE__ */ k("div", { children: [/* @__PURE__ */ O("div", {
|
|
76
|
+
className: "text-sm text-text-primary",
|
|
77
|
+
children: e.searchedParties.join(", ") || "Conflict search"
|
|
78
|
+
}), /* @__PURE__ */ k("div", {
|
|
79
|
+
className: "text-xs text-text-tertiary",
|
|
80
|
+
children: [h(e.createdAt), e.findings ? ` · ${e.findings}` : ""]
|
|
81
|
+
})] }), /* @__PURE__ */ O(f, { status: e.status })]
|
|
82
|
+
}, e.id))
|
|
83
|
+
})
|
|
84
|
+
})]
|
|
85
|
+
}),
|
|
86
|
+
/* @__PURE__ */ O(d, {
|
|
87
|
+
title: "Engagements",
|
|
88
|
+
action: /* @__PURE__ */ k(A, {
|
|
89
|
+
size: "sm",
|
|
90
|
+
variant: "outline",
|
|
91
|
+
onClick: () => i("engagements"),
|
|
92
|
+
children: [/* @__PURE__ */ O(T, { className: "mr-1.5 h-4 w-4" }), "Manage"]
|
|
93
|
+
}),
|
|
94
|
+
children: s.engagements.length === 0 ? /* @__PURE__ */ O(a, {
|
|
95
|
+
title: "No engagements",
|
|
96
|
+
description: "Scope the first engagement for this client.",
|
|
97
|
+
icon: /* @__PURE__ */ O(y, { className: "h-7 w-7" })
|
|
98
|
+
}) : /* @__PURE__ */ O("ul", {
|
|
99
|
+
className: "divide-y divide-border-seam",
|
|
100
|
+
children: s.engagements.map((e) => /* @__PURE__ */ O("li", { children: /* @__PURE__ */ k("button", {
|
|
101
|
+
type: "button",
|
|
102
|
+
onClick: () => i(`engagements/${e.id}`),
|
|
103
|
+
className: "flex w-full items-center justify-between py-2.5 text-left hover:text-accent",
|
|
104
|
+
children: [/* @__PURE__ */ k("div", { children: [/* @__PURE__ */ O("div", {
|
|
105
|
+
className: "text-sm font-medium text-text-primary",
|
|
106
|
+
children: e.name
|
|
107
|
+
}), /* @__PURE__ */ k("div", {
|
|
108
|
+
className: "text-xs text-text-tertiary",
|
|
109
|
+
children: [
|
|
110
|
+
g(e.feeAmount, e.currency),
|
|
111
|
+
" · ",
|
|
112
|
+
e.feeArrangement.toLowerCase()
|
|
113
|
+
]
|
|
114
|
+
})] }), /* @__PURE__ */ O(f, { status: e.status })]
|
|
115
|
+
}) }, e.id))
|
|
116
|
+
})
|
|
117
|
+
}),
|
|
118
|
+
/* @__PURE__ */ O(d, {
|
|
119
|
+
title: "Matters",
|
|
120
|
+
action: /* @__PURE__ */ k(A, {
|
|
121
|
+
size: "sm",
|
|
122
|
+
variant: "outline",
|
|
123
|
+
onClick: () => i("matters"),
|
|
124
|
+
children: [/* @__PURE__ */ O(T, { className: "mr-1.5 h-4 w-4" }), "Manage"]
|
|
125
|
+
}),
|
|
126
|
+
children: s.matters.length === 0 ? /* @__PURE__ */ O(a, {
|
|
127
|
+
title: "No matters",
|
|
128
|
+
description: "Open a matter to track regulated work for this client.",
|
|
129
|
+
icon: /* @__PURE__ */ O(b, { className: "h-7 w-7" })
|
|
130
|
+
}) : /* @__PURE__ */ O("ul", {
|
|
131
|
+
className: "divide-y divide-border-seam",
|
|
132
|
+
children: s.matters.map((e) => /* @__PURE__ */ k("li", {
|
|
133
|
+
className: "flex items-center justify-between py-2.5",
|
|
134
|
+
children: [/* @__PURE__ */ k("div", { children: [/* @__PURE__ */ O("div", {
|
|
135
|
+
className: "text-sm font-medium text-text-primary",
|
|
136
|
+
children: e.title
|
|
137
|
+
}), /* @__PURE__ */ k("div", {
|
|
138
|
+
className: "text-xs text-text-tertiary",
|
|
139
|
+
children: [
|
|
140
|
+
e.reference || "No reference",
|
|
141
|
+
" · Due ",
|
|
142
|
+
h(e.dueDate)
|
|
143
|
+
]
|
|
144
|
+
})] }), /* @__PURE__ */ k("div", {
|
|
145
|
+
className: "flex items-center gap-2",
|
|
146
|
+
children: [/* @__PURE__ */ O(f, { status: e.priority }), /* @__PURE__ */ O(f, { status: e.status })]
|
|
147
|
+
})]
|
|
148
|
+
}, e.id))
|
|
149
|
+
})
|
|
150
|
+
}),
|
|
151
|
+
j && /* @__PURE__ */ O(P, {
|
|
152
|
+
clientId: n,
|
|
153
|
+
onClose: () => F(!1),
|
|
154
|
+
onDone: () => void D.refetch()
|
|
155
|
+
})
|
|
156
|
+
]
|
|
157
|
+
}) : /* @__PURE__ */ O(a, {
|
|
158
|
+
title: "Client not found",
|
|
159
|
+
description: "It may have been archived or belongs to another workspace."
|
|
160
|
+
});
|
|
161
|
+
}, M = ({ label: e, value: t }) => t ? /* @__PURE__ */ k("div", {
|
|
162
|
+
className: "flex justify-between gap-4",
|
|
163
|
+
children: [/* @__PURE__ */ O("dt", {
|
|
164
|
+
className: "text-text-tertiary",
|
|
165
|
+
children: e
|
|
166
|
+
}), /* @__PURE__ */ O("dd", {
|
|
167
|
+
className: "text-right text-text-primary",
|
|
168
|
+
children: t
|
|
169
|
+
})]
|
|
170
|
+
}) : null, N = ({ icon: e, value: t }) => /* @__PURE__ */ k("div", {
|
|
171
|
+
className: "flex items-center gap-2 text-text-secondary",
|
|
172
|
+
children: [/* @__PURE__ */ O("span", {
|
|
173
|
+
className: "text-text-tertiary",
|
|
174
|
+
children: e
|
|
175
|
+
}), t]
|
|
176
|
+
}), P = ({ clientId: e, onClose: t, onDone: r }) => {
|
|
177
|
+
let a = n(), [o, c] = _(""), [u, d] = _(""), [f, h] = _(null);
|
|
178
|
+
return /* @__PURE__ */ O(l, {
|
|
179
|
+
open: !0,
|
|
180
|
+
onClose: t,
|
|
181
|
+
title: "Run conflict check",
|
|
182
|
+
footer: /* @__PURE__ */ k(D, { children: [/* @__PURE__ */ O(A, {
|
|
183
|
+
variant: "outline",
|
|
184
|
+
onClick: t,
|
|
185
|
+
disabled: a.isPending,
|
|
186
|
+
children: "Cancel"
|
|
187
|
+
}), /* @__PURE__ */ O(A, {
|
|
188
|
+
onClick: () => {
|
|
189
|
+
h(null);
|
|
190
|
+
let n = o.split(",").map((e) => e.trim()).filter(Boolean);
|
|
191
|
+
if (n.length === 0) return h("Add at least one party to search.");
|
|
192
|
+
a.mutate({
|
|
193
|
+
clientId: e,
|
|
194
|
+
searchedParties: n,
|
|
195
|
+
findings: u.trim() || null
|
|
196
|
+
}, {
|
|
197
|
+
onSuccess: () => {
|
|
198
|
+
r(), t();
|
|
199
|
+
},
|
|
200
|
+
onError: (e) => h(e instanceof Error ? e.message : "Failed.")
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
disabled: a.isPending,
|
|
204
|
+
children: a.isPending ? "Running…" : "Record check"
|
|
205
|
+
})] }),
|
|
206
|
+
children: /* @__PURE__ */ k("div", {
|
|
207
|
+
className: "space-y-3",
|
|
208
|
+
children: [
|
|
209
|
+
f && /* @__PURE__ */ O(i, {
|
|
210
|
+
tone: "danger",
|
|
211
|
+
children: f
|
|
212
|
+
}),
|
|
213
|
+
/* @__PURE__ */ O(s, {
|
|
214
|
+
label: "Parties searched",
|
|
215
|
+
required: !0,
|
|
216
|
+
hint: "Comma-separated — counterparties, related entities, principals.",
|
|
217
|
+
children: /* @__PURE__ */ O(m, {
|
|
218
|
+
value: o,
|
|
219
|
+
onChange: (e) => c(e.target.value),
|
|
220
|
+
placeholder: "Beta Corp, John Roe, Gamma LLP"
|
|
221
|
+
})
|
|
222
|
+
}),
|
|
223
|
+
/* @__PURE__ */ O(s, {
|
|
224
|
+
label: "Findings",
|
|
225
|
+
children: /* @__PURE__ */ O(p, {
|
|
226
|
+
value: u,
|
|
227
|
+
onChange: (e) => d(e.target.value),
|
|
228
|
+
placeholder: "Notes on any potential conflicts…"
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
]
|
|
232
|
+
})
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
//#endregion
|
|
236
|
+
export { j as ClientDetailPage };
|
|
237
|
+
|
|
238
|
+
//# sourceMappingURL=ClientDetailPage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ClientDetailPage.js","names":[],"sources":["../../../src/proserviceworld/pages/ClientDetailPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport {\n ArrowLeft,\n Briefcase,\n FolderKanban,\n GitBranch,\n Mail,\n Phone,\n MapPin,\n Plus,\n} from 'lucide-react';\nimport {\n useClient,\n useConflictChecks,\n useCreateConflictCheck,\n type PswEngagement,\n type PswMatter,\n} from '../hooks/useProServiceWorldApi';\nimport { useNav } from '../utils/nav';\nimport {\n PageHeader,\n SectionCard,\n LoadingBlock,\n ErrorState,\n Empty,\n StatusPill,\n Modal,\n Field,\n TextInput,\n TextArea,\n Banner,\n fmtDate,\n fmtMoney,\n} from '../components/ui';\n\nexport const ClientDetailPage: FC = () => {\n const { id = '' } = useParams();\n const nav = useNav();\n const { data: client, isLoading, error, refetch } = useClient(id);\n const conflicts = useConflictChecks({ clientId: id });\n const [runConflict, setRunConflict] = useState(false);\n\n if (isLoading) return <LoadingBlock />;\n if (error) return <ErrorState error={error} onRetry={() => void refetch()} />;\n if (!client)\n return (\n <Empty\n title=\"Client not found\"\n description=\"It may have been archived or belongs to another workspace.\"\n />\n );\n\n return (\n <div className=\"space-y-6\">\n <button\n type=\"button\"\n onClick={() => nav('clients')}\n className=\"flex items-center gap-1.5 text-sm text-text-secondary hover:text-accent\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back to clients\n </button>\n\n <PageHeader\n title={client.name}\n subtitle={[client.type === 'ORGANIZATION' ? 'Organization' : 'Individual', client.industry]\n .filter(Boolean)\n .join(' · ')}\n actions={<StatusPill status={client.status} />}\n />\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <SectionCard title=\"Contact\" className=\"lg:col-span-1\">\n <dl className=\"space-y-2 text-sm\">\n <Row label=\"Contact\" value={client.contactName} />\n {client.contactEmail && (\n <IconRow icon={<Mail className=\"h-3.5 w-3.5\" />} value={client.contactEmail} />\n )}\n {client.contactPhone && (\n <IconRow icon={<Phone className=\"h-3.5 w-3.5\" />} value={client.contactPhone} />\n )}\n {client.address && (\n <IconRow icon={<MapPin className=\"h-3.5 w-3.5\" />} value={client.address} />\n )}\n {client.notes && <p className=\"pt-2 text-text-secondary\">{client.notes}</p>}\n </dl>\n </SectionCard>\n\n <SectionCard\n title=\"Conflict checks\"\n className=\"lg:col-span-2\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => setRunConflict(true)}>\n <GitBranch className=\"mr-1.5 h-4 w-4\" /> Run check\n </Button>\n }\n >\n {conflicts.isLoading ? (\n <LoadingBlock label=\"Loading…\" />\n ) : !conflicts.data || conflicts.data.length === 0 ? (\n <Empty\n title=\"No conflict checks\"\n description=\"Run a conflict check before accepting new work for this client.\"\n />\n ) : (\n <ul className=\"divide-y divide-border-seam\">\n {conflicts.data.map((c) => (\n <li key={c.id} className=\"flex items-center justify-between py-2.5\">\n <div>\n <div className=\"text-sm text-text-primary\">\n {c.searchedParties.join(', ') || 'Conflict search'}\n </div>\n <div className=\"text-xs text-text-tertiary\">\n {fmtDate(c.createdAt)}\n {c.findings ? ` · ${c.findings}` : ''}\n </div>\n </div>\n <StatusPill status={c.status} />\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n </div>\n\n <SectionCard\n title=\"Engagements\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => nav('engagements')}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n Manage\n </Button>\n }\n >\n {client.engagements.length === 0 ? (\n <Empty\n title=\"No engagements\"\n description=\"Scope the first engagement for this client.\"\n icon={<Briefcase className=\"h-7 w-7\" />}\n />\n ) : (\n <ul className=\"divide-y divide-border-seam\">\n {client.engagements.map((e: PswEngagement) => (\n <li key={e.id}>\n <button\n type=\"button\"\n onClick={() => nav(`engagements/${e.id}`)}\n className=\"flex w-full items-center justify-between py-2.5 text-left hover:text-accent\"\n >\n <div>\n <div className=\"text-sm font-medium text-text-primary\">{e.name}</div>\n <div className=\"text-xs text-text-tertiary\">\n {fmtMoney(e.feeAmount, e.currency)} · {e.feeArrangement.toLowerCase()}\n </div>\n </div>\n <StatusPill status={e.status} />\n </button>\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n\n <SectionCard\n title=\"Matters\"\n action={\n <Button size=\"sm\" variant=\"outline\" onClick={() => nav('matters')}>\n <Plus className=\"mr-1.5 h-4 w-4\" />\n Manage\n </Button>\n }\n >\n {client.matters.length === 0 ? (\n <Empty\n title=\"No matters\"\n description=\"Open a matter to track regulated work for this client.\"\n icon={<FolderKanban className=\"h-7 w-7\" />}\n />\n ) : (\n <ul className=\"divide-y divide-border-seam\">\n {client.matters.map((m: PswMatter) => (\n <li key={m.id} className=\"flex items-center justify-between py-2.5\">\n <div>\n <div className=\"text-sm font-medium text-text-primary\">{m.title}</div>\n <div className=\"text-xs text-text-tertiary\">\n {m.reference || 'No reference'} · Due {fmtDate(m.dueDate)}\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <StatusPill status={m.priority} />\n <StatusPill status={m.status} />\n </div>\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n\n {runConflict && (\n <ConflictModal\n clientId={id}\n onClose={() => setRunConflict(false)}\n onDone={() => void conflicts.refetch()}\n />\n )}\n </div>\n );\n};\n\nconst Row: FC<{ label: string; value: string | null }> = ({ label, value }) =>\n value ? (\n <div className=\"flex justify-between gap-4\">\n <dt className=\"text-text-tertiary\">{label}</dt>\n <dd className=\"text-right text-text-primary\">{value}</dd>\n </div>\n ) : null;\n\nconst IconRow: FC<{ icon: React.ReactNode; value: string }> = ({ icon, value }) => (\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <span className=\"text-text-tertiary\">{icon}</span>\n {value}\n </div>\n);\n\nconst ConflictModal: FC<{ clientId: string; onClose: () => void; onDone: () => void }> = ({\n clientId,\n onClose,\n onDone,\n}) => {\n const create = useCreateConflictCheck();\n const [parties, setParties] = useState('');\n const [findings, setFindings] = useState('');\n const [err, setErr] = useState<string | null>(null);\n\n const submit = () => {\n setErr(null);\n const searchedParties = parties\n .split(',')\n .map((p) => p.trim())\n .filter(Boolean);\n if (searchedParties.length === 0) return setErr('Add at least one party to search.');\n create.mutate(\n { clientId, searchedParties, findings: findings.trim() || null },\n {\n onSuccess: () => {\n onDone();\n onClose();\n },\n onError: (e) => setErr(e instanceof Error ? e.message : 'Failed.'),\n }\n );\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"Run conflict check\"\n footer={\n <>\n <Button variant=\"outline\" onClick={onClose} disabled={create.isPending}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={create.isPending}>\n {create.isPending ? 'Running…' : 'Record check'}\n </Button>\n </>\n }\n >\n <div className=\"space-y-3\">\n {err && <Banner tone=\"danger\">{err}</Banner>}\n <Field\n label=\"Parties searched\"\n required\n hint=\"Comma-separated — counterparties, related entities, principals.\"\n >\n <TextInput\n value={parties}\n onChange={(e) => setParties(e.target.value)}\n placeholder=\"Beta Corp, John Roe, Gamma LLP\"\n />\n </Field>\n <Field label=\"Findings\">\n <TextArea\n value={findings}\n onChange={(e) => setFindings(e.target.value)}\n placeholder=\"Notes on any potential conflicts…\"\n />\n </Field>\n </div>\n </Modal>\n );\n};\n"],"mappings":"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"}
|