create-crm-tmp 1.1.2 → 2.0.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 (220) hide show
  1. package/package.json +1 -1
  2. package/template/.prettierignore +2 -0
  3. package/template/README.md +53 -67
  4. package/template/components.json +22 -0
  5. package/template/exemple-contacts.csv +54 -0
  6. package/template/next.config.ts +27 -1
  7. package/template/package.json +64 -27
  8. package/template/prisma/schema.prisma +821 -72
  9. package/template/skills-lock.json +25 -0
  10. package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
  11. package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
  12. package/template/src/app/(auth)/reset-password/page.tsx +12 -8
  13. package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
  14. package/template/src/app/(auth)/signin/page.tsx +20 -17
  15. package/template/src/app/(dashboard)/agenda/page.tsx +2231 -2188
  16. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
  17. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
  18. package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
  19. package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
  20. package/template/src/app/(dashboard)/closing/page.tsx +500 -468
  21. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5035 -4126
  22. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
  23. package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
  24. package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
  25. package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
  26. package/template/src/app/(dashboard)/error.tsx +37 -0
  27. package/template/src/app/(dashboard)/layout.tsx +1 -1
  28. package/template/src/app/(dashboard)/loading.tsx +5 -0
  29. package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
  30. package/template/src/app/(dashboard)/settings/page.tsx +2685 -2489
  31. package/template/src/app/(dashboard)/templates/page.tsx +500 -300
  32. package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
  33. package/template/src/app/(dashboard)/users/page.tsx +279 -310
  34. package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
  35. package/template/src/app/(dashboard)/users/roles/page.tsx +164 -137
  36. package/template/src/app/api/audit-logs/route.ts +1 -1
  37. package/template/src/app/api/auth/google/callback/route.ts +8 -5
  38. package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
  39. package/template/src/app/api/companies/[id]/activities/route.ts +131 -0
  40. package/template/src/app/api/companies/[id]/route.ts +195 -0
  41. package/template/src/app/api/companies/export/route.ts +206 -0
  42. package/template/src/app/api/companies/route.ts +166 -0
  43. package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
  44. package/template/src/app/api/contact-views/[id]/route.ts +197 -0
  45. package/template/src/app/api/contact-views/route.ts +146 -0
  46. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +77 -0
  47. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
  48. package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
  49. package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
  50. package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
  51. package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
  52. package/template/src/app/api/contacts/[id]/route.ts +111 -20
  53. package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
  54. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
  55. package/template/src/app/api/contacts/export/route.ts +12 -17
  56. package/template/src/app/api/contacts/import/route.ts +22 -19
  57. package/template/src/app/api/contacts/import-preview/route.ts +139 -0
  58. package/template/src/app/api/contacts/route.ts +202 -49
  59. package/template/src/app/api/dashboard/stats/route.ts +9 -292
  60. package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
  61. package/template/src/app/api/invite/complete/route.ts +20 -23
  62. package/template/src/app/api/reminders/route.ts +1 -0
  63. package/template/src/app/api/reset-password/complete/route.ts +11 -13
  64. package/template/src/app/api/send/route.ts +9 -85
  65. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
  66. package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
  67. package/template/src/app/api/settings/company/route.ts +19 -26
  68. package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
  69. package/template/src/app/api/settings/google-ads/route.ts +20 -23
  70. package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
  71. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
  72. package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
  73. package/template/src/app/api/settings/google-sheet/route.ts +20 -23
  74. package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
  75. package/template/src/app/api/settings/meta-leads/route.ts +20 -23
  76. package/template/src/app/api/settings/statuses/[id]/route.ts +33 -23
  77. package/template/src/app/api/settings/statuses/route.ts +24 -22
  78. package/template/src/app/api/statuses/route.ts +2 -5
  79. package/template/src/app/api/tasks/[id]/attendees/route.ts +14 -7
  80. package/template/src/app/api/tasks/[id]/route.ts +161 -137
  81. package/template/src/app/api/tasks/meet/route.ts +11 -8
  82. package/template/src/app/api/tasks/route.ts +155 -95
  83. package/template/src/app/api/templates/[id]/route.ts +22 -13
  84. package/template/src/app/api/templates/route.ts +22 -5
  85. package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
  86. package/template/src/app/api/users/[id]/route.ts +16 -1
  87. package/template/src/app/api/users/commercials/route.ts +38 -0
  88. package/template/src/app/api/users/for-agenda/route.ts +1 -2
  89. package/template/src/app/api/users/route.ts +94 -55
  90. package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
  91. package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
  92. package/template/src/app/api/workflows/[id]/route.ts +33 -6
  93. package/template/src/app/api/workflows/process/route.ts +509 -146
  94. package/template/src/app/api/workflows/route.ts +46 -4
  95. package/template/src/app/globals.css +210 -101
  96. package/template/src/app/layout.tsx +19 -8
  97. package/template/src/app/page.tsx +37 -7
  98. package/template/src/components/address-autocomplete.tsx +232 -0
  99. package/template/src/components/contacts/filter-bar.tsx +181 -0
  100. package/template/src/components/contacts/filter-builder.tsx +589 -0
  101. package/template/src/components/contacts/save-view-dialog.tsx +160 -0
  102. package/template/src/components/contacts/views-tab-bar.tsx +440 -0
  103. package/template/src/components/dashboard/activity-chart.tsx +31 -39
  104. package/template/src/components/dashboard/dashboard-content.tsx +79 -0
  105. package/template/src/components/dashboard/stat-card.tsx +40 -42
  106. package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
  107. package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
  108. package/template/src/components/date-picker.tsx +396 -0
  109. package/template/src/components/editor.tsx +27 -13
  110. package/template/src/components/email-template.tsx +4 -2
  111. package/template/src/components/global-search.tsx +358 -0
  112. package/template/src/components/header.tsx +57 -62
  113. package/template/src/components/invitation-email-template.tsx +4 -2
  114. package/template/src/components/lazy-editor.tsx +11 -0
  115. package/template/src/components/meet-cancellation-email-template.tsx +11 -3
  116. package/template/src/components/meet-confirmation-email-template.tsx +10 -3
  117. package/template/src/components/meet-update-email-template.tsx +10 -3
  118. package/template/src/components/page-header.tsx +19 -15
  119. package/template/src/components/protected-page.tsx +94 -0
  120. package/template/src/components/reset-password-email-template.tsx +4 -2
  121. package/template/src/components/sidebar.tsx +92 -94
  122. package/template/src/components/skeleton.tsx +128 -42
  123. package/template/src/components/ui/accordion.tsx +64 -0
  124. package/template/src/components/ui/alert-dialog.tsx +139 -0
  125. package/template/src/components/ui/button.tsx +60 -0
  126. package/template/src/components/view-as-banner.tsx +1 -1
  127. package/template/src/components/view-as-modal.tsx +21 -16
  128. package/template/src/config/nav-pages.ts +108 -0
  129. package/template/src/contexts/app-toast-context.tsx +174 -0
  130. package/template/src/contexts/sidebar-context.tsx +16 -47
  131. package/template/src/contexts/task-reminder-context.tsx +6 -6
  132. package/template/src/contexts/view-as-context.tsx +11 -16
  133. package/template/src/hooks/use-alert.tsx +65 -0
  134. package/template/src/hooks/use-confirm.tsx +87 -0
  135. package/template/src/hooks/use-contact-views.ts +140 -0
  136. package/template/src/hooks/use-contacts.ts +69 -0
  137. package/template/src/hooks/use-fetch.ts +17 -0
  138. package/template/src/hooks/use-focus-trap.ts +73 -0
  139. package/template/src/hooks/use-statuses.ts +22 -0
  140. package/template/src/lib/address-api.ts +155 -0
  141. package/template/src/lib/cache.ts +73 -0
  142. package/template/src/lib/check-permission.ts +12 -177
  143. package/template/src/lib/contact-interactions.ts +3 -1
  144. package/template/src/lib/contact-view-filters.ts +341 -0
  145. package/template/src/lib/dashboard-stats.ts +224 -0
  146. package/template/src/lib/date-utils.ts +49 -0
  147. package/template/src/lib/get-auth-user.ts +25 -0
  148. package/template/src/lib/google-calendar.ts +54 -12
  149. package/template/src/lib/google-drive.ts +796 -75
  150. package/template/src/lib/google-fetch.ts +63 -0
  151. package/template/src/lib/local-storage.ts +34 -0
  152. package/template/src/lib/permissions.ts +245 -47
  153. package/template/src/lib/prisma.ts +11 -11
  154. package/template/src/lib/roles.ts +14 -39
  155. package/template/src/lib/template-variables.ts +67 -33
  156. package/template/src/lib/utils.ts +26 -2
  157. package/template/src/lib/workflow-executor.ts +445 -229
  158. package/template/src/proxy.ts +34 -73
  159. package/template/src/types/contact-views.ts +351 -0
  160. package/template/src/types/yousign.ts +52 -0
  161. package/template/vercel.json +12 -0
  162. package/template/WORKFLOWS_CRON.md +0 -185
  163. package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
  164. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
  165. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
  166. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
  167. package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
  168. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
  169. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
  170. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
  171. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
  172. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
  173. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
  174. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
  175. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
  176. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
  177. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
  178. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
  179. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
  180. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
  181. package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
  182. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
  183. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
  184. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
  185. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
  186. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
  187. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
  188. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
  189. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
  190. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
  191. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
  192. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
  193. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
  194. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
  195. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
  196. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
  197. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
  198. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
  199. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
  200. package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
  201. package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
  202. package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
  203. package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
  204. package/template/prisma/migrations/migration_lock.toml +0 -3
  205. package/template/src/app/(dashboard)/users/layout.tsx +0 -30
  206. package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
  207. package/template/src/app/api/dashboard/widgets/route.ts +0 -181
  208. package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
  209. package/template/src/components/dashboard/color-picker.tsx +0 -65
  210. package/template/src/components/dashboard/contacts-chart.tsx +0 -69
  211. package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
  212. package/template/src/components/dashboard/recent-activity.tsx +0 -157
  213. package/template/src/components/dashboard/sales-analytics-chart.tsx +0 -77
  214. package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
  215. package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
  216. package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
  217. package/template/src/contexts/dashboard-theme-context.tsx +0 -58
  218. package/template/src/lib/dashboard-themes.ts +0 -140
  219. package/template/src/lib/default-widgets.ts +0 -14
  220. package/template/src/lib/widget-registry.ts +0 -177
