@hed-hog/contact 0.0.298 → 0.0.300
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/person/person.service.js +350 -350
- package/hedhog/data/dashboard.yaml +6 -0
- package/hedhog/data/dashboard_component.yaml +87 -0
- package/hedhog/data/dashboard_component_role.yaml +55 -0
- package/hedhog/data/dashboard_item.yaml +95 -0
- package/hedhog/data/dashboard_role.yaml +6 -0
- package/hedhog/data/route.yaml +68 -68
- package/hedhog/frontend/app/dashboard/_components/dashboard-widgets.tsx.ejs +508 -0
- package/hedhog/frontend/app/dashboard/_components/use-crm-dashboard-data.ts.ejs +104 -0
- package/hedhog/frontend/app/dashboard/page.tsx.ejs +37 -431
- package/hedhog/frontend/widgets/next-actions.tsx.ejs +40 -0
- package/hedhog/frontend/widgets/overview-kpis.tsx.ejs +47 -0
- package/hedhog/frontend/widgets/owner-performance.tsx.ejs +42 -0
- package/hedhog/frontend/widgets/quick-access.tsx.ejs +29 -0
- package/hedhog/frontend/widgets/source-breakdown.tsx.ejs +40 -0
- package/hedhog/frontend/widgets/stage-distribution.tsx.ejs +40 -0
- package/hedhog/frontend/widgets/top-owners.tsx.ejs +42 -0
- package/hedhog/frontend/widgets/unattended.tsx.ejs +40 -0
- package/hedhog/table/crm_activity.yaml +68 -68
- package/hedhog/table/crm_stage_history.yaml +34 -34
- package/hedhog/table/person_company.yaml +27 -27
- package/package.json +5 -5
- package/src/person/dto/account.dto.ts +100 -100
- package/src/person/dto/activity.dto.ts +54 -54
- package/src/person/dto/dashboard-query.dto.ts +25 -25
- package/src/person/dto/followup-query.dto.ts +25 -25
- package/src/person/dto/reports-query.dto.ts +25 -25
- package/src/person/person.controller.ts +176 -176
- package/src/person/person.service.ts +4825 -4825
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|