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,193 +1,28 @@
1
- import { auth } from '@/lib/auth';
2
- import { headers } from 'next/headers';
3
- import { prisma } from '@/lib/prisma';
1
+ import { getAuthUser } from '@/lib/get-auth-user';
4
2
 
5
- /**
6
- * Vérifie si l'utilisateur actuel a une permission spécifique
7
- * @param requiredPermission - Code de la permission à vérifier
8
- * @returns true si l'utilisateur a la permission, false sinon
9
- */
10
3
  export async function checkPermission(requiredPermission: string): Promise<boolean> {
11
4
  try {
12
- const session = await auth.api.getSession({
13
- headers: await headers(),
14
- });
15
-
16
- if (!session) {
17
- return false;
18
- }
19
-
20
- const userId = session.user.id;
21
-
22
- // Récupérer l'utilisateur avec son profil personnalisé si applicable
23
- const user = await prisma.user.findUnique({
24
- where: { id: userId },
25
- include: {
26
- customRole: true,
27
- },
28
- });
29
-
30
- if (!user) {
31
- return false;
32
- }
33
-
34
- // Les permissions viennent uniquement du profil assigné
35
- if (!user.customRole) {
36
- // Aucun profil assigné = aucune permission
37
- return false;
38
- }
39
-
40
- const userPermissions = user.customRole.permissions as string[];
41
-
42
- // Vérifier si la permission est dans la liste
43
- return userPermissions.includes(requiredPermission);
5
+ const authUser = await getAuthUser();
6
+ if (!authUser) return false;
7
+ return authUser.permissions.includes(requiredPermission);
44
8
  } catch (error) {
45
9
  console.error('Erreur lors de la vérification des permissions:', error);
46
10
  return false;
47
11
  }
48
12
  }
49
13
 
