create-crm-tmp 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/bin/create-crm-tmp.js +56 -35
  2. package/package.json +1 -1
  3. package/template/README.md +230 -115
  4. package/template/eslint.config.mjs +13 -0
  5. package/template/next.config.ts +14 -0
  6. package/template/package.json +15 -2
  7. package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
  8. package/template/prisma/migrations/migration_lock.toml +3 -0
  9. package/template/prisma/schema.prisma +132 -637
  10. package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
  11. package/template/src/app/(auth)/layout.tsx +1 -1
  12. package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
  13. package/template/src/app/(auth)/reset-password/page.tsx +4 -4
  14. package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
  15. package/template/src/app/(auth)/signin/page.tsx +14 -6
  16. package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
  17. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
  18. package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
  19. package/template/src/app/(dashboard)/closing/page.tsx +78 -62
  20. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
  21. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
  22. package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
  23. package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
  24. package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
  25. package/template/src/app/(dashboard)/layout.tsx +6 -2
  26. package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
  27. package/template/src/app/(dashboard)/templates/page.tsx +55 -54
  28. package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
  29. package/template/src/app/(dashboard)/users/page.tsx +1 -1
  30. package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
  31. package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
  32. package/template/src/app/api/agenda/google-events/route.ts +92 -0
  33. package/template/src/app/api/auth/check-active/route.ts +3 -2
  34. package/template/src/app/api/auth/google/route.ts +2 -1
  35. package/template/src/app/api/auth/google/status/route.ts +7 -31
  36. package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
  37. package/template/src/app/api/companies/[id]/route.ts +1 -2
  38. package/template/src/app/api/companies/route.ts +42 -12
  39. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
  40. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
  41. package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
  42. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
  43. package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
  44. package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
  45. package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
  46. package/template/src/app/api/contacts/[id]/route.ts +106 -34
  47. package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
  48. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
  49. package/template/src/app/api/contacts/export/route.ts +9 -13
  50. package/template/src/app/api/contacts/import/route.ts +55 -25
  51. package/template/src/app/api/contacts/import-preview/route.ts +1 -1
  52. package/template/src/app/api/contacts/origins/route.ts +63 -0
  53. package/template/src/app/api/contacts/route.ts +153 -41
  54. package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
  55. package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
  56. package/template/src/app/api/dashboard/widgets/route.ts +181 -0
  57. package/template/src/app/api/dev/reminders/test/route.ts +114 -0
  58. package/template/src/app/api/editor/upload-image/route.ts +61 -0
  59. package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
  60. package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
  61. package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
  62. package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
  63. package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
  64. package/template/src/app/api/reminders/clear/route.ts +120 -0
  65. package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
  66. package/template/src/app/api/reminders/route.ts +164 -39
  67. package/template/src/app/api/reminders/state/route.ts +164 -0
  68. package/template/src/app/api/reset-password/request/route.ts +1 -1
  69. package/template/src/app/api/reset-password/verify/route.ts +1 -1
  70. package/template/src/app/api/send/route.ts +16 -4
  71. package/template/src/app/api/settings/google-ads/route.ts +14 -0
  72. package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
  73. package/template/src/app/api/settings/google-calendar/route.ts +124 -0
  74. package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
  75. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
  76. package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
  77. package/template/src/app/api/settings/google-sheet/route.ts +14 -0
  78. package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
  79. package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
  80. package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
  81. package/template/src/app/api/settings/meta-leads/route.ts +14 -2
  82. package/template/src/app/api/settings/smtp/route.ts +53 -6
  83. package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
  84. package/template/src/app/api/tasks/[id]/route.ts +234 -58
  85. package/template/src/app/api/tasks/meet/route.ts +27 -19
  86. package/template/src/app/api/tasks/route.ts +62 -17
  87. package/template/src/app/api/users/[id]/route.ts +20 -14
  88. package/template/src/app/api/users/list/route.ts +57 -19
  89. package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
  90. package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
  91. package/template/src/app/api/workflows/[id]/route.ts +0 -4
  92. package/template/src/app/api/workflows/process/route.ts +22 -51
  93. package/template/src/app/api/workflows/route.ts +0 -4
  94. package/template/src/app/globals.css +342 -4
  95. package/template/src/app/layout.tsx +11 -3
  96. package/template/src/app/page.tsx +1 -1
  97. package/template/src/components/address-autocomplete.tsx +7 -6
  98. package/template/src/components/config-error-alert.tsx +46 -0
  99. package/template/src/components/contacts/filter-bar.tsx +12 -3
  100. package/template/src/components/contacts/filter-builder.tsx +28 -43
  101. package/template/src/components/contacts/save-view-dialog.tsx +1 -1
  102. package/template/src/components/contacts/views-tab-bar.tsx +15 -6
  103. package/template/src/components/dashboard/activity-chart.tsx +41 -28
  104. package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
  105. package/template/src/components/dashboard/color-picker.tsx +64 -0
  106. package/template/src/components/dashboard/contacts-chart.tsx +69 -0
  107. package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
  108. package/template/src/components/dashboard/recent-activity.tsx +154 -0
  109. package/template/src/components/dashboard/stat-card.tsx +40 -40
  110. package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
  111. package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
  112. package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
  113. package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
  114. package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
  115. package/template/src/components/date-picker.tsx +9 -6
  116. package/template/src/components/editor/upload-editor-image.ts +42 -0
  117. package/template/src/components/editor.tsx +161 -22
  118. package/template/src/components/email-template.tsx +2 -2
  119. package/template/src/components/global-search.tsx +30 -28
  120. package/template/src/components/header.tsx +178 -80
  121. package/template/src/components/inactive-account-guard.tsx +58 -0
  122. package/template/src/components/integration-notifications-listener.tsx +12 -0
  123. package/template/src/components/invitation-email-template.tsx +2 -2
  124. package/template/src/components/meet-cancellation-email-template.tsx +3 -3
  125. package/template/src/components/meet-confirmation-email-template.tsx +3 -3
  126. package/template/src/components/meet-update-email-template.tsx +3 -3
  127. package/template/src/components/page-header.tsx +5 -5
  128. package/template/src/components/protected-page.tsx +1 -1
  129. package/template/src/components/reset-password-email-template.tsx +2 -2
  130. package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
  131. package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
  132. package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
  133. package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
  134. package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
  135. package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
  136. package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
  137. package/template/src/components/sidebar.tsx +45 -26
  138. package/template/src/components/skeleton.tsx +40 -43
  139. package/template/src/components/ui/accordion.tsx +2 -2
  140. package/template/src/components/ui/alert-dialog.tsx +1 -1
  141. package/template/src/components/ui/button.tsx +20 -9
  142. package/template/src/components/ui/components.tsx +1 -1
  143. package/template/src/components/ui/date-picker.tsx +422 -0
  144. package/template/src/components/ui/datetime-picker.tsx +338 -0
  145. package/template/src/components/ui/status-select.tsx +271 -0
  146. package/template/src/components/ui/tooltip.tsx +37 -0
  147. package/template/src/components/view-as-modal.tsx +13 -7
  148. package/template/src/contexts/app-toast-context.tsx +245 -57
  149. package/template/src/contexts/dashboard-theme-context.tsx +53 -0
  150. package/template/src/contexts/sidebar-context.tsx +22 -17
  151. package/template/src/contexts/task-reminder-context.tsx +134 -160
  152. package/template/src/contexts/view-as-context.tsx +33 -6
  153. package/template/src/hooks/use-focus-trap.ts +2 -2
  154. package/template/src/hooks/useIntegrationNotifications.ts +49 -0
  155. package/template/src/lib/auth.ts +8 -1
  156. package/template/src/lib/config-links.ts +14 -0
  157. package/template/src/lib/contact-duplicate.ts +79 -61
  158. package/template/src/lib/contact-interactions.ts +21 -21
  159. package/template/src/lib/contact-view-filters.ts +24 -64
  160. package/template/src/lib/contacts-list-url.ts +190 -0
  161. package/template/src/lib/dashboard-stats.ts +65 -7
  162. package/template/src/lib/dashboard-themes.ts +135 -0
  163. package/template/src/lib/date-utils.ts +127 -0
  164. package/template/src/lib/default-widgets.ts +12 -0
  165. package/template/src/lib/editor-html-image-dimensions.ts +172 -0
  166. package/template/src/lib/editor-image-limits.ts +19 -0
  167. package/template/src/lib/email-html-sanitize.ts +19 -0
  168. package/template/src/lib/encryption.ts +9 -6
  169. package/template/src/lib/fr-geography.ts +192 -0
  170. package/template/src/lib/google-calendar-agenda.ts +201 -0
  171. package/template/src/lib/google-calendar.ts +255 -5
  172. package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
  173. package/template/src/lib/google-sheet-sync-runner.ts +514 -0
  174. package/template/src/lib/integration-import-log.ts +21 -0
  175. package/template/src/lib/permissions.ts +40 -10
  176. package/template/src/lib/prisma.ts +4 -1
  177. package/template/src/lib/qstash.ts +65 -0
  178. package/template/src/lib/reminder-state-server.ts +80 -0
  179. package/template/src/lib/reminder-state.ts +29 -0
  180. package/template/src/lib/supabase-storage.ts +113 -0
  181. package/template/src/lib/template-variables.ts +164 -23
  182. package/template/src/lib/utils.ts +45 -0
  183. package/template/src/lib/widget-registry.ts +173 -0
  184. package/template/src/lib/workflow-executor.ts +16 -70
  185. package/template/src/proxy.ts +1 -0
  186. package/template/vercel.json +3 -10
  187. package/template/skills-lock.json +0 -25
  188. package/template/src/components/dashboard/dashboard-content.tsx +0 -79
  189. package/template/src/lib/google-drive.ts +0 -1101
  190. package/template/src/types/yousign.ts +0 -52
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useSession } from '@/lib/auth-client';
4
4
  import { useEffect, useState, useMemo } from 'react';
