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.
Files changed (190) hide show
  1. package/bin/create-crm-tmp.js +56 -35
  2. package/package.json +1 -1
  3. package/template/README.md +230 -115
  4. package/template/eslint.config.mjs +13 -0
  5. package/template/next.config.ts +14 -0
  6. package/template/package.json +15 -2
  7. package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
  8. package/template/prisma/migrations/migration_lock.toml +3 -0
  9. package/template/prisma/schema.prisma +132 -637
  10. package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
  11. package/template/src/app/(auth)/layout.tsx +1 -1
  12. package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
  13. package/template/src/app/(auth)/reset-password/page.tsx +4 -4
  14. package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
  15. package/template/src/app/(auth)/signin/page.tsx +14 -6
  16. package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
  17. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
  18. package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
  19. package/template/src/app/(dashboard)/closing/page.tsx +78 -62
  20. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
  21. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
  22. package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
  23. package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
  24. package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
  25. package/template/src/app/(dashboard)/layout.tsx +6 -2
  26. package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
  27. package/template/src/app/(dashboard)/templates/page.tsx +55 -54
  28. package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
  29. package/template/src/app/(dashboard)/users/page.tsx +1 -1
  30. package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
  31. package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
  32. package/template/src/app/api/agenda/google-events/route.ts +92 -0
  33. package/template/src/app/api/auth/check-active/route.ts +3 -2
  34. package/template/src/app/api/auth/google/route.ts +2 -1
  35. package/template/src/app/api/auth/google/status/route.ts +7 -31
  36. package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
  37. package/template/src/app/api/companies/[id]/route.ts +1 -2
  38. package/template/src/app/api/companies/route.ts +42 -12
  39. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
  40. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
  41. package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
  42. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
  43. package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
  44. package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
  45. package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
  46. package/template/src/app/api/contacts/[id]/route.ts +106 -34
  47. package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
  48. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
  49. package/template/src/app/api/contacts/export/route.ts +9 -13
  50. package/template/src/app/api/contacts/import/route.ts +55 -25
  51. package/template/src/app/api/contacts/import-preview/route.ts +1 -1
  52. package/template/src/app/api/contacts/origins/route.ts +63 -0
  53. package/template/src/app/api/contacts/route.ts +153 -41
  54. package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
  55. package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
  56. package/template/src/app/api/dashboard/widgets/route.ts +181 -0
  57. package/template/src/app/api/dev/reminders/test/route.ts +114 -0
  58. package/template/src/app/api/editor/upload-image/route.ts +61 -0
  59. package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
  60. package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
  61. package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
  62. package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
  63. package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
  64. package/template/src/app/api/reminders/clear/route.ts +120 -0
  65. package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
  66. package/template/src/app/api/reminders/route.ts +164 -39
  67. package/template/src/app/api/reminders/state/route.ts +164 -0
  68. package/template/src/app/api/reset-password/request/route.ts +1 -1
  69. package/template/src/app/api/reset-password/verify/route.ts +1 -1
  70. package/template/src/app/api/send/route.ts +16 -4
  71. package/template/src/app/api/settings/google-ads/route.ts +14 -0
  72. package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
  73. package/template/src/app/api/settings/google-calendar/route.ts +124 -0
  74. package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
  75. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
  76. package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
  77. package/template/src/app/api/settings/google-sheet/route.ts +14 -0
  78. package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
  79. package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
  80. package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
  81. package/template/src/app/api/settings/meta-leads/route.ts +14 -2
  82. package/template/src/app/api/settings/smtp/route.ts +53 -6
  83. package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
  84. package/template/src/app/api/tasks/[id]/route.ts +234 -58
  85. package/template/src/app/api/tasks/meet/route.ts +27 -19
  86. package/template/src/app/api/tasks/route.ts +62 -17
  87. package/template/src/app/api/users/[id]/route.ts +20 -14
  88. package/template/src/app/api/users/list/route.ts +57 -19
  89. package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
  90. package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
  91. package/template/src/app/api/workflows/[id]/route.ts +0 -4
  92. package/template/src/app/api/workflows/process/route.ts +22 -51
  93. package/template/src/app/api/workflows/route.ts +0 -4
  94. package/template/src/app/globals.css +342 -4
  95. package/template/src/app/layout.tsx +11 -3
  96. package/template/src/app/page.tsx +1 -1
  97. package/template/src/components/address-autocomplete.tsx +7 -6
  98. package/template/src/components/config-error-alert.tsx +46 -0
  99. package/template/src/components/contacts/filter-bar.tsx +12 -3
  100. package/template/src/components/contacts/filter-builder.tsx +28 -43
  101. package/template/src/components/contacts/save-view-dialog.tsx +1 -1
  102. package/template/src/components/contacts/views-tab-bar.tsx +15 -6
  103. package/template/src/components/dashboard/activity-chart.tsx +41 -28
  104. package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
  105. package/template/src/components/dashboard/color-picker.tsx +64 -0
  106. package/template/src/components/dashboard/contacts-chart.tsx +69 -0
  107. package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
  108. package/template/src/components/dashboard/recent-activity.tsx +154 -0
  109. package/template/src/components/dashboard/stat-card.tsx +40 -40
  110. package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
  111. package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
  112. package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
  113. package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
  114. package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
  115. package/template/src/components/date-picker.tsx +9 -6
  116. package/template/src/components/editor/upload-editor-image.ts +42 -0
  117. package/template/src/components/editor.tsx +161 -22
  118. package/template/src/components/email-template.tsx +2 -2
  119. package/template/src/components/global-search.tsx +30 -28
  120. package/template/src/components/header.tsx +178 -80
  121. package/template/src/components/inactive-account-guard.tsx +58 -0
  122. package/template/src/components/integration-notifications-listener.tsx +12 -0
  123. package/template/src/components/invitation-email-template.tsx +2 -2
  124. package/template/src/components/meet-cancellation-email-template.tsx +3 -3
  125. package/template/src/components/meet-confirmation-email-template.tsx +3 -3
  126. package/template/src/components/meet-update-email-template.tsx +3 -3
  127. package/template/src/components/page-header.tsx +5 -5
  128. package/template/src/components/protected-page.tsx +1 -1
  129. package/template/src/components/reset-password-email-template.tsx +2 -2
  130. package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
  131. package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
  132. package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
  133. package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
  134. package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
  135. package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
  136. package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
  137. package/template/src/components/sidebar.tsx +45 -26
  138. package/template/src/components/skeleton.tsx +40 -43
  139. package/template/src/components/ui/accordion.tsx +2 -2
  140. package/template/src/components/ui/alert-dialog.tsx +1 -1
  141. package/template/src/components/ui/button.tsx +20 -9
  142. package/template/src/components/ui/components.tsx +1 -1
  143. package/template/src/components/ui/date-picker.tsx +422 -0
  144. package/template/src/components/ui/datetime-picker.tsx +338 -0
  145. package/template/src/components/ui/status-select.tsx +271 -0
  146. package/template/src/components/ui/tooltip.tsx +37 -0
  147. package/template/src/components/view-as-modal.tsx +13 -7
  148. package/template/src/contexts/app-toast-context.tsx +245 -57
  149. package/template/src/contexts/dashboard-theme-context.tsx +53 -0
  150. package/template/src/contexts/sidebar-context.tsx +22 -17
  151. package/template/src/contexts/task-reminder-context.tsx +134 -160
  152. package/template/src/contexts/view-as-context.tsx +33 -6
  153. package/template/src/hooks/use-focus-trap.ts +2 -2
  154. package/template/src/hooks/useIntegrationNotifications.ts +49 -0
  155. package/template/src/lib/auth.ts +8 -1
  156. package/template/src/lib/config-links.ts +14 -0
  157. package/template/src/lib/contact-duplicate.ts +79 -61
  158. package/template/src/lib/contact-interactions.ts +21 -21
  159. package/template/src/lib/contact-view-filters.ts +24 -64
  160. package/template/src/lib/contacts-list-url.ts +190 -0
  161. package/template/src/lib/dashboard-stats.ts +65 -7
  162. package/template/src/lib/dashboard-themes.ts +135 -0
  163. package/template/src/lib/date-utils.ts +127 -0
  164. package/template/src/lib/default-widgets.ts +12 -0
  165. package/template/src/lib/editor-html-image-dimensions.ts +172 -0
  166. package/template/src/lib/editor-image-limits.ts +19 -0
  167. package/template/src/lib/email-html-sanitize.ts +19 -0
  168. package/template/src/lib/encryption.ts +9 -6
  169. package/template/src/lib/fr-geography.ts +192 -0
  170. package/template/src/lib/google-calendar-agenda.ts +201 -0
  171. package/template/src/lib/google-calendar.ts +255 -5
  172. package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
  173. package/template/src/lib/google-sheet-sync-runner.ts +514 -0
  174. package/template/src/lib/integration-import-log.ts +21 -0
  175. package/template/src/lib/permissions.ts +40 -10
  176. package/template/src/lib/prisma.ts +4 -1
  177. package/template/src/lib/qstash.ts +65 -0
  178. package/template/src/lib/reminder-state-server.ts +80 -0
  179. package/template/src/lib/reminder-state.ts +29 -0
  180. package/template/src/lib/supabase-storage.ts +113 -0
  181. package/template/src/lib/template-variables.ts +164 -23
  182. package/template/src/lib/utils.ts +45 -0
  183. package/template/src/lib/widget-registry.ts +173 -0
  184. package/template/src/lib/workflow-executor.ts +16 -70
  185. package/template/src/proxy.ts +1 -0
  186. package/template/vercel.json +3 -10
  187. package/template/skills-lock.json +0 -25
  188. package/template/src/components/dashboard/dashboard-content.tsx +0 -79
  189. package/template/src/lib/google-drive.ts +0 -1101
  190. package/template/src/types/yousign.ts +0 -52
