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
@@ -0,0 +1,224 @@
1
+ import { prisma } from '@/lib/prisma';
2
+ import { getCachedStatuses } from '@/lib/cache';
3
+
4
+ export interface DashboardStats {
5
+ overview: {
6
+ totalContacts: number;
7
+ contactsThisMonth: number;
8
+ contactsGrowth: number;
9
+ statusData: Array<{ name: string; value: number }>;
10
+ monthlyContacts: Array<{ month: string; count: number }>;
11
+ };
12
+ tasks: {
13
+ total: number;
14
+ completed: number;
15
+ pending: number;
16
+ upcoming: Array<{
17
+ id: string;
18
+ title: string;
19
+ type: string;
20
+ scheduledAt: string;
21
+ contact: { id: string; name: string } | null;
22
+ priority: string;
23
+ }>;
24
+ byType: Array<{ type: string; count: number }>;
25
+ };
26
+ interactions: {
27
+ recent: any[];
28
+ byType: Array<{ type: string; count: number }>;
29
+ };
30
+ activity: {
31
+ last7Days: Array<{ date: string; interactions: number; tasks: number }>;
32
+ };
33
+ }
34
+
35
+ export async function getDashboardStats(
36
+ userId: string,
37
+ permissions: string[],
38
+ ): Promise<DashboardStats> {
39
+ const canViewAllContacts = permissions.includes('contacts.view_all');
40
+ const canViewOwnContacts = permissions.includes('contacts.view_own');
41
+ const canViewUnassigned = permissions.includes('contacts.view_unassigned');
42
+
43
+ let contactFilter: any = {};
44
+ if (!canViewAllContacts && canViewOwnContacts) {
45
+ const conditions: any[] = [
46
+ { assignedCommercialId: userId },
47
+ { assignedTeleproId: userId },
48
+ { createdById: userId },
49
+ ];
50
+ if (canViewUnassigned) {
51
+ conditions.push({ AND: [{ assignedCommercialId: null }, { assignedTeleproId: null }] });
52
+ }
53
+ contactFilter = { OR: conditions };
54
+ } else if (!canViewAllContacts && !canViewOwnContacts) {
55
+ contactFilter = { id: '__none__' };
56
+ }
57
+
58
+ const now = new Date();
59
+ const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
60
+ const lastMonthStart = new Date(startOfMonth);
61
+ lastMonthStart.setMonth(lastMonthStart.getMonth() - 1);
62
+
63
+ const twelveMonthsAgo = new Date(now.getFullYear(), now.getMonth() - 11, 1);
64
+ const sevenDaysAgo = new Date(now);
65
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 6);
66
+ sevenDaysAgo.setHours(0, 0, 0, 0);
67
+
68
+ const [
69
+ totalContacts,
70
+ contactsThisMonth,
71
+ contactsLastMonth,
72
+ statusDistribution,
73
+ statuses,
74
+ upcomingTasks,
75
+ recentInteractions,
76
+ totalTasks,
77
+ completedTasks,
78
+ tasksThisMonthByType,
79
+ interactionsThisMonth,
80
+ contactsLast12Months,
81
+ interactionsLast7Days,
82
+ tasksLast7Days,
83
+ ] = await Promise.all([
84
+ prisma.contact.count({ where: contactFilter }),
85
+ prisma.contact.count({
86
+ where: { ...contactFilter, createdAt: { gte: startOfMonth } },
87
+ }),
88
+ prisma.contact.count({
89
+ where: { ...contactFilter, createdAt: { gte: lastMonthStart, lt: startOfMonth } },
90
+ }),
91
+ prisma.contact.groupBy({ by: ['statusId'], where: contactFilter, _count: true }),
92
+ getCachedStatuses(),
93
+ prisma.task.findMany({
94
+ where: { assignedUserId: userId, completed: false, scheduledAt: { gte: now } },
95
+ select: {
96
+ id: true,
97
+ title: true,
98
+ type: true,
99
+ scheduledAt: true,
100
+ priority: true,
101
+ contact: { select: { id: true, firstName: true, lastName: true, phone: true } },
102
+ },
103
+ orderBy: { scheduledAt: 'asc' },
104
+ take: 6,
105
+ }),
106
+ prisma.interaction.findMany({
107
+ where: { userId },
108
+ select: {
109
+ id: true,
110
+ type: true,
111
+ content: true,
112
+ createdAt: true,
113
+ contact: { select: { id: true, firstName: true, lastName: true, phone: true } },
114
+ user: { select: { id: true, name: true } },
115
+ },
116
+ orderBy: { createdAt: 'desc' },
117
+ take: 5,
118
+ }),
119
+ prisma.task.count({ where: { assignedUserId: userId } }),
120
+ prisma.task.count({ where: { assignedUserId: userId, completed: true } }),
121
+ prisma.task.groupBy({
122
+ by: ['type'],
123
+ where: { assignedUserId: userId, createdAt: { gte: startOfMonth } },
124
+ _count: true,
125
+ }),
126
+ prisma.interaction.groupBy({
127
+ by: ['type'],
128
+ where: { userId, createdAt: { gte: startOfMonth } },
129
+ _count: true,
130
+ }),
131
+ prisma.contact.findMany({
132
+ where: { ...contactFilter, createdAt: { gte: twelveMonthsAgo } },
133
+ select: { createdAt: true },
134
+ }),
135
+ prisma.interaction.findMany({
136
+ where: { userId, createdAt: { gte: sevenDaysAgo } },
137
+ select: { createdAt: true },
138
+ }),
139
+ prisma.task.findMany({
140
+ where: { assignedUserId: userId, createdAt: { gte: sevenDaysAgo } },
141
+ select: { createdAt: true },
142
+ }),
143
+ ]);
144
+
145
+ const contactsGrowth =
146
+ contactsLastMonth > 0 ? ((contactsThisMonth - contactsLastMonth) / contactsLastMonth) * 100 : 0;
147
+
148
+ const monthsData = [];
149
+ for (let i = 11; i >= 0; i--) {
150
+ const monthStart = new Date(now.getFullYear(), now.getMonth() - i, 1);
151
+ const monthEnd = new Date(monthStart.getFullYear(), monthStart.getMonth() + 1, 1);
152
+ const count = contactsLast12Months.filter(
153
+ (c) => c.createdAt >= monthStart && c.createdAt < monthEnd,
154
+ ).length;
155
+ monthsData.push({
156
+ month: monthStart.toLocaleString('fr-FR', { month: 'short' }),
157
+ count,
158
+ });
159
+ }
160
+
161
+ const statusData = statuses.map((status) => ({
162
+ name: status.name,
163
+ value: statusDistribution.find((s) => s.statusId === status.id)?._count || 0,
164
+ }));
165
+
166
+ const pendingTasks = totalTasks - completedTasks;
167
+ const tasksByType = tasksThisMonthByType.map((t) => ({ type: t.type, count: t._count }));
168
+ const interactionsByType = interactionsThisMonth.map((i) => ({ type: i.type, count: i._count }));
169
+
170
+ const last7Days = [];
171
+ for (let i = 6; i >= 0; i--) {
172
+ const dayStart = new Date(now);
173
+ dayStart.setDate(dayStart.getDate() - i);
174
+ dayStart.setHours(0, 0, 0, 0);
175
+ const dayEnd = new Date(dayStart);
176
+ dayEnd.setDate(dayEnd.getDate() + 1);
177
+
178
+ last7Days.push({
179
+ date: dayStart.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' }),
180
+ interactions: interactionsLast7Days.filter(
181
+ (x) => x.createdAt >= dayStart && x.createdAt < dayEnd,
182
+ ).length,
183
+ tasks: tasksLast7Days.filter((x) => x.createdAt >= dayStart && x.createdAt < dayEnd).length,
184
+ });
185
+ }
186
+
187
+ return {
188
+ overview: {
189
+ totalContacts,
190
+ contactsThisMonth,
191
+ contactsGrowth: Math.round(contactsGrowth * 10) / 10,
192
+ statusData,
193
+ monthlyContacts: monthsData,
194
+ },
195
+ tasks: {
196
+ total: totalTasks,
197
+ completed: completedTasks,
198
+ pending: pendingTasks,
199
+ upcoming: upcomingTasks.map((task) => ({
200
+ id: task.id,
201
+ title: task.title || 'Sans titre',
202
+ type: task.type,
203
+ scheduledAt: task.scheduledAt?.toISOString() ?? new Date().toISOString(),
204
+ contact: task.contact
205
+ ? {
206
+ id: task.contact.id,
207
+ name:
208
+ `${task.contact.firstName || ''} ${task.contact.lastName || ''}`.trim() ||
209
+ task.contact.phone,
210
+ }
211
+ : null,
212
+ priority: task.priority || 'MEDIUM',
213
+ })),
214
+ byType: tasksByType,
215
+ },
216
+ interactions: {
217
+ recent: recentInteractions,
218
+ byType: interactionsByType,
219
+ },
220
+ activity: {
221
+ last7Days,
222
+ },
223
+ };
224
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Calcule la semaine ISO 8601 pour une date donnée
3
+ * @param date - La date pour laquelle calculer la semaine ISO
4
+ * @returns Le numéro de la semaine ISO (1-53)
5
+ */
6
+ export function getISOWeek(date: Date): number {
7
+ const target = new Date(date.valueOf());
8
+ const dayNr = (date.getDay() + 6) % 7;
9
+ target.setDate(target.getDate() - dayNr + 3);
10
+ const firstThursday = target.valueOf();
11
+ target.setMonth(0, 1);
12
+ if (target.getDay() !== 4) {
13
+ target.setMonth(0, 1 + ((4 - target.getDay() + 7) % 7));
14
+ }
15
+ return 1 + Math.ceil((firstThursday - target.valueOf()) / 604800000);
16
+ }
17
+
18
+ /**
19
+ * Formate une durée en heures et minutes
20
+ * @param hours - Nombre d'heures (peut être décimal)
21
+ * @returns Une chaîne formatée (ex: "2h 30min")
22
+ */
23
+ export function formatDuration(hours: number): string {
24
+ const h = Math.floor(hours);
25
+ const m = Math.round((hours - h) * 60);
26
+ if (m === 0) return `${h}h`;
27
+ return `${h}h ${m}min`;
28
+ }
29
+
30
+ /**
31
+ * Calcule le mercredi de N semaines avant la semaine ISO d'une date donnée
32
+ * @param date - La date de référence
33
+ * @param weeksBefore - Nombre de semaines avant (par défaut 4)
34
+ * @returns Le mercredi de la semaine cible
35
+ */
36
+ export function getWednesdayBeforeISOWeek(date: Date, weeksBefore: number = 4): Date {
37
+ // Obtenir le lundi de la semaine ISO de la date donnée
38
+ const target = new Date(date.valueOf());
39
+ const dayNr = (date.getDay() + 6) % 7; // 0 = lundi, 6 = dimanche
40
+ target.setDate(target.getDate() - dayNr); // Aller au lundi de cette semaine
41
+
42
+ // Reculer de N semaines
43
+ target.setDate(target.getDate() - weeksBefore * 7);
44
+
45
+ // Aller au mercredi de cette semaine (lundi + 2 jours)
46
+ target.setDate(target.getDate() + 2);
47
+
48
+ return target;
49
+ }
@@ -0,0 +1,25 @@
1
+ import { cache } from 'react';
2
+ import { auth } from '@/lib/auth';
3
+ import { headers } from 'next/headers';
4
+ import { prisma } from '@/lib/prisma';
5
+
6
+ export const getAuthUser = cache(async () => {
7
+ const session = await auth.api.getSession({
8
+ headers: await headers(),
9
+ });
10
+
11
+ if (!session) return null;
12
+
13
+ const user = await prisma.user.findUnique({
14
+ where: { id: session.user.id },
15
+ include: { customRole: true },
16
+ });
17
+
18
+ if (!user) return null;
19
+
20
+ return {
21
+ session,
22
+ user,
23
+ permissions: (user.customRole?.permissions as string[]) ?? [],
24
+ };
25
+ });
@@ -2,6 +2,10 @@
2
2
  * Utilitaires pour gérer l'authentification et les appels à Google Calendar API