50
- /**
51
- * Vérifie si l'utilisateur actuel a plusieurs permissions
52
- * @param requiredPermissions - Tableau des codes de permissions à vérifier
53
- * @param requireAll - Si true, toutes les permissions sont requises. Si false, au moins une est requise
54
- * @returns true si l'utilisateur a les permissions, false sinon
55
- */
56
14
  export async function checkPermissions(
57
- requiredPermissions: string[],
58
- requireAll: boolean = true,
59
- ): Promise<boolean> {
15
+ ...requiredPermissions: string[]
16
+ ): Promise<Record<string, boolean>> {
60
17
  try {
61
- const session = await auth.api.getSession({
62
- headers: await headers(),
63
- });
64
-
65
- if (!session) {
66
- return false;
67
- }
68
-
69
- const userId = session.user.id;
70
-
71
- // Récupérer l'utilisateur avec son profil personnalisé si applicable
72
- const user = await prisma.user.findUnique({
73
- where: { id: userId },
74
- include: {
75
- customRole: true,
76
- },
77
- });
78
-
79
- if (!user) {
80
- return false;
81
- }
82
-
83
- // Les permissions viennent uniquement du profil assigné
84
- if (!user.customRole) {
85
- // Aucun profil assigné = aucune permission
86
- return false;
87
- }
88
-
89
- const userPermissions = user.customRole.permissions as string[];
90
-
91
- // Vérifier les permissions
92
- if (requireAll) {
93
- return requiredPermissions.every((perm) => userPermissions.includes(perm));
94
- } else {
95
- return requiredPermissions.some((perm) => userPermissions.includes(perm));
18
+ const authUser = await getAuthUser();
19
+ if (!authUser) {
20
+ return Object.fromEntries(requiredPermissions.map((p) => [p, false]));
96
21
  }
22
+ const permSet = new Set(authUser.permissions);
23
+ return Object.fromEntries(requiredPermissions.map((p) => [p, permSet.has(p)]));
97
24
  } catch (error) {
98
25
  console.error('Erreur lors de la vérification des permissions:', error);
99
- return false;
100
- }
101
- }
102
-
103
- /**
104
- * Récupère toutes les permissions de l'utilisateur actuel
105
- * @returns Tableau des codes de permissions de l'utilisateur
106
- */
107
- export async function getUserPermissions(): Promise<string[]> {
108
- try {
109
- const session = await auth.api.getSession({
110
- headers: await headers(),
111
- });
112
-
113
- if (!session) {
114
- return [];
115
- }
116
-
117
- const userId = session.user.id;
118
-
119
- // Récupérer l'utilisateur avec son profil personnalisé si applicable
120
- const user = await prisma.user.findUnique({
121
- where: { id: userId },
122
- include: {
123
- customRole: true,
124
- },
125
- });
126
-
127
- if (!user) {
128
- return [];
129
- }
130
-
131
- // Les permissions viennent uniquement du profil assigné
132
- if (!user.customRole) {
133
- // Aucun profil assigné = aucune permission
134
- return [];
135
- }
136
-
137
- return user.customRole.permissions as string[];
138
- } catch (error) {
139
- console.error('Erreur lors de la récupération des permissions:', error);
140
- return [];
141
- }
142
- }
143
-
144
- /**
145
- * Middleware pour protéger une route API avec des permissions
146
- * Exemple d'utilisation :
147
- *
148
- * export async function GET(req: NextRequest) {
149
- * const hasPermission = await requirePermission('contacts.view_all');
150
- * if (!hasPermission) {
151
- * return NextResponse.json({ error: 'Non autorisé' }, { status: 403 });
152
- * }
153
- * // ... reste du code
154
- * }
155
- */
156
- export async function requirePermission(requiredPermission: string): Promise<boolean> {
157
- return checkPermission(requiredPermission);
158
- }
159
-
160
- /**
161
- * Helper pour vérifier si un utilisateur est admin
162
- * Un admin est un utilisateur avec un profil ayant toutes les permissions
163
- */
164
- export async function isAdmin(): Promise<boolean> {
165
- try {
166
- const session = await auth.api.getSession({
167
- headers: await headers(),
168
- });
169
-
170
- if (!session) {
171
- return false;
172
- }
173
-
174
- const userId = session.user.id;
175
-
176
- const user = await prisma.user.findUnique({
177
- where: { id: userId },
178
- include: {
179
- customRole: true,
180
- },
181
- });
182
-
183
- if (!user || !user.customRole) {
184
- return false;
185
- }
186
-
187
- // Vérifier si le profil a la permission de gestion des utilisateurs
188
- const permissions = user.customRole.permissions as string[];
189
- return permissions.includes('users.manage_roles');
190
- } catch (error) {
191
- return false;
26
+ return Object.fromEntries(requiredPermissions.map((p) => [p, false]));
192
27
  }
193
28
  }
@@ -81,7 +81,7 @@ export async function logContactUpdate(
81
81
  postalCode: 'Code postal',
82
82
  civility: 'Civilité',
83
83
  origin: 'Origine',
84
- companyName: 'Entreprise',
84
+ company: 'Entreprise',
85
85
  closingReason: 'Motif de fermeture',
86
86
  };
87
87
 
@@ -103,6 +103,8 @@ export async function logContactUpdate(
103
103
 
104
104
  const formatValue = (value: any) => {
105
105
  if (value === null || value === undefined) return 'Aucun';
106
+ if (typeof value === 'object' && value !== null && 'name' in value)
107
+ return value.name ?? 'Aucun';
106
108
  return String(value);
107
109
  };
108
110
 
@@ -0,0 +1,341 @@
1
+ import type { ViewFilter, DatePreset } from '@/types/contact-views';
2
+ import { FRENCH_DEPARTMENTS, type Department } from '@/lib/french-regions';
3
+
4
+ function startOfDay(date: Date): Date {
5
+ const d = new Date(date);
6
+ d.setHours(0, 0, 0, 0);
7
+ return d;
8
+ }
9
+
10
+ function endOfDay(date: Date): Date {
11
+ const d = new Date(date);
12
+ d.setHours(23, 59, 59, 999);
13
+ return d;
14
+ }
15
+
16
+ function getMonday(date: Date): Date {
17
+ const d = new Date(date);
18
+ const day = d.getDay();
19
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1);
20
+ d.setDate(diff);
21
+ return startOfDay(d);
22
+ }
23
+
24
+ function getQuarterStart(date: Date): Date {
25
+ const month = date.getMonth();
26
+ const quarterStartMonth = month - (month % 3);
27
+ return startOfDay(new Date(date.getFullYear(), quarterStartMonth, 1));
28
+ }
29
+
30
+ function getQuarterEnd(date: Date): Date {
31
+ const month = date.getMonth();
32
+ const quarterEndMonth = month - (month % 3) + 3;
33
+ return endOfDay(new Date(date.getFullYear(), quarterEndMonth, 0));
34
+ }
35
+
36
+ export function resolveDatePreset(preset: DatePreset): { gte?: Date; lte?: Date } {
37
+ const now = new Date();
38
+ const today = startOfDay(now);
39
+
40
+ switch (preset) {
41
+ case 'today':
42
+ return { gte: startOfDay(today), lte: endOfDay(today) };
43
+ case 'yesterday': {
44
+ const d = new Date(today);
45
+ d.setDate(d.getDate() - 1);
46
+ return { gte: startOfDay(d), lte: endOfDay(d) };
47
+ }
48
+ case 'tomorrow': {
49
+ const d = new Date(today);
50
+ d.setDate(d.getDate() + 1);
51
+ return { gte: startOfDay(d), lte: endOfDay(d) };
52
+ }
53
+ case 'this_week': {
54
+ const monday = getMonday(today);
55
+ const sunday = new Date(monday);
56
+ sunday.setDate(sunday.getDate() + 6);
57
+ return { gte: monday, lte: endOfDay(sunday) };
58
+ }
59
+ case 'this_week_so_far': {
60
+ const monday = getMonday(today);
61
+ return { gte: monday, lte: endOfDay(today) };
62
+ }
63
+ case 'last_week': {
64
+ const monday = getMonday(today);
65
+ monday.setDate(monday.getDate() - 7);
66
+ const sunday = new Date(monday);
67
+ sunday.setDate(sunday.getDate() + 6);
68
+ return { gte: monday, lte: endOfDay(sunday) };
69
+ }
70
+ case 'next_week': {
71
+ const monday = getMonday(today);
72
+ monday.setDate(monday.getDate() + 7);
73
+ const sunday = new Date(monday);
74
+ sunday.setDate(sunday.getDate() + 6);
75
+ return { gte: monday, lte: endOfDay(sunday) };
76
+ }
77
+ case 'this_month':
78
+ return {
79
+ gte: startOfDay(new Date(now.getFullYear(), now.getMonth(), 1)),
80
+ lte: endOfDay(new Date(now.getFullYear(), now.getMonth() + 1, 0)),
81
+ };
82
+ case 'this_month_so_far':
83
+ return {
84
+ gte: startOfDay(new Date(now.getFullYear(), now.getMonth(), 1)),
85
+ lte: endOfDay(today),
86
+ };
87
+ case 'last_month':
88
+ return {
89
+ gte: startOfDay(new Date(now.getFullYear(), now.getMonth() - 1, 1)),
90
+ lte: endOfDay(new Date(now.getFullYear(), now.getMonth(), 0)),
91
+ };
92
+ case 'next_month':
93
+ return {
94
+ gte: startOfDay(new Date(now.getFullYear(), now.getMonth() + 1, 1)),
95
+ lte: endOfDay(new Date(now.getFullYear(), now.getMonth() + 2, 0)),
96
+ };
97
+ case 'this_quarter':
98
+ return { gte: getQuarterStart(now), lte: getQuarterEnd(now) };
99
+ case 'this_quarter_so_far':
100
+ return { gte: getQuarterStart(now), lte: endOfDay(today) };
101
+ case 'last_quarter': {
102
+ const prevQuarter = new Date(now);
103
+ prevQuarter.setMonth(prevQuarter.getMonth() - 3);
104
+ return {
105
+ gte: getQuarterStart(prevQuarter),
106
+ lte: getQuarterEnd(prevQuarter),
107
+ };
108
+ }
109
+ case 'this_year':
110
+ return {
111
+ gte: startOfDay(new Date(now.getFullYear(), 0, 1)),
112
+ lte: endOfDay(new Date(now.getFullYear(), 11, 31)),
113
+ };
114
+ case 'this_year_so_far':
115
+ return {
116
+ gte: startOfDay(new Date(now.getFullYear(), 0, 1)),
117
+ lte: endOfDay(today),
118
+ };
119
+ case 'last_year':
120
+ return {
121
+ gte: startOfDay(new Date(now.getFullYear() - 1, 0, 1)),
122
+ lte: endOfDay(new Date(now.getFullYear() - 1, 11, 31)),
123
+ };
124
+ case 'last_7_days':
125
+ case 'last_14_days':
126
+ case 'last_30_days':
127
+ case 'last_60_days':
128
+ case 'last_90_days':
129
+ case 'last_180_days':
130
+ case 'last_365_days': {
131
+ const daysMap: Record<string, number> = {
132
+ last_7_days: 7,
133
+ last_14_days: 14,
134
+ last_30_days: 30,
135
+ last_60_days: 60,
136
+ last_90_days: 90,
137
+ last_180_days: 180,
138
+ last_365_days: 365,
139
+ };
140
+ const d = new Date(today);
141
+ d.setDate(d.getDate() - daysMap[preset]);
142
+ return { gte: startOfDay(d), lte: endOfDay(today) };
143
+ }
144
+ default:
145
+ return {};
146
+ }
147
+ }
148
+
149
+ const departmentsByRegion = new Map<string, Department[]>();
150
+ for (const dept of FRENCH_DEPARTMENTS) {
151
+ const list = departmentsByRegion.get(dept.regionCode) ?? [];
152
+ list.push(dept);
153
+ departmentsByRegion.set(dept.regionCode, list);
154
+ }
155
+
156
+ function buildPostalCodeConditions(deptCodes: string[], negate: boolean): any | null {
157
+ if (deptCodes.length === 0) return null;
158
+
159
+ const startsWithConditions: any[] = [];
160
+ const corsicaCodes: string[] = [];
161
+
162
+ for (const code of deptCodes) {
163
+ if (code === '2A' || code === '2B') {
164
+ corsicaCodes.push(code);
165
+ } else {
166
+ startsWithConditions.push({ postalCode: { startsWith: code } });
167
+ }
168
+ }
169
+
170
+ if (corsicaCodes.length > 0) {
171
+ if (corsicaCodes.includes('2A') && corsicaCodes.includes('2B')) {
172
+ startsWithConditions.push({ postalCode: { startsWith: '20' } });
173
+ } else if (corsicaCodes.includes('2A')) {
174
+ startsWithConditions.push({
175
+ AND: [{ postalCode: { startsWith: '20' } }, { postalCode: { lt: '20200' } }],
176
+ });
177
+ } else {
178
+ startsWithConditions.push({
179
+ AND: [{ postalCode: { startsWith: '20' } }, { postalCode: { gte: '20200' } }],
180
+ });
181
+ }
182
+ }
183
+
184
+ if (startsWithConditions.length === 0) return null;
185
+
186
+ const orCondition =
187
+ startsWithConditions.length === 1 ? startsWithConditions[0] : { OR: startsWithConditions };
188
+
189
+ return negate ? { NOT: orCondition } : orCondition;
190
+ }
191
+
192
+ function buildGeoCondition(filter: ViewFilter): any | null {
193
+ const { field, operator, value } = filter;
194
+ if (!Array.isArray(value) || value.length === 0) return null;
195
+
196
+ if (field === 'department') {
197
+ return buildPostalCodeConditions(value, operator === 'is_none_of');
198
+ }
199
+
200
+ if (field === 'region') {
201
+ const deptCodes: string[] = [];
202
+ for (const regionCode of value) {
203
+ const depts = departmentsByRegion.get(regionCode);
204
+ if (depts) {
205
+ for (const d of depts) deptCodes.push(d.code);
206
+ }
207
+ }
208
+ return buildPostalCodeConditions(deptCodes, operator === 'is_none_of');
209
+ }
210
+
211
+ return null;
212
+ }
213
+
214
+ function buildFieldCondition(filter: ViewFilter): any | null {
215
+ const { field, operator, value, preset } = filter;
216
+
217
+ if (field === 'region' || field === 'department') {
218
+ return buildGeoCondition(filter);
219
+ }
220
+
221
+ switch (operator) {
222
+ case 'equals': {
223
+ if (field === 'createdAt' || field === 'updatedAt') {
224
+ if (!value || typeof value !== 'string') return null;
225
+ const d = new Date(value);
226
+ if (isNaN(d.getTime())) return null;
227
+ return { [field]: { gte: startOfDay(d), lte: endOfDay(d) } };
228
+ }
229
+ return { [field]: value };
230
+ }
231
+
232
+ case 'not_equals':
233
+ return { NOT: { [field]: value } };
234
+
235
+ case 'contains':
236
+ return { [field]: { contains: value as string, mode: 'insensitive' } };
237
+
238
+ case 'not_contains':
239
+ return {
240
+ NOT: { [field]: { contains: value as string, mode: 'insensitive' } },
241
+ };
242
+
243
+ case 'starts_with':
244
+ return {
245
+ [field]: { startsWith: value as string, mode: 'insensitive' },
246
+ };
247
+
248
+ case 'ends_with':
249
+ return { [field]: { endsWith: value as string, mode: 'insensitive' } };
250
+
251
+ case 'is_any_of': {
252
+ if (!Array.isArray(value) || value.length === 0) return null;
253
+ const hasUnassigned = value.includes('UNASSIGNED');
254
+ const realValues = value.filter((v) => v !== 'UNASSIGNED');
255
+
256
+ if (hasUnassigned && realValues.length > 0) {
257
+ return { OR: [{ [field]: null }, { [field]: { in: realValues } }] };
258
+ } else if (hasUnassigned) {
259
+ return { [field]: null };
260
+ }
261
+ return { [field]: { in: realValues } };
262
+ }
263
+
264
+ case 'is_none_of': {
265
+ if (!Array.isArray(value) || value.length === 0) return null;
266
+ const hasUnassigned = value.includes('UNASSIGNED');
267
+ const realValues = value.filter((v) => v !== 'UNASSIGNED');
268
+
269
+ if (hasUnassigned && realValues.length > 0) {
270
+ return {
271
+ AND: [{ [field]: { not: null } }, { [field]: { notIn: realValues } }],
272
+ };
273
+ } else if (hasUnassigned) {
274
+ return { [field]: { not: null } };
275
+ }
276
+ return { [field]: { notIn: realValues } };
277
+ }
278
+
279
+ case 'gt': {
280
+ const d = new Date(value as string);
281
+ if (isNaN(d.getTime())) return null;
282
+ return { [field]: { gt: endOfDay(d) } };
283
+ }
284
+
285
+ case 'gte': {
286
+ const d = new Date(value as string);
287
+ if (isNaN(d.getTime())) return null;
288
+ return { [field]: { gte: startOfDay(d) } };
289
+ }
290
+
291
+ case 'lt': {
292
+ const d = new Date(value as string);
293
+ if (isNaN(d.getTime())) return null;
294
+ return { [field]: { lt: startOfDay(d) } };
295
+ }
296
+
297
+ case 'lte': {
298
+ const d = new Date(value as string);
299
+ if (isNaN(d.getTime())) return null;
300
+ return { [field]: { lte: endOfDay(d) } };
301
+ }
302
+
303
+ case 'between': {
304
+ if (!Array.isArray(value) || value.length !== 2) return null;
305
+ const start = new Date(value[0]);
306
+ const end = new Date(value[1]);
307
+ if (isNaN(start.getTime()) || isNaN(end.getTime())) return null;
308
+ return { [field]: { gte: startOfDay(start), lte: endOfDay(end) } };
309
+ }
310
+
311
+ case 'is_known':
312
+ return { [field]: { not: null } };
313
+
314
+ case 'is_unknown':
315
+ return { [field]: null };
316
+
317
+ case 'date_preset': {
318
+ if (!preset) return null;
319
+ const range = resolveDatePreset(preset);
320
+ return { [field]: range };
321
+ }
322
+
323
+ default:
324
+ return null;
325
+ }
326
+ }
327
+
328
+ export function buildPrismaWhereFromFilters(filters: ViewFilter[]): Record<string, any> {
329
+ const conditions: any[] = [];
330
+
331
+ for (const filter of filters) {
332
+ const condition = buildFieldCondition(filter);
333
+ if (condition) {
334
+ conditions.push(condition);
335
+ }
336
+ }
337
+
338
+ if (conditions.length === 0) return {};
339
+ if (conditions.length === 1) return conditions[0];
340
+ return { AND: conditions };
341
+ }