create-crm-tmp 2.0.0 → 2.1.0
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/bin/create-crm-tmp.js +56 -35
- package/package.json +1 -1
- package/template/README.md +230 -115
- package/template/eslint.config.mjs +13 -0
- package/template/next.config.ts +14 -0
- package/template/package.json +15 -2
- package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +132 -637
- package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
- package/template/src/app/(auth)/layout.tsx +1 -1
- package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
- package/template/src/app/(auth)/reset-password/page.tsx +4 -4
- package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
- package/template/src/app/(auth)/signin/page.tsx +14 -6
- package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
- package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
- package/template/src/app/(dashboard)/closing/page.tsx +78 -62
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
- package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
- package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
- package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
- package/template/src/app/(dashboard)/layout.tsx +6 -2
- package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
- package/template/src/app/(dashboard)/templates/page.tsx +55 -54
- package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
- package/template/src/app/(dashboard)/users/page.tsx +1 -1
- package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
- package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
- package/template/src/app/api/agenda/google-events/route.ts +92 -0
- package/template/src/app/api/auth/check-active/route.ts +3 -2
- package/template/src/app/api/auth/google/route.ts +2 -1
- package/template/src/app/api/auth/google/status/route.ts +7 -31
- package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
- package/template/src/app/api/companies/[id]/route.ts +1 -2
- package/template/src/app/api/companies/route.ts +42 -12
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
- package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
- package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
- package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
- package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
- package/template/src/app/api/contacts/[id]/route.ts +106 -34
- package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
- package/template/src/app/api/contacts/export/route.ts +9 -13
- package/template/src/app/api/contacts/import/route.ts +55 -25
- package/template/src/app/api/contacts/import-preview/route.ts +1 -1
- package/template/src/app/api/contacts/origins/route.ts +63 -0
- package/template/src/app/api/contacts/route.ts +153 -41
- package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
- package/template/src/app/api/dashboard/widgets/route.ts +181 -0
- package/template/src/app/api/dev/reminders/test/route.ts +114 -0
- package/template/src/app/api/editor/upload-image/route.ts +61 -0
- package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
- package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
- package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
- package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
- package/template/src/app/api/reminders/clear/route.ts +120 -0
- package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
- package/template/src/app/api/reminders/route.ts +164 -39
- package/template/src/app/api/reminders/state/route.ts +164 -0
- package/template/src/app/api/reset-password/request/route.ts +1 -1
- package/template/src/app/api/reset-password/verify/route.ts +1 -1
- package/template/src/app/api/send/route.ts +16 -4
- package/template/src/app/api/settings/google-ads/route.ts +14 -0
- package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
- package/template/src/app/api/settings/google-calendar/route.ts +124 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
- package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
- package/template/src/app/api/settings/google-sheet/route.ts +14 -0
- package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
- package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
- package/template/src/app/api/settings/meta-leads/route.ts +14 -2
- package/template/src/app/api/settings/smtp/route.ts +53 -6
- package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
- package/template/src/app/api/tasks/[id]/route.ts +234 -58
- package/template/src/app/api/tasks/meet/route.ts +27 -19
- package/template/src/app/api/tasks/route.ts +62 -17
- package/template/src/app/api/users/[id]/route.ts +20 -14
- package/template/src/app/api/users/list/route.ts +57 -19
- package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
- package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
- package/template/src/app/api/workflows/[id]/route.ts +0 -4
- package/template/src/app/api/workflows/process/route.ts +22 -51
- package/template/src/app/api/workflows/route.ts +0 -4
- package/template/src/app/globals.css +342 -4
- package/template/src/app/layout.tsx +11 -3
- package/template/src/app/page.tsx +1 -1
- package/template/src/components/address-autocomplete.tsx +7 -6
- package/template/src/components/config-error-alert.tsx +46 -0
- package/template/src/components/contacts/filter-bar.tsx +12 -3
- package/template/src/components/contacts/filter-builder.tsx +28 -43
- package/template/src/components/contacts/save-view-dialog.tsx +1 -1
- package/template/src/components/contacts/views-tab-bar.tsx +15 -6
- package/template/src/components/dashboard/activity-chart.tsx +41 -28
- package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
- package/template/src/components/dashboard/color-picker.tsx +64 -0
- package/template/src/components/dashboard/contacts-chart.tsx +69 -0
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
- package/template/src/components/dashboard/recent-activity.tsx +154 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -40
- package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
- package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
- package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
- package/template/src/components/date-picker.tsx +9 -6
- package/template/src/components/editor/upload-editor-image.ts +42 -0
- package/template/src/components/editor.tsx +161 -22
- package/template/src/components/email-template.tsx +2 -2
- package/template/src/components/global-search.tsx +30 -28
- package/template/src/components/header.tsx +178 -80
- package/template/src/components/inactive-account-guard.tsx +58 -0
- package/template/src/components/integration-notifications-listener.tsx +12 -0
- package/template/src/components/invitation-email-template.tsx +2 -2
- package/template/src/components/meet-cancellation-email-template.tsx +3 -3
- package/template/src/components/meet-confirmation-email-template.tsx +3 -3
- package/template/src/components/meet-update-email-template.tsx +3 -3
- package/template/src/components/page-header.tsx +5 -5
- package/template/src/components/protected-page.tsx +1 -1
- package/template/src/components/reset-password-email-template.tsx +2 -2
- package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
- package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
- package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
- package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
- package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
- package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
- package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
- package/template/src/components/sidebar.tsx +45 -26
- package/template/src/components/skeleton.tsx +40 -43
- package/template/src/components/ui/accordion.tsx +2 -2
- package/template/src/components/ui/alert-dialog.tsx +1 -1
- package/template/src/components/ui/button.tsx +20 -9
- package/template/src/components/ui/components.tsx +1 -1
- package/template/src/components/ui/date-picker.tsx +422 -0
- package/template/src/components/ui/datetime-picker.tsx +338 -0
- package/template/src/components/ui/status-select.tsx +271 -0
- package/template/src/components/ui/tooltip.tsx +37 -0
- package/template/src/components/view-as-modal.tsx +13 -7
- package/template/src/contexts/app-toast-context.tsx +245 -57
- package/template/src/contexts/dashboard-theme-context.tsx +53 -0
- package/template/src/contexts/sidebar-context.tsx +22 -17
- package/template/src/contexts/task-reminder-context.tsx +134 -160
- package/template/src/contexts/view-as-context.tsx +33 -6
- package/template/src/hooks/use-focus-trap.ts +2 -2
- package/template/src/hooks/useIntegrationNotifications.ts +49 -0
- package/template/src/lib/auth.ts +8 -1
- package/template/src/lib/config-links.ts +14 -0
- package/template/src/lib/contact-duplicate.ts +79 -61
- package/template/src/lib/contact-interactions.ts +21 -21
- package/template/src/lib/contact-view-filters.ts +24 -64
- package/template/src/lib/contacts-list-url.ts +190 -0
- package/template/src/lib/dashboard-stats.ts +65 -7
- package/template/src/lib/dashboard-themes.ts +135 -0
- package/template/src/lib/date-utils.ts +127 -0
- package/template/src/lib/default-widgets.ts +12 -0
- package/template/src/lib/editor-html-image-dimensions.ts +172 -0
- package/template/src/lib/editor-image-limits.ts +19 -0
- package/template/src/lib/email-html-sanitize.ts +19 -0
- package/template/src/lib/encryption.ts +9 -6
- package/template/src/lib/fr-geography.ts +192 -0
- package/template/src/lib/google-calendar-agenda.ts +201 -0
- package/template/src/lib/google-calendar.ts +255 -5
- package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
- package/template/src/lib/google-sheet-sync-runner.ts +514 -0
- package/template/src/lib/integration-import-log.ts +21 -0
- package/template/src/lib/permissions.ts +40 -10
- package/template/src/lib/prisma.ts +4 -1
- package/template/src/lib/qstash.ts +65 -0
- package/template/src/lib/reminder-state-server.ts +80 -0
- package/template/src/lib/reminder-state.ts +29 -0
- package/template/src/lib/supabase-storage.ts +113 -0
- package/template/src/lib/template-variables.ts +164 -23
- package/template/src/lib/utils.ts +45 -0
- package/template/src/lib/widget-registry.ts +173 -0
- package/template/src/lib/workflow-executor.ts +16 -70
- package/template/src/proxy.ts +1 -0
- package/template/vercel.json +3 -10
- package/template/skills-lock.json +0 -25
- package/template/src/components/dashboard/dashboard-content.tsx +0 -79
- package/template/src/lib/google-drive.ts +0 -1101
- package/template/src/types/yousign.ts +0 -52
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Area,
|
|
5
|
+
AreaChart,
|
|
6
|
+
ResponsiveContainer,
|
|
7
|
+
Tooltip,
|
|
8
|
+
XAxis,
|
|
9
|
+
YAxis,
|
|
10
|
+
CartesianGrid,
|
|
11
|
+
} from 'recharts';
|
|
12
|
+
import { useDashboardTheme } from '@/contexts/dashboard-theme-context';
|
|
13
|
+
|
|
14
|
+
interface ContactsChartProps {
|
|
15
|
+
readonly data: Array<{ month: string; count: number }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ContactsChart({ data }: Readonly<ContactsChartProps>) {
|
|
19
|
+
const { theme } = useDashboardTheme();
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex h-full flex-col rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
|
|
23
|
+
<div className="mb-4">
|
|
24
|
+
<h3 className="text-base font-semibold text-gray-900">Évolution des Contacts</h3>
|
|
25
|
+
<p className="mt-0.5 text-xs text-gray-400">Contacts créés par mois</p>
|
|
26
|
+
</div>
|
|
27
|
+
<div className="min-h-0 flex-1">
|
|
28
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
29
|
+
<AreaChart data={data} margin={{ top: 5, right: 5, left: -20, bottom: 0 }}>
|
|
30
|
+
<defs>
|
|
31
|
+
<linearGradient id="colorContactsAccent" x1="0" y1="0" x2="0" y2="1">
|
|
32
|
+
<stop offset="0%" stopColor={theme.hex[500]} stopOpacity={0.3} />
|
|
33
|
+
<stop offset="95%" stopColor={theme.hex[500]} stopOpacity={0.02} />
|
|
34
|
+
</linearGradient>
|
|
35
|
+
</defs>
|
|
36
|
+
<CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" vertical={false} />
|
|
37
|
+
<XAxis
|
|
38
|
+
dataKey="month"
|
|
39
|
+
stroke="#d1d5db"
|
|
40
|
+
fontSize={11}
|
|
41
|
+
tickLine={false}
|
|
42
|
+
axisLine={false}
|
|
43
|
+
dy={8}
|
|
44
|
+
/>
|
|
45
|
+
<YAxis stroke="#d1d5db" fontSize={11} tickLine={false} axisLine={false} width={40} />
|
|
46
|
+
<Tooltip
|
|
47
|
+
contentStyle={{
|
|
48
|
+
backgroundColor: '#fff',
|
|
49
|
+
border: '1px solid #f3f4f6',
|
|
50
|
+
borderRadius: '12px',
|
|
51
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
|
|
52
|
+
fontSize: '13px',
|
|
53
|
+
}}
|
|
54
|
+
labelStyle={{ color: '#374151', fontWeight: 600 }}
|
|
55
|
+
/>
|
|
56
|
+
<Area
|
|
57
|
+
type="monotone"
|
|
58
|
+
dataKey="count"
|
|
59
|
+
stroke={theme.hex[500]}
|
|
60
|
+
strokeWidth={2.5}
|
|
61
|
+
fill="url(#colorContactsAccent)"
|
|
62
|
+
name="Contacts"
|
|
63
|
+
/>
|
|
64
|
+
</AreaChart>
|
|
65
|
+
</ResponsiveContainer>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Bar,
|
|
5
|
+
BarChart,
|
|
6
|
+
ResponsiveContainer,
|
|
7
|
+
Tooltip,
|
|
8
|
+
XAxis,
|
|
9
|
+
YAxis,
|
|
10
|
+
CartesianGrid,
|
|
11
|
+
Cell,
|
|
12
|
+
} from 'recharts';
|
|
13
|
+
|
|
14
|
+
interface InteractionsByTypeChartProps {
|
|
15
|
+
readonly data: Array<{ type: string; count: number }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const TYPE_LABELS: Record<string, string> = {
|
|
19
|
+
CALL: 'Appels',
|
|
20
|
+
SMS: 'SMS',
|
|
21
|
+
EMAIL: 'Emails',
|
|
22
|
+
MEETING: 'Réunions',
|
|
23
|
+
NOTE: 'Notes',
|
|
24
|
+
TASK: 'Tâches',
|
|
25
|
+
STATUS_CHANGE: 'Changements',
|
|
26
|
+
APPOINTMENT_CREATED: 'RDV créés',
|
|
27
|
+
APPOINTMENT_DELETED: 'RDV supprimés',
|
|
28
|
+
APPOINTMENT_CHANGED: 'RDV modifiés',
|
|
29
|
+
ASSIGNMENT_CHANGE: 'Assignations',
|
|
30
|
+
CONTACT_UPDATE: 'Mises à jour',
|
|
31
|
+
FILE_UPLOADED: 'Fichiers',
|
|
32
|
+
FILE_REPLACED: 'Fichiers remplacés',
|
|
33
|
+
FILE_DELETED: 'Fichiers supprimés',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const TYPE_COLORS: Record<string, string> = {
|
|
37
|
+
CALL: '#3b82f6',
|
|
38
|
+
SMS: '#10b981',
|
|
39
|
+
EMAIL: '#f97316',
|
|
40
|
+
MEETING: '#8b5cf6',
|
|
41
|
+
NOTE: '#6b7280',
|
|
42
|
+
TASK: '#f59e0b',
|
|
43
|
+
STATUS_CHANGE: '#ef4444',
|
|
44
|
+
APPOINTMENT_CREATED: '#14b8a6',
|
|
45
|
+
APPOINTMENT_DELETED: '#ef4444',
|
|
46
|
+
APPOINTMENT_CHANGED: '#f97316',
|
|
47
|
+
ASSIGNMENT_CHANGE: '#ec4899',
|
|
48
|
+
CONTACT_UPDATE: '#06b6d4',
|
|
49
|
+
FILE_UPLOADED: '#84cc16',
|
|
50
|
+
FILE_REPLACED: '#a855f7',
|
|
51
|
+
FILE_DELETED: '#dc2626',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function InteractionsByTypeChart({ data }: Readonly<InteractionsByTypeChartProps>) {
|
|
55
|
+
const chartData = data
|
|
56
|
+
.filter((d) => d.count > 0)
|
|
57
|
+
.map((d) => ({
|
|
58
|
+
...d,
|
|
59
|
+
label: TYPE_LABELS[d.type] || d.type,
|
|
60
|
+
fill: TYPE_COLORS[d.type] || '#f97316',
|
|
61
|
+
}))
|
|
62
|
+
.sort((a, b) => b.count - a.count);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="flex h-full flex-col rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
|
|
66
|
+
<div className="mb-4">
|
|
67
|
+
<h3 className="text-base font-semibold text-gray-900">Interactions par Type</h3>
|
|
68
|
+
<p className="mt-0.5 text-xs text-gray-400">Répartition des interactions ce mois</p>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{chartData.length === 0 ? (
|
|
72
|
+
<div className="flex flex-1 items-center justify-center">
|
|
73
|
+
<p className="text-sm text-gray-400">Aucune interaction ce mois</p>
|
|
74
|
+
</div>
|
|
75
|
+
) : (
|
|
76
|
+
<div className="min-h-0 flex-1">
|
|
77
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
78
|
+
<BarChart
|
|
79
|
+
data={chartData}
|
|
80
|
+
layout="vertical"
|
|
81
|
+
margin={{ top: 0, right: 20, left: 0, bottom: 0 }}
|
|
82
|
+
>
|
|
83
|
+
<CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" horizontal={false} />
|
|
84
|
+
<XAxis
|
|
85
|
+
type="number"
|
|
86
|
+
stroke="#d1d5db"
|
|
87
|
+
fontSize={11}
|
|
88
|
+
tickLine={false}
|
|
89
|
+
axisLine={false}
|
|
90
|
+
/>
|
|
91
|
+
<YAxis
|
|
92
|
+
dataKey="label"
|
|
93
|
+
type="category"
|
|
94
|
+
stroke="#d1d5db"
|
|
95
|
+
fontSize={11}
|
|
96
|
+
tickLine={false}
|
|
97
|
+
axisLine={false}
|
|
98
|
+
width={80}
|
|
99
|
+
/>
|
|
100
|
+
<Tooltip
|
|
101
|
+
contentStyle={{
|
|
102
|
+
backgroundColor: '#fff',
|
|
103
|
+
border: '1px solid #f3f4f6',
|
|
104
|
+
borderRadius: '12px',
|
|
105
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
|
|
106
|
+
fontSize: '13px',
|
|
107
|
+
}}
|
|
108
|
+
formatter={(value: number) => [value, 'Interactions']}
|
|
109
|
+
/>
|
|
110
|
+
<Bar dataKey="count" radius={[0, 6, 6, 0]} barSize={16}>
|
|
111
|
+
{chartData.map((entry) => (
|
|
112
|
+
<Cell key={`type-${entry.label}`} fill={entry.fill} />
|
|
113
|
+
))}
|
|
114
|
+
</Bar>
|
|
115
|
+
</BarChart>
|
|
116
|
+
</ResponsiveContainer>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import {
|
|
5
|
+
Phone,
|
|
6
|
+
Mail,
|
|
7
|
+
Calendar,
|
|
8
|
+
MessageSquare,
|
|
9
|
+
FileText,
|
|
10
|
+
TrendingUp,
|
|
11
|
+
RefreshCw,
|
|
12
|
+
CalendarCheck,
|
|
13
|
+
CalendarX,
|
|
14
|
+
CalendarClock,
|
|
15
|
+
UserCheck,
|
|
16
|
+
} from 'lucide-react';
|
|
17
|
+
import { cn } from '@/lib/utils';
|
|
18
|
+
|
|
19
|
+
interface Interaction {
|
|
20
|
+
id: string;
|
|
21
|
+
type: string;
|
|
22
|
+
title: string | null;
|
|
23
|
+
content: string;
|
|
24
|
+
date: string;
|
|
25
|
+
contact: {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface RecentActivityProps {
|
|
32
|
+
readonly interactions: Interaction[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const interactionIcons: Record<string, typeof Phone> = {
|
|
36
|
+
CALL: Phone,
|
|
37
|
+
SMS: MessageSquare,
|
|
38
|
+
EMAIL: Mail,
|
|
39
|
+
MEETING: Calendar,
|
|
40
|
+
NOTE: FileText,
|
|
41
|
+
STATUS_CHANGE: TrendingUp,
|
|
42
|
+
CONTACT_UPDATE: RefreshCw,
|
|
43
|
+
APPOINTMENT_CREATED: CalendarCheck,
|
|
44
|
+
APPOINTMENT_DELETED: CalendarX,
|
|
45
|
+
APPOINTMENT_CHANGED: CalendarClock,
|
|
46
|
+
ASSIGNMENT_CHANGE: UserCheck,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const interactionColors: Record<string, string> = {
|
|
50
|
+
CALL: 'bg-blue-50 text-blue-500',
|
|
51
|
+
SMS: 'bg-emerald-50 text-emerald-500',
|
|
52
|
+
EMAIL: 'bg-orange-50 text-orange-500',
|
|
53
|
+
MEETING: 'bg-purple-50 text-purple-500',
|
|
54
|
+
NOTE: 'bg-gray-50 text-gray-500',
|
|
55
|
+
STATUS_CHANGE: 'bg-amber-50 text-amber-500',
|
|
56
|
+
CONTACT_UPDATE: 'bg-cyan-50 text-cyan-500',
|
|
57
|
+
APPOINTMENT_CREATED: 'bg-emerald-50 text-emerald-500',
|
|
58
|
+
APPOINTMENT_DELETED: 'bg-red-50 text-red-500',
|
|
59
|
+
APPOINTMENT_CHANGED: 'bg-orange-50 text-orange-500',
|
|
60
|
+
ASSIGNMENT_CHANGE: 'bg-pink-50 text-pink-500',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const interactionLabels: Record<string, string> = {
|
|
64
|
+
CALL: 'Appel',
|
|
65
|
+
SMS: 'SMS',
|
|
66
|
+
EMAIL: 'Email',
|
|
67
|
+
MEETING: 'Réunion',
|
|
68
|
+
NOTE: 'Note',
|
|
69
|
+
STATUS_CHANGE: 'Changement de statut',
|
|
70
|
+
CONTACT_UPDATE: 'Mise à jour',
|
|
71
|
+
APPOINTMENT_CREATED: 'RDV créé',
|
|
72
|
+
APPOINTMENT_DELETED: 'RDV supprimé',
|
|
73
|
+
APPOINTMENT_CHANGED: 'RDV modifié',
|
|
74
|
+
ASSIGNMENT_CHANGE: 'Assignation',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export function RecentActivity({ interactions }: RecentActivityProps) {
|
|
78
|
+
return (
|
|
79
|
+
<div className="flex h-full flex-col rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
|
|
80
|
+
<div className="mb-4 flex items-center justify-between">
|
|
81
|
+
<div>
|
|
82
|
+
<h3 className="text-base font-semibold text-gray-900">Activité Récente</h3>
|
|
83
|
+
<p className="mt-0.5 text-xs text-gray-400">Dernières interactions</p>
|
|
84
|
+
</div>
|
|
85
|
+
<Link href="/contacts" className="dash-link cursor-pointer text-xs font-medium">
|
|
86
|
+
Voir tout →
|
|
87
|
+
</Link>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{interactions.length === 0 ? (
|
|
91
|
+
<div className="flex flex-1 items-center justify-center">
|
|
92
|
+
<div className="text-center">
|
|
93
|
+
<FileText className="mx-auto h-10 w-10 text-gray-200" />
|
|
94
|
+
<p className="mt-2 text-sm text-gray-400">Aucune activité récente</p>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
) : (
|
|
98
|
+
<div className="flex-1 space-y-1 overflow-auto">
|
|
99
|
+
{interactions.map((interaction) => {
|
|
100
|
+
const Icon = interactionIcons[interaction.type] || FileText;
|
|
101
|
+
const color = interactionColors[interaction.type] || 'bg-gray-50 text-gray-500';
|
|
102
|
+
const label = interactionLabels[interaction.type] || interaction.type;
|
|
103
|
+
const date = new Date(interaction.date);
|
|
104
|
+
const now = new Date();
|
|
105
|
+
const diffMs = now.getTime() - date.getTime();
|
|
106
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
107
|
+
const diffMinutes = Math.floor(diffMs / (1000 * 60));
|
|
108
|
+
|
|
109
|
+
let timeAgo = '';
|
|
110
|
+
if (diffMinutes < 1) {
|
|
111
|
+
timeAgo = "À l'instant";
|
|
112
|
+
} else if (diffMinutes < 60) {
|
|
113
|
+
timeAgo = `Il y a ${diffMinutes} min`;
|
|
114
|
+
} else if (diffHours < 24) {
|
|
115
|
+
timeAgo = `Il y a ${diffHours}h`;
|
|
116
|
+
} else {
|
|
117
|
+
timeAgo = date.toLocaleDateString('fr-FR', {
|
|
118
|
+
day: 'numeric',
|
|
119
|
+
month: 'short',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div
|
|
125
|
+
key={interaction.id}
|
|
126
|
+
className="group dash-hover-border-left flex items-start gap-3 rounded-xl border-l-2 border-transparent px-3 py-2.5 transition-colors duration-150"
|
|
127
|
+
>
|
|
128
|
+
<div className={cn('rounded-lg p-1.5', color)}>
|
|
129
|
+
<Icon className="h-3.5 w-3.5" />
|
|
130
|
+
</div>
|
|
131
|
+
<div className="min-w-0 flex-1">
|
|
132
|
+
<div className="flex items-start justify-between gap-2">
|
|
133
|
+
<div className="min-w-0">
|
|
134
|
+
<p className="text-sm font-medium text-gray-900">{label}</p>
|
|
135
|
+
<Link
|
|
136
|
+
href={`/contacts/${interaction.contact.id}`}
|
|
137
|
+
className="dash-hover-text cursor-pointer text-xs text-gray-500 transition-colors"
|
|
138
|
+
>
|
|
139
|
+
{interaction.contact.name}
|
|
140
|
+
</Link>
|
|
141
|
+
</div>
|
|
142
|
+
<span className="shrink-0 text-[10px] font-medium text-gray-400">
|
|
143
|
+
{timeAgo}
|
|
144
|
+
</span>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
})}
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
@@ -1,61 +1,61 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { LucideIcon } from 'lucide-react';
|
|
4
3
|
import { cn } from '@/lib/utils';
|
|
5
4
|
|
|
6
5
|
interface StatCardProps {
|
|
7
|
-
title: string;
|
|
8
|
-
value: string | number;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
label: string;
|
|
6
|
+
readonly title: string;
|
|
7
|
+
readonly value: string | number;
|
|
8
|
+
readonly trend?: {
|
|
9
|
+
readonly value: number;
|
|
10
|
+
readonly label: string;
|
|
13
11
|
};
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
readonly subtitle?: string;
|
|
13
|
+
readonly accentColor?: string;
|
|
16
14
|
}
|
|
17
15
|
|
|
18
16
|
export function StatCard({
|
|
19
17
|
title,
|
|
20
18
|
value,
|
|
21
|
-
icon: Icon,
|
|
22
19
|
trend,
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
subtitle,
|
|
21
|
+
accentColor = 'dash-accent-bar',
|
|
25
22
|
}: StatCardProps) {
|
|
26
23
|
return (
|
|
27
|
-
<div className="group relative overflow-hidden rounded-
|
|
28
|
-
<div className=
|
|
29
|
-
|
|
24
|
+
<div className="group ui-lift-hover relative flex h-full flex-col justify-between overflow-hidden rounded-2xl border border-gray-100 bg-white p-5 shadow-sm transition-shadow duration-200 hover:shadow-md">
|
|
25
|
+
<div className={cn('absolute top-0 left-0 h-1 w-full', accentColor)} />
|
|
26
|
+
|
|
27
|
+
<div className="flex items-start justify-between">
|
|
30
28
|
<div className="flex-1">
|
|
31
|
-
<p className="text-sm font-medium text-
|
|
32
|
-
<p className="mt-2
|
|
33
|
-
{value}
|
|
34
|
-
</p>
|
|
35
|
-
{trend && (
|
|
36
|
-
<p className="mt-2 flex items-center gap-1 text-sm">
|
|
37
|
-
<span
|
|
38
|
-
className={cn(
|
|
39
|
-
'font-semibold',
|
|
40
|
-
trend.value >= 0 ? 'text-emerald-600' : 'text-red-600',
|
|
41
|
-
)}
|
|
42
|
-
>
|
|
43
|
-
{trend.value >= 0 ? '+' : ''}
|
|
44
|
-
{trend.value}%
|
|
45
|
-
</span>
|
|
46
|
-
<span className="text-muted-foreground">{trend.label}</span>
|
|
47
|
-
</p>
|
|
48
|
-
)}
|
|
29
|
+
<p className="text-sm font-medium text-gray-500">{title}</p>
|
|
30
|
+
<p className="mt-2 text-3xl font-bold tracking-tight text-gray-900">{value}</p>
|
|
49
31
|
</div>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
32
|
+
|
|
33
|
+
<div className="flex items-end gap-[3px] opacity-60">
|
|
34
|
+
{[40, 65, 45, 80, 55, 70, 90].map((barH) => (
|
|
35
|
+
<div
|
|
36
|
+
key={`bar-${barH}`}
|
|
37
|
+
className={cn('w-[4px] rounded-full', accentColor)}
|
|
38
|
+
style={{ height: `${barH * 0.3}px`, opacity: 0.4 + (barH / 90) * 0.6 }}
|
|
39
|
+
/>
|
|
40
|
+
))}
|
|
57
41
|
</div>
|
|
58
42
|
</div>
|
|
43
|
+
|
|
44
|
+
<div className="mt-3 flex items-center gap-2">
|
|
45
|
+
{trend && (
|
|
46
|
+
<span
|
|
47
|
+
className={cn(
|
|
48
|
+
'inline-flex items-center gap-0.5 rounded-full px-2 py-0.5 text-xs font-semibold',
|
|
49
|
+
trend.value >= 0 ? 'bg-emerald-50 text-emerald-600' : 'bg-red-50 text-red-600',
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
{trend.value >= 0 ? '↑' : '↓'} {Math.abs(trend.value)}%
|
|
53
|
+
</span>
|
|
54
|
+
)}
|
|
55
|
+
{(subtitle || trend?.label) && (
|
|
56
|
+
<span className="text-xs text-gray-400">{subtitle || trend?.label}</span>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
59
|
</div>
|
|
60
60
|
);
|
|
61
61
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Bar,
|
|
5
|
+
BarChart,
|
|
6
|
+
ResponsiveContainer,
|
|
7
|
+
Tooltip,
|
|
8
|
+
XAxis,
|
|
9
|
+
YAxis,
|
|
10
|
+
CartesianGrid,
|
|
11
|
+
Cell,
|
|
12
|
+
} from 'recharts';
|
|
13
|
+
import { useDashboardTheme } from '@/contexts/dashboard-theme-context';
|
|
14
|
+
|
|
15
|
+
interface StatusDistributionChartProps {
|
|
16
|
+
readonly data: Array<{ name: string; value: number }>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function StatusDistributionChart({ data }: Readonly<StatusDistributionChartProps>) {
|
|
20
|
+
const { theme } = useDashboardTheme();
|
|
21
|
+
|
|
22
|
+
const statusColors = [
|
|
23
|
+
theme.hex[500],
|
|
24
|
+
theme.hex[400],
|
|
25
|
+
theme.hex[300],
|
|
26
|
+
theme.hex[200],
|
|
27
|
+
'#10b981',
|
|
28
|
+
'#34d399',
|
|
29
|
+
'#6ee7b7',
|
|
30
|
+
'#3b82f6',
|
|
31
|
+
'#60a5fa',
|
|
32
|
+
'#93c5fd',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="flex h-full flex-col rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
|
|
37
|
+
<div className="mb-4">
|
|
38
|
+
<h3 className="text-base font-semibold text-gray-900">Répartition par Statut</h3>
|
|
39
|
+
<p className="mt-0.5 text-xs text-gray-400">Distribution des contacts</p>
|
|
40
|
+
</div>
|
|
41
|
+
<div className="min-h-0 flex-1">
|
|
42
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
43
|
+
<BarChart
|
|
44
|
+
data={data}
|
|
45
|
+
layout="vertical"
|
|
46
|
+
margin={{ top: 0, right: 20, left: 0, bottom: 0 }}
|
|
47
|
+
>
|
|
48
|
+
<CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" horizontal={false} />
|
|
49
|
+
<XAxis type="number" stroke="#d1d5db" fontSize={11} tickLine={false} axisLine={false} />
|
|
50
|
+
<YAxis
|
|
51
|
+
dataKey="name"
|
|
52
|
+
type="category"
|
|
53
|
+
stroke="#d1d5db"
|
|
54
|
+
fontSize={11}
|
|
55
|
+
tickLine={false}
|
|
56
|
+
axisLine={false}
|
|
57
|
+
width={90}
|
|
58
|
+
/>
|
|
59
|
+
<Tooltip
|
|
60
|
+
contentStyle={{
|
|
61
|
+
backgroundColor: '#fff',
|
|
62
|
+
border: '1px solid #f3f4f6',
|
|
63
|
+
borderRadius: '12px',
|
|
64
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
|
|
65
|
+
fontSize: '13px',
|
|
66
|
+
}}
|
|
67
|
+
/>
|
|
68
|
+
<Bar dataKey="value" radius={[0, 6, 6, 0]} name="Contacts" barSize={20}>
|
|
69
|
+
{data.map((entry, index) => (
|
|
70
|
+
<Cell
|
|
71
|
+
key={`status-${entry.name}`}
|
|
72
|
+
fill={statusColors[index % statusColors.length]}
|
|
73
|
+
/>
|
|
74
|
+
))}
|
|
75
|
+
</Bar>
|
|
76
|
+
</BarChart>
|
|
77
|
+
</ResponsiveContainer>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts';
|
|
4
|
+
import { useDashboardTheme } from '@/contexts/dashboard-theme-context';
|
|
4
5
|
|
|
5
6
|
interface TasksPieChartProps {
|
|
6
|
-
completed: number;
|
|
7
|
-
pending: number;
|
|
7
|
+
readonly completed: number;
|
|
8
|
+
readonly pending: number;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
export function TasksPieChart({ completed, pending }: Readonly<TasksPieChartProps>) {
|
|
12
|
+
const { theme } = useDashboardTheme();
|
|
13
|
+
|
|
14
|
+
const COLORS = {
|
|
15
|
+
completed: '#10b981',
|
|
16
|
+
pending: theme.hex[500],
|
|
17
|
+
};
|
|
14
18
|
|
|
15
|
-
export function TasksPieChart({ completed, pending }: TasksPieChartProps) {
|
|
16
19
|
const data = [
|
|
17
20
|
{ name: 'Complétées', value: completed },
|
|
18
21
|
{ name: 'En attente', value: pending },
|
|
@@ -22,64 +25,64 @@ export function TasksPieChart({ completed, pending }: TasksPieChartProps) {
|
|
|
22
25
|
const completionRate = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
23
26
|
|
|
24
27
|
return (
|
|
25
|
-
<div className="rounded-
|
|
26
|
-
<div className="mb-
|
|
27
|
-
<h3 className="text-
|
|
28
|
-
<p className="mt-
|
|
28
|
+
<div className="flex h-full flex-col rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
|
|
29
|
+
<div className="mb-2">
|
|
30
|
+
<h3 className="text-base font-semibold text-gray-900">Statut des Tâches</h3>
|
|
31
|
+
<p className="mt-0.5 text-xs text-gray-400">Répartition des tâches</p>
|
|
29
32
|
</div>
|
|
30
|
-
<div className="flex items-center justify-center">
|
|
31
|
-
<div className="relative h-[
|
|
33
|
+
<div className="flex min-h-0 flex-1 items-center justify-center">
|
|
34
|
+
<div className="relative h-[160px] w-[160px]">
|
|
32
35
|
<ResponsiveContainer width="100%" height="100%">
|
|
33
36
|
<PieChart>
|
|
34
37
|
<Pie
|
|
35
38
|
data={data}
|
|
36
39
|
cx="50%"
|
|
37
40
|
cy="50%"
|
|
38
|
-
innerRadius={
|
|
39
|
-
outerRadius={
|
|
40
|
-
paddingAngle={
|
|
41
|
+
innerRadius={50}
|
|
42
|
+
outerRadius={70}
|
|
43
|
+
paddingAngle={3}
|
|
41
44
|
dataKey="value"
|
|
45
|
+
strokeWidth={0}
|
|
42
46
|
>
|
|
43
|
-
{data.map((entry
|
|
47
|
+
{data.map((entry) => (
|
|
44
48
|
<Cell
|
|
45
|
-
key={
|
|
49
|
+
key={entry.name}
|
|
46
50
|
fill={entry.name === 'Complétées' ? COLORS.completed : COLORS.pending}
|
|
47
51
|
/>
|
|
48
52
|
))}
|
|
49
53
|
</Pie>
|
|
50
54
|
<Tooltip
|
|
51
55
|
contentStyle={{
|
|
52
|
-
backgroundColor: '
|
|
53
|
-
border: '1px solid #
|
|
56
|
+
backgroundColor: '#fff',
|
|
57
|
+
border: '1px solid #f3f4f6',
|
|
54
58
|
borderRadius: '12px',
|
|
55
|
-
boxShadow: '0 4px
|
|
59
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
|
|
60
|
+
fontSize: '13px',
|
|
56
61
|
}}
|
|
57
62
|
/>
|
|
58
63
|
</PieChart>
|
|
59
64
|
</ResponsiveContainer>
|
|
60
65
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
61
66
|
<div className="text-center">
|
|
62
|
-
<p className="
|
|
63
|
-
|
|
64
|
-
</p>
|
|
65
|
-
<p className="text-xs text-gray-500">Complétées</p>
|
|
67
|
+
<p className="text-2xl font-bold text-gray-900">{completionRate}%</p>
|
|
68
|
+
<p className="text-[10px] text-gray-400">Complétées</p>
|
|
66
69
|
</div>
|
|
67
70
|
</div>
|
|
68
71
|
</div>
|
|
69
72
|
</div>
|
|
70
|
-
<div className="mt-
|
|
71
|
-
<div className="flex items-center gap-2 rounded-
|
|
72
|
-
<div className="h-
|
|
73
|
+
<div className="mt-2 grid grid-cols-2 gap-2">
|
|
74
|
+
<div className="flex items-center gap-2 rounded-xl bg-emerald-50/80 px-3 py-2">
|
|
75
|
+
<div className="h-2.5 w-2.5 rounded-full bg-emerald-500" />
|
|
73
76
|
<div>
|
|
74
|
-
<p className="text-
|
|
75
|
-
<p className="font-semibold text-gray-900">{completed}</p>
|
|
77
|
+
<p className="text-[10px] text-gray-500">Complétées</p>
|
|
78
|
+
<p className="text-sm font-semibold text-gray-900">{completed}</p>
|
|
76
79
|
</div>
|
|
77
80
|
</div>
|
|
78
|
-
<div className="flex items-center gap-2 rounded-
|
|
79
|
-
<div className="h-
|
|
81
|
+
<div className="dash-legend-bg flex items-center gap-2 rounded-xl px-3 py-2">
|
|
82
|
+
<div className="dash-legend-dot h-2.5 w-2.5 rounded-full" />
|
|
80
83
|
<div>
|
|
81
|
-
<p className="text-
|
|
82
|
-
<p className="font-semibold text-gray-900">{pending}</p>
|
|
84
|
+
<p className="text-[10px] text-gray-500">En attente</p>
|
|
85
|
+
<p className="text-sm font-semibold text-gray-900">{pending}</p>
|
|
83
86
|
</div>
|
|
84
87
|
</div>
|
|
85
88
|
</div>
|