5
- import { cn } from '@/lib/utils';
5
+ import { cn, devToast } from '@/lib/utils';
6
6
  import { UsersTableSkeleton, Spinner } from '@/components/skeleton';
7
7
  import { Search, RefreshCw, Send } from 'lucide-react';
8
8
  import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
@@ -64,7 +64,7 @@ export default function UsersPage() {
64
64
  setUsers(data);
65
65
  } catch (error) {
66
66
  console.error('Erreur:', error);
67
- setError('Erreur lors du chargement des utilisateurs');
67
+ setError(devToast('Erreur lors du chargement des utilisateurs', error));
68
68
  } finally {
69
69
  setLoading(false);
70
70
  }
@@ -124,7 +124,7 @@ export default function UsersPage() {
124
124
  setFormData({ name: '', email: '', customRoleId: '' });
125
125
  fetchUsers();
126
126
  } catch (error: any) {
127
- setError(error.message);
127
+ setError(devToast("Erreur lors de la création de l'utilisateur", error));
128
128
  } finally {
129
129
  setIsSubmitting(false);
130
130
  }
@@ -149,7 +149,7 @@ export default function UsersPage() {
149
149
  );
150
150
  fetchUsers();
151
151
  } catch (error: any) {
152
- setError(error.message);
152
+ setError(devToast("Erreur lors de la mise à jour du compte", error));
153
153
  }
