@hed-hog/contact 0.0.299 → 0.0.301

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.
Files changed (81) hide show
  1. package/dist/contact.module.d.ts.map +1 -1
  2. package/dist/contact.module.js +2 -0
  3. package/dist/contact.module.js.map +1 -1
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +3 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/person/person.service.js +350 -350
  9. package/dist/proposal/dto/proposal.dto.d.ts +152 -0
  10. package/dist/proposal/dto/proposal.dto.d.ts.map +1 -0
  11. package/dist/proposal/dto/proposal.dto.js +396 -0
  12. package/dist/proposal/dto/proposal.dto.js.map +1 -0
  13. package/dist/proposal/proposal-contract.subscriber.d.ts +11 -0
  14. package/dist/proposal/proposal-contract.subscriber.d.ts.map +1 -0
  15. package/dist/proposal/proposal-contract.subscriber.js +51 -0
  16. package/dist/proposal/proposal-contract.subscriber.js.map +1 -0
  17. package/dist/proposal/proposal-event.types.d.ts +122 -0
  18. package/dist/proposal/proposal-event.types.d.ts.map +1 -0
  19. package/dist/proposal/proposal-event.types.js +13 -0
  20. package/dist/proposal/proposal-event.types.js.map +1 -0
  21. package/dist/proposal/proposal.controller.d.ts +56 -0
  22. package/dist/proposal/proposal.controller.d.ts.map +1 -0
  23. package/dist/proposal/proposal.controller.js +191 -0
  24. package/dist/proposal/proposal.controller.js.map +1 -0
  25. package/dist/proposal/proposal.module.d.ts +3 -0
  26. package/dist/proposal/proposal.module.d.ts.map +1 -0
  27. package/dist/proposal/proposal.module.js +32 -0
  28. package/dist/proposal/proposal.module.js.map +1 -0
  29. package/dist/proposal/proposal.service.d.ts +95 -0
  30. package/dist/proposal/proposal.service.d.ts.map +1 -0
  31. package/dist/proposal/proposal.service.js +1914 -0
  32. package/dist/proposal/proposal.service.js.map +1 -0
  33. package/dist/proposal/proposal.service.spec.d.ts +2 -0
  34. package/dist/proposal/proposal.service.spec.d.ts.map +1 -0
  35. package/dist/proposal/proposal.service.spec.js +187 -0
  36. package/dist/proposal/proposal.service.spec.js.map +1 -0
  37. package/hedhog/data/dashboard.yaml +6 -0
  38. package/hedhog/data/dashboard_component.yaml +87 -0
  39. package/hedhog/data/dashboard_component_role.yaml +55 -0
  40. package/hedhog/data/dashboard_item.yaml +95 -0
  41. package/hedhog/data/dashboard_role.yaml +6 -0
  42. package/hedhog/data/route.yaml +112 -68
  43. package/hedhog/frontend/app/dashboard/_components/dashboard-widgets.tsx.ejs +508 -0
  44. package/hedhog/frontend/app/dashboard/_components/use-crm-dashboard-data.ts.ejs +104 -0
  45. package/hedhog/frontend/app/dashboard/page.tsx.ejs +37 -431
  46. package/hedhog/frontend/app/pipeline/_components/lead-detail-sheet.tsx.ejs +252 -209
  47. package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +1584 -0
  48. package/hedhog/frontend/messages/en.json +136 -42
  49. package/hedhog/frontend/messages/pt.json +135 -41
  50. package/hedhog/frontend/widgets/next-actions.tsx.ejs +40 -0
  51. package/hedhog/frontend/widgets/overview-kpis.tsx.ejs +47 -0
  52. package/hedhog/frontend/widgets/owner-performance.tsx.ejs +42 -0
  53. package/hedhog/frontend/widgets/quick-access.tsx.ejs +29 -0
  54. package/hedhog/frontend/widgets/source-breakdown.tsx.ejs +40 -0
  55. package/hedhog/frontend/widgets/stage-distribution.tsx.ejs +40 -0
  56. package/hedhog/frontend/widgets/top-owners.tsx.ejs +42 -0
  57. package/hedhog/frontend/widgets/unattended.tsx.ejs +40 -0
  58. package/hedhog/table/crm_activity.yaml +68 -68
  59. package/hedhog/table/crm_stage_history.yaml +34 -34
  60. package/hedhog/table/person_company.yaml +27 -27
  61. package/hedhog/table/proposal.yaml +112 -0
  62. package/hedhog/table/proposal_approval.yaml +63 -0
  63. package/hedhog/table/proposal_document.yaml +77 -0
  64. package/hedhog/table/proposal_item.yaml +64 -0
  65. package/hedhog/table/proposal_revision.yaml +78 -0
  66. package/package.json +6 -5
  67. package/src/contact.module.ts +2 -0
  68. package/src/index.ts +3 -0
  69. package/src/person/dto/account.dto.ts +100 -100
  70. package/src/person/dto/activity.dto.ts +54 -54
  71. package/src/person/dto/dashboard-query.dto.ts +25 -25
  72. package/src/person/dto/followup-query.dto.ts +25 -25
  73. package/src/person/dto/reports-query.dto.ts +25 -25
  74. package/src/person/person.controller.ts +176 -176
  75. package/src/person/person.service.ts +4825 -4825
  76. package/src/proposal/dto/proposal.dto.ts +341 -0
  77. package/src/proposal/proposal-contract.subscriber.ts +43 -0
  78. package/src/proposal/proposal-event.types.ts +130 -0
  79. package/src/proposal/proposal.controller.ts +168 -0
  80. package/src/proposal/proposal.module.ts +19 -0
  81. package/src/proposal/proposal.service.ts +2525 -0
