@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
@@ -29,74 +29,74 @@
29
29
  - url: /person/duplicates
30
30
  method: GET
31
31
  relations: *a3
32
- - url: /person/stats
33
- method: GET
34
- relations:
35
- role:
36
- - where:
37
- slug: admin
38
- - where:
39
- slug: admin-contact
40
- - url: /person/dashboard
41
- method: GET
42
- relations:
43
- role:
44
- - where:
45
- slug: admin
46
- - where:
47
- slug: admin-contact
48
- - url: /person/reports
49
- method: GET
50
- relations:
51
- role:
52
- - where:
53
- slug: admin
54
- - where:
55
- slug: admin-contact
56
- - url: /person/owner-options
57
- method: GET
58
- relations:
59
- role:
60
- - where:
61
- slug: admin
62
- - where:
63
- slug: admin-contact
64
- - url: /person/followups
65
- method: GET
66
- relations: *a3
67
- - url: /person/followups/stats
68
- method: GET
69
- relations: *a3
70
- - url: /person/activities
71
- method: GET
72
- relations: *a3
73
- - url: /person/activities/stats
74
- method: GET
75
- relations: *a3
76
- - url: /person/activities/:id
77
- method: GET
78
- relations: *a3
79
- - url: /person/activities/:id/complete
80
- method: POST
81
- relations: *a3
82
- - url: /person/accounts
83
- method: GET
84
- relations: *a3
85
- - url: /person/accounts/stats
86
- method: GET
87
- relations: *a3
88
- - url: /person/accounts
89
- method: POST
90
- relations: *a3
91
- - url: /person/accounts/:id
92
- method: PATCH
93
- relations: *a3
94
- - url: /person/accounts
95
- method: DELETE
96
- relations: *a3
97
- - url: /person
98
- method: POST
99
- relations: *a3
32
+ - url: /person/stats
33
+ method: GET
34
+ relations:
35
+ role:
36
+ - where:
37
+ slug: admin
38
+ - where:
39
+ slug: admin-contact
40
+ - url: /person/dashboard
41
+ method: GET
42
+ relations:
43
+ role:
44
+ - where:
45
+ slug: admin
46
+ - where:
47
+ slug: admin-contact
48
+ - url: /person/reports
49
+ method: GET
50
+ relations:
51
+ role:
52
+ - where:
53
+ slug: admin
54
+ - where:
55
+ slug: admin-contact
56
+ - url: /person/owner-options
57
+ method: GET
58
+ relations:
59
+ role:
60
+ - where:
61
+ slug: admin
62
+ - where:
63
+ slug: admin-contact
64
+ - url: /person/followups
65
+ method: GET
66
+ relations: *a3
67
+ - url: /person/followups/stats
68
+ method: GET
69
+ relations: *a3
70
+ - url: /person/activities
71
+ method: GET
72
+ relations: *a3
73
+ - url: /person/activities/stats
74
+ method: GET
75
+ relations: *a3
76
+ - url: /person/activities/:id
77
+ method: GET
78
+ relations: *a3
79
+ - url: /person/activities/:id/complete
80
+ method: POST
81
+ relations: *a3
82
+ - url: /person/accounts
83
+ method: GET
84
+ relations: *a3
85
+ - url: /person/accounts/stats
86
+ method: GET
87
+ relations: *a3
88
+ - url: /person/accounts
89
+ method: POST
90
+ relations: *a3
91
+ - url: /person/accounts/:id
92
+ method: PATCH
93
+ relations: *a3
94
+ - url: /person/accounts
95
+ method: DELETE
96
+ relations: *a3
97
+ - url: /person
98
+ method: POST
99
+ relations: *a3
100
100
  - url: /person/merge
101
101
  method: POST
102
102
  relations: *a3
@@ -121,6 +121,50 @@
121
121
  - url: /person
122
122
  method: DELETE
123
123
  relations: *a3