@@ -1,51 +1,547 @@
1
- import { Suspense } from 'react';
2
- import { redirect } from 'next/navigation';
3
- import { getDashboardStats } from '@/lib/dashboard-stats';
4
- import { DashboardContent } from '@/components/dashboard/dashboard-content';
5
- import { Skeleton } from '@/components/skeleton';
1
+ 'use client';
2
+
3
+ import React, { useEffect, useState, useCallback, useRef, useMemo } from 'react';
4
+ import dynamic from 'next/dynamic';
5
+ import { useContainerWidth, type Layout, type LayoutItem } from 'react-grid-layout';
6
+ import { Plus, LayoutDashboard, RotateCcw, ShieldAlert } from 'lucide-react';
6
7
  import { PageHeader } from '@/components/page-header';
7
- import { getAuthUser } from '@/lib/get-auth-user';
8
+ import { WidgetWrapper } from '@/components/dashboard/widget-wrapper';
9
+ import { AddWidgetDialog } from '@/components/dashboard/add-widget-dialog';
10
+ import { DashboardColorPicker } from '@/components/dashboard/color-picker';
11
+ import { useUserRole } from '@/hooks/use-user-role';
12
+ import { DashboardThemeProvider, useDashboardTheme } from '@/contexts/dashboard-theme-context';
13
+ import { getWidgetDefinition } from '@/lib/widget-registry';
14
+ import { useAppToast } from '@/contexts/app-toast-context';
15
+ import { devToast } from '@/lib/utils';
16
+
17
+ const GridLayout = dynamic(() => import('react-grid-layout').then((mod) => mod.GridLayout), {
18
+ ssr: false,
19
+ });
20
+ const StatCard = dynamic(
21
+ () => import('@/components/dashboard/stat-card').then((mod) => mod.StatCard),
22
+ { ssr: false },
23
+ );
24
+ const ContactsChart = dynamic(
25
+ () => import('@/components/dashboard/contacts-chart').then((mod) => mod.ContactsChart),
26
+ { ssr: false },
27
+ );
28
+ const ActivityChart = dynamic(
29
+ () => import('@/components/dashboard/activity-chart').then((mod) => mod.ActivityChart),
30
+ { ssr: false },
31
+ );
32
+ const StatusDistributionChart = dynamic(
33
+ () =>
34
+ import('@/components/dashboard/status-distribution-chart').then(
35
+ (mod) => mod.StatusDistributionChart,
36
+ ),
37
+ { ssr: false },
38
+ );
39
+ const TasksPieChart = dynamic(
40
+ () => import('@/components/dashboard/tasks-pie-chart').then((mod) => mod.TasksPieChart),
41
+ { ssr: false },
42
+ );
43
+ const UpcomingTasksList = dynamic(
44
+ () => import('@/components/dashboard/upcoming-tasks-list').then((mod) => mod.UpcomingTasksList),
45
+ { ssr: false },
46
+ );
47
+ const RecentActivity = dynamic(
48
+ () => import('@/components/dashboard/recent-activity').then((mod) => mod.RecentActivity),
49
+ { ssr: false },
50
+ );
51
+ const TopContactsList = dynamic(
52
+ () => import('@/components/dashboard/top-contacts-list').then((mod) => mod.TopContactsList),
53
+ { ssr: false },
54
+ );
55
+ const InteractionsByTypeChart = dynamic(
56
+ () =>
57
+ import('@/components/dashboard/interactions-by-type-chart').then(
58
+ (mod) => mod.InteractionsByTypeChart,
59
+ ),
60
+ { ssr: false },
61
+ );
62
+
63
+ interface DashboardWidget {
64
+ id: string;
65
+ type: string;
66
+ x: number;
67
+ y: number;
68
+ w: number;
69
+ h: number;
70
+ }
71
+
72
+ interface DashboardStats {
73
+ overview: {
74
+ totalContacts: number;
75
+ contactsThisMonth: number;
76
+ contactsGrowth: number;
77
+ monthsData: Array<{ month: string; count: number }>;
78
+ };
79
+ statusDistribution: Array<{ name: string; value: number }>;
80
+ tasks: {
81
+ total: number;
82
+ completed: number;
83
+ pending: number;
84
+ upcoming: Array<{
85
+ id: string;
86
+ title: string;
87
+ type: string;
88
+ scheduledAt: string;
89
+ contact: { id: string; name: string } | null;
90
+ priority: string;
91
+ }>;
92
+ byType: Array<{ type: string; count: number }>;
93
+ };
94
+ interactions: {
95
+ recent: Array<{
96
+ id: string;
97
+ type: string;
98
+ title: string | null;
99
+ content: string;
100
+ date: string;
101
+ contact: {
102
+ id: string;
103
+ name: string;
104
+ };
105
+ }>;
106
+ byType: Array<{ type: string; count: number }>;
107
+ };
108
+ activity: {
109
+ last7Days: Array<{ date: string; interactions: number; tasks: number }>;
110
+ };
111
+ topContacts: Array<{
112
+ id: string;
113
+ name: string;
114
+ phone: string;
115
+ email: string | null;
116
+ status: string;
117
+ interactionsCount: number;
118
+ assignedCommercial?: string;
119
+ assignedTelepro?: string;
120
+ }>;
121
+ }
8
122
 
