@hed-hog/contact 0.0.274 → 0.0.276

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 (170) hide show
  1. package/README.md +60 -47
  2. package/hedhog/data/menu.yaml +163 -43
  3. package/hedhog/data/setting_group.yaml +21 -21
  4. package/hedhog/frontend/app/_components/crm-coming-soon.tsx.ejs +110 -0
  5. package/hedhog/frontend/app/_components/crm-nav.tsx.ejs +73 -0
  6. package/hedhog/frontend/app/_lib/crm-mocks.ts.ejs +256 -0
  7. package/hedhog/frontend/app/_lib/crm-sections.tsx.ejs +81 -0
  8. package/hedhog/frontend/app/accounts/page.tsx.ejs +15 -0
  9. package/hedhog/frontend/app/activities/page.tsx.ejs +15 -0
  10. package/hedhog/frontend/app/contact-type/page.tsx.ejs +1 -1
  11. package/hedhog/frontend/app/dashboard/page.tsx.ejs +573 -0
  12. package/hedhog/frontend/app/document-type/page.tsx.ejs +1 -1
  13. package/hedhog/frontend/app/follow-ups/page.tsx.ejs +15 -0
  14. package/hedhog/frontend/app/page.tsx.ejs +5 -0
  15. package/hedhog/frontend/app/person/_components/delete-person-dialog.tsx.ejs +59 -59
  16. package/hedhog/frontend/app/person/_components/person-interaction-dialog.tsx.ejs +275 -0
  17. package/hedhog/frontend/app/person/page.tsx.ejs +5 -0
  18. package/hedhog/frontend/app/pipeline/page.tsx.ejs +299 -0
  19. package/hedhog/frontend/app/reports/page.tsx.ejs +15 -0
  20. package/hedhog/frontend/messages/en.json +223 -3
  21. package/hedhog/frontend/messages/pt.json +217 -3
  22. package/hedhog/table/person_address.yaml +18 -18
  23. package/hedhog/table/person_company.yaml +26 -26
  24. package/hedhog/table/person_individual_relation.yaml +39 -39
  25. package/package.json +5 -5
  26. package/src/address-type.enum.ts +8 -8
  27. package/src/contact.module.ts +45 -45
  28. package/src/contact.service.ts +28 -28
  29. package/src/index.ts +5 -5
  30. package/src/person-relation-type/person-relation-type.service.ts +84 -84
  31. package/dist/address-type/address-type.controller.d.ts +0 -22
  32. package/dist/address-type/address-type.controller.d.ts.map +0 -1
  33. package/dist/address-type/address-type.controller.js +0 -94
  34. package/dist/address-type/address-type.controller.js.map +0 -1
  35. package/dist/address-type/address-type.enum.d.ts +0 -10
  36. package/dist/address-type/address-type.enum.d.ts.map +0 -1
  37. package/dist/address-type/address-type.enum.js +0 -14
  38. package/dist/address-type/address-type.enum.js.map +0 -1
  39. package/dist/address-type/address-type.module.d.ts +0 -3
  40. package/dist/address-type/address-type.module.d.ts.map +0 -1
  41. package/dist/address-type/address-type.module.js +0 -31
  42. package/dist/address-type/address-type.module.js.map +0 -1
  43. package/dist/address-type/address-type.service.d.ts +0 -30
  44. package/dist/address-type/address-type.service.d.ts.map +0 -1
  45. package/dist/address-type/address-type.service.js +0 -87
  46. package/dist/address-type/address-type.service.js.map +0 -1
  47. package/dist/address-type/dto/create.dto.d.ts +0 -7
  48. package/dist/address-type/dto/create.dto.d.ts.map +0 -1
  49. package/dist/address-type/dto/create.dto.js +0 -33
  50. package/dist/address-type/dto/create.dto.js.map +0 -1
  51. package/dist/address-type/dto/update.dto.d.ts +0 -9
  52. package/dist/address-type/dto/update.dto.d.ts.map +0 -1
  53. package/dist/address-type/dto/update.dto.js +0 -24
  54. package/dist/address-type/dto/update.dto.js.map +0 -1
  55. package/dist/person/address/address.controller.d.ts +0 -48
  56. package/dist/person/address/address.controller.d.ts.map +0 -1
  57. package/dist/person/address/address.controller.js +0 -90
  58. package/dist/person/address/address.controller.js.map +0 -1
  59. package/dist/person/address/address.service.d.ts +0 -50
  60. package/dist/person/address/address.service.d.ts.map +0 -1
  61. package/dist/person/address/address.service.js +0 -88
  62. package/dist/person/address/address.service.js.map +0 -1
  63. package/dist/person/address/dto/create.dto.d.ts +0 -11
  64. package/dist/person/address/dto/create.dto.d.ts.map +0 -1
  65. package/dist/person/address/dto/create.dto.js +0 -52
  66. package/dist/person/address/dto/create.dto.js.map +0 -1
  67. package/dist/person/address/dto/update.dto.d.ts +0 -6
  68. package/dist/person/address/dto/update.dto.d.ts.map +0 -1
  69. package/dist/person/address/dto/update.dto.js +0 -9
  70. package/dist/person/address/dto/update.dto.js.map +0 -1
  71. package/dist/person/contact/contact.controller.d.ts +0 -38
  72. package/dist/person/contact/contact.controller.d.ts.map +0 -1
  73. package/dist/person/contact/contact.controller.js +0 -90
  74. package/dist/person/contact/contact.controller.js.map +0 -1
  75. package/dist/person/contact/contact.service.d.ts +0 -40
  76. package/dist/person/contact/contact.service.d.ts.map +0 -1
  77. package/dist/person/contact/contact.service.js +0 -72
  78. package/dist/person/contact/contact.service.js.map +0 -1
  79. package/dist/person/contact/dto/create.dto.d.ts +0 -6
  80. package/dist/person/contact/dto/create.dto.d.ts.map +0 -1
  81. package/dist/person/contact/dto/create.dto.js +0 -31
  82. package/dist/person/contact/dto/create.dto.js.map +0 -1
  83. package/dist/person/contact/dto/update.dto.d.ts +0 -6
  84. package/dist/person/contact/dto/update.dto.d.ts.map +0 -1
  85. package/dist/person/contact/dto/update.dto.js +0 -9
  86. package/dist/person/contact/dto/update.dto.js.map +0 -1
  87. package/dist/person/document/document.controller.d.ts +0 -36
  88. package/dist/person/document/document.controller.d.ts.map +0 -1
  89. package/dist/person/document/document.controller.js +0 -90
  90. package/dist/person/document/document.controller.js.map +0 -1
  91. package/dist/person/document/document.service.d.ts +0 -38
  92. package/dist/person/document/document.service.d.ts.map +0 -1
  93. package/dist/person/document/document.service.js +0 -72
  94. package/dist/person/document/document.service.js.map +0 -1
  95. package/dist/person/document/dto/create.dto.d.ts +0 -5
  96. package/dist/person/document/dto/create.dto.d.ts.map +0 -1
  97. package/dist/person/document/dto/create.dto.js +0 -26
  98. package/dist/person/document/dto/create.dto.js.map +0 -1
  99. package/dist/person/document/dto/update.dto.d.ts +0 -6
  100. package/dist/person/document/dto/update.dto.d.ts.map +0 -1
  101. package/dist/person/document/dto/update.dto.js +0 -9
  102. package/dist/person/document/dto/update.dto.js.map +0 -1
  103. package/dist/person/dto/interaction-create.dto.d.ts +0 -16
  104. package/dist/person/dto/interaction-create.dto.d.ts.map +0 -1
  105. package/dist/person/dto/interaction-create.dto.js +0 -57
  106. package/dist/person/dto/interaction-create.dto.js.map +0 -1
  107. package/dist/person/person-company/dto/create.dto.d.ts +0 -5
  108. package/dist/person/person-company/dto/create.dto.d.ts.map +0 -1
  109. package/dist/person/person-company/dto/create.dto.js +0 -30
  110. package/dist/person/person-company/dto/create.dto.js.map +0 -1
  111. package/dist/person/person-company/dto/update.dto.d.ts +0 -6
  112. package/dist/person/person-company/dto/update.dto.d.ts.map +0 -1
  113. package/dist/person/person-company/dto/update.dto.js +0 -9
  114. package/dist/person/person-company/dto/update.dto.js.map +0 -1
  115. package/dist/person/person-company/person-company.controller.d.ts +0 -29
  116. package/dist/person/person-company/person-company.controller.d.ts.map +0 -1
  117. package/dist/person/person-company/person-company.controller.js +0 -65
  118. package/dist/person/person-company/person-company.controller.js.map +0 -1
  119. package/dist/person/person-company/person-company.service.d.ts +0 -29
  120. package/dist/person/person-company/person-company.service.d.ts.map +0 -1
  121. package/dist/person/person-company/person-company.service.js +0 -45
  122. package/dist/person/person-company/person-company.service.js.map +0 -1
  123. package/dist/person/person-individual/dto/create.dto.d.ts +0 -10
  124. package/dist/person/person-individual/dto/create.dto.d.ts.map +0 -1
  125. package/dist/person/person-individual/dto/create.dto.js +0 -36
  126. package/dist/person/person-individual/dto/create.dto.js.map +0 -1
  127. package/dist/person/person-individual/dto/update.dto.d.ts +0 -6
  128. package/dist/person/person-individual/dto/update.dto.d.ts.map +0 -1
  129. package/dist/person/person-individual/dto/update.dto.js +0 -9
  130. package/dist/person/person-individual/dto/update.dto.js.map +0 -1
  131. package/dist/person/person-individual/person-individual.controller.d.ts +0 -23
  132. package/dist/person/person-individual/person-individual.controller.d.ts.map +0 -1
  133. package/dist/person/person-individual/person-individual.controller.js +0 -65
  134. package/dist/person/person-individual/person-individual.controller.js.map +0 -1
  135. package/dist/person/person-individual/person-individual.service.d.ts +0 -23
  136. package/dist/person/person-individual/person-individual.service.d.ts.map +0 -1
  137. package/dist/person/person-individual/person-individual.service.js +0 -49
  138. package/dist/person/person-individual/person-individual.service.js.map +0 -1
  139. package/dist/person/person-metadata/dto/create.dto.d.ts +0 -5
  140. package/dist/person/person-metadata/dto/create.dto.d.ts.map +0 -1
  141. package/dist/person/person-metadata/dto/create.dto.js +0 -22
  142. package/dist/person/person-metadata/dto/create.dto.js.map +0 -1
  143. package/dist/person/person-metadata/dto/update.dto.d.ts +0 -6
  144. package/dist/person/person-metadata/dto/update.dto.d.ts.map +0 -1
  145. package/dist/person/person-metadata/dto/update.dto.js +0 -9
  146. package/dist/person/person-metadata/dto/update.dto.js.map +0 -1
  147. package/dist/person/person-metadata/person-metadata.controller.d.ts +0 -36
  148. package/dist/person/person-metadata/person-metadata.controller.d.ts.map +0 -1
  149. package/dist/person/person-metadata/person-metadata.controller.js +0 -90
  150. package/dist/person/person-metadata/person-metadata.controller.js.map +0 -1
  151. package/dist/person/person-metadata/person-metadata.service.d.ts +0 -38
  152. package/dist/person/person-metadata/person-metadata.service.d.ts.map +0 -1
  153. package/dist/person/person-metadata/person-metadata.service.js +0 -72
  154. package/dist/person/person-metadata/person-metadata.service.js.map +0 -1
  155. package/dist/person/person-relation/dto/create.dto.d.ts +0 -5
  156. package/dist/person/person-relation/dto/create.dto.d.ts.map +0 -1
  157. package/dist/person/person-relation/dto/create.dto.js +0 -26
  158. package/dist/person/person-relation/dto/create.dto.js.map +0 -1
  159. package/dist/person/person-relation/dto/update.dto.d.ts +0 -6
  160. package/dist/person/person-relation/dto/update.dto.d.ts.map +0 -1
  161. package/dist/person/person-relation/dto/update.dto.js +0 -9
  162. package/dist/person/person-relation/dto/update.dto.js.map +0 -1
  163. package/dist/person/person-relation/person-relation.controller.d.ts +0 -51
  164. package/dist/person/person-relation/person-relation.controller.d.ts.map +0 -1
  165. package/dist/person/person-relation/person-relation.controller.js +0 -90
  166. package/dist/person/person-relation/person-relation.controller.js.map +0 -1
  167. package/dist/person/person-relation/person-relation.service.d.ts +0 -53
  168. package/dist/person/person-relation/person-relation.service.d.ts.map +0 -1
  169. package/dist/person/person-relation/person-relation.service.js +0 -80
  170. package/dist/person/person-relation/person-relation.service.js.map +0 -1