@@ -1,533 +1,51 @@
1
- 'use client';
2
-
3
- import React, { useEffect, useState, useCallback, useRef, useMemo } from 'react';
4
- import { GridLayout, useContainerWidth, type Layout, type LayoutItem } from 'react-grid-layout';
5
- import { Plus, LayoutDashboard, RotateCcw, ShieldAlert } from 'lucide-react';
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';
6
6
  import { PageHeader } from '@/components/page-header';
7
- import { WidgetWrapper } from '@/components/dashboard/widget-wrapper';
8
- import { AddWidgetDialog } from '@/components/dashboard/add-widget-dialog';
9
- import { DashboardColorPicker } from '@/components/dashboard/color-picker';
10
- import { useUserRole } from '@/hooks/use-user-role';
11
- import { DashboardThemeProvider, useDashboardTheme } from '@/contexts/dashboard-theme-context';
12
- import { StatCard } from '@/components/dashboard/stat-card';
13
- import { ContactsChart } from '@/components/dashboard/contacts-chart';
14
- import { ActivityChart } from '@/components/dashboard/activity-chart';
15
- import { StatusDistributionChart } from '@/components/dashboard/status-distribution-chart';
16
- import { TasksPieChart } from '@/components/dashboard/tasks-pie-chart';
17
- import { UpcomingTasksList } from '@/components/dashboard/upcoming-tasks-list';
18
- import { RecentActivity } from '@/components/dashboard/recent-activity';
19
- import { TopContactsList } from '@/components/dashboard/top-contacts-list';
20
- import { InteractionsByTypeChart } from '@/components/dashboard/interactions-by-type-chart';
21
- import { getWidgetDefinition } from '@/lib/widget-registry';
22
-
23
- interface DashboardWidget {
24
- id: string;
25
- type: string;
26
- x: number;
27
- y: number;
28
- w: number;
29
- h: number;
30
- }
7
+ import { getAuthUser } from '@/lib/get-auth-user';
31
8
 