9
- function DashboardSkeleton() {
123
+ export default function DashboardPage() {
10
124
  return (
11
- <div className="h-full">
12
- <PageHeader title="Tableau de Bord" description="Vue d'ensemble de votre activité" />
13
- <div className="p-4 sm:p-6">
14
- <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
15
- {Array.from({ length: 4 }).map((_, i) => (
16
- <Skeleton key={i} className="h-32 rounded-lg" />
17
- ))}
18
- </div>
19
- <div className="mt-6 grid gap-6 lg:grid-cols-2">
20
- {Array.from({ length: 4 }).map((_, i) => (
21
- <Skeleton key={i} className="h-96 rounded-lg" />
22
- ))}
23
- </div>
24
- </div>
25
- </div>
125
+ <DashboardThemeProvider>
126
+ <DashboardContent />
127
+ </DashboardThemeProvider>
26
128
  );
27
129
  }
28
130
 
29
- async function DashboardData() {
30
- const authUser = await getAuthUser();
131
+ function DashboardContent() {
132
+ const toast = useAppToast();
133
+ const [widgets, setWidgets] = useState<DashboardWidget[]>([]);
134
+ const [stats, setStats] = useState<DashboardStats | null>(null);
135
+ const [loading, setLoading] = useState(true);
136
+ const [error, setError] = useState<string | null>(null);
137
+ const [showAddDialog, setShowAddDialog] = useState(false);
138
+ const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
139
+ const { hasPermission, isLoading: permissionsLoading } = useUserRole();
140
+ const { theme } = useDashboardTheme();
31
141
 
32
- if (!authUser) {
33
- redirect('/signin');
34
- }
142
+ const canViewDashboard = hasPermission('dashboard.view');
143
+ const canManageWidgets = hasPermission('dashboard.widgets.manage');
144
+ const canResetDashboard = hasPermission('dashboard.widgets.reset');
145
+ const {
146
+ width: containerWidth,
147
+ containerRef,
148
+ mounted,
149
+ } = useContainerWidth({ initialWidth: 1200 });
35
150
 
36
- if (!authUser.permissions.includes('dashboard.view')) {
37
- redirect('/contacts');
38
- }
151
+ const cols = useMemo(() => {
152
+ if (containerWidth < 768) return 2;
153
+ return 12;
154
+ }, [containerWidth]);
39
155
 
40
- const stats = await getDashboardStats(authUser.session.user.id, authUser.permissions);
156
+ useEffect(() => {
157
+ async function fetchData() {
158
+ try {
159
+ const [widgetsRes, statsRes] = await Promise.all([
160
+ fetch('/api/dashboard/widgets'),
161
+ fetch('/api/dashboard/stats'),
162
+ ]);
41
163
 
42
- return <DashboardContent stats={stats} />;
43
- }
164
+ if (widgetsRes.ok) {
165
+ setWidgets(await widgetsRes.json());
166
+ }
167
+
168
+ if (statsRes.ok) {
169
+ setStats(await statsRes.json());
170
+ }
171
+ } catch (err) {
172
+ const message = err instanceof Error ? err.message : 'Une erreur est survenue';
173
+ setError(message);
174
+ toast.error(devToast('Une erreur est survenue lors du chargement du tableau de bord', err));
175
+ } finally {
176
+ setLoading(false);
177
+ }
178
+ }
179
+
180
+ fetchData();
181
+ }, []);
182
+
183
+ const saveLayout = useCallback((updatedWidgets: DashboardWidget[]) => {
184
+ if (saveTimeoutRef.current) {
185
+ clearTimeout(saveTimeoutRef.current);
186
+ }
187
+
188
+ saveTimeoutRef.current = setTimeout(async () => {
189
+ try {
190
+ await fetch('/api/dashboard/widgets', {
191
+ method: 'PUT',
192
+ headers: { 'Content-Type': 'application/json' },
193
+ body: JSON.stringify({
194
+ widgets: updatedWidgets.map((w) => ({
195
+ id: w.id,
196
+ x: w.x,
197
+ y: w.y,
198
+ w: w.w,
199
+ h: w.h,
200
+ })),
201
+ }),
202
+ });
203
+ } catch (err) {
204
+ console.error('Erreur sauvegarde layout:', err);
205
+ toast.error(devToast('Impossible de sauvegarder la disposition du tableau de bord. Veuillez réessayer.', err));
206
+ }
207
+ }, 500);
208
+ }, []);
209
+
210
+ const handleLayoutChange = useCallback(
211
+ (layout: Layout) => {
212
+ if (cols !== 12) return;
213
+
214
+ const updatedWidgets = widgets.map((widget) => {
215
+ const layoutItem = layout.find((l: LayoutItem) => l.i === widget.id);
216
+ if (layoutItem) {
217
+ return {
218
+ ...widget,
219
+ x: layoutItem.x,
220
+ y: layoutItem.y,
221
+ w: layoutItem.w,
222
+ h: layoutItem.h,
223
+ };
224
+ }
225
+ return widget;
226
+ });
227
+
228
+ setWidgets(updatedWidgets);
229
+ saveLayout(updatedWidgets);
230
+ },
231
+ [widgets, saveLayout, cols],
232
+ );
233
+
234
+ const handleAddWidget = useCallback(async (type: string, w: number, h: number) => {
235
+ try {
236
+ const res = await fetch('/api/dashboard/widgets', {
237
+ method: 'POST',
238
+ headers: { 'Content-Type': 'application/json' },
239
+ body: JSON.stringify({ type, w, h }),
240
+ });
241
+
242
+ if (res.ok) {
243
+ const newWidget = await res.json();
244
+ setWidgets((prev) => [...prev, newWidget]);
245
+ }
246
+ } catch (err) {
247
+ console.error('Erreur ajout widget:', err);
248
+ toast.error(devToast('Impossible d\'ajouter le widget. Veuillez réessayer.', err));
249
+ }
250
+
251
+ setShowAddDialog(false);
252
+ }, []);
253
+
254
+ const handleRemoveWidget = useCallback(async (widgetId: string) => {
255
+ try {
256
+ const res = await fetch(`/api/dashboard/widgets/${widgetId}`, {
257
+ method: 'DELETE',
258
+ });
259
+
260
+ if (res.ok) {
261
+ setWidgets((prev) => prev.filter((w) => w.id !== widgetId));
262
+ }
263
+ } catch (err) {
264
+ console.error('Erreur suppression widget:', err);
265
+ toast.error(devToast('Impossible de retirer le widget. Veuillez réessayer.', err));
266
+ }
267
+ }, []);
268
+
269
+ const handleResetLayout = useCallback(async () => {
270
+ try {
271
+ const res = await fetch('/api/dashboard/widgets', {
272
+ method: 'POST',
273
+ headers: { 'Content-Type': 'application/json' },
274
+ body: JSON.stringify({ initDefault: true }),
275
+ });
276
+
277
+ if (res.ok) {
278
+ setWidgets(await res.json());
279
+ }
280
+ } catch (err) {
281
+ console.error('Erreur réinitialisation layout:', err);
282
+ toast.error(devToast('Impossible de réinitialiser le tableau de bord. Veuillez réessayer.', err));
283
+ }
284
+ }, []);
285
+
286
+ const gridLayout = useMemo((): Layout => {
287
+ return widgets.map((widget) => {
288
+ const def = getWidgetDefinition(widget.type);
289
+
290
+ if (cols === 2) {
291
+ const isSmallWidget = widget.w <= 4;
292
+ const mobileW = isSmallWidget ? 1 : 2;
293
+ return {
294
+ i: widget.id,
295
+ x: isSmallWidget ? widget.x % 2 : 0,
296
+ y: widget.y,
297
+ w: mobileW,
298
+ h: widget.h,
299
+ minW: 1,
300
+ minH: def?.minH ?? 2,
301
+ maxH: def?.maxH,
302
+ };
303
+ }
304
+
305
+ return {
306
+ i: widget.id,
307
+ x: widget.x,
308
+ y: widget.y,
309
+ w: widget.w,
310
+ h: widget.h,
311
+ minW: def?.minW ?? 2,
312
+ minH: def?.minH ?? 2,
313
+ maxH: def?.maxH,
314
+ };
315
+ });
316
+ }, [widgets, cols]);
317
+
318
+ const renderWidgetContent = useCallback(
319
+ (widget: DashboardWidget) => {
320
+ if (!stats) return null;
321
+
322
+ switch (widget.type) {
323
+ case 'stat_total_contacts':
324
+ return (
325
+ <StatCard
326
+ title="Total Contacts"
327
+ value={stats.overview.totalContacts.toLocaleString('fr-FR')}
328
+ trend={{
329
+ value: stats.overview.contactsGrowth,
330
+ label: 'vs mois dernier',
331
+ }}
332
+ accentColor="dash-accent-bar"
333
+ />
334
+ );
335
+ case 'stat_new_contacts':
336
+ return (
337
+ <StatCard
338
+ title="Nouveaux ce Mois"
339
+ value={stats.overview.contactsThisMonth.toLocaleString('fr-FR')}
340
+ subtitle="contacts créés"
341
+ accentColor="bg-emerald-500"
342
+ />
343
+ );
344
+ case 'stat_completed_tasks':
345
+ return (
346
+ <StatCard
347
+ title="Tâches Complétées"
348
+ value={stats.tasks.completed.toLocaleString('fr-FR')}
349
+ subtitle={`sur ${stats.tasks.total} au total`}
350
+ accentColor="bg-blue-500"
351
+ />
352
+ );
353
+ case 'stat_pending_tasks':
354
+ return (
355
+ <StatCard
356
+ title="Tâches en Attente"
357
+ value={stats.tasks.pending.toLocaleString('fr-FR')}
358
+ subtitle="à traiter"
359
+ accentColor="bg-amber-500"
360
+ />
361
+ );
362
+ case 'contacts_chart':
363
+ return <ContactsChart data={stats.overview.monthsData} />;
364
+ case 'activity_chart':
365
+ return <ActivityChart data={stats.activity.last7Days} />;
366
+ case 'status_distribution':
367
+ return <StatusDistributionChart data={stats.statusDistribution} />;
368
+ case 'tasks_pie':
369
+ return <TasksPieChart completed={stats.tasks.completed} pending={stats.tasks.pending} />;
370
+ case 'upcoming_tasks':
371
+ return <UpcomingTasksList tasks={stats.tasks.upcoming} />;
372
+ case 'recent_activity':
373
+ return <RecentActivity interactions={stats.interactions.recent} />;
374
+ case 'top_contacts':
375
+ return <TopContactsList contacts={stats.topContacts} />;
376
+ case 'interactions_by_type':
377
+ return <InteractionsByTypeChart data={stats.interactions.byType} />;
378
+ default:
379
+ return (
380
+ <div className="flex h-full items-center justify-center rounded-2xl border border-gray-100 bg-white p-5 text-sm text-gray-400">
381
+ Widget inconnu : {widget.type}
382
+ </div>
383
+ );
384
+ }
385
+ },
386
+ [stats],
387
+ );
388
+
389
+ const renderContent = () => {
390
+ if (loading || permissionsLoading) {
391
+ return (
392
+ <>
393
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
394
+ {[1, 2, 3, 4].map((i) => (
395
+ <div key={i} className="h-[100px] animate-pulse rounded-2xl bg-gray-100" />
396
+ ))}
397
+ </div>
398
+ <div className="mt-4 grid gap-4 lg:grid-cols-2">
399
+ {[1, 2, 3, 4].map((i) => (
400
+ <div key={i} className="h-[300px] animate-pulse rounded-2xl bg-gray-100" />
401
+ ))}
402
+ </div>
403
+ </>
404
+ );
405
+ }
406
+
407
+ if (!canViewDashboard) {
408
+ return (
409
+ <div className="ui-fade-in flex h-96 flex-col items-center justify-center rounded-2xl border-2 border-dashed border-gray-200 bg-gray-50/50">
410
+ <ShieldAlert className="h-12 w-12 text-gray-300" />
411
+ <h3 className="mt-4 text-base font-medium text-gray-600">Accès restreint</h3>
412
+ <p className="mt-1 text-center text-sm text-gray-400">
413
+ Vous n&apos;avez pas la permission d&apos;accéder au tableau de bord.
414
+ <br />
415
+ Contactez votre administrateur pour obtenir les droits nécessaires.
416
+ </p>
417
+ </div>
418
+ );
419
+ }
420
+
421
+ if (error) {
422
+ return null;
423
+ }
424
+
425
+ return null;
426
+ };
427
+
428
+ const themeVars = {
429
+ '--dash-50': theme.hex[50],
430
+ '--dash-100': theme.hex[100],
431
+ '--dash-200': theme.hex[200],
432
+ '--dash-300': theme.hex[300],
433
+ '--dash-400': theme.hex[400],
434
+ '--dash-500': theme.hex[500],
435
+ '--dash-600': theme.hex[600],
436
+ '--dash-700': theme.hex[700],
437
+ } as React.CSSProperties;
44
438
 
45
- export default function DashboardPage() {
46
439
  return (
47
- <Suspense fallback={<DashboardSkeleton />}>
48
- <DashboardData />
49
- </Suspense>
440
+ <div className="h-full w-full min-w-0" style={themeVars}>
441
+ <PageHeader
442
+ title="Tableau de Bord"
443
+ description="Vue d'ensemble de votre activité"
444
+ action={
445
+ !loading && !permissionsLoading && !error && canViewDashboard ? (
446
+ <div className="flex items-center gap-2">
447
+ <DashboardColorPicker />
448
+ {widgets.length > 0 && canResetDashboard && (
449
+ <button
450
+ onClick={handleResetLayout}
451
+ className="inline-flex cursor-pointer items-center gap-2 rounded-xl border border-gray-200 bg-white px-4 py-2.5 text-sm font-medium text-gray-600 shadow-sm transition-[background-color,color,box-shadow,transform] duration-150 hover:bg-gray-50 hover:text-gray-900 hover:shadow-md active:scale-[0.98]"
452
+ >
453
+ <RotateCcw className="h-4 w-4" />
454
+ Réinitialiser
455
+ </button>
456
+ )}
457
+ {canManageWidgets && (
458
+ <button
459
+ onClick={() => setShowAddDialog(true)}
460
+ className="dash-btn inline-flex cursor-pointer items-center gap-2 rounded-xl px-4 py-2.5 text-sm font-medium shadow-sm transition-[box-shadow,transform] duration-150 hover:shadow-md active:scale-[0.98]"
461
+ >
462
+ <Plus className="h-4 w-4" />
463
+ Ajouter un Widget
464
+ </button>
465
+ )}
466
+ </div>
467
+ ) : undefined
468
+ }
469
+ />
470
+
471
+ <div ref={containerRef} className="p-4 sm:p-6">
472
+ {renderContent()}
473
+ {!loading && !permissionsLoading && !error && canViewDashboard && widgets.length === 0 && (
474
+ <div className="ui-fade-in flex h-96 flex-col items-center justify-center rounded-2xl border-2 border-dashed border-gray-200 bg-gray-50/50">
475
+ <LayoutDashboard className="h-12 w-12 text-gray-300" />
476
+ <h3 className="mt-4 text-base font-medium text-gray-600">Aucun widget configuré</h3>
477
+ <p className="mt-1 text-sm text-gray-400">
478
+ {canManageWidgets
479
+ ? 'Ajoutez des widgets pour personnaliser votre tableau de bord'
480
+ : "Aucun widget n'est configuré. Contactez votre administrateur."}
481
+ </p>
482
+ {canManageWidgets && (
483
+ <button
484
+ onClick={() => setShowAddDialog(true)}
485
+ className="dash-btn mt-4 inline-flex cursor-pointer items-center gap-2 rounded-xl px-4 py-2.5 text-sm font-medium transition-colors"
486
+ >
487
+ <Plus className="h-4 w-4" />
488
+ Ajouter un Widget
489
+ </button>
490
+ )}
491
+ </div>
492
+ )}
493
+
494
+ {!loading &&
495
+ !permissionsLoading &&
496
+ !error &&
497
+ canViewDashboard &&
498
+ widgets.length > 0 &&
499
+ mounted && (
500
+ <GridLayout
501
+ className="layout ui-fade-in"
502
+ width={containerWidth}
503
+ layout={gridLayout}
504
+ gridConfig={{
505
+ cols,
506
+ rowHeight: 60,
507
+ margin: [16, 16] as const,
508
+ containerPadding: [0, 0] as const,
509
+ maxRows: Infinity,
510
+ }}
511
+ onLayoutChange={handleLayoutChange}
512
+ dragConfig={{
513
+ handle: '.drag-handle',
514
+ enabled: canManageWidgets,
515
+ threshold: 3,
516
+ bounded: false,
517
+ }}
518
+ resizeConfig={{ enabled: canManageWidgets, handles: ['se'] }}
519
+ >
520
+ {widgets.map((widget) => (
521
+ <div key={widget.id}>
522
+ <WidgetWrapper
523
+ onRemove={
524
+ canManageWidgets
525
+ ? () => {
526
+ handleRemoveWidget(widget.id);
527
+ }
528
+ : undefined
529
+ }
530
+ >
531
+ {renderWidgetContent(widget)}
532
+ </WidgetWrapper>
533
+ </div>
534
+ ))}
535
+ </GridLayout>
536
+ )}
537
+ </div>
538
+
539
+ <AddWidgetDialog
540
+ isOpen={showAddDialog}
541
+ onClose={() => setShowAddDialog(false)}
542
+ onAdd={handleAddWidget}
543
+ existingTypes={widgets.map((w) => w.type)}
544
+ />
545
+ </div>
50
546
  );
51
547
  }