154
154
  };
155
155
 
@@ -170,7 +170,7 @@ export default function UsersPage() {
170
170
  setSuccessMessage('Profil modifié avec succès');
171
171
  fetchUsers();
172
172
  } catch (error: any) {
173
- setError(error.message);
173
+ setError(devToast('Erreur lors du changement de profil', error));
174
174
  }
175
175
  };
176
176
 
@@ -191,7 +191,7 @@ export default function UsersPage() {
191
191
  setSuccessMessage(`Invitation renvoyée à ${userName}`);
192
192
  fetchUsers();
193
193
  } catch (err: any) {
194
- setError(err.message);
194
+ setError(devToast("Erreur lors de l'envoi de l'invitation", err));
195
195
  } finally {
196
196
  setResendingIds((prev) => {
197
197
  const next = new Set(prev);
@@ -219,12 +219,12 @@ export default function UsersPage() {
219
219
  <ProtectedPage requiredPermission="users.view">
220
220
  <div className="kb-tab-scope bg-surface-page flex h-full flex-col">
221
221
  {/* Header */}
222
- <div className="border-b border-border bg-background/95 px-4 py-4 backdrop-blur-sm sm:px-6 lg:px-8">
222
+ <div className="border-border bg-background/95 border-b px-4 py-4 backdrop-blur-sm sm:px-6 lg:px-8">
223
223
  <div className="mb-3 flex items-start justify-between gap-3">
224
224
  {/* Bouton menu mobile */}
225
225
  <button
226
226
  onClick={toggleMobileMenu}
227
- className="mt-1 shrink-0 cursor-pointer rounded-lg p-2 text-foreground/80 transition-colors duration-200 hover:bg-muted lg:hidden"
227
+ className="text-foreground/80 hover:bg-muted mt-1 shrink-0 cursor-pointer rounded-lg p-2 transition-colors duration-200 lg:hidden"
228
228
  aria-label="Basculer le menu"
229
229
  >
230
230
  <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -249,19 +249,19 @@ export default function UsersPage() {
249
249
  {/* Titre et breadcrumbs */}
250
250
  <div className="flex-1">
251
251
  <div className="mb-1 flex items-center gap-2">
252
- <h1 className="text-2xl font-bold text-foreground">Utilisateurs</h1>
253
- <span className="rounded-full bg-primary/20 px-2.5 py-0.5 text-xs font-semibold text-primary">
252
+ <h1 className="text-foreground text-2xl font-bold">Utilisateurs</h1>
253
+ <span className="bg-primary/20 text-primary rounded-full px-2.5 py-0.5 text-xs font-semibold">
254
254
  {users.length}
255
255
  </span>
256
256
  </div>
257
- <p className="text-sm text-muted-foreground">Home &gt; Utilisateurs</p>
257
+ <p className="text-muted-foreground text-sm">Home &gt; Utilisateurs</p>
258
258
  </div>
259
259
 
260
260
  {/* Actions globales */}
261
261
  <div className="flex items-center gap-2">
262
262
  <button
263
263
  onClick={fetchUsers}
264
- className="cursor-pointer rounded-lg p-2 text-muted-foreground transition-colors duration-200 hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
264
+ className="text-muted-foreground hover:bg-muted focus-visible:ring-primary cursor-pointer rounded-lg p-2 transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
265
265
  title="Actualiser"
266
266
  >
267
267
  <RefreshCw className="h-5 w-5" />
@@ -273,20 +273,20 @@ export default function UsersPage() {
273
273
  <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
274
274
  {/* Recherche */}
275
275
  <div className="relative w-full max-w-sm">
276
- <Search className="pointer-events-none absolute top-2.5 left-3 h-4 w-4 text-muted-foreground" />
276
+ <Search className="text-muted-foreground pointer-events-none absolute top-2.5 left-3 h-4 w-4" />
277
277
  <input
278
278
  type="text"
279
279
  value={search}
280
280
  onChange={(e) => setSearch(e.target.value)}
281
281
  placeholder="Rechercher un utilisateur (nom, email, profil)"
282
- className="w-full rounded-lg border border-border bg-muted py-2 pr-3 pl-9 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary/50 focus:bg-background focus:ring-2 focus:ring-primary/20 focus:outline-none"
282
+ className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus:border-primary/50 focus:bg-background focus:ring-primary/20 w-full rounded-lg border py-2 pr-3 pl-9 text-sm focus:ring-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
283
283
  />
284
284
  </div>
285
285
 
286
286
  {/* Bouton d’ajout */}
287
287
  <button
288
288
  onClick={() => setShowAddModal(true)}
289
- className="inline-flex cursor-pointer items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2 text-xs font-semibold text-primary-foreground shadow-(--shadow-card) transition-colors duration-200 hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:outline-none sm:text-sm"
289
+ className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-primary inline-flex cursor-pointer items-center justify-center gap-2 rounded-lg px-4 py-2 text-xs font-semibold shadow-(--shadow-card) transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none sm:text-sm"
290
290
  >
291
291
  + Nouvel utilisateur
292
292
  </button>
@@ -299,56 +299,59 @@ export default function UsersPage() {
299
299
  {loading ? (
300
300
  <UsersTableSkeleton />
301
301
  ) : (
302
- <div className="overflow-hidden rounded-xl border border-border bg-card shadow-(--shadow-card)">
302
+ <div className="border-border bg-card overflow-hidden rounded-xl border shadow-(--shadow-card)">
303
303
  <div className="overflow-x-auto">
304
- <table className="min-w-full divide-y divide-border text-sm">
304
+ <table className="divide-border min-w-full divide-y text-sm">
305
305
  <thead className="bg-muted/70">
306
306
  <tr>
307
- <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-muted-foreground uppercase sm:px-6">
307
+ <th className="text-muted-foreground px-3 py-3 text-left text-xs font-medium tracking-wider uppercase sm:px-6">
308
308
  Utilisateur
309
309
  </th>
310
- <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-muted-foreground uppercase sm:px-6">
310
+ <th className="text-muted-foreground px-3 py-3 text-left text-xs font-medium tracking-wider uppercase sm:px-6">
311
311
  Email
312
312
  </th>
313
- <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-muted-foreground uppercase sm:px-6">
313
+ <th className="text-muted-foreground px-3 py-3 text-left text-xs font-medium tracking-wider uppercase sm:px-6">
314
314
  Profil
315
315
  </th>
316
- <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-muted-foreground uppercase sm:px-6">
316
+ <th className="text-muted-foreground px-3 py-3 text-left text-xs font-medium tracking-wider uppercase sm:px-6">
317
317
  Email vérifié
318
318
  </th>
319
- <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-muted-foreground uppercase sm:px-6">
319
+ <th className="text-muted-foreground px-3 py-3 text-left text-xs font-medium tracking-wider uppercase sm:px-6">
320
320
  Compte
321
321
  </th>
322
322
  </tr>
323
323
  </thead>
324
- <tbody className="divide-y divide-border bg-card">
324
+ <tbody className="divide-border bg-card divide-y">
325
325
  {filteredUsers.length === 0 ? (
326
326
  <tr>
327
327
  <td
328
328
  colSpan={5}
329
- className="px-3 py-6 text-center text-sm text-muted-foreground sm:px-6"
329
+ className="text-muted-foreground px-3 py-6 text-center text-sm sm:px-6"
330
330
  >
331
331
  Aucun utilisateur ne correspond à votre recherche
332
332
  </td>
333
333
  </tr>
334
334
  ) : (
335
335
  filteredUsers.map((user) => (
336
- <tr key={user.id} className="transition-colors duration-200 hover:bg-muted/70">
336
+ <tr
337
+ key={user.id}
338
+ className="hover:bg-muted/70 transition-colors duration-200"
339
+ >
337
340
  <td className="px-3 py-4 whitespace-nowrap sm:px-6">
338
341
  <div className="flex items-center">
339
- <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/20 text-xs font-semibold text-primary sm:h-10 sm:w-10">
342
+ <div className="bg-primary/20 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-xs font-semibold sm:h-10 sm:w-10">
340
343
  {(user.name?.[0] || user.email?.[0] || '?').toUpperCase()}
341
344
  </div>
342
345
  <div className="ml-3 min-w-0">
343
- <div className="truncate text-sm font-medium text-foreground sm:text-base">
346
+ <div className="text-foreground truncate text-sm font-medium sm:text-base">
344
347
  {user.name}
345
348
  </div>
346
349
  <div className="mt-0.5 flex flex-wrap items-center gap-1">
347
- <span className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium tracking-wide text-muted-foreground uppercase">
350
+ <span className="bg-muted text-muted-foreground inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium tracking-wide uppercase">
348
351
  {user.customRole?.name || user.role.toLowerCase()}
349
352
  </span>
350
353
  {user.id === session?.user?.id && (
351
- <span className="inline-flex items-center rounded-full bg-primary/20 px-2 py-0.5 text-[10px] font-medium text-primary">
354
+ <span className="bg-primary/20 text-primary inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium">
352
355
  Vous
353
356
  </span>
354
357
  )}
@@ -356,7 +359,7 @@ export default function UsersPage() {
356
359
  </div>
357
360
  </div>
358
361
  </td>
359
- <td className="px-3 py-4 text-xs whitespace-nowrap text-muted-foreground sm:px-6 sm:text-sm">
362
+ <td className="text-muted-foreground px-3 py-4 text-xs whitespace-nowrap sm:px-6 sm:text-sm">
360
363
  <span className="block max-w-[180px] truncate sm:max-w-xs">
361
364
  {user.email}
362
365
  </span>
@@ -366,7 +369,7 @@ export default function UsersPage() {
366
369
  value={user.customRoleId || ''}
367
370
  onChange={(e) => handleChangeRole(user.id, e.target.value)}
368
371
  disabled={user.id === session?.user?.id}
369
- className="w-full rounded-md border border-border bg-background px-2 py-1 text-xs font-medium text-foreground disabled:cursor-not-allowed disabled:opacity-50 sm:px-3 sm:text-sm"
372
+ className="border-border bg-background text-foreground w-full rounded-md border px-2 py-1 text-xs font-medium disabled:cursor-not-allowed disabled:opacity-50 sm:px-3 sm:text-sm"
370
373
  >
371
374
  <option value="">Sélectionner un profil</option>
372
375
  {roles.map((role) => (
@@ -398,7 +401,7 @@ export default function UsersPage() {
398
401
  type="button"
399
402
  disabled={resendingIds.has(user.id)}
400
403
  onClick={() => handleResendInvite(user.id, user.name)}
401
- className="inline-flex cursor-pointer items-center gap-1 rounded-md bg-primary/20 px-2 py-1 text-xs font-medium text-primary transition-colors duration-200 hover:bg-primary/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50"
404
+ className="bg-primary/20 text-primary hover:bg-primary/25 focus-visible:ring-primary inline-flex cursor-pointer items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
402
405
  title="Renvoyer l'email d'invitation"
403
406
  >
404
407
  {resendingIds.has(user.id) ? (
@@ -435,7 +438,7 @@ export default function UsersPage() {
435
438
  )}
436
439
  />
437
440
  </button>
438
- <span className="text-xs font-medium text-muted-foreground">
441
+ <span className="text-muted-foreground text-xs font-medium">
439
442
  {user.active ? 'Actif' : 'Inactif'}
440
443
  </span>
441
444
  </div>
@@ -452,12 +455,12 @@ export default function UsersPage() {
452
455
 
453
456
  {/* Modal d'ajout */}
454
457
  {showAddModal && (
455
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-foreground/10 p-4 backdrop-blur-sm sm:p-6">
456
- <div className="flex max-h-[90vh] w-full max-w-2xl flex-col rounded-xl border border-border bg-card p-6 shadow-(--shadow-dropdown) sm:p-8">
458
+ <div className="bg-foreground/10 fixed inset-0 z-50 flex items-center justify-center p-4 backdrop-blur-sm sm:p-6">
459
+ <div className="border-border bg-card flex max-h-[90vh] w-full max-w-2xl flex-col rounded-xl border p-6 shadow-(--shadow-dropdown) sm:p-8">
457
460
  {/* En-tête fixe */}
458
- <div className="shrink-0 border-b border-border pb-4">
461
+ <div className="border-border shrink-0 border-b pb-4">
459
462
  <div className="flex items-center justify-between">
460
- <h2 className="text-xl font-bold text-foreground sm:text-2xl">
463
+ <h2 className="text-foreground text-xl font-bold sm:text-2xl">
461
464
  Ajouter un utilisateur
462
465
  </h2>
463
466
  <button
@@ -467,7 +470,7 @@ export default function UsersPage() {
467
470
  setFormData({ name: '', email: '', customRoleId: '' });
468
471
  setError('');
469
472
  }}
470
- className="cursor-pointer rounded-lg p-2 text-muted-foreground transition-colors duration-200 hover:bg-muted"
473
+ className="text-muted-foreground hover:bg-muted cursor-pointer rounded-lg p-2 transition-colors duration-200"
471
474
  >
472
475
  <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
473
476
  <path
@@ -488,37 +491,37 @@ export default function UsersPage() {
488
491
  className="flex-1 space-y-4 overflow-y-auto pt-4 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
489
492
  >
490
493
  <div>
491
- <label className="block text-sm font-medium text-foreground">Nom complet</label>
494
+ <label className="text-foreground block text-sm font-medium">Nom complet</label>
492
495
  <input
493
496
  type="text"
494
497
  required
495
498
  value={formData.name}
496
499
  onChange={(e) => setFormData({ ...formData, name: e.target.value })}
497
- className="mt-1 block w-full rounded-lg border border-border bg-background px-4 py-2 focus:border-primary/50 focus:ring-2 focus:ring-primary/20"
500
+ className="border-border bg-background focus:border-primary/50 focus:ring-primary/20 mt-1 block w-full rounded-lg border px-4 py-2 focus:ring-2"
498
501
  />
499
502
  </div>
500
503
 
501
504
  <div>
502
- <label className="block text-sm font-medium text-foreground">Email</label>
505
+ <label className="text-foreground block text-sm font-medium">Email</label>
503
506
  <input
504
507
  type="email"
505
508
  required
506
509
  value={formData.email}
507
510
  onChange={(e) => setFormData({ ...formData, email: e.target.value })}
508
- className="mt-1 block w-full rounded-lg border border-border bg-background px-4 py-2 focus:border-primary/50 focus:ring-2 focus:ring-primary/20"
511
+ className="border-border bg-background focus:border-primary/50 focus:ring-primary/20 mt-1 block w-full rounded-lg border px-4 py-2 focus:ring-2"
509
512
  />
510
- <p className="mt-1 text-xs text-muted-foreground">
513
+ <p className="text-muted-foreground mt-1 text-xs">
511
514
  Un email d&apos;invitation sera envoyé à cet utilisateur
512
515
  </p>
513
516
  </div>
514
517
 
515
518
  <div>
516
- <label className="block text-sm font-medium text-foreground">Profil</label>
519
+ <label className="text-foreground block text-sm font-medium">Profil</label>
517
520
  <select
518
521
  value={formData.customRoleId}
519
522
  onChange={(e) => setFormData({ ...formData, customRoleId: e.target.value })}
520
523
  required
521
- className="mt-1 block w-full rounded-lg border border-border bg-background px-4 py-2 focus:border-primary/50 focus:ring-2 focus:ring-primary/20"
524
+ className="border-border bg-background focus:border-primary/50 focus:ring-primary/20 mt-1 block w-full rounded-lg border px-4 py-2 focus:ring-2"
522
525
  >
523
526
  <option value="">Sélectionner un profil</option>
524
527
  {roles.map((role) => (
@@ -532,7 +535,7 @@ export default function UsersPage() {
532
535
  </form>
533
536
 
534
537
  {/* Pied de modal fixe */}
535
- <div className="shrink-0 border-t border-border pt-4">
538
+ <div className="border-border shrink-0 border-t pt-4">
536
539
  <div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
537
540
  <button
538
541
  type="button"
@@ -543,7 +546,7 @@ export default function UsersPage() {
543
546
  setFormData({ name: '', email: '', customRoleId: '' });
544
547
  setError('');
545
548
  }}
546
- className="w-full cursor-pointer rounded-lg border border-border px-4 py-2 text-sm font-medium text-foreground transition-colors duration-200 hover:bg-muted disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto"
549
+ className="border-border text-foreground hover:bg-muted w-full cursor-pointer rounded-lg border px-4 py-2 text-sm font-medium transition-colors duration-200 disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto"
547
550
  >
548
551
  Annuler
549
552
  </button>
@@ -551,7 +554,7 @@ export default function UsersPage() {
551
554
  type="submit"
552
555
  form="add-user-form"
553
556
  disabled={isSubmitting}
554
- className="inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors duration-200 hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto"
557
+ className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors duration-200 disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto"
555
558
  >
556
559
  {isSubmitting && <Spinner size="sm" className="text-white" />}
557
560
  {isSubmitting ? 'Création en cours...' : 'Créer'}
@@ -129,7 +129,7 @@ export default function AccessControlPage() {
129
129
  <Link
130
130
  key={card.title}
131
131
  href={card.href}
132
- className="group cursor-pointer rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-all hover:border-blue-300 hover:shadow-md"
132
+ className="group cursor-pointer rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-[border-color,box-shadow] hover:border-blue-300 hover:shadow-md"
133
133
  >
134
134
  <div className="flex items-start gap-4">
135
135
  <div className={`rounded-lg p-3 ${card.iconBg}`}>
@@ -99,14 +99,14 @@ export default function PermissionsPage() {
99
99
  placeholder="Rechercher une permission..."
100
100
  value={searchTerm}
101
101
  onChange={(e) => setSearchTerm(e.target.value)}
102
- className="w-full rounded-lg border border-gray-300 py-2 pr-4 pl-10 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
102
+ className="w-full rounded-lg border border-gray-300 py-2 pr-4 pl-10 text-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
103
103
  />
104
104
  </div>
105
105
 
106
106
  <select
107
107
  value={selectedCategory}
108
108
  onChange={(e) => setSelectedCategory(e.target.value)}
109
- className="rounded-lg border border-gray-300 px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
109
+ className="rounded-lg border border-gray-300 px-4 py-2 text-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
110
110
  >
111
111
  <option value="all">Toutes les catégories</option>
112
112
  {Object.values(PERMISSION_CATEGORIES).map((category) => (
@@ -8,6 +8,7 @@ import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
8
8
  import { useConfirm } from '@/hooks/use-confirm';
9
9
  import { ProtectedPage } from '@/components/protected-page';
10
10
  import { useAppToast } from '@/contexts/app-toast-context';
11
+ import { devToast } from '@/lib/utils';
11
12
 
12
13
  interface Role {
13
14
  id: string;
@@ -94,7 +95,7 @@ function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
94
95
  onSave();
95
96
  onClose();
96
97
  } catch (err: any) {
97
- setError(err.message);
98
+ setError(devToast('Erreur lors de la sauvegarde du profil', err));
98
99
  } finally {
99
100
  setIsSubmitting(false);
100
101
  }
@@ -135,7 +136,7 @@ function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
135
136
  id="name"
136
137
  value={formData.name}
137
138
  onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
138
- className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
139
+ className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
139
140
  required
140
141
  />
141
142
  </div>
@@ -153,7 +154,7 @@ function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
153
154
  }))
154
155
  }
155
156
  rows={2}
156
- className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
157
+ className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
157
158
  />
158
159
  </div>
159
160
  </div>
@@ -238,7 +239,7 @@ export default function RolesPage() {
238
239
  const data = await response.json();
239
240
  setRoles(data);
240
241
  } catch (err: any) {
241
- setError(err.message);
242
+ setError(devToast('Erreur lors du chargement des profils', err));
242
243
  } finally {
243
244
  setLoading(false);
244
245
  }
@@ -294,9 +295,10 @@ export default function RolesPage() {
294
295
  throw new Error(data.error || 'Erreur lors de la suppression');
295
296
  }
296
297
 
298
+ toast.success('Profil supprimé');
297
299
  await fetchRoles();
298
300
  } catch (err: any) {
299
- setError(err.message);
301
+ setError(devToast('Erreur lors de la suppression du profil', err));
300
302
  setTimeout(() => setError(''), 5000);
301
303
  }
302
304
  };
@@ -0,0 +1,92 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+ import { encrypt } from '@/lib/encryption';
5
+ import {
6
+ getValidAccessToken,
7
+ getUserGoogleAccount,
8
+ listGoogleCalendarEvents,
9
+ } from '@/lib/google-calendar';
10
+
11
+ /**
12
+ * GET /api/agenda/google-events?startDate=&endDate= (ISO)
13
+ * Événements Google des calendriers cochés dans les préférences utilisateur.
14
+ */
15
+ export async function GET(request: NextRequest) {
16
+ try {
17
+ const session = await auth.api.getSession({
18
+ headers: request.headers,
19
+ });
20
+
21
+ if (!session) {
22
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
23
+ }
24
+
25
+ const { searchParams } = new URL(request.url);
26
+ const startRaw = searchParams.get('startDate');
27
+ const endRaw = searchParams.get('endDate');
28
+ if (!startRaw || !endRaw) {
29
+ return NextResponse.json(
30
+ { error: 'Paramètres startDate et endDate (ISO) requis' },
31
+ { status: 400 },
32
+ );
33
+ }
34
+
35
+ const timeMin = new Date(startRaw);
36
+ const timeMax = new Date(endRaw);
37
+ if (Number.isNaN(timeMin.getTime()) || Number.isNaN(timeMax.getTime())) {
38
+ return NextResponse.json({ error: 'Dates invalides' }, { status: 400 });
39
+ }
40
+
41
+ let googleAccount;
42
+ try {
43
+ googleAccount = await getUserGoogleAccount(session.user.id);
44
+ } catch {
45
+ return NextResponse.json({ events: [] });
46
+ }
47
+
48
+ const visible = normalizeAgendaIds(googleAccount.agendaVisibleGoogleCalendarIds);
49
+ if (visible.length === 0) {
50
+ return NextResponse.json({ events: [] });
51
+ }
52
+
53
+ const accessToken = await getValidAccessToken(
54
+ googleAccount.accessToken,
55
+ googleAccount.refreshToken,
56
+ googleAccount.tokenExpiresAt,
57
+ );
58
+
59
+ if (accessToken !== googleAccount.accessToken) {
60
+ const tokenExpiresAt = new Date();
61
+ tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
62
+ await prisma.userGoogleAccount.update({
63
+ where: { userId: session.user.id },
64
+ data: {
65
+ accessToken: encrypt(accessToken),
66
+ tokenExpiresAt,
67
+ },
68
+ });
69
+ }
70
+
71
+ const results = await Promise.all(
72
+ visible.map((calendarId) =>
73
+ listGoogleCalendarEvents(accessToken, calendarId, timeMin, timeMax).catch((err) => {
74
+ console.error(`google-events ${calendarId}:`, err);
75
+ return [] as Awaited<ReturnType<typeof listGoogleCalendarEvents>>;
76
+ }),
77
+ ),
78
+ );
79
+
80
+ const events = results.flat();
81
+
82
+ return NextResponse.json({ events });
83
+ } catch (error: any) {
84
+ console.error('GET agenda google-events:', error);
85
+ return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
86
+ }
87
+ }
88
+
89
+ function normalizeAgendaIds(raw: unknown): string[] {
90
+ if (!raw || !Array.isArray(raw)) return [];
91
+ return raw.filter((x): x is string => typeof x === 'string' && x.trim() !== '');
92
+ }
@@ -23,9 +23,10 @@ export async function GET(request: NextRequest) {
23
23
  return NextResponse.json({ active: isActive }, { status: 200 });
24
24
  } catch (error: any) {
25
25
  console.error('Erreur lors de la vérification du statut utilisateur:', error);
26
+ // 500 : ne pas renvoyer active: false pour éviter une déconnexion client sur erreur transitoire
26
27
  return NextResponse.json(
27
- { active: false, error: error.message || 'Erreur serveur' },
28
- { status: 200 },
28
+ { error: error.message || 'Erreur serveur' },
29
+ { status: 500 },
29
30
  );
30
31
  }
31
32
  }
@@ -15,8 +15,9 @@ export async function GET(request: NextRequest) {
15
15
 
16
16
  const scopes = [
17
17
  'https://www.googleapis.com/auth/calendar.events',
18
+ // Liste des calendriers (partagés, etc.) — les comptes déjà connectés doivent se reconnecter pour obtenir ce scope.
19
+ 'https://www.googleapis.com/auth/calendar.calendarlist.readonly',
18
20
  'https://www.googleapis.com/auth/spreadsheets.readonly',
19
- 'https://www.googleapis.com/auth/drive.file', // Accès aux fichiers créés par l'application
20
21
  ];
21
22
 
22
23
  const params = new URLSearchParams({
@@ -4,7 +4,7 @@ import { prisma } from '@/lib/prisma';
4
4
 
5
5
  /**
6
6
  * GET /api/auth/google/status
7
- * Récupère le statut de connexion Google Drive (admin) et Google Calendar (utilisateur)
7
+ * Récupère le statut de connexion Google Calendar (utilisateur courant)
8
8
  */
9
9
  export async function GET(request: NextRequest) {
10
10
  try {
@@ -16,29 +16,6 @@ export async function GET(request: NextRequest) {
16
16
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
17
17
  }
18
18
 
19
- // Statut Google Drive (admin uniquement)
20
- let driveConnected = false;
21
- let driveEmail: string | null = null;
22
- try {
23
- const adminUser = await prisma.user.findFirst({
24
- where: { role: 'ADMIN' },
25
- include: {
26
- googleAccount: true,
27
- },
28
- orderBy: {
29
- createdAt: 'asc',
30
- },
31
- });
32
-
33
- if (adminUser && adminUser.googleAccount) {
34
- driveConnected = true;
35
- driveEmail = adminUser.googleAccount.email || adminUser.email || null;
36
- }
37
- } catch (error) {
38
- // Ignorer l'erreur
39
- }
40
-
41
- // Statut Google Calendar (utilisateur courant)
42
19
  let calendarConnected = false;
43
20
  let calendarEmail: string | null = null;
44
21
  try {
@@ -50,22 +27,21 @@ export async function GET(request: NextRequest) {
50
27
  calendarConnected = true;
51
28
  calendarEmail = userGoogleAccount.email || null;
52
29
  }
53
- } catch (error) {
30
+ } catch {
54
31
  // Ignorer l'erreur
55
32
  }
56
33
 
57
34
  return NextResponse.json({
58
- drive: {
59
- connected: driveConnected,
60
- email: driveEmail,
61
- },
62
35
  calendar: {
63
36
  connected: calendarConnected,
64
37
  email: calendarEmail,
65
38
  },
66
39
  });
67
- } catch (error: any) {
40
+ } catch (error: unknown) {
68
41
  console.error('Erreur lors de la récupération du statut Google:', error);
69
- return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
42
+ return NextResponse.json(
43
+ { error: error instanceof Error ? error.message : 'Erreur serveur' },
44
+ { status: 500 },
45
+ );
70
46
  }
71
47
  }
@@ -81,9 +81,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
81
81
  }
82
82
 
83
83
  const canAddActivity =
84
- (await checkPermission('companies.add_activity')) ||
85
- (await checkPermission('companies.edit')) ||
86
- (await checkPermission('contacts.edit'));
84
+ (await checkPermission('companies.add_activity')) || (await checkPermission('companies.edit'));
87
85
  if (!canAddActivity) {
88
86
  return NextResponse.json({ error: 'Permission insuffisante' }, { status: 403 });
89
87
  }
@@ -75,8 +75,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
75
75
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
76
76
  }
77
77
 
78
- const canEdit =
79
- (await checkPermission('companies.edit')) || (await checkPermission('contacts.edit'));
78
+ const canEdit = await checkPermission('companies.edit');
80
79
  if (!canEdit) {
81
80
  return NextResponse.json({ error: 'Permission insuffisante' }, { status: 403 });
82
81
  }