32
- interface DashboardStats {
33
- overview: {
34
- totalContacts: number;
35
- contactsThisMonth: number;
36
- contactsGrowth: number;
37
- monthsData: Array<{ month: string; count: number }>;
38
- };
39
- statusDistribution: Array<{ name: string; value: number }>;
40
- tasks: {
41
- total: number;
42
- completed: number;
43
- pending: number;
44
- upcoming: Array<{
45
- id: string;
46
- title: string;
47
- type: string;
48
- scheduledAt: string;
49
- contact: { id: string; name: string } | null;
50
- priority: string;
51
- }>;
52
- byType: Array<{ type: string; count: number }>;
53
- };
54
- interactions: {
55
- recent: Array<{
56
- id: string;
57
- type: string;
58
- title: string | null;
59
- content: string;
60
- date: string;
61
- contact: {
62
- id: string;
63
- name: string;
64
- };
65
- }>;
66
- byType: Array<{ type: string; count: number }>;
67
- };
68
- activity: {
69
- last7Days: Array<{ date: string; interactions: number; tasks: number }>;
70
- };
71
- topContacts: Array<{
72
- id: string;
73
- name: string;
74
- phone: string;
75
- email: string | null;
76
- status: string;
77
- interactionsCount: number;
78
- assignedCommercial?: string;
79
- assignedTelepro?: string;
80
- }>;
81
- }
82
-
83
- export default function DashboardPage() {
9
+ function DashboardSkeleton() {
84
10
  return (
85
- <DashboardThemeProvider>
86
- <DashboardContent />
87
- </DashboardThemeProvider>
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>
88
26
  );
89
27
  }
90
28
 
91
- function DashboardContent() {
92
- const [widgets, setWidgets] = useState<DashboardWidget[]>([]);
93
- const [stats, setStats] = useState<DashboardStats | null>(null);
94
- const [loading, setLoading] = useState(true);
95
- const [error, setError] = useState<string | null>(null);
96
- const [showAddDialog, setShowAddDialog] = useState(false);
97
- const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
98
- const { hasPermission, isLoading: permissionsLoading } = useUserRole();
99
- const { theme } = useDashboardTheme();
100
-
101
- // Permissions dashboard
102
- const canViewDashboard = hasPermission('dashboard.view');
103
- const canManageWidgets = hasPermission('dashboard.widgets.manage');
104
- const canResetDashboard = hasPermission('dashboard.widgets.reset');
105
- const {
106
- width: containerWidth,
107
- containerRef,
108
- mounted,
109
- } = useContainerWidth({ initialWidth: 1200 });
110
-
111
- // Nombre de colonnes dynamique basé sur la largeur réelle
112
- const cols = useMemo(() => {
113
- if (containerWidth < 768) return 2;
114
- return 12;
115
- }, [containerWidth]);
116
-
117
- // Charger widgets et stats
118
- useEffect(() => {
119
- async function fetchData() {
120
- try {
121
- const widgetsRes = await fetch('/api/dashboard/widgets');
122
- let widgetsData: DashboardWidget[] = [];
123
-
124
- if (widgetsRes.ok) {
125
- widgetsData = await widgetsRes.json();
126
- }
127
-
128
- // Initialiser le layout par défaut uniquement à la toute première visite
129
- // (pas de widgets ET jamais initialisé auparavant)
130
- const hasBeenInitialized = localStorage.getItem('dashboard_initialized');
131
- if (widgetsData.length === 0 && !hasBeenInitialized) {
132
- const initRes = await fetch('/api/dashboard/widgets', {
133
- method: 'POST',
134
- headers: { 'Content-Type': 'application/json' },
135
- body: JSON.stringify({ initDefault: true }),
136
- });
137
- if (initRes.ok) {
138
- widgetsData = await initRes.json();
139
- }
140
- localStorage.setItem('dashboard_initialized', 'true');
141
- }
142
-
143
- setWidgets(widgetsData);
144
-
145
- const statsRes = await fetch('/api/dashboard/stats');
146
- if (statsRes.ok) {
147
- const statsData = await statsRes.json();
148
- setStats(statsData);
149
- }
150
- } catch (err) {
151
- setError(err instanceof Error ? err.message : 'Une erreur est survenue');
152
- } finally {
153
- setLoading(false);
154
- }
155
- }
156
-
157
- fetchData();
158
- }, []);
159
-
160
- // Sauvegarder les positions avec debounce
161
- const saveLayout = useCallback((updatedWidgets: DashboardWidget[]) => {
162
- if (saveTimeoutRef.current) {
163
- clearTimeout(saveTimeoutRef.current);
164
- }
165
-
166
- saveTimeoutRef.current = setTimeout(async () => {
167
- try {
168
- await fetch('/api/dashboard/widgets', {
169
- method: 'PUT',
170
- headers: { 'Content-Type': 'application/json' },
171
- body: JSON.stringify({
172
- widgets: updatedWidgets.map((w) => ({
173
- id: w.id,
174
- x: w.x,
175
- y: w.y,
176
- w: w.w,
177
- h: w.h,
178
- })),
179
- }),
180
- });
181
- } catch (err) {
182
- console.error('Erreur sauvegarde layout:', err);
183
- }
184
- }, 500);
185
- }, []);
186
-
187
- // Gérer le changement de layout (drag/resize)
188
- // Ne sauvegarde en base que quand on est en mode desktop (12 cols)
189
- // pour ne pas écraser les positions desktop avec les positions mobile
190
- const handleLayoutChange = useCallback(
191
- (layout: Layout) => {
192
- if (cols !== 12) return;
193
-
194
- const updatedWidgets = widgets.map((widget) => {
195
- const layoutItem = layout.find((l: LayoutItem) => l.i === widget.id);
196
- if (layoutItem) {
197
- return {
198
- ...widget,
199
- x: layoutItem.x,
200
- y: layoutItem.y,
201
- w: layoutItem.w,
202
- h: layoutItem.h,
203
- };
204
- }
205
- return widget;
206
- });
207
-
208
- setWidgets(updatedWidgets);
209
- saveLayout(updatedWidgets);
210
- },
211
- [widgets, saveLayout, cols],
212
- );
213
-
214
- // Ajouter un widget
215
- const handleAddWidget = useCallback(async (type: string, w: number, h: number) => {
216
- try {
217
- const res = await fetch('/api/dashboard/widgets', {
218
- method: 'POST',
219
- headers: { 'Content-Type': 'application/json' },
220
- body: JSON.stringify({ type, w, h }),
221
- });
222
-
223
- if (res.ok) {
224
- const newWidget = await res.json();
225
- setWidgets((prev) => [...prev, newWidget]);
226
- }
227
- } catch (err) {
228
- console.error('Erreur ajout widget:', err);
229
- }
230
-
231
- setShowAddDialog(false);
232
- }, []);
233
-
234
- // Supprimer un widget
235
- const handleRemoveWidget = useCallback(async (widgetId: string) => {
236
- try {
237
- const res = await fetch(`/api/dashboard/widgets/${widgetId}`, {
238
- method: 'DELETE',
239
- });
240
-
241
- if (res.ok) {
242
- setWidgets((prev) => prev.filter((w) => w.id !== widgetId));
243
- }
244
- } catch (err) {
245
- console.error('Erreur suppression widget:', err);
246
- }
247
- }, []);
248
-
249
- // Réinitialiser le layout par défaut
250
- const handleResetLayout = useCallback(async () => {
251
- try {
252
- // Supprimer tous les widgets existants
253
- await Promise.all(
254
- widgets.map((w) => fetch(`/api/dashboard/widgets/${w.id}`, { method: 'DELETE' })),
255
- );
256
-
257
- // Recréer le layout par défaut
258
- const initRes = await fetch('/api/dashboard/widgets', {
259
- method: 'POST',
260
- headers: { 'Content-Type': 'application/json' },
261
- body: JSON.stringify({ initDefault: true }),
262
- });
263
-
264
- if (initRes.ok) {
265
- const newWidgets = await initRes.json();
266
- setWidgets(newWidgets);
267
- }
268
- } catch (err) {
269
- console.error('Erreur réinitialisation layout:', err);
270
- }
271
- }, [widgets]);
272
-
273
- // Construire le layout adapté au nombre de colonnes actuel
274
- const gridLayout = useMemo((): Layout => {
275
- return widgets.map((widget) => {
276
- const def = getWidgetDefinition(widget.type);
277
-
278
- if (cols === 2) {
279
- // Mobile : stat cards (w <= 3 en 12 cols) → 1 col, le reste → 2 cols (pleine largeur)
280
- const isSmallWidget = widget.w <= 4;
281
- const mobileW = isSmallWidget ? 1 : 2;
282
- return {
283
- i: widget.id,
284
- x: isSmallWidget ? widget.x % 2 : 0,
285
- y: widget.y,
286
- w: mobileW,
287
- h: widget.h,
288
- minW: 1,
289
- minH: def?.minH ?? 2,
290
- maxH: def?.maxH,
291
- };
292
- }
293
-
294
- // Desktop : layout stocké en base (grille 12 colonnes)
295
- return {
296
- i: widget.id,
297
- x: widget.x,
298
- y: widget.y,
299
- w: widget.w,
300
- h: widget.h,
301
- minW: def?.minW ?? 2,
302
- minH: def?.minH ?? 2,
303
- maxH: def?.maxH,
304
- };
305
- });
306
- }, [widgets, cols]);
307
-
308
- // Rendu du contenu d'un widget selon son type
309
- const renderWidgetContent = useCallback(
310
- (widget: DashboardWidget) => {
311
- if (!stats) return null;
29
+ async function DashboardData() {
30
+ const authUser = await getAuthUser();
312
31
 
313
- switch (widget.type) {
314
- case 'stat_total_contacts':
315
- return (
316
- <StatCard
317
- title="Total Contacts"
318
- value={stats.overview.totalContacts.toLocaleString('fr-FR')}
319
- trend={{
320
- value: stats.overview.contactsGrowth,
321
- label: 'vs mois dernier',
322
- }}
323
- accentColor="dash-accent-bar"
324
- />
325
- );
326
- case 'stat_new_contacts':
327
- return (
328
- <StatCard
329
- title="Nouveaux ce Mois"
330
- value={stats.overview.contactsThisMonth.toLocaleString('fr-FR')}
331
- subtitle="contacts créés"
332
- accentColor="bg-emerald-500"
333
- />
334
- );
335
- case 'stat_completed_tasks':
336
- return (
337
- <StatCard
338
- title="Tâches Complétées"
339
- value={stats.tasks.completed.toLocaleString('fr-FR')}
340
- subtitle={`sur ${stats.tasks.total} au total`}
341
- accentColor="bg-blue-500"
342
- />
343
- );
344
- case 'stat_pending_tasks':
345
- return (
346
- <StatCard
347
- title="Tâches en Attente"
348
- value={stats.tasks.pending.toLocaleString('fr-FR')}
349
- subtitle="à traiter"
350
- accentColor="bg-amber-500"
351
- />
352
- );
353
- case 'contacts_chart':
354
- return <ContactsChart data={stats.overview.monthsData} />;
355
- case 'activity_chart':
356
- return <ActivityChart data={stats.activity.last7Days} />;
357
- case 'status_distribution':
358
- return <StatusDistributionChart data={stats.statusDistribution} />;
359
- case 'tasks_pie':
360
- return <TasksPieChart completed={stats.tasks.completed} pending={stats.tasks.pending} />;
361
- case 'upcoming_tasks':
362
- return <UpcomingTasksList tasks={stats.tasks.upcoming} />;
363
- case 'recent_activity':
364
- return <RecentActivity interactions={stats.interactions.recent} />;
365
- case 'top_contacts':
366
- return <TopContactsList contacts={stats.topContacts} />;
367
- case 'interactions_by_type':
368
- return <InteractionsByTypeChart data={stats.interactions.byType} />;
369
- default:
370
- return (
371
- <div className="flex h-full items-center justify-center rounded-2xl border border-gray-100 bg-white p-5 text-sm text-gray-400">
372
- Widget inconnu : {widget.type}
373
- </div>
374
- );
375
- }
376
- },
377
- [stats],
378
- );
379
-
380
- // Contenu intérieur selon l'état
381
- const renderContent = () => {
382
- if (loading || permissionsLoading) {
383
- return (
384
- <>
385
- <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
386
- {[1, 2, 3, 4].map((i) => (
387
- <div key={i} className="h-[100px] animate-pulse rounded-2xl bg-gray-100" />
388
- ))}
389
- </div>
390
- <div className="mt-4 grid gap-4 lg:grid-cols-2">
391
- {[1, 2, 3, 4].map((i) => (
392
- <div key={i} className="h-[300px] animate-pulse rounded-2xl bg-gray-100" />
393
- ))}
394
- </div>
395
- </>
396
- );
397
- }
398
-
399
- if (!canViewDashboard) {
400
- return (
401
- <div className="flex h-96 flex-col items-center justify-center rounded-2xl border-2 border-dashed border-gray-200 bg-gray-50/50">
402
- <ShieldAlert className="h-12 w-12 text-gray-300" />
403
- <h3 className="mt-4 text-base font-medium text-gray-600">Accès restreint</h3>
404
- <p className="mt-1 text-center text-sm text-gray-400">
405
- Vous n&apos;avez pas la permission d&apos;accéder au tableau de bord.<br />
406
- Contactez votre administrateur pour obtenir les droits nécessaires.
407
- </p>
408
- </div>
409
- );
410
- }
32
+ if (!authUser) {
33
+ redirect('/signin');
34
+ }
411
35
 
412
- if (error) {
413
- return (
414
- <div className="flex h-96 items-center justify-center p-4">
415
- <p className="text-sm text-red-500">{error}</p>
416
- </div>
417
- );
418
- }
36
+ if (!authUser.permissions.includes('dashboard.view')) {
37
+ redirect('/contacts');
38
+ }
419
39
 
420
- return null;
421
- };
40
+ const stats = await getDashboardStats(authUser.session.user.id, authUser.permissions);
422
41
 
423
- // CSS variables pour le thème
424
- const themeVars = {
425
- '--dash-50': theme.hex[50],
426
- '--dash-100': theme.hex[100],
427
- '--dash-200': theme.hex[200],
428
- '--dash-300': theme.hex[300],
429
- '--dash-400': theme.hex[400],
430
- '--dash-500': theme.hex[500],
431
- '--dash-600': theme.hex[600],
432
- '--dash-700': theme.hex[700],
433
- } as React.CSSProperties;
42
+ return <DashboardContent stats={stats} />;
43
+ }
434
44
 
45
+ export default function DashboardPage() {
435
46
  return (
436
- <div className="h-full w-full min-w-0" style={themeVars}>
437
- <PageHeader
438
- title="Tableau de Bord"
439
- description="Vue d'ensemble de votre activité"
440
- action={
441
- !loading && !permissionsLoading && !error && canViewDashboard ? (
442
- <div className="flex items-center gap-2">
443
- <DashboardColorPicker />
444
- {widgets.length > 0 && canResetDashboard && (
445
- <button
446
- onClick={handleResetLayout}
447
- 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-all duration-150 hover:bg-gray-50 hover:text-gray-900 hover:shadow-md active:scale-[0.98]"
448
- >
449
- <RotateCcw className="h-4 w-4" />
450
- Réinitialiser
451
- </button>
452
- )}
453
- {canManageWidgets && (
454
- <button
455
- onClick={() => setShowAddDialog(true)}
456
- 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-all duration-150 hover:shadow-md active:scale-[0.98]"
457
- >
458
- <Plus className="h-4 w-4" />
459
- Ajouter un Widget
460
- </button>
461
- )}
462
- </div>
463
- ) : undefined
464
- }
465
- />
466
-
467
- {/* Le ref est TOUJOURS dans le DOM pour que useContainerWidth mesure correctement */}
468
- <div ref={containerRef} className="p-4 sm:p-6">
469
- {renderContent()}
470
- {!loading && !permissionsLoading && !error && canViewDashboard && widgets.length === 0 && (
471
- <div className="flex h-96 flex-col items-center justify-center rounded-2xl border-2 border-dashed border-gray-200 bg-gray-50/50">
472
- <LayoutDashboard className="h-12 w-12 text-gray-300" />
473
- <h3 className="mt-4 text-base font-medium text-gray-600">Aucun widget configuré</h3>
474
- <p className="mt-1 text-sm text-gray-400">
475
- {canManageWidgets
476
- ? 'Ajoutez des widgets pour personnaliser votre tableau de bord'
477
- : 'Aucun widget n\'est configuré. Contactez votre administrateur.'}
478
- </p>
479
- {canManageWidgets && (
480
- <button
481
- onClick={() => setShowAddDialog(true)}
482
- 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"
483
- >
484
- <Plus className="h-4 w-4" />
485
- Ajouter un Widget
486
- </button>
487
- )}
488
- </div>
489
- )}
490
-
491
- {!loading && !permissionsLoading && !error && canViewDashboard && widgets.length > 0 && mounted && (
492
- <GridLayout
493
- className="layout"
494
- width={containerWidth}
495
- layout={gridLayout}
496
- gridConfig={{
497
- cols,
498
- rowHeight: 60,
499
- margin: [16, 16] as const,
500
- containerPadding: [0, 0] as const,
501
- maxRows: Infinity,
502
- }}
503
- onLayoutChange={handleLayoutChange}
504
- dragConfig={{
505
- handle: '.drag-handle',
506
- enabled: canManageWidgets,
507
- threshold: 3,
508
- bounded: false,
509
- }}
510
- resizeConfig={{ enabled: canManageWidgets, handles: ['se'] }}
511
- >
512
- {widgets.map((widget) => (
513
- <div key={widget.id}>
514
- <WidgetWrapper
515
- onRemove={canManageWidgets ? () => { handleRemoveWidget(widget.id); } : undefined}
516
- >
517
- {renderWidgetContent(widget)}
518
- </WidgetWrapper>
519
- </div>
520
- ))}
521
- </GridLayout>
522
- )}
523
- </div>
524
-
525
- <AddWidgetDialog
526
- isOpen={showAddDialog}
527
- onClose={() => setShowAddDialog(false)}
528
- onAdd={handleAddWidget}
529
- existingTypes={widgets.map((w) => w.type)}
530
- />
531
- </div>
47
+ <Suspense fallback={<DashboardSkeleton />}>
48
+ <DashboardData />
49
+ </Suspense>
532
50
  );
533
51
  }
@@ -0,0 +1,37 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import { AlertTriangle, RefreshCw } from 'lucide-react';
5
+
6
+ export default function DashboardError({
7
+ error,
8
+ reset,
9
+ }: {
10
+ error: Error & { digest?: string };
11
+ reset: () => void;
12
+ }) {
13
+ useEffect(() => {
14
+ console.error('Dashboard error:', error);
15
+ }, [error]);
16
+
17
+ return (
18
+ <div className="flex h-full items-center justify-center p-6">
19
+ <div className="max-w-md text-center">
20
+ <AlertTriangle className="mx-auto h-12 w-12 text-red-500" />
21
+ <h2 className="mt-4 text-lg font-semibold text-gray-900">Une erreur est survenue</h2>
22
+ <p className="mt-2 text-sm text-gray-600">
23
+ {process.env.NODE_ENV === 'development'
24
+ ? error.message
25
+ : "Quelque chose s'est mal passé. Veuillez réessayer."}
26
+ </p>
27
+ <button
28
+ onClick={reset}
29
+ className="mt-4 inline-flex cursor-pointer items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
30
+ >
31
+ <RefreshCw className="h-4 w-4" />
32
+ Réessayer
33
+ </button>
34
+ </div>
35
+ </div>
36
+ );
37
+ }
@@ -14,7 +14,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
14
14
  <MobileMenuProvider>
15
15
  <SidebarProvider>
16
16
  <TaskReminderProvider>
17
- <div className="flex h-screen overflow-hidden bg-gray-50">
17
+ <div className="flex h-screen overflow-hidden bg-surface-page">
18
18
  <Sidebar />
19
19
  <main className="flex flex-1 flex-col overflow-hidden lg:ml-0">
20
20
  <Header />
@@ -0,0 +1,5 @@
1
+ import { PageLoader } from '@/components/skeleton';
2
+
3
+ export default function DashboardLoading() {
4
+ return <PageLoader />;
5
+ }
@@ -0,0 +1,19 @@
1
+ import { Skeleton } from '@/components/skeleton';
2
+
3
+ export default function SettingsLoading() {
4
+ return (
5
+ <div className="h-full">
6
+ <div className="border-b bg-white px-4 py-4 sm:px-6">
7
+ <Skeleton className="h-7 w-32" />
8
+ <Skeleton className="mt-1 h-4 w-64" />
9
+ </div>
10
+ <div className="p-4 sm:p-6">
11
+ <div className="space-y-6">
12
+ {Array.from({ length: 4 }).map((_, i) => (
13
+ <Skeleton key={i} className="h-40 rounded-lg" />
14
+ ))}
15
+ </div>
16
+ </div>
17
+ </div>
18
+ );
19
+ }