@@ -0,0 +1,573 @@
1
+ 'use client';
2
+
3
+ import { CrmNav } from '../_components/crm-nav';
4
+ import {
5
+ crmMockLeads,
6
+ crmOwners,
7
+ crmSourceOrder,
8
+ crmStageOrder,
9
+ } from '../_lib/crm-mocks';
10
+ import { crmImplementedSections } from '../_lib/crm-sections';
11
+ import { Page, PageHeader } from '@/components/entity-list';
12
+ import { Badge } from '@/components/ui/badge';
13
+ import { Button } from '@/components/ui/button';
14
+ import {
15
+ Card,
16
+ CardContent,
17
+ CardDescription,
18
+ CardHeader,
19
+ CardTitle,
20
+ } from '@/components/ui/card';
21
+ import { ScrollArea } from '@/components/ui/scroll-area';
22
+ import { useApp } from '@hed-hog/next-app-provider';
23
+ import {
24
+ AlertTriangle,
25
+ ArrowUpRight,
26
+ BriefcaseBusiness,
27
+ CalendarClock,
28
+ ChartNoAxesCombined,
29
+ CircleDollarSign,
30
+ RefreshCcw,
31
+ Sparkles,
32
+ Target,
33
+ TrendingUp,
34
+ UserRoundX,
35
+ } from 'lucide-react';
36
+ import Link from 'next/link';
37
+ import { useTranslations } from 'next-intl';
38
+ import {
39
+ Bar,
40
+ BarChart,
41
+ CartesianGrid,
42
+ Cell,
43
+ Pie,
44
+ PieChart,
45
+ ResponsiveContainer,
46
+ Tooltip,
47
+ XAxis,
48
+ YAxis,
49
+ } from 'recharts';
50
+
51
+ const chartPalette = [
52
+ '#f97316',
53
+ '#14b8a6',
54
+ '#0ea5e9',
55
+ '#84cc16',
56
+ '#f59e0b',
57
+ '#ef4444',
58
+ '#8b5cf6',
59
+ ];
60
+
61
+ const chartTooltipStyle = {
62
+ backgroundColor: 'hsl(var(--card))',
63
+ border: '1px solid hsl(var(--border))',
64
+ borderRadius: '12px',
65
+ fontSize: '12px',
66
+ };
67
+
68
+ const fallbackKpiAccentClass =
69
+ 'from-orange-500/20 via-amber-500/10 to-transparent';
70
+
71
+ export default function CrmDashboardPage() {
72
+ const t = useTranslations('contact.CrmDashboard');
73
+ const menuT = useTranslations('contact.CrmMenu');
74
+ const { currentLocaleCode } = useApp();
75
+
76
+ const qualifiedCount = crmMockLeads.filter((lead) =>
77
+ ['qualified', 'proposal', 'negotiation', 'customer'].includes(
78
+ lead.lifecycle_stage ?? ''
79
+ )
80
+ ).length;
81
+ const proposalCount = crmMockLeads.filter(
82
+ (lead) => lead.lifecycle_stage === 'proposal'
83
+ ).length;
84
+ const customerCount = crmMockLeads.filter(
85
+ (lead) => lead.lifecycle_stage === 'customer'
86
+ ).length;
87
+ const lostCount = crmMockLeads.filter(
88
+ (lead) => lead.lifecycle_stage === 'lost'
89
+ ).length;
90
+ const unassignedCount = crmMockLeads.filter(
91
+ (lead) => !lead.owner_user_id
92
+ ).length;
93
+ const overdueFollowups = crmMockLeads.filter(
94
+ (lead) =>
95
+ lead.next_action_at &&
96
+ new Date(lead.next_action_at) < new Date('2026-03-16T00:00:00.000Z')
97
+ ).length;
98
+
99
+ const totalDealValue = crmMockLeads.reduce(
100
+ (sum, lead) => sum + lead.dealValue,
101
+ 0
102
+ );
103
+ const conversionRate = Math.round(
104
+ (customerCount / Math.max(crmMockLeads.length, 1)) * 100
105
+ );
106
+
107
+ const stageChartData = crmStageOrder.map((stage, index) => ({
108
+ name: t(`stageLabels.${stage}`),
109
+ total: crmMockLeads.filter((lead) => lead.lifecycle_stage === stage).length,
110
+ fill: chartPalette[index % chartPalette.length],
111
+ }));
112
+
113
+ const sourceChartData = crmSourceOrder.map((source, index) => ({
114
+ name: t(`sourceLabels.${source}`),
115
+ total: crmMockLeads.filter((lead) => lead.source === source).length,
116
+ fill: chartPalette[index % chartPalette.length],
117
+ }));
118
+
119
+ const ownerPerformanceData = crmOwners.map((owner) => {
120
+ const leads = crmMockLeads.filter(
121
+ (lead) => lead.owner_user_id === owner.id
122
+ );
123
+
124
+ return {
125
+ name: owner.name,
126
+ leads: leads.length,
127
+ customers: leads.filter((lead) => lead.lifecycle_stage === 'customer')
128
+ .length,
129
+ pipeline: leads
130
+ .filter((lead) => lead.lifecycle_stage !== 'lost')
131
+ .reduce((sum, lead) => sum + lead.dealValue, 0),
132
+ };
133
+ });
134
+
135
+ const nextActions = [...crmMockLeads]
136
+ .filter((lead) => lead.next_action_at)
137
+ .sort(
138
+ (left, right) =>
139
+ new Date(left.next_action_at ?? 0).getTime() -
140
+ new Date(right.next_action_at ?? 0).getTime()
141
+ )
142
+ .slice(0, 5);
143
+
144
+ const unattendedLeads = crmMockLeads
145
+ .filter((lead) => !lead.owner_user_id || lead.lifecycle_stage === 'new')
146
+ .slice(0, 5);
147
+
148
+ const topOwners = [...ownerPerformanceData]
149
+ .sort((left, right) => right.pipeline - left.pipeline)
150
+ .slice(0, 4);
151
+
152
+ const currencyFormatter = new Intl.NumberFormat(
153
+ currentLocaleCode === 'pt' ? 'pt-BR' : 'en-US',
154
+ {
155
+ style: 'currency',
156
+ currency: 'BRL',
157
+ maximumFractionDigits: 0,
158
+ }
159
+ );
160
+
161
+ return (
162
+ <Page>
163
+ <PageHeader
164
+ title={t('title')}
165
+ description={t('subtitle')}
166
+ breadcrumbs={[
167
+ { label: t('breadcrumbs.home'), href: '/' },
168
+ { label: t('breadcrumbs.crm') },
169
+ ]}
170
+ actions={[
171
+ {
172
+ label: t('refresh'),
173
+ onClick: () => window.location.reload(),
174
+ variant: 'outline',
175
+ icon: <RefreshCcw className="size-4" />,
176
+ },
177
+ ]}
178
+ />
179
+
180
+ <div className="min-w-0 space-y-6 overflow-x-hidden">
181
+ <Card className="overflow-hidden border-orange-200/70 bg-gradient-to-br from-orange-50 via-background to-amber-50 py-0">
182
+ <CardContent className="grid min-w-0 gap-6 px-6 py-6 lg:grid-cols-[minmax(0,1.3fr)_minmax(280px,0.9fr)]">
183
+ <div className="min-w-0 space-y-4">
184
+ <Badge className="w-fit rounded-full bg-orange-500/10 px-3 py-1 text-orange-700 hover:bg-orange-500/10">
185
+ <Sparkles className="mr-2 size-3.5" />
186
+ {t('heroBadge')}
187
+ </Badge>
188
+ <div className="space-y-2">
189
+ <h2 className="text-3xl font-semibold tracking-tight text-balance">
190
+ {t('heroTitle')}
191
+ </h2>
192
+ <p className="max-w-2xl text-sm leading-6 text-muted-foreground">
193
+ {t('heroDescription')}
194
+ </p>
195
+ </div>
196
+ <div className="flex flex-wrap gap-3">
197
+ <Button asChild>
198
+ <Link href="/contact/pipeline">{t('primaryAction')}</Link>
199
+ </Button>
200
+ <Button asChild variant="outline">
201
+ <Link href="/contact/person">{t('secondaryAction')}</Link>
202
+ </Button>
203
+ </div>
204
+ </div>
205
+
206
+ <div className="grid gap-3 sm:grid-cols-3 lg:grid-cols-1">
207
+ {[
208
+ {
209
+ icon: CircleDollarSign,
210
+ label: t('summary.pipelineValue'),
211
+ value: currencyFormatter.format(totalDealValue),
212
+ },
213
+ {
214
+ icon: Target,
215
+ label: t('summary.conversionRate'),
216
+ value: `${conversionRate}%`,
217
+ },
218
+ {
219
+ icon: BriefcaseBusiness,
220
+ label: t('summary.activeOwners'),
221
+ value: crmOwners.length,
222
+ },
223
+ ].map((item) => (
224
+ <div
225
+ key={item.label}
226
+ className="rounded-2xl border border-white/70 bg-white/80 p-4 shadow-sm backdrop-blur"
227
+ >
228
+ <div className="mb-3 flex items-center gap-2 text-muted-foreground">
229
+ <item.icon className="size-4" />
230
+ <span className="text-xs uppercase tracking-[0.2em]">
231
+ {item.label}
232
+ </span>
233
+ </div>
234
+ <div className="text-2xl font-semibold tracking-tight">
235
+ {item.value}
236
+ </div>
237
+ </div>
238
+ ))}
239
+ </div>
240
+ </CardContent>
241
+ </Card>
242
+
243
+ <CrmNav currentHref="/contact/dashboard" />
244
+
245
+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
246
+ {[
247
+ { key: 'totalLeads', value: crmMockLeads.length, icon: TrendingUp },
248
+ { key: 'qualified', value: qualifiedCount, icon: Target },
249
+ { key: 'proposal', value: proposalCount, icon: BriefcaseBusiness },
250
+ { key: 'customers', value: customerCount, icon: CircleDollarSign },
251
+ { key: 'lost', value: lostCount, icon: AlertTriangle },
252
+ { key: 'unassigned', value: unassignedCount, icon: UserRoundX },
253
+ { key: 'overdue', value: overdueFollowups, icon: CalendarClock },
254
+ {
255
+ key: 'nextActions',
256
+ value: nextActions.length,
257
+ icon: ChartNoAxesCombined,
258
+ },
259
+ ].map((item, index) => {
260
+ const sectionStyle =
261
+ crmImplementedSections[index % crmImplementedSections.length] ??
262
+ crmImplementedSections[0];
263
+
264
+ return (
265
+ <Card
266
+ key={item.key}
267
+ className="min-w-0 overflow-hidden border-border/70 py-0"
268
+ >
269
+ <div
270
+ className={`h-1 w-full bg-gradient-to-r ${sectionStyle?.colorClass ?? fallbackKpiAccentClass}`}
271
+ />
272
+ <CardContent className="flex items-start justify-between gap-3 px-6 py-5">
273
+ <div className="min-w-0">
274
+ <p className="text-xs uppercase tracking-[0.2em] text-muted-foreground">
275
+ {t(`kpis.${item.key}.title`)}
276
+ </p>
277
+ <p className="mt-2 text-3xl font-semibold tracking-tight">
278
+ {item.value}
279
+ </p>
280
+ <p className="mt-1 text-sm text-muted-foreground">
281
+ {t(`kpis.${item.key}.description`)}
282
+ </p>
283
+ </div>
284
+ <div className="rounded-2xl bg-muted p-3 text-muted-foreground">
285
+ <item.icon className="size-5" />
286
+ </div>
287
+ </CardContent>
288
+ </Card>
289
+ );
290
+ })}
291
+ </div>
292
+
293
+ <div className="grid min-w-0 gap-6 xl:grid-cols-[minmax(0,1.2fr)_minmax(320px,0.8fr)]">
294
+ <Card className="min-w-0">
295
+ <CardHeader>
296
+ <CardTitle>{t('charts.stage.title')}</CardTitle>
297
+ <CardDescription>{t('charts.stage.description')}</CardDescription>
298
+ </CardHeader>
299
+ <CardContent className="h-[320px]">
300
+ <ResponsiveContainer width="100%" height="100%">
301
+ <BarChart data={stageChartData}>
302
+ <CartesianGrid
303
+ strokeDasharray="3 3"
304
+ stroke="hsl(var(--border))"
305
+ vertical={false}
306
+ />
307
+ <XAxis
308
+ dataKey="name"
309
+ tickLine={false}
310
+ axisLine={false}
311
+ fontSize={12}
312
+ />
313
+ <YAxis
314
+ tickLine={false}
315
+ axisLine={false}
316
+ fontSize={12}
317
+ allowDecimals={false}
318
+ />
319
+ <Tooltip contentStyle={chartTooltipStyle} />
320
+ <Bar dataKey="total" radius={[8, 8, 0, 0]}>
321
+ {stageChartData.map((entry) => (
322
+ <Cell key={entry.name} fill={entry.fill} />
323
+ ))}
324
+ </Bar>
325
+ </BarChart>
326
+ </ResponsiveContainer>
327
+ </CardContent>
328
+ </Card>
329
+
330
+ <Card className="min-w-0 overflow-hidden">
331
+ <CardHeader>
332
+ <CardTitle>{t('charts.source.title')}</CardTitle>
333
+ <CardDescription>
334
+ {t('charts.source.description')}
335
+ </CardDescription>
336
+ </CardHeader>
337
+ <CardContent className="grid min-w-0 gap-4 overflow-hidden lg:h-[320px] lg:grid-cols-[minmax(220px,0.9fr)_minmax(0,1fr)]">
338
+ <div className="h-[220px] min-w-0 lg:h-full">
339
+ <ResponsiveContainer width="100%" height="100%">
340
+ <PieChart>
341
+ <Pie
342
+ data={sourceChartData}
343
+ dataKey="total"
344
+ nameKey="name"
345
+ innerRadius={58}
346
+ outerRadius={94}
347
+ paddingAngle={2}
348
+ >
349
+ {sourceChartData.map((entry) => (
350
+ <Cell key={entry.name} fill={entry.fill} />
351
+ ))}
352
+ </Pie>
353
+ <Tooltip contentStyle={chartTooltipStyle} />
354
+ </PieChart>
355
+ </ResponsiveContainer>
356
+ </div>
357
+ <ScrollArea className="min-w-0 lg:h-full">
358
+ <div className="space-y-3 lg:pr-3">
359
+ {sourceChartData.map((item) => (
360
+ <div
361
+ key={item.name}
362
+ className="flex min-w-0 items-center justify-between gap-3 rounded-2xl border border-border/70 px-4 py-3"
363
+ >
364
+ <div className="flex min-w-0 items-center gap-3">
365
+ <span
366
+ className="inline-block size-3 rounded-full"
367
+ style={{ backgroundColor: item.fill }}
368
+ />
369
+ <span className="truncate font-medium">
370
+ {item.name}
371
+ </span>
372
+ </div>
373
+ <span className="shrink-0 text-sm text-muted-foreground">
374
+ {item.total}
375
+ </span>
376
+ </div>
377
+ ))}
378
+ </div>
379
+ </ScrollArea>
380
+ </CardContent>
381
+ </Card>
382
+ </div>
383
+
384
+ <div className="grid min-w-0 gap-6 xl:grid-cols-[minmax(0,1.1fr)_minmax(320px,0.9fr)]">
385
+ <Card className="min-w-0">
386
+ <CardHeader>
387
+ <CardTitle>{t('charts.owner.title')}</CardTitle>
388
+ <CardDescription>{t('charts.owner.description')}</CardDescription>
389
+ </CardHeader>
390
+ <CardContent className="h-[320px]">
391
+ <ResponsiveContainer width="100%" height="100%">
392
+ <BarChart data={ownerPerformanceData}>
393
+ <CartesianGrid
394
+ strokeDasharray="3 3"
395
+ stroke="hsl(var(--border))"
396
+ vertical={false}
397
+ />
398
+ <XAxis
399
+ dataKey="name"
400
+ tickLine={false}
401
+ axisLine={false}
402
+ fontSize={12}
403
+ />
404
+ <YAxis
405
+ tickLine={false}
406
+ axisLine={false}
407
+ fontSize={12}
408
+ allowDecimals={false}
409
+ />
410
+ <Tooltip contentStyle={chartTooltipStyle} />
411
+ <Bar dataKey="leads" fill="#f97316" radius={[8, 8, 0, 0]} />
412
+ <Bar
413
+ dataKey="customers"
414
+ fill="#0ea5e9"
415
+ radius={[8, 8, 0, 0]}
416
+ />
417
+ </BarChart>
418
+ </ResponsiveContainer>
419
+ </CardContent>
420
+ </Card>
421
+
422
+ <Card className="min-w-0 overflow-hidden">
423
+ <CardHeader>
424
+ <CardTitle>{t('bestOwners.title')}</CardTitle>
425
+ <CardDescription>{t('bestOwners.description')}</CardDescription>
426
+ </CardHeader>
427
+ <CardContent>
428
+ <div className="space-y-3">
429
+ {topOwners.map((owner, index) => (
430
+ <div
431
+ key={owner.name}
432
+ className="flex min-w-0 items-center justify-between gap-3 rounded-2xl border border-border/70 px-4 py-3"
433
+ >
434
+ <div className="min-w-0">
435
+ <p className="truncate font-medium">
436
+ {index + 1}. {owner.name}
437
+ </p>
438
+ <p className="truncate text-xs text-muted-foreground">
439
+ {t('bestOwners.meta', {
440
+ leads: owner.leads,
441
+ customers: owner.customers,
442
+ })}
443
+ </p>
444
+ </div>
445
+ <span className="shrink-0 text-sm font-medium text-foreground">
446
+ {currencyFormatter.format(owner.pipeline)}
447
+ </span>
448
+ </div>
449
+ ))}
450
+ </div>
451
+ </CardContent>
452
+ </Card>
453
+ </div>
454
+
455
+ <div className="grid gap-6 xl:grid-cols-3">
456
+ <Card className="min-w-0 overflow-hidden xl:col-span-1">
457
+ <CardHeader>
458
+ <CardTitle>{t('blocks.nextActions.title')}</CardTitle>
459
+ <CardDescription>
460
+ {t('blocks.nextActions.description')}
461
+ </CardDescription>
462
+ </CardHeader>
463
+ <CardContent>
464
+ <ScrollArea className="h-[320px] pr-3">
465
+ <div className="space-y-3">
466
+ {nextActions.map((lead) => (
467
+ <div
468
+ key={lead.id}
469
+ className="rounded-2xl border border-border/70 px-4 py-3"
470
+ >
471
+ <p className="font-medium">{lead.name}</p>
472
+ <p className="text-xs text-muted-foreground">
473
+ {lead.owner_user?.name || t('common.unassigned')}
474
+ </p>
475
+ <div className="mt-2 flex flex-wrap gap-2">
476
+ <Badge variant="outline">
477
+ {t(`stageLabels.${lead.lifecycle_stage ?? 'new'}`)}
478
+ </Badge>
479
+ <Badge variant="secondary">
480
+ {t(`sourceLabels.${lead.source ?? 'other'}`)}
481
+ </Badge>
482
+ </div>
483
+ <p className="mt-2 text-sm text-muted-foreground">
484
+ {t('blocks.nextActions.when', {
485
+ date: new Date(
486
+ lead.next_action_at ?? ''
487
+ ).toLocaleString(),
488
+ })}
489
+ </p>
490
+ </div>
491
+ ))}
492
+ </div>
493
+ </ScrollArea>
494
+ </CardContent>
495
+ </Card>
496
+
497
+ <Card className="min-w-0 overflow-hidden xl:col-span-1">
498
+ <CardHeader>
499
+ <CardTitle>{t('blocks.unattended.title')}</CardTitle>
500
+ <CardDescription>
501
+ {t('blocks.unattended.description')}
502
+ </CardDescription>
503
+ </CardHeader>
504
+ <CardContent>
505
+ <ScrollArea className="h-[320px] pr-3">
506
+ <div className="space-y-3">
507
+ {unattendedLeads.map((lead) => (
508
+ <div
509
+ key={lead.id}
510
+ className="rounded-2xl border border-border/70 px-4 py-3"
511
+ >
512
+ <p className="font-medium">{lead.name}</p>
513
+ <p className="text-xs text-muted-foreground">
514
+ {lead.owner_user?.name || t('common.unassigned')}
515
+ </p>
516
+ <p className="mt-2 text-sm text-muted-foreground">
517
+ {t('blocks.unattended.meta', {
518
+ stage: t(
519
+ `stageLabels.${lead.lifecycle_stage ?? 'new'}`
520
+ ),
521
+ source: t(`sourceLabels.${lead.source ?? 'other'}`),
522
+ })}
523
+ </p>
524
+ </div>
525
+ ))}
526
+ </div>
527
+ </ScrollArea>
528
+ </CardContent>
529
+ </Card>
530
+
531
+ <Card className="min-w-0 overflow-hidden xl:col-span-1">
532
+ <CardHeader>
533
+ <CardTitle>{t('blocks.quickAccess.title')}</CardTitle>
534
+ <CardDescription>
535
+ {t('blocks.quickAccess.description')}
536
+ </CardDescription>
537
+ </CardHeader>
538
+ <CardContent className="space-y-3">
539
+ {crmImplementedSections.map((section) => {
540
+ const Icon = section.icon;
541
+
542
+ return (
543
+ <Link
544
+ key={section.href}
545
+ href={section.href}
546
+ 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"
547
+ >
548
+ <div className="flex min-w-0 items-center gap-3">
549
+ <div className={`rounded-2xl p-3 ${section.glowClass}`}>
550
+ <Icon className="size-4" />
551
+ </div>
552
+ <div className="min-w-0">
553
+ <p className="truncate font-medium">
554
+ {menuT(`sections.${section.translationKey}.title`)}
555
+ </p>
556
+ <p className="truncate text-xs text-muted-foreground">
557
+ {menuT(
558
+ `sections.${section.translationKey}.description`
559
+ )}
560
+ </p>
561
+ </div>
562
+ </div>
563
+ <ArrowUpRight className="size-4 shrink-0 text-muted-foreground" />
564
+ </Link>
565
+ );
566
+ })}
567
+ </CardContent>
568
+ </Card>
569
+ </div>
570
+ </div>
571
+ </Page>
572
+ );
573
+ }
@@ -246,7 +246,7 @@ export default function DocumentTypePage() {
246
246
  <div className="flex flex-col h-screen px-4">
247
247
  <PageHeader
248
248
  breadcrumbs={[
249
- { label: t('breadcrumbContact'), href: '/contact' },
249
+ { label: t('breadcrumbContact'), href: '/contact/dashboard' },
250
250
  { label: t('breadcrumbTitle') },
251
251
  ]}
252
252
  title={t('pageTitle')}
@@ -0,0 +1,15 @@
1
+ 'use client';
2
+
3
+ import { CrmComingSoon } from '../_components/crm-coming-soon';
4
+ import { CalendarClock } from 'lucide-react';
5
+
6
+ export default function CrmFollowupsPage() {
7
+ return (
8
+ <CrmComingSoon
9
+ currentHref="/contact/follow-ups"
10
+ titleKey="followups"
11
+ descriptionKey="followups"
12
+ icon={CalendarClock}
13
+ />
14
+ );
15
+ }
@@ -0,0 +1,5 @@
1
+ import { redirect } from 'next/navigation';
2
+
3
+ export default function ContactPage() {
4
+ redirect('/contact/dashboard');
5
+ }