124
+ - url: /proposal
125
+ method: GET
126
+ relations: &a13
127
+ role:
128
+ - where:
129
+ slug: admin
130
+ - where:
131
+ slug: admin-contact
132
+ - url: /proposal/stats
133
+ method: GET
134
+ relations: *a13
135
+ - url: /proposal
136
+ method: POST
137
+ relations: *a13
138
+ - url: /proposal/:id
139
+ method: GET
140
+ relations: *a13
141
+ - url: /proposal/:id
142
+ method: PATCH
143
+ relations: *a13
144
+ - url: /proposal/:id/generate-pdf
145
+ method: POST
146
+ relations: *a13
147
+ - url: /proposal/:id/submit
148
+ method: POST
149
+ relations: *a13
150
+ - url: /proposal/:id/send
151
+ method: POST
152
+ relations: *a13
153
+ - url: /proposal/:id/approve
154
+ method: POST
155
+ relations: *a13
156
+ - url: /proposal/:id/reject
157
+ method: POST
158
+ relations: *a13
159
+ - url: /proposal/:id/cancel
160
+ method: POST
161
+ relations: *a13
162
+ - url: /proposal/:id/convert
163
+ method: POST
164
+ relations: *a13
165
+ - url: /proposal
166
+ method: DELETE
167
+ relations: *a13
124
168
  - url: /person-document-type
125
169
  method: GET
126
170
  relations: &a4