@@ -0,0 +1,104 @@
1
+ 'use client';
2
+
3
+ import { useWidgetData } from '@/hooks/use-widget-data';
4
+ import type { DashboardPeriod, DashboardResponse } from './dashboard-types';
5
+
6
+ export const emptyDashboard: DashboardResponse = {
7
+ kpis: {
8
+ total_leads: 0,
9
+ qualified: 0,
10
+ proposal: 0,
11
+ customers: 0,
12
+ lost: 0,
13
+ unassigned: 0,
14
+ overdue: 0,
15
+ next_actions: 0,
16
+ },
17
+ charts: {
18
+ stage: [],
19
+ source: [],
20
+ owner_performance: [],
21
+ },
22
+ lists: {
23
+ next_actions: [],
24
+ unattended: [],
25
+ },
26
+ };
27
+
28
+ type ContactDashboardEndpointOptions = {
29
+ ownerUserId?: string | number;
30
+ period?: DashboardPeriod;
31
+ dateFrom?: string;
32
+ dateTo?: string;
33
+ };
34
+
35
+ type UseContactDashboardWidgetDataOptions<R> =
36
+ ContactDashboardEndpointOptions & {
37
+ queryKey: string;
38
+ enabled?: boolean;
39
+ select?: (data: DashboardResponse) => R;
40
+ };
41
+
42
+ type ContactDashboardWidgetDataResult<R> = {
43
+ data: R | undefined;
44
+ isLoading: boolean;
45
+ isError: boolean;
46
+ isAccessDenied: boolean;
47
+ error: any;
48
+ };
49
+
50
+ export function buildContactDashboardEndpoint({
51
+ ownerUserId = 'all',
52
+ period = '30d',
53
+ dateFrom,
54
+ dateTo,
55
+ }: ContactDashboardEndpointOptions = {}) {
56
+ const params = new URLSearchParams();
57
+
58
+ if (
59
+ ownerUserId !== 'all' &&
60
+ ownerUserId !== '' &&
61
+ ownerUserId !== null &&
62
+ ownerUserId !== undefined
63
+ ) {
64
+ params.set('owner_user_id', String(ownerUserId));
65
+ }
66
+
67
+ params.set('period', period);
68
+
69
+ if (period === 'custom') {
70
+ if (dateFrom) {
71
+ params.set('date_from', dateFrom);
72
+ }
73
+
74
+ if (dateTo) {
75
+ params.set('date_to', dateTo);
76
+ }
77
+ }
78
+
79
+ const query = params.toString();
80
+
81
+ return `/person/dashboard${query ? `?${query}` : ''}`;
82
+ }
83
+
84
+ export function useContactDashboardWidgetData<R = DashboardResponse>({
85
+ queryKey,
86
+ ownerUserId = 'all',
87
+ period = '30d',
88
+ dateFrom,
89
+ dateTo,
90
+ enabled = true,
91
+ select,
92
+ }: UseContactDashboardWidgetDataOptions<R>): ContactDashboardWidgetDataResult<R> {
93
+ return useWidgetData<DashboardResponse, R>({
94
+ endpoint: buildContactDashboardEndpoint({
95
+ ownerUserId,
96
+ period,
97
+ dateFrom,
98
+ dateTo,
99
+ }),
100
+ queryKey: `contact-dashboard-widget-${queryKey}-${period}-${String(ownerUserId)}`,
101
+ enabled,
102
+ select,
103
+ });
104
+ }
@@ -1,17 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { Page, PageHeader } from '@/components/entity-list';
4
- import { Badge } from '@/components/ui/badge';
5
- import {
6
- Card,
7
- CardContent,
8
- CardDescription,
9
- CardHeader,
10
- CardTitle,
11
- } from '@/components/ui/card';
12
4
  import { Input } from '@/components/ui/input';
13
- import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
14
- import { ScrollArea } from '@/components/ui/scroll-area';
15
5
  import {
16
6
  Select,
17
7
  SelectContent,
@@ -20,64 +10,26 @@ import {
20
10
  SelectValue,
21
11
  } from '@/components/ui/select';
22
12
  import { Skeleton } from '@/components/ui/skeleton';
23
- import { formatDateTime } from '@/lib/format-date';
24
13
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
25
- import {
26
- AlertTriangle,
27
- ArrowUpRight,
28
- BriefcaseBusiness,
29
- CalendarClock,
30
- ChartNoAxesCombined,
31
- CircleDollarSign,
32
- Loader2,
33
- RefreshCcw,
34
- Target,
35
- TrendingUp,
36
- UserRoundX,
37
- } from 'lucide-react';
14
+ import { Loader2, RefreshCcw } from 'lucide-react';
38
15
  import { useTranslations } from 'next-intl';
39
- import Link from 'next/link';
40
16
  import { useState } from 'react';
41
- import {
42
- Bar,
43
- BarChart,
44
- CartesianGrid,
45
- Cell,
46
- Pie,
47
- PieChart,
48
- ResponsiveContainer,
49
- Tooltip,
50
- XAxis,
51
- YAxis,
52
- } from 'recharts';
53
- import { crmImplementedSections } from '../_lib/crm-sections';
54
17
  import type { UserOption } from '../person/_components/person-types';
55
- import {
56
- CRM_DASHBOARD_SOURCE_ORDER,
57
- CRM_DASHBOARD_STAGE_ORDER,
58
- type DashboardPeriod,
59
- type DashboardResponse,
18
+ import type {
19
+ DashboardPeriod,
20
+ DashboardResponse,
60
21
  } from './_components/dashboard-types';
61
-
62
- const chartPalette = [
63
- '#f97316',
64
- '#14b8a6',
65
- '#0ea5e9',
66
- '#84cc16',
67
- '#f59e0b',
68
- '#ef4444',
69
- '#8b5cf6',
70
- ];
71
-
72
- const chartTooltipStyle = {
73
- backgroundColor: 'hsl(var(--card))',
74
- border: '1px solid hsl(var(--border))',
75
- borderRadius: '12px',
76
- fontSize: '12px',
77
- };
78
-
79
- const fallbackKpiAccentClass =
80
- 'from-orange-500/20 via-amber-500/10 to-transparent';
22
+ import {
23
+ CrmDashboardKpiGrid,
24
+ CrmNextActionsCard,
25
+ CrmOwnerPerformanceCard,
26
+ CrmQuickAccessCard,
27
+ CrmSourceBreakdownCard,
28
+ CrmStageChartCard,
29
+ CrmTopOwnersCard,
30
+ CrmUnattendedCard,
31
+ } from './_components/dashboard-widgets';
32
+ import { emptyDashboard } from './_components/use-crm-dashboard-data';
81
33
 
82
34
  function formatDateInputValue(date: Date) {
83
35
  const year = date.getFullYear();
@@ -100,32 +52,9 @@ function getDefaultCustomRange() {
100
52
  };
101
53
  }
102
54
 
103
- const emptyDashboard: DashboardResponse = {
104
- kpis: {
105
- total_leads: 0,
106
- qualified: 0,
107
- proposal: 0,
108
- customers: 0,
109
- lost: 0,
110
- unassigned: 0,
111
- overdue: 0,
112
- next_actions: 0,
113
- },
114
- charts: {
115
- stage: CRM_DASHBOARD_STAGE_ORDER.map((key) => ({ key, total: 0 })),
116
- source: CRM_DASHBOARD_SOURCE_ORDER.map((key) => ({ key, total: 0 })),
117
- owner_performance: [],
118
- },
119
- lists: {
120
- next_actions: [],
121
- unattended: [],
122
- },
123
- };
124
-
125
55
  export default function CrmDashboardPage() {
126
56
  const t = useTranslations('contact.CrmDashboard');
127
- const menuT = useTranslations('contact.CrmMenu');
128
- const { request, currentLocaleCode, getSettingValue } = useApp();
57
+ const { request, currentLocaleCode } = useApp();
129
58
  const defaultCustomRange = getDefaultCustomRange();
130
59
 
131
60
  const [ownerUserId, setOwnerUserId] = useState('all');
@@ -184,65 +113,10 @@ export default function CrmDashboardPage() {
184
113
  placeholderData: (previous) => previous ?? emptyDashboard,
185
114
  });
186
115
 
187
- const stageChartData = dashboard.charts.stage.map((item, index) => ({
188
- name: t(`stageLabels.${item.key}`),
189
- total: item.total,
190
- fill: chartPalette[index % chartPalette.length],
191
- }));
192
-
193
- const sourceChartData = dashboard.charts.source.map((item, index) => ({
194
- name: t(`sourceLabels.${item.key}`),
195
- total: item.total,
196
- fill: chartPalette[index % chartPalette.length],
197
- }));
198
-
199
- const topOwners = [...dashboard.charts.owner_performance]
200
- .sort((left, right) => right.pipeline_value - left.pipeline_value)
201
- .slice(0, 4);
202
-
203
- const kpiCards = [
204
- { key: 'totalLeads', value: dashboard.kpis.total_leads, icon: TrendingUp },
205
- { key: 'qualified', value: dashboard.kpis.qualified, icon: Target },
206
- { key: 'proposal', value: dashboard.kpis.proposal, icon: BriefcaseBusiness },
207
- { key: 'customers', value: dashboard.kpis.customers, icon: CircleDollarSign },
208
- { key: 'lost', value: dashboard.kpis.lost, icon: AlertTriangle },
209
- { key: 'unassigned', value: dashboard.kpis.unassigned, icon: UserRoundX },
210
- { key: 'overdue', value: dashboard.kpis.overdue, icon: CalendarClock },
211
- {
212
- key: 'nextActions',
213
- value: dashboard.kpis.next_actions,
214
- icon: ChartNoAxesCombined,
215
- },
216
- ].map((item, index) => {
217
- const sectionStyle =
218
- crmImplementedSections[index % crmImplementedSections.length] ??
219
- crmImplementedSections[0];
220
-
221
- return {
222
- key: item.key,
223
- title: t(`kpis.${item.key}.title`),
224
- value: item.value,
225
- description: t(`kpis.${item.key}.description`),
226
- icon: item.icon,
227
- accentClassName: sectionStyle?.colorClass ?? fallbackKpiAccentClass,
228
- iconContainerClassName:
229
- sectionStyle?.glowClass ?? 'bg-sky-500/10 text-sky-700',
230
- };
231
- });
232
-
233
- const currencyFormatter = new Intl.NumberFormat(
234
- currentLocaleCode === 'pt' ? 'pt-BR' : 'en-US',
235
- {
236
- style: 'currency',
237
- currency: 'BRL',
238
- maximumFractionDigits: 0,
239
- }
240
- );
241
-
242
116
  const filters = (
243
117
  <div className="flex flex-wrap items-center justify-end gap-2">
244
118
  <Select value={ownerUserId} onValueChange={setOwnerUserId}>
245
- <SelectTrigger className="w-full sm:w-[180px]">
119
+ <SelectTrigger className="w-full sm:w-45">
246
120
  <SelectValue placeholder={t('filters.ownerPlaceholder')} />
247
121
  </SelectTrigger>
248
122
  <SelectContent>
@@ -267,7 +141,7 @@ export default function CrmDashboardPage() {
267
141
  }
268
142
  }}
269
143
  >
270
- <SelectTrigger className="w-full sm:w-[180px]">
144
+ <SelectTrigger className="w-full sm:w-45">
271
145
  <SelectValue placeholder={t('filters.periodPlaceholder')} />
272
146
  </SelectTrigger>
273
147
  <SelectContent>
@@ -284,14 +158,14 @@ export default function CrmDashboardPage() {
284
158
  type="date"
285
159
  value={dateFrom}
286
160
  onChange={(event) => setDateFrom(event.target.value)}
287
- className="w-full sm:w-[170px]"
161
+ className="w-full sm:w-42.5"
288
162
  aria-label={t('filters.dateFrom')}
289
163
  />
290
164
  <Input
291
165
  type="date"
292
166
  value={dateTo}
293
167
  onChange={(event) => setDateTo(event.target.value)}
294
- className="w-full sm:w-[170px]"
168
+ className="w-full sm:w-42.5"
295
169
  aria-label={t('filters.dateTo')}
296
170
  />
297
171
  </>
@@ -326,310 +200,42 @@ export default function CrmDashboardPage() {
326
200
  />
327
201
 
328
202
  <div className="min-w-0 space-y-6 overflow-x-hidden">
329
- <KpiCardsGrid items={kpiCards} />
203
+ <CrmDashboardKpiGrid dashboard={dashboard} />
330
204
 
331
205
  {isLoading ? (
332
206
  <div className="space-y-6">
333
207
  <div className="grid gap-6 xl:grid-cols-2">
334
- <Skeleton className="h-[320px] w-full" />
335
- <Skeleton className="h-[320px] w-full" />
208
+ <Skeleton className="h-80 w-full" />
209
+ <Skeleton className="h-80 w-full" />
336
210
  </div>
337
211
  <div className="grid gap-6 xl:grid-cols-2">
338
- <Skeleton className="h-[320px] w-full" />
339
- <Skeleton className="h-[320px] w-full" />
212
+ <Skeleton className="h-80 w-full" />
213
+ <Skeleton className="h-80 w-full" />
340
214
  </div>
341
215
  <div className="grid gap-6 xl:grid-cols-3">
342
- <Skeleton className="h-[320px] w-full" />
343
- <Skeleton className="h-[320px] w-full" />
344
- <Skeleton className="h-[320px] w-full" />
216
+ <Skeleton className="h-80 w-full" />
217
+ <Skeleton className="h-80 w-full" />
218
+ <Skeleton className="h-80 w-full" />
345
219
  </div>
346
220
  </div>
347
221
  ) : (
348
222
  <>
349
223
  <div className="grid min-w-0 gap-6 xl:grid-cols-[minmax(0,1.2fr)_minmax(320px,0.8fr)]">
350
- <Card className="min-w-0">
351
- <CardHeader>
352
- <CardTitle>{t('charts.stage.title')}</CardTitle>
353
- <CardDescription>{t('charts.stage.description')}</CardDescription>
354
- </CardHeader>
355
- <CardContent className="h-[320px]">
356
- <ResponsiveContainer width="100%" height="100%">
357
- <BarChart data={stageChartData}>
358
- <CartesianGrid
359
- strokeDasharray="3 3"
360
- stroke="hsl(var(--border))"
361
- vertical={false}
362
- />
363
- <XAxis
364
- dataKey="name"
365
- tickLine={false}
366
- axisLine={false}
367
- fontSize={12}
368
- />
369
- <YAxis
370
- tickLine={false}
371
- axisLine={false}
372
- fontSize={12}
373
- allowDecimals={false}
374
- />
375
- <Tooltip contentStyle={chartTooltipStyle} />
376
- <Bar dataKey="total" radius={[8, 8, 0, 0]}>
377
- {stageChartData.map((entry) => (
378
- <Cell key={entry.name} fill={entry.fill} />
379
- ))}
380
- </Bar>
381
- </BarChart>
382
- </ResponsiveContainer>
383
- </CardContent>
384
- </Card>
385
-
386
- <Card className="min-w-0 overflow-hidden">
387
- <CardHeader>
388
- <CardTitle>{t('charts.source.title')}</CardTitle>
389
- <CardDescription>{t('charts.source.description')}</CardDescription>
390
- </CardHeader>
391
- <CardContent className="grid min-w-0 gap-4 overflow-hidden lg:h-[320px] lg:grid-cols-[minmax(220px,0.9fr)_minmax(0,1fr)]">
392
- <div className="h-[220px] min-w-0 lg:h-full">
393
- <ResponsiveContainer width="100%" height="100%">
394
- <PieChart>
395
- <Pie
396
- data={sourceChartData}
397
- dataKey="total"
398
- nameKey="name"
399
- innerRadius={58}
400
- outerRadius={94}
401
- paddingAngle={2}
402
- >
403
- {sourceChartData.map((entry) => (
404
- <Cell key={entry.name} fill={entry.fill} />
405
- ))}
406
- </Pie>
407
- <Tooltip contentStyle={chartTooltipStyle} />
408
- </PieChart>
409
- </ResponsiveContainer>
410
- </div>
411
- <ScrollArea className="min-w-0 lg:h-full">
412
- <div className="space-y-3 lg:pr-3">
413
- {sourceChartData.map((item) => (
414
- <div
415
- key={item.name}
416
- className="flex min-w-0 items-center justify-between gap-3 rounded-2xl border border-border/70 px-4 py-3"
417
- >
418
- <div className="flex min-w-0 items-center gap-3">
419
- <span
420
- className="inline-block size-3 rounded-full"
421
- style={{ backgroundColor: item.fill }}
422
- />
423
- <span className="truncate font-medium">{item.name}</span>
424
- </div>
425
- <span className="shrink-0 text-sm text-muted-foreground">
426
- {item.total}
427
- </span>
428
- </div>
429
- ))}
430
- </div>
431
- </ScrollArea>
432
- </CardContent>
433
- </Card>
224
+ <CrmStageChartCard items={dashboard.charts.stage} />
225
+ <CrmSourceBreakdownCard items={dashboard.charts.source} />
434
226
  </div>
435
227
 
436
228
  <div className="grid min-w-0 gap-6 xl:grid-cols-[minmax(0,1.1fr)_minmax(320px,0.9fr)]">
437
- <Card className="min-w-0">
438
- <CardHeader>
439
- <CardTitle>{t('charts.owner.title')}</CardTitle>
440
- <CardDescription>{t('charts.owner.description')}</CardDescription>
441
- </CardHeader>
442
- <CardContent className="h-[320px]">
443
- <ResponsiveContainer width="100%" height="100%">
444
- <BarChart data={dashboard.charts.owner_performance}>
445
- <CartesianGrid
446
- strokeDasharray="3 3"
447
- stroke="hsl(var(--border))"
448
- vertical={false}
449
- />
450
- <XAxis
451
- dataKey="owner_name"
452
- tickLine={false}
453
- axisLine={false}
454
- fontSize={12}
455
- />
456
- <YAxis
457
- tickLine={false}
458
- axisLine={false}
459
- fontSize={12}
460
- allowDecimals={false}
461
- />
462
- <Tooltip contentStyle={chartTooltipStyle} />
463
- <Bar dataKey="leads" fill="#f97316" radius={[8, 8, 0, 0]} />
464
- <Bar
465
- dataKey="customers"
466
- fill="#0ea5e9"
467
- radius={[8, 8, 0, 0]}
468
- />
469
- </BarChart>
470
- </ResponsiveContainer>
471
- </CardContent>
472
- </Card>
473
-
474
- <Card className="min-w-0 overflow-hidden">
475
- <CardHeader>
476
- <CardTitle>{t('bestOwners.title')}</CardTitle>
477
- <CardDescription>{t('bestOwners.description')}</CardDescription>
478
- </CardHeader>
479
- <CardContent>
480
- <div className="space-y-3">
481
- {topOwners.length === 0 ? (
482
- <p className="text-sm text-muted-foreground">
483
- {t('common.noData')}
484
- </p>
485
- ) : (
486
- topOwners.map((owner, index) => (
487
- <div
488
- key={`${owner.owner_user_id}-${owner.owner_name}`}
489
- className="flex min-w-0 items-center justify-between gap-3 rounded-2xl border border-border/70 px-4 py-3"
490
- >
491
- <div className="min-w-0">
492
- <p className="truncate font-medium">
493
- {index + 1}. {owner.owner_name}
494
- </p>
495
- <p className="truncate text-xs text-muted-foreground">
496
- {t('bestOwners.meta', {
497
- leads: owner.leads,
498
- customers: owner.customers,
499
- })}
500
- </p>
501
- </div>
502
- <span className="shrink-0 text-sm font-medium text-foreground">
503
- {currencyFormatter.format(owner.pipeline_value)}
504
- </span>
505
- </div>
506
- ))
507
- )}
508
- </div>
509
- </CardContent>
510
- </Card>
229
+ <CrmOwnerPerformanceCard
230
+ items={dashboard.charts.owner_performance}
231
+ />
232
+ <CrmTopOwnersCard items={dashboard.charts.owner_performance} />
511
233
  </div>
512
234
 
513
235
  <div className="grid gap-6 xl:grid-cols-3">
514
- <Card className="min-w-0 overflow-hidden xl:col-span-1">
515
- <CardHeader>
516
- <CardTitle>{t('blocks.nextActions.title')}</CardTitle>
517
- <CardDescription>{t('blocks.nextActions.description')}</CardDescription>
518
- </CardHeader>
519
- <CardContent>
520
- <ScrollArea className="h-[320px] pr-3">
521
- <div className="space-y-3">
522
- {dashboard.lists.next_actions.length === 0 ? (
523
- <p className="text-sm text-muted-foreground">
524
- {t('common.noData')}
525
- </p>
526
- ) : (
527
- dashboard.lists.next_actions.map((lead) => (
528
- <div
529
- key={lead.id}
530
- className="rounded-2xl border border-border/70 px-4 py-3"
531
- >
532
- <p className="font-medium">{lead.name}</p>
533
- <p className="text-xs text-muted-foreground">
534
- {lead.owner_user?.name || t('common.unassigned')}
535
- </p>
536
- <div className="mt-2 flex flex-wrap gap-2">
537
- <Badge variant="outline">
538
- {t(`stageLabels.${lead.lifecycle_stage}`)}
539
- </Badge>
540
- <Badge variant="secondary">
541
- {t(`sourceLabels.${lead.source}`)}
542
- </Badge>
543
- </div>
544
- <p className="mt-2 text-sm text-muted-foreground">
545
- {t('blocks.nextActions.when', {
546
- date: formatDateTime(
547
- lead.next_action_at ?? '',
548
- getSettingValue,
549
- currentLocaleCode
550
- ),
551
- })}
552
- </p>
553
- </div>
554
- ))
555
- )}
556
- </div>
557
- </ScrollArea>
558
- </CardContent>
559
- </Card>
560
-
561
- <Card className="min-w-0 overflow-hidden xl:col-span-1">
562
- <CardHeader>
563
- <CardTitle>{t('blocks.unattended.title')}</CardTitle>
564
- <CardDescription>{t('blocks.unattended.description')}</CardDescription>
565
- </CardHeader>
566
- <CardContent>
567
- <ScrollArea className="h-[320px] pr-3">
568
- <div className="space-y-3">
569
- {dashboard.lists.unattended.length === 0 ? (
570
- <p className="text-sm text-muted-foreground">
571
- {t('common.noData')}
572
- </p>
573
- ) : (
574
- dashboard.lists.unattended.map((lead) => (
575
- <div
576
- key={lead.id}
577
- className="rounded-2xl border border-border/70 px-4 py-3"
578
- >
579
- <p className="font-medium">{lead.name}</p>
580
- <p className="text-xs text-muted-foreground">
581
- {lead.owner_user?.name || t('common.unassigned')}
582
- </p>
583
- <p className="mt-2 text-sm text-muted-foreground">
584
- {t('blocks.unattended.meta', {
585
- stage: t(`stageLabels.${lead.lifecycle_stage}`),
586
- source: t(`sourceLabels.${lead.source}`),
587
- })}
588
- </p>
589
- </div>
590
- ))
591
- )}
592
- </div>
593
- </ScrollArea>
594
- </CardContent>
595
- </Card>
596
-
597
- <Card className="min-w-0 overflow-hidden xl:col-span-1">
598
- <CardHeader>
599
- <CardTitle>{t('blocks.quickAccess.title')}</CardTitle>
600
- <CardDescription>{t('blocks.quickAccess.description')}</CardDescription>
601
- </CardHeader>
602
- <CardContent className="space-y-3">
603
- {crmImplementedSections.map((section) => {
604
- const Icon = section.icon;
605
-
606
- return (
607
- <Link
608
- key={section.href}
609
- href={section.href}
610
- className="flex min-w-0 items-center justify-between gap-3 rounded-2xl border border-border/70 px-4 py-3 transition-colors hover:bg-muted/40"
611
- >
612
- <div className="flex min-w-0 items-center gap-3">
613
- <div className={`rounded-2xl p-3 ${section.glowClass}`}>
614
- <Icon className="size-4" />
615
- </div>
616
- <div className="min-w-0">
617
- <p className="truncate font-medium">
618
- {menuT(`sections.${section.translationKey}.title`)}
619
- </p>
620
- <p className="truncate text-xs text-muted-foreground">
621
- {menuT(
622
- `sections.${section.translationKey}.description`
623
- )}
624
- </p>
625
- </div>
626
- </div>
627
- <ArrowUpRight className="size-4 shrink-0 text-muted-foreground" />
628
- </Link>
629
- );
630
- })}
631
- </CardContent>
632
- </Card>
236
+ <CrmNextActionsCard items={dashboard.lists.next_actions} />
237
+ <CrmUnattendedCard items={dashboard.lists.unattended} />
238
+ <CrmQuickAccessCard />
633
239
  </div>
634
240
  </>
635
241
  )}