3
3
  */
4
4
 
5
+ import { prisma } from './prisma';
6
+ import { decrypt } from './encryption';
7
+ import { googleFetch } from './google-fetch';
8
+
5
9
  interface GoogleTokenResponse {
6
10
  access_token: string;
7
11
  refresh_token?: string;
@@ -13,12 +17,15 @@ interface GoogleCalendarEvent {
13
17
  id?: string;
14
18
  summary: string;
15
19
  description?: string;
20
+ location?: string;
16
21
  start: {
17
- dateTime: string;
22
+ dateTime?: string;
23
+ date?: string;
18
24
  timeZone?: string;
19
25
  };
20
26
  end: {
21
- dateTime: string;
27
+ dateTime?: string;
28
+ date?: string;
22
29
  timeZone?: string;
23
30
  };
24
31
  attendees?: Array<{ email: string }>;
@@ -37,12 +44,15 @@ interface GoogleCalendarEventResponse {
37
44
  id: string;
38
45
  summary: string;
39
46
  description?: string;
47
+ location?: string;
40
48
  start: {
41
- dateTime: string;
49
+ dateTime?: string;
50
+ date?: string;
42
51
  timeZone?: string;
43
52
  };
44
53
  end: {
45
- dateTime: string;
54
+ dateTime?: string;
55
+ date?: string;
46
56
  timeZone?: string;
47
57
  };
48
58
  attendees?: Array<{ email: string }>;
@@ -74,7 +84,7 @@ export async function exchangeGoogleCodeForTokens(
74
84
  throw new Error('GOOGLE_CLIENT_ID et GOOGLE_CLIENT_SECRET doivent être configurés');
75
85
  }
76
86
 
77
- const response = await fetch('https://oauth2.googleapis.com/token', {
87
+ const response = await googleFetch('https://oauth2.googleapis.com/token', {
78
88
  method: 'POST',
79
89
  headers: {
80
90
  'Content-Type': 'application/x-www-form-urlencoded',
@@ -96,6 +106,28 @@ export async function exchangeGoogleCodeForTokens(
96
106
  return response.json();
97
107
  }
98
108
 
109
+ /**
110
+ * Récupère le compte Google de l'utilisateur courant
111
+ * Chaque utilisateur utilise son propre compte Google pour Google Calendar
112
+ */
113
+ export async function getUserGoogleAccount(userId: string) {
114
+ const googleAccount = await prisma.userGoogleAccount.findUnique({
115
+ where: { userId },
116
+ });
117
+
118
+ if (!googleAccount) {
119
+ throw new Error(
120
+ 'Aucun compte Google connecté. Veuillez connecter votre compte Google dans les paramètres pour utiliser Google Calendar.',
121
+ );
122
+ }
123
+
124
+ return {
125
+ ...googleAccount,
126
+ accessToken: decrypt(googleAccount.accessToken),
127
+ refreshToken: decrypt(googleAccount.refreshToken),
128
+ };
129
+ }
130
+
99
131
  /**
100
132
  * Erreur personnalisée pour les tokens Google invalides
101
133
  */
@@ -121,7 +153,7 @@ export async function refreshGoogleToken(refreshToken: string): Promise<GoogleTo
121
153
  throw new Error('GOOGLE_CLIENT_ID et GOOGLE_CLIENT_SECRET doivent être configurés');
122
154
  }
123
155
 
124
- const response = await fetch('https://oauth2.googleapis.com/token', {
156
+ const response = await googleFetch('https://oauth2.googleapis.com/token', {
125
157
  method: 'POST',
126
158
  headers: {
127
159
  'Content-Type': 'application/x-www-form-urlencoded',
@@ -175,8 +207,18 @@ export async function getValidAccessToken(
175
207
  const fiveMinutesFromNow = new Date(now.getTime() + 5 * 60 * 1000);
176
208
 
177
209
  if (expiresAt <= fiveMinutesFromNow) {
178
- const newTokens = await refreshGoogleToken(refreshToken);
179
- return newTokens.access_token;
210
+ try {
211
+ const newTokens = await refreshGoogleToken(refreshToken);
212
+ return newTokens.access_token;
213
+ } catch (error: any) {
214
+ // Si le token ne peut pas être rafraîchi (révoqué, expiré, etc.)
215
+ if (error instanceof GoogleTokenError && error.isRevoked) {
216
+ throw new Error(
217
+ 'La connexion Google a expiré ou a été révoquée. Veuillez reconnecter votre compte Google dans les paramètres.',
218
+ );
219
+ }
220
+ throw error;
221
+ }
180
222
  }
181
223
 
182
224
  return accessToken;
@@ -189,7 +231,7 @@ export async function createGoogleCalendarEvent(
189
231
  accessToken: string,
190
232
  event: GoogleCalendarEvent,
191
233
  ): Promise<GoogleCalendarEventResponse> {
192
- const response = await fetch(
234
+ const response = await googleFetch(
193
235
  'https://www.googleapis.com/calendar/v3/calendars/primary/events?conferenceDataVersion=1',
194
236
  {
195
237
  method: 'POST',
@@ -217,7 +259,7 @@ export async function updateGoogleCalendarEvent(
217
259
  eventId: string,
218
260
  event: Partial<GoogleCalendarEvent>,
219
261
  ): Promise<GoogleCalendarEventResponse> {
220
- const response = await fetch(
262
+ const response = await googleFetch(
221
263
  `https://www.googleapis.com/calendar/v3/calendars/primary/events/${eventId}?conferenceDataVersion=1`,
222
264
  {
223
265
  method: 'PATCH',
@@ -244,7 +286,7 @@ export async function deleteGoogleCalendarEvent(
244
286
  accessToken: string,
245
287
  eventId: string,
246
288
  ): Promise<void> {
247
- const response = await fetch(
289
+ const response = await googleFetch(
248
290
  `https://www.googleapis.com/calendar/v3/calendars/primary/events/${eventId}`,
249
291
  {
250
292
  method: 'DELETE',
@@ -280,7 +322,7 @@ export async function getGoogleCalendarEvent(
280
322
  accessToken: string,
281
323
  eventId: string,
282
324
  ): Promise<GoogleCalendarEventResponse> {
283
- const response = await fetch(
325
+ const response = await googleFetch(
284
326
  `https://www.googleapis.com/calendar/v3/calendars/primary/events/${eventId}`,
285
327
  {
286
328
  method: 'GET',