@@ -0,0 +1,508 @@
1
+ 'use client';
2
+
3
+ import { Badge } from '@/components/ui/badge';
4
+ import {
5
+ Card,
6
+ CardContent,
7
+ CardDescription,
8
+ CardHeader,
9
+ CardTitle,
10
+ } from '@/components/ui/card';
11
+ import { KpiCardsGrid, type KpiCardItem } from '@/components/ui/kpi-cards-grid';
12
+ import { ScrollArea } from '@/components/ui/scroll-area';
13
+ import { formatDateTime } from '@/lib/format-date';
14
+ import { cn } from '@/lib/utils';
15
+ import { useApp } from '@hed-hog/next-app-provider';
16
+ import {
17
+ AlertTriangle,
18
+ ArrowUpRight,
19
+ BriefcaseBusiness,
20
+ CalendarClock,
21
+ ChartNoAxesCombined,
22
+ CircleDollarSign,
23
+ Target,
24
+ TrendingUp,
25
+ UserRoundX,
26
+ } from 'lucide-react';
27
+ import { useTranslations } from 'next-intl';
28
+ import Link from 'next/link';
29
+ import {
30
+ Bar,
31
+ BarChart,
32
+ CartesianGrid,
33
+ Cell,
34
+ Pie,
35
+ PieChart,
36
+ ResponsiveContainer,
37
+ Tooltip,
38
+ XAxis,
39
+ YAxis,
40
+ } from 'recharts';
41
+ import { crmImplementedSections } from '../../_lib/crm-sections';
42
+ import type {
43
+ DashboardListItem,
44
+ DashboardOwnerPerformanceItem,
45
+ DashboardResponse,
46
+ } from './dashboard-types';
47
+
48
+ const chartPalette = [
49
+ '#f97316',
50
+ '#14b8a6',
51
+ '#0ea5e9',
52
+ '#84cc16',
53
+ '#f59e0b',
54
+ '#ef4444',
55
+ '#8b5cf6',
56
+ ];
57
+
58
+ const chartTooltipStyle = {
59
+ backgroundColor: 'hsl(var(--card))',
60
+ border: '1px solid hsl(var(--border))',
61
+ borderRadius: '12px',
62
+ fontSize: '12px',
63
+ };
64
+
65
+ const fallbackKpiAccentClass =
66
+ 'from-orange-500/20 via-amber-500/10 to-transparent';
67
+
68
+ const kpiDefinitions = [
69
+ { key: 'totalLeads', metricKey: 'total_leads', icon: TrendingUp },
70
+ { key: 'qualified', metricKey: 'qualified', icon: Target },
71
+ { key: 'proposal', metricKey: 'proposal', icon: BriefcaseBusiness },
72
+ { key: 'customers', metricKey: 'customers', icon: CircleDollarSign },
73
+ { key: 'lost', metricKey: 'lost', icon: AlertTriangle },
74
+ { key: 'unassigned', metricKey: 'unassigned', icon: UserRoundX },
75
+ { key: 'overdue', metricKey: 'overdue', icon: CalendarClock },
76
+ { key: 'nextActions', metricKey: 'next_actions', icon: ChartNoAxesCombined },
77
+ ] as const;
78
+
79
+ type CrmDashboardCardProps = {
80
+ className?: string;
81
+ contentClassName?: string;
82
+ };
83
+
84
+ type CrmDashboardListCardProps = CrmDashboardCardProps & {
85
+ scrollAreaClassName?: string;
86
+ };
87
+
88
+ export function CrmDashboardKpiGrid({
89
+ dashboard,
90
+ className,
91
+ cardClassName,
92
+ }: {
93
+ dashboard: DashboardResponse;
94
+ className?: string;
95
+ cardClassName?: string;
96
+ }) {
97
+ const t = useTranslations('contact.CrmDashboard');
98
+
99
+ const items: KpiCardItem[] = kpiDefinitions.map((item, index) => {
100
+ const sectionStyle =
101
+ crmImplementedSections[index % crmImplementedSections.length] ??
102
+ crmImplementedSections[0];
103
+
104
+ return {
105
+ key: item.key,
106
+ title: t(`kpis.${item.key}.title`),
107
+ value: dashboard.kpis[item.metricKey],
108
+ description: t(`kpis.${item.key}.description`),
109
+ icon: item.icon,
110
+ accentClassName: sectionStyle?.colorClass ?? fallbackKpiAccentClass,
111
+ iconContainerClassName:
112
+ sectionStyle?.glowClass ?? 'bg-sky-500/10 text-sky-700',
113
+ className: cardClassName,
114
+ };
115
+ });
116
+
117
+ return <KpiCardsGrid items={items} className={className} />;
118
+ }
119
+
120
+ export function CrmStageChartCard({
121
+ items,
122
+ className,
123
+ contentClassName,
124
+ }: CrmDashboardCardProps & {
125
+ items: DashboardResponse['charts']['stage'];
126
+ }) {
127
+ const t = useTranslations('contact.CrmDashboard');
128
+
129
+ const stageChartData = items.map((item, index) => ({
130
+ name: t(`stageLabels.${item.key}`),
131
+ total: item.total,
132
+ fill: chartPalette[index % chartPalette.length],
133
+ }));
134
+
135
+ return (
136
+ <Card className={cn('min-w-0 overflow-hidden', className)}>
137
+ <CardHeader>
138
+ <CardTitle>{t('charts.stage.title')}</CardTitle>
139
+ <CardDescription>{t('charts.stage.description')}</CardDescription>
140
+ </CardHeader>
141
+ <CardContent className={cn('h-80', contentClassName)}>
142
+ <ResponsiveContainer width="100%" height="100%">
143
+ <BarChart data={stageChartData}>
144
+ <CartesianGrid
145
+ strokeDasharray="3 3"
146
+ stroke="hsl(var(--border))"
147
+ vertical={false}
148
+ />
149
+ <XAxis
150
+ dataKey="name"
151
+ tickLine={false}
152
+ axisLine={false}
153
+ fontSize={12}
154
+ />
155
+ <YAxis
156
+ tickLine={false}
157
+ axisLine={false}
158
+ fontSize={12}
159
+ allowDecimals={false}
160
+ />
161
+ <Tooltip contentStyle={chartTooltipStyle} />
162
+ <Bar dataKey="total" radius={[8, 8, 0, 0]}>
163
+ {stageChartData.map((entry) => (
164
+ <Cell key={entry.name} fill={entry.fill} />
165
+ ))}
166
+ </Bar>
167
+ </BarChart>
168
+ </ResponsiveContainer>
169
+ </CardContent>
170
+ </Card>
171
+ );
172
+ }
173
+
174
+ export function CrmSourceBreakdownCard({
175
+ items,
176
+ className,
177
+ contentClassName,
178
+ }: CrmDashboardCardProps & {
179
+ items: DashboardResponse['charts']['source'];
180
+ }) {
181
+ const t = useTranslations('contact.CrmDashboard');
182
+
183
+ const sourceChartData = items.map((item, index) => ({
184
+ name: t(`sourceLabels.${item.key}`),
185
+ total: item.total,
186
+ fill: chartPalette[index % chartPalette.length],
187
+ }));
188
+
189
+ return (
190
+ <Card className={cn('min-w-0 overflow-hidden', className)}>
191
+ <CardHeader>
192
+ <CardTitle>{t('charts.source.title')}</CardTitle>
193
+ <CardDescription>{t('charts.source.description')}</CardDescription>
194
+ </CardHeader>
195
+ <CardContent
196
+ className={cn(
197
+ 'grid min-w-0 gap-4 overflow-hidden lg:h-80 lg:grid-cols-[minmax(220px,0.9fr)_minmax(0,1fr)]',
198
+ contentClassName
199
+ )}
200
+ >
201
+ <div className="h-55 min-w-0 lg:h-full">
202
+ <ResponsiveContainer width="100%" height="100%">
203
+ <PieChart>
204
+ <Pie
205
+ data={sourceChartData}
206
+ dataKey="total"
207
+ nameKey="name"
208
+ innerRadius={58}
209
+ outerRadius={94}
210
+ paddingAngle={2}
211
+ >
212
+ {sourceChartData.map((entry) => (
213
+ <Cell key={entry.name} fill={entry.fill} />
214
+ ))}
215
+ </Pie>
216
+ <Tooltip contentStyle={chartTooltipStyle} />
217
+ </PieChart>
218
+ </ResponsiveContainer>
219
+ </div>
220
+ <ScrollArea className="min-w-0 lg:h-full">
221
+ <div className="space-y-3 lg:pr-3">
222
+ {sourceChartData.map((item) => (
223
+ <div
224
+ key={item.name}
225
+ className="flex min-w-0 items-center justify-between gap-3 rounded-2xl border border-border/70 px-4 py-3"
226
+ >
227
+ <div className="flex min-w-0 items-center gap-3">
228
+ <span
229
+ className="inline-block size-3 rounded-full"
230
+ style={{ backgroundColor: item.fill }}
231
+ />
232
+ <span className="truncate font-medium">{item.name}</span>
233
+ </div>
234
+ <span className="shrink-0 text-sm text-muted-foreground">
235
+ {item.total}
236
+ </span>
237
+ </div>
238
+ ))}
239
+ </div>
240
+ </ScrollArea>
241
+ </CardContent>
242
+ </Card>
243
+ );
244
+ }
245
+
246
+ export function CrmOwnerPerformanceCard({
247
+ items,
248
+ className,
249
+ contentClassName,
250
+ }: CrmDashboardCardProps & {
251
+ items: DashboardOwnerPerformanceItem[];
252
+ }) {
253
+ const t = useTranslations('contact.CrmDashboard');
254
+
255
+ return (
256
+ <Card className={cn('min-w-0 overflow-hidden', className)}>
257
+ <CardHeader>
258
+ <CardTitle>{t('charts.owner.title')}</CardTitle>
259
+ <CardDescription>{t('charts.owner.description')}</CardDescription>
260
+ </CardHeader>
261
+ <CardContent className={cn('h-80', contentClassName)}>
262
+ <ResponsiveContainer width="100%" height="100%">
263
+ <BarChart data={items}>
264
+ <CartesianGrid
265
+ strokeDasharray="3 3"
266
+ stroke="hsl(var(--border))"
267
+ vertical={false}
268
+ />
269
+ <XAxis
270
+ dataKey="owner_name"
271
+ tickLine={false}
272
+ axisLine={false}
273
+ fontSize={12}
274
+ />
275
+ <YAxis
276
+ tickLine={false}
277
+ axisLine={false}
278
+ fontSize={12}
279
+ allowDecimals={false}
280
+ />
281
+ <Tooltip contentStyle={chartTooltipStyle} />
282
+ <Bar dataKey="leads" fill="#f97316" radius={[8, 8, 0, 0]} />
283
+ <Bar dataKey="customers" fill="#0ea5e9" radius={[8, 8, 0, 0]} />
284
+ </BarChart>
285
+ </ResponsiveContainer>
286
+ </CardContent>
287
+ </Card>
288
+ );
289
+ }
290
+
291
+ export function CrmTopOwnersCard({
292
+ items,
293
+ className,
294
+ }: CrmDashboardCardProps & {
295
+ items: DashboardOwnerPerformanceItem[];
296
+ }) {
297
+ const t = useTranslations('contact.CrmDashboard');
298
+ const { currentLocaleCode } = useApp();
299
+
300
+ const topOwners = [...items]
301
+ .sort((left, right) => right.pipeline_value - left.pipeline_value)
302
+ .slice(0, 4);
303
+
304
+ const currencyFormatter = new Intl.NumberFormat(
305
+ currentLocaleCode === 'pt' ? 'pt-BR' : 'en-US',
306
+ {
307
+ style: 'currency',
308
+ currency: 'BRL',
309
+ maximumFractionDigits: 0,
310
+ }
311
+ );
312
+
313
+ return (
314
+ <Card className={cn('min-w-0 overflow-hidden', className)}>
315
+ <CardHeader>
316
+ <CardTitle>{t('bestOwners.title')}</CardTitle>
317
+ <CardDescription>{t('bestOwners.description')}</CardDescription>
318
+ </CardHeader>
319
+ <CardContent>
320
+ <div className="space-y-3">
321
+ {topOwners.length === 0 ? (
322
+ <p className="text-sm text-muted-foreground">
323
+ {t('common.noData')}
324
+ </p>
325
+ ) : (
326
+ topOwners.map((owner, index) => (
327
+ <div
328
+ key={`${owner.owner_user_id}-${owner.owner_name}`}
329
+ className="flex min-w-0 items-center justify-between gap-3 rounded-2xl border border-border/70 px-4 py-3"
330
+ >
331
+ <div className="min-w-0">
332
+ <p className="truncate font-medium">
333
+ {index + 1}. {owner.owner_name}
334
+ </p>
335
+ <p className="truncate text-xs text-muted-foreground">
336
+ {t('bestOwners.meta', {
337
+ leads: owner.leads,
338
+ customers: owner.customers,
339
+ })}
340
+ </p>
341
+ </div>
342
+ <span className="shrink-0 text-sm font-medium text-foreground">
343
+ {currencyFormatter.format(owner.pipeline_value)}
344
+ </span>
345
+ </div>
346
+ ))
347
+ )}
348
+ </div>
349
+ </CardContent>
350
+ </Card>
351
+ );
352
+ }
353
+
354
+ function CrmLeadBadges({ lead }: { lead: DashboardListItem }) {
355
+ const t = useTranslations('contact.CrmDashboard');
356
+
357
+ return (
358
+ <div className="mt-2 flex flex-wrap gap-2">
359
+ <Badge variant="outline">
360
+ {t(`stageLabels.${lead.lifecycle_stage}`)}
361
+ </Badge>
362
+ <Badge variant="secondary">{t(`sourceLabels.${lead.source}`)}</Badge>
363
+ </div>
364
+ );
365
+ }
366
+
367
+ export function CrmNextActionsCard({
368
+ items,
369
+ className,
370
+ scrollAreaClassName,
371
+ }: CrmDashboardListCardProps & {
372
+ items: DashboardListItem[];
373
+ }) {
374
+ const t = useTranslations('contact.CrmDashboard');
375
+ const { currentLocaleCode, getSettingValue } = useApp();
376
+
377
+ return (
378
+ <Card className={cn('min-w-0 overflow-hidden', className)}>
379
+ <CardHeader>
380
+ <CardTitle>{t('blocks.nextActions.title')}</CardTitle>
381
+ <CardDescription>{t('blocks.nextActions.description')}</CardDescription>
382
+ </CardHeader>
383
+ <CardContent>
384
+ <ScrollArea className={scrollAreaClassName ?? 'h-80 pr-3'}>
385
+ <div className="space-y-3">
386
+ {items.length === 0 ? (
387
+ <p className="text-sm text-muted-foreground">
388
+ {t('common.noData')}
389
+ </p>
390
+ ) : (
391
+ items.map((lead) => (
392
+ <div
393
+ key={lead.id}
394
+ className="rounded-2xl border border-border/70 px-4 py-3"
395
+ >
396
+ <p className="font-medium">{lead.name}</p>
397
+ <p className="text-xs text-muted-foreground">
398
+ {lead.owner_user?.name || t('common.unassigned')}
399
+ </p>
400
+ <CrmLeadBadges lead={lead} />
401
+ <p className="mt-2 text-sm text-muted-foreground">
402
+ {t('blocks.nextActions.when', {
403
+ date: formatDateTime(
404
+ lead.next_action_at ?? '',
405
+ getSettingValue,
406
+ currentLocaleCode
407
+ ),
408
+ })}
409
+ </p>
410
+ </div>
411
+ ))
412
+ )}
413
+ </div>
414
+ </ScrollArea>
415
+ </CardContent>
416
+ </Card>
417
+ );
418
+ }
419
+
420
+ export function CrmUnattendedCard({
421
+ items,
422
+ className,
423
+ scrollAreaClassName,
424
+ }: CrmDashboardListCardProps & {
425
+ items: DashboardListItem[];
426
+ }) {
427
+ const t = useTranslations('contact.CrmDashboard');
428
+
429
+ return (
430
+ <Card className={cn('min-w-0 overflow-hidden', className)}>
431
+ <CardHeader>
432
+ <CardTitle>{t('blocks.unattended.title')}</CardTitle>
433
+ <CardDescription>{t('blocks.unattended.description')}</CardDescription>
434
+ </CardHeader>
435
+ <CardContent>
436
+ <ScrollArea className={scrollAreaClassName ?? 'h-80 pr-3'}>
437
+ <div className="space-y-3">
438
+ {items.length === 0 ? (
439
+ <p className="text-sm text-muted-foreground">
440
+ {t('common.noData')}
441
+ </p>
442
+ ) : (
443
+ items.map((lead) => (
444
+ <div
445
+ key={lead.id}
446
+ className="rounded-2xl border border-border/70 px-4 py-3"
447
+ >
448
+ <p className="font-medium">{lead.name}</p>
449
+ <p className="text-xs text-muted-foreground">
450
+ {lead.owner_user?.name || t('common.unassigned')}
451
+ </p>
452
+ <p className="mt-2 text-sm text-muted-foreground">
453
+ {t('blocks.unattended.meta', {
454
+ stage: t(`stageLabels.${lead.lifecycle_stage}`),
455
+ source: t(`sourceLabels.${lead.source}`),
456
+ })}
457
+ </p>
458
+ </div>
459
+ ))
460
+ )}
461
+ </div>
462
+ </ScrollArea>
463
+ </CardContent>
464
+ </Card>
465
+ );
466
+ }
467
+
468
+ export function CrmQuickAccessCard({ className }: CrmDashboardCardProps) {
469
+ const menuT = useTranslations('contact.CrmMenu');
470
+ const t = useTranslations('contact.CrmDashboard');
471
+
472
+ return (
473
+ <Card className={cn('min-w-0 overflow-hidden', className)}>
474
+ <CardHeader>
475
+ <CardTitle>{t('blocks.quickAccess.title')}</CardTitle>
476
+ <CardDescription>{t('blocks.quickAccess.description')}</CardDescription>
477
+ </CardHeader>
478
+ <CardContent className="space-y-3">
479
+ {crmImplementedSections.map((section) => {
480
+ const Icon = section.icon;
481
+
482
+ return (
483
+ <Link
484
+ key={section.href}
485
+ href={section.href}
486
+ className="flex min-w-0 cursor-pointer items-center justify-between gap-3 rounded-2xl border border-border/70 px-4 py-3 transition-colors hover:bg-muted/40"
487
+ >
488
+ <div className="flex min-w-0 items-center gap-3">
489
+ <div className={`rounded-2xl p-3 ${section.glowClass}`}>
490
+ <Icon className="size-4" />
491
+ </div>
492
+ <div className="min-w-0">
493
+ <p className="truncate font-medium">
494
+ {menuT(`sections.${section.translationKey}.title`)}
495
+ </p>
496
+ <p className="truncate text-xs text-muted-foreground">
497
+ {menuT(`sections.${section.translationKey}.description`)}
498
+ </p>
499
+ </div>
500
+ </div>
501
+ <ArrowUpRight className="size-4 shrink-0 text-muted-foreground" />
502
+ </Link>
503
+ );
504
+ })}
505
+ </CardContent>
506
+ </Card>
507
+ );
508
+ }