@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.
- package/dist/contact.module.d.ts.map +1 -1
- package/dist/contact.module.js +2 -0
- package/dist/contact.module.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/person/person.service.js +350 -350
- package/dist/proposal/dto/proposal.dto.d.ts +152 -0
- package/dist/proposal/dto/proposal.dto.d.ts.map +1 -0
- package/dist/proposal/dto/proposal.dto.js +396 -0
- package/dist/proposal/dto/proposal.dto.js.map +1 -0
- package/dist/proposal/proposal-contract.subscriber.d.ts +11 -0
- package/dist/proposal/proposal-contract.subscriber.d.ts.map +1 -0
- package/dist/proposal/proposal-contract.subscriber.js +51 -0
- package/dist/proposal/proposal-contract.subscriber.js.map +1 -0
- package/dist/proposal/proposal-event.types.d.ts +122 -0
- package/dist/proposal/proposal-event.types.d.ts.map +1 -0
- package/dist/proposal/proposal-event.types.js +13 -0
- package/dist/proposal/proposal-event.types.js.map +1 -0
- package/dist/proposal/proposal.controller.d.ts +56 -0
- package/dist/proposal/proposal.controller.d.ts.map +1 -0
- package/dist/proposal/proposal.controller.js +191 -0
- package/dist/proposal/proposal.controller.js.map +1 -0
- package/dist/proposal/proposal.module.d.ts +3 -0
- package/dist/proposal/proposal.module.d.ts.map +1 -0
- package/dist/proposal/proposal.module.js +32 -0
- package/dist/proposal/proposal.module.js.map +1 -0
- package/dist/proposal/proposal.service.d.ts +95 -0
- package/dist/proposal/proposal.service.d.ts.map +1 -0
- package/dist/proposal/proposal.service.js +1914 -0
- package/dist/proposal/proposal.service.js.map +1 -0
- package/dist/proposal/proposal.service.spec.d.ts +2 -0
- package/dist/proposal/proposal.service.spec.d.ts.map +1 -0
- package/dist/proposal/proposal.service.spec.js +187 -0
- package/dist/proposal/proposal.service.spec.js.map +1 -0
- 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 +112 -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/app/pipeline/_components/lead-detail-sheet.tsx.ejs +252 -209
- package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +1584 -0
- package/hedhog/frontend/messages/en.json +136 -42
- package/hedhog/frontend/messages/pt.json +135 -41
- 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/hedhog/table/proposal.yaml +112 -0
- package/hedhog/table/proposal_approval.yaml +63 -0
- package/hedhog/table/proposal_document.yaml +77 -0
- package/hedhog/table/proposal_item.yaml +64 -0
- package/hedhog/table/proposal_revision.yaml +78 -0
- package/package.json +6 -5
- package/src/contact.module.ts +2 -0
- package/src/index.ts +3 -0
- 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
- package/src/proposal/dto/proposal.dto.ts +341 -0
- package/src/proposal/proposal-contract.subscriber.ts +43 -0
- package/src/proposal/proposal-event.types.ts +130 -0
- package/src/proposal/proposal.controller.ts +168 -0
- package/src/proposal/proposal.module.ts +19 -0
- 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
|
-
|
|
57
|
-
|
|
58
|
-
type DashboardPeriod,
|
|
59
|
-
type DashboardResponse,
|
|
18
|
+
import type {
|
|
19
|
+
DashboardPeriod,
|
|
20
|
+
DashboardResponse,
|
|
60
21
|
} from './_components/dashboard-types';
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
<
|
|
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-
|
|
335
|
-
<Skeleton className="h-
|
|
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-
|
|
339
|
-
<Skeleton className="h-
|
|
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-
|
|
343
|
-
<Skeleton className="h-
|
|
344
|
-
<Skeleton className="h-
|
|
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
|
-
<
|
|
351
|
-
|
|
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
|
-
<
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
<
|
|
515
|
-
|
|
516
|
-
|
|
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
|
)}
|