create-crm-tmp 1.1.3 → 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 (276) hide show
  1. package/bin/create-crm-tmp.js +56 -35
  2. package/package.json +1 -1
  3. package/template/.prettierignore +2 -0
  4. package/template/README.md +230 -115
  5. package/template/components.json +22 -0
  6. package/template/eslint.config.mjs +13 -0
  7. package/template/exemple-contacts.csv +54 -0
  8. package/template/next.config.ts +41 -1
  9. package/template/package.json +63 -15
  10. package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
  11. package/template/prisma/schema.prisma +311 -67
  12. package/template/src/app/(auth)/invite/[token]/page.tsx +28 -29
  13. package/template/src/app/(auth)/layout.tsx +1 -1
  14. package/template/src/app/(auth)/reset-password/complete/page.tsx +21 -27
  15. package/template/src/app/(auth)/reset-password/page.tsx +14 -10
  16. package/template/src/app/(auth)/reset-password/verify/page.tsx +14 -10
  17. package/template/src/app/(auth)/signin/page.tsx +34 -23
  18. package/template/src/app/(dashboard)/agenda/page.tsx +3655 -2357
  19. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
  20. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +609 -338
  21. package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
  22. package/template/src/app/(dashboard)/automatisation/page.tsx +463 -186
  23. package/template/src/app/(dashboard)/closing/page.tsx +517 -469
  24. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6151 -4210
  25. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1702 -0
  26. package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
  27. package/template/src/app/(dashboard)/contacts/page.tsx +4124 -2130
  28. package/template/src/app/(dashboard)/dashboard/page.tsx +119 -105
  29. package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
  30. package/template/src/app/(dashboard)/error.tsx +37 -0
  31. package/template/src/app/(dashboard)/layout.tsx +6 -2
  32. package/template/src/app/(dashboard)/loading.tsx +5 -0
  33. package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
  34. package/template/src/app/(dashboard)/settings/page.tsx +1773 -3362
  35. package/template/src/app/(dashboard)/templates/page.tsx +504 -303
  36. package/template/src/app/(dashboard)/users/list/page.tsx +364 -355
  37. package/template/src/app/(dashboard)/users/page.tsx +279 -310
  38. package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
  39. package/template/src/app/(dashboard)/users/roles/page.tsx +169 -140
  40. package/template/src/app/api/agenda/google-events/route.ts +92 -0
  41. package/template/src/app/api/audit-logs/route.ts +1 -1
  42. package/template/src/app/api/auth/check-active/route.ts +3 -2
  43. package/template/src/app/api/auth/google/callback/route.ts +8 -5
  44. package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
  45. package/template/src/app/api/auth/google/route.ts +2 -1
  46. package/template/src/app/api/auth/google/status/route.ts +7 -31
  47. package/template/src/app/api/companies/[id]/activities/route.ts +129 -0
  48. package/template/src/app/api/companies/[id]/route.ts +194 -0
  49. package/template/src/app/api/companies/export/route.ts +206 -0
  50. package/template/src/app/api/companies/route.ts +196 -0
  51. package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
  52. package/template/src/app/api/contact-views/[id]/route.ts +197 -0
  53. package/template/src/app/api/contact-views/route.ts +146 -0
  54. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +55 -0
  55. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +20 -48
  56. package/template/src/app/api/contacts/[id]/files/route.ts +125 -186
  57. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
  58. package/template/src/app/api/contacts/[id]/interactions/route.ts +45 -8
  59. package/template/src/app/api/contacts/[id]/kyc/route.ts +81 -0
  60. package/template/src/app/api/contacts/[id]/meet/route.ts +55 -29
  61. package/template/src/app/api/contacts/[id]/route.ts +184 -21
  62. package/template/src/app/api/contacts/[id]/send-email/route.ts +33 -11
  63. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +67 -0
  64. package/template/src/app/api/contacts/export/route.ts +22 -31
  65. package/template/src/app/api/contacts/import/route.ts +77 -44
  66. package/template/src/app/api/contacts/import-preview/route.ts +139 -0
  67. package/template/src/app/api/contacts/origins/route.ts +63 -0
  68. package/template/src/app/api/contacts/route.ts +322 -57
  69. package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
  70. package/template/src/app/api/dashboard/stats/route.ts +9 -292
  71. package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -3
  72. package/template/src/app/api/dashboard/widgets/route.ts +19 -19
  73. package/template/src/app/api/dev/reminders/test/route.ts +114 -0
  74. package/template/src/app/api/editor/upload-image/route.ts +61 -0
  75. package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
  76. package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
  77. package/template/src/app/api/integrations/google-sheet/sync/route.ts +28 -542
  78. package/template/src/app/api/invite/complete/route.ts +20 -23
  79. package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
  80. package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
  81. package/template/src/app/api/reminders/clear/route.ts +120 -0
  82. package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
  83. package/template/src/app/api/reminders/route.ts +165 -39
  84. package/template/src/app/api/reminders/state/route.ts +164 -0
  85. package/template/src/app/api/reset-password/complete/route.ts +11 -13
  86. package/template/src/app/api/reset-password/request/route.ts +1 -1
  87. package/template/src/app/api/reset-password/verify/route.ts +1 -1
  88. package/template/src/app/api/send/route.ts +25 -47
  89. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
  90. package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
  91. package/template/src/app/api/settings/company/route.ts +19 -26
  92. package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
  93. package/template/src/app/api/settings/google-ads/route.ts +34 -23
  94. package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
  95. package/template/src/app/api/settings/google-calendar/route.ts +124 -0
  96. package/template/src/app/api/settings/google-sheet/[id]/route.ts +48 -23
  97. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +56 -32
  98. package/template/src/app/api/settings/google-sheet/preview/route.ts +110 -0
  99. package/template/src/app/api/settings/google-sheet/route.ts +34 -23
  100. package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
  101. package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
  102. package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -24
  103. package/template/src/app/api/settings/meta-leads/route.ts +34 -25
  104. package/template/src/app/api/settings/smtp/route.ts +53 -6
  105. package/template/src/app/api/settings/statuses/[id]/route.ts +29 -32
  106. package/template/src/app/api/settings/statuses/route.ts +24 -22
  107. package/template/src/app/api/statuses/route.ts +2 -5
  108. package/template/src/app/api/tasks/[id]/attendees/route.ts +36 -13
  109. package/template/src/app/api/tasks/[id]/route.ts +357 -145
  110. package/template/src/app/api/tasks/meet/route.ts +37 -26
  111. package/template/src/app/api/tasks/route.ts +201 -96
  112. package/template/src/app/api/templates/[id]/route.ts +22 -13
  113. package/template/src/app/api/templates/route.ts +22 -5
  114. package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
  115. package/template/src/app/api/users/[id]/route.ts +22 -16
  116. package/template/src/app/api/users/commercials/route.ts +38 -0
  117. package/template/src/app/api/users/for-agenda/route.ts +1 -2
  118. package/template/src/app/api/users/list/route.ts +57 -19
  119. package/template/src/app/api/users/route.ts +89 -34
  120. package/template/src/app/api/webhooks/google-ads/route.ts +40 -1
  121. package/template/src/app/api/webhooks/meta-leads/route.ts +38 -1
  122. package/template/src/app/api/workflows/[id]/route.ts +29 -6
  123. package/template/src/app/api/workflows/process/route.ts +505 -170
  124. package/template/src/app/api/workflows/route.ts +42 -4
  125. package/template/src/app/globals.css +512 -32
  126. package/template/src/app/layout.tsx +28 -9
  127. package/template/src/app/page.tsx +37 -7
  128. package/template/src/components/address-autocomplete.tsx +233 -0
  129. package/template/src/components/config-error-alert.tsx +46 -0
  130. package/template/src/components/contacts/filter-bar.tsx +190 -0
  131. package/template/src/components/contacts/filter-builder.tsx +574 -0
  132. package/template/src/components/contacts/save-view-dialog.tsx +160 -0
  133. package/template/src/components/contacts/views-tab-bar.tsx +449 -0
  134. package/template/src/components/dashboard/activity-chart.tsx +6 -1
  135. package/template/src/components/dashboard/add-widget-dialog.tsx +13 -17
  136. package/template/src/components/dashboard/color-picker.tsx +7 -8
  137. package/template/src/components/dashboard/recent-activity.tsx +2 -5
  138. package/template/src/components/dashboard/stat-card.tsx +1 -3
  139. package/template/src/components/dashboard/status-distribution-chart.tsx +0 -1
  140. package/template/src/components/dashboard/top-contacts-list.tsx +7 -13
  141. package/template/src/components/dashboard/upcoming-tasks-list.tsx +2 -5
  142. package/template/src/components/dashboard/widget-wrapper.tsx +3 -6
  143. package/template/src/components/date-picker.tsx +399 -0
  144. package/template/src/components/editor/upload-editor-image.ts +42 -0
  145. package/template/src/components/editor.tsx +188 -35
  146. package/template/src/components/email-template.tsx +4 -2
  147. package/template/src/components/global-search.tsx +360 -0
  148. package/template/src/components/header.tsx +200 -107
  149. package/template/src/components/inactive-account-guard.tsx +58 -0
  150. package/template/src/components/integration-notifications-listener.tsx +12 -0
  151. package/template/src/components/invitation-email-template.tsx +4 -2
  152. package/template/src/components/lazy-editor.tsx +11 -0
  153. package/template/src/components/meet-cancellation-email-template.tsx +11 -3
  154. package/template/src/components/meet-confirmation-email-template.tsx +10 -3
  155. package/template/src/components/meet-update-email-template.tsx +10 -3
  156. package/template/src/components/page-header.tsx +19 -15
  157. package/template/src/components/protected-page.tsx +94 -0
  158. package/template/src/components/reset-password-email-template.tsx +4 -2
  159. package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
  160. package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
  161. package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
  162. package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
  163. package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
  164. package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
  165. package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
  166. package/template/src/components/sidebar.tsx +117 -100
  167. package/template/src/components/skeleton.tsx +128 -45
  168. package/template/src/components/ui/accordion.tsx +64 -0
  169. package/template/src/components/ui/alert-dialog.tsx +139 -0
  170. package/template/src/components/ui/button.tsx +71 -0
  171. package/template/src/components/ui/components.tsx +1 -1
  172. package/template/src/components/ui/date-picker.tsx +422 -0
  173. package/template/src/components/ui/datetime-picker.tsx +338 -0
  174. package/template/src/components/ui/status-select.tsx +271 -0
  175. package/template/src/components/ui/tooltip.tsx +37 -0
  176. package/template/src/components/view-as-banner.tsx +1 -1
  177. package/template/src/components/view-as-modal.tsx +30 -19
  178. package/template/src/config/nav-pages.ts +108 -0
  179. package/template/src/contexts/app-toast-context.tsx +362 -0
  180. package/template/src/contexts/dashboard-theme-context.tsx +2 -7
  181. package/template/src/contexts/sidebar-context.tsx +27 -53
  182. package/template/src/contexts/task-reminder-context.tsx +134 -160
  183. package/template/src/contexts/view-as-context.tsx +32 -10
  184. package/template/src/hooks/use-alert.tsx +65 -0
  185. package/template/src/hooks/use-confirm.tsx +87 -0
  186. package/template/src/hooks/use-contact-views.ts +140 -0
  187. package/template/src/hooks/use-contacts.ts +69 -0
  188. package/template/src/hooks/use-fetch.ts +17 -0
  189. package/template/src/hooks/use-focus-trap.ts +73 -0
  190. package/template/src/hooks/use-statuses.ts +22 -0
  191. package/template/src/hooks/useIntegrationNotifications.ts +49 -0
  192. package/template/src/lib/address-api.ts +155 -0
  193. package/template/src/lib/auth.ts +8 -1
  194. package/template/src/lib/cache.ts +73 -0
  195. package/template/src/lib/check-permission.ts +12 -177
  196. package/template/src/lib/config-links.ts +14 -0
  197. package/template/src/lib/contact-duplicate.ts +79 -61
  198. package/template/src/lib/contact-interactions.ts +24 -22
  199. package/template/src/lib/contact-view-filters.ts +301 -0
  200. package/template/src/lib/contacts-list-url.ts +190 -0
  201. package/template/src/lib/dashboard-stats.ts +282 -0
  202. package/template/src/lib/dashboard-themes.ts +0 -5
  203. package/template/src/lib/date-utils.ts +176 -0
  204. package/template/src/lib/default-widgets.ts +0 -2
  205. package/template/src/lib/editor-html-image-dimensions.ts +172 -0
  206. package/template/src/lib/editor-image-limits.ts +19 -0
  207. package/template/src/lib/email-html-sanitize.ts +19 -0
  208. package/template/src/lib/encryption.ts +9 -6
  209. package/template/src/lib/fr-geography.ts +192 -0
  210. package/template/src/lib/get-auth-user.ts +25 -0
  211. package/template/src/lib/google-calendar-agenda.ts +201 -0
  212. package/template/src/lib/google-calendar.ts +309 -17
  213. package/template/src/lib/google-fetch.ts +63 -0
  214. package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
  215. package/template/src/lib/google-sheet-sync-runner.ts +514 -0
  216. package/template/src/lib/integration-import-log.ts +21 -0
  217. package/template/src/lib/local-storage.ts +34 -0
  218. package/template/src/lib/permissions.ts +268 -40
  219. package/template/src/lib/prisma.ts +15 -12
  220. package/template/src/lib/qstash.ts +65 -0
  221. package/template/src/lib/reminder-state-server.ts +80 -0
  222. package/template/src/lib/reminder-state.ts +29 -0
  223. package/template/src/lib/roles.ts +12 -15
  224. package/template/src/lib/supabase-storage.ts +113 -0
  225. package/template/src/lib/template-variables.ts +204 -29
  226. package/template/src/lib/utils.ts +71 -11
  227. package/template/src/lib/widget-registry.ts +0 -4
  228. package/template/src/lib/workflow-executor.ts +391 -228
  229. package/template/src/proxy.ts +35 -73
  230. package/template/src/types/contact-views.ts +351 -0
  231. package/template/vercel.json +5 -0
  232. package/template/WORKFLOWS_CRON.md +0 -185
  233. package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
  234. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
  235. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
  236. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
  237. package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
  238. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
  239. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
  240. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
  241. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
  242. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
  243. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
  244. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
  245. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
  246. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
  247. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
  248. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
  249. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
  250. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
  251. package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
  252. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
  253. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
  254. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
  255. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
  256. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
  257. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
  258. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
  259. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
  260. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
  261. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
  262. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
  263. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
  264. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
  265. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
  266. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
  267. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
  268. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
  269. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
  270. package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
  271. package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
  272. package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
  273. package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
  274. package/template/prisma/migrations/20260226093949_fix_cascade_on_user_delete/migration.sql +0 -69
  275. package/template/src/app/(dashboard)/users/layout.tsx +0 -30
  276. package/template/src/lib/google-drive.ts +0 -380
@@ -4,6 +4,11 @@ import { useState, useEffect } from 'react';
4
4
  import Link from 'next/link';
5
5
  import { ArrowLeft, Shield, Users as UsersIcon, Key, Plus, Edit, Trash2, X } from 'lucide-react';
6
6
  import { PERMISSIONS, PERMISSIONS_BY_CATEGORY } from '@/lib/permissions';
7
+ import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
8
+ import { useConfirm } from '@/hooks/use-confirm';
9
+ import { ProtectedPage } from '@/components/protected-page';
10
+ import { useAppToast } from '@/contexts/app-toast-context';
11
+ import { devToast } from '@/lib/utils';
7
12
 
8
13
  interface Role {
9
14
  id: string;
@@ -24,6 +29,7 @@ interface RoleModalProps {
24
29
  }
25
30
 
26
31
  function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
32
+ const toast = useAppToast();
27
33
  const [formData, setFormData] = useState({
28
34
  name: role?.name || '',
29
35
  description: role?.description || '',
@@ -50,6 +56,12 @@ function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
50
56
  setError('');
51
57
  }, [role, isOpen]);
52
58
 
59
+ useEffect(() => {
60
+ if (!error) return;
61
+ toast.error(error);
62
+ setError('');
63
+ }, [error, toast]);
64
+
53
65
  const togglePermission = (permissionCode: string) => {
54
66
  setFormData((prev) => ({
55
67
  ...prev,
@@ -83,7 +95,7 @@ function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
83
95
  onSave();
84
96
  onClose();
85
97
  } catch (err: any) {
86
- setError(err.message);
98
+ setError(devToast('Erreur lors de la sauvegarde du profil', err));
87
99
  } finally {
88
100
  setIsSubmitting(false);
89
101
  }
@@ -112,9 +124,6 @@ function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
112
124
 
113
125
  {/* Content */}
114
126
  <div className="max-h-[70vh] overflow-y-auto p-6">
115
- {error && (
116
- <div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>
117
- )}
118
127
  <div className="space-y-6">
119
128
  {/* Nom et description */}
120
129
  <div className="grid gap-4 sm:grid-cols-2">
@@ -127,7 +136,7 @@ function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
127
136
  id="name"
128
137
  value={formData.name}
129
138
  onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
130
- className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 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"
131
140
  required
132
141
  />
133
142
  </div>
@@ -145,7 +154,7 @@ function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
145
154
  }))
146
155
  }
147
156
  rows={2}
148
- className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 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"
149
158
  />
150
159
  </div>
151
160
  </div>
@@ -170,7 +179,7 @@ function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
170
179
  type="checkbox"
171
180
  checked={formData.permissions.includes(permission.code)}
172
181
  onChange={() => togglePermission(permission.code)}
173
- className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
182
+ className="mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-gray-400/30"
174
183
  />
175
184
  <div className="flex-1">
176
185
  <div className="font-medium text-gray-900">{permission.name}</div>
@@ -211,6 +220,9 @@ function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
211
220
  }
212
221
 
213
222
  export default function RolesPage() {
223
+ const { toggle: toggleMobileMenu, isOpen: isMobileMenuOpen } = useMobileMenuContext();
224
+ const { confirm, ConfirmDialog } = useConfirm();
225
+ const toast = useAppToast();
214
226
  const [showModal, setShowModal] = useState(false);
215
227
  const [selectedRole, setSelectedRole] = useState<Role | null>(null);
216
228
  const [roles, setRoles] = useState<Role[]>([]);
@@ -227,7 +239,7 @@ export default function RolesPage() {
227
239
  const data = await response.json();
228
240
  setRoles(data);
229
241
  } catch (err: any) {
230
- setError(err.message);
242
+ setError(devToast('Erreur lors du chargement des profils', err));
231
243
  } finally {
232
244
  setLoading(false);
233
245
  }
@@ -237,6 +249,12 @@ export default function RolesPage() {
237
249
  fetchRoles();
238
250
  }, []);
239
251
 
252
+ useEffect(() => {
253
+ if (!error) return;
254
+ toast.error(error);
255
+ setError('');
256
+ }, [error, toast]);
257
+
240
258
  const handleEditRole = (roleId: string) => {
241
259
  const role = roles.find((r) => r.id === roleId);
242
260
  if (role) {
@@ -249,11 +267,20 @@ export default function RolesPage() {
249
267
  const role = roles.find((r) => r.id === roleId);
250
268
  if (!role) return;
251
269
 
270
+ const confirmTitle = role.isSystem ? '⚠️ Supprimer un profil système' : 'Supprimer le profil';
252
271
  const confirmMessage = role.isSystem
253
- ? `⚠️ Attention : "${role.name}" est un profil système.\n\nÊtes-vous sûr de vouloir le supprimer ?`
272
+ ? `Attention : "${role.name}" est un profil système. Êtes-vous sûr de vouloir le supprimer ?`
254
273
  : `Êtes-vous sûr de vouloir supprimer le profil "${role.name}" ?`;
255
274
 
256
- if (!confirm(confirmMessage)) {
275
+ const confirmed = await confirm({
276
+ title: confirmTitle,
277
+ description: confirmMessage,
278
+ confirmText: 'Supprimer',
279
+ cancelText: 'Annuler',
280
+ variant: 'destructive',
281
+ });
282
+
283
+ if (!confirmed) {
257
284
  return;
258
285
  }
259
286
 
@@ -268,9 +295,10 @@ export default function RolesPage() {
268
295
  throw new Error(data.error || 'Erreur lors de la suppression');
269
296
  }
270
297
 
298
+ toast.success('Profil supprimé');
271
299
  await fetchRoles();
272
300
  } catch (err: any) {
273
- setError(err.message);
301
+ setError(devToast('Erreur lors de la suppression du profil', err));
274
302
  setTimeout(() => setError(''), 5000);
275
303
  }
276
304
  };
@@ -285,148 +313,149 @@ export default function RolesPage() {
285
313
  };
286
314
 
287
315
  return (
288
- <div className="h-full">
289
- <div className="border-b border-gray-200 bg-white">
290
- <div className="p-4 sm:p-6">
291
- <div className="mb-4">
292
- <Link
293
- href="/users"
294
- className="inline-flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900"
295
- >
296
- <ArrowLeft className="h-4 w-4" />
297
- Retour
298
- </Link>
299
- </div>
300
- <div className="flex items-center justify-between">
301
- <div>
302
- <h1 className="text-2xl font-bold text-gray-900">Gestion des profils</h1>
303
- <p className="mt-1 text-sm text-gray-500">
304
- Créer et configurer les profils avec leurs permissions
305
- </p>
316
+ <ProtectedPage requiredPermission="users.manage_roles">
317
+ <div className="h-full">
318
+ <div className="border-b border-gray-200 bg-white">
319
+ <div className="p-4 sm:p-6">
320
+ <div className="mb-4">
321
+ <Link
322
+ href="/users"
323
+ className="inline-flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900"
324
+ >
325
+ <ArrowLeft className="h-4 w-4" />
326
+ Retour
327
+ </Link>
328
+ </div>
329
+ <div className="flex items-center justify-between">
330
+ <div>
331
+ <h1 className="text-2xl font-bold text-gray-900">Gestion des profils</h1>
332
+ <p className="mt-1 text-sm text-gray-500">
333
+ Créer et configurer les profils avec leurs permissions
334
+ </p>
335
+ </div>
336
+ <button
337
+ onClick={() => {
338
+ setSelectedRole(null);
339
+ setShowModal(true);
340
+ }}
341
+ className="flex cursor-pointer items-center gap-2 rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700"
342
+ >
343
+ <Plus className="h-4 w-4" />
344
+ Nouveau profil
345
+ </button>
306
346
  </div>
307
- <button
308
- onClick={() => {
309
- setSelectedRole(null);
310
- setShowModal(true);
311
- }}
312
- className="flex cursor-pointer items-center gap-2 rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700"
313
- >
314
- <Plus className="h-4 w-4" />
315
- Nouveau profil
316
- </button>
317
347
  </div>
318
348
  </div>
319
- </div>
320
-
321
- <div className="p-4 sm:p-6">
322
- {error && <div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>}
323
349
 
324
- {loading ? (
325
- <div className="grid gap-6 lg:grid-cols-2">
326
- {[1, 2, 3, 4].map((i) => (
327
- <div key={i} className="h-64 animate-pulse rounded-lg bg-gray-200" />
328
- ))}
329
- </div>
330
- ) : (
331
- <div className="grid gap-6 lg:grid-cols-2">
332
- {roles
333
- .sort((a, b) => b.permissions.length - a.permissions.length) // Trier par nombre de permissions (DESC)
334
- .map((role) => {
335
- const visiblePermissions = role.permissions.slice(0, 4);
336
- const remainingCount = role.permissions.length - 4;
337
-
338
- return (
339
- <div
340
- key={role.id}
341
- className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm"
342
- >
343
- <div className="flex items-start justify-between">
344
- <div className="flex items-start gap-3">
345
- <div className="rounded-lg bg-green-100 p-2">
346
- <Shield className="h-5 w-5 text-green-600" />
350
+ <div className="p-4 sm:p-6">
351
+ {loading ? (
352
+ <div className="grid gap-6 lg:grid-cols-2">
353
+ {[1, 2, 3, 4].map((i) => (
354
+ <div key={i} className="h-64 animate-pulse rounded-lg bg-gray-200" />
355
+ ))}
356
+ </div>
357
+ ) : (
358
+ <div className="grid gap-6 lg:grid-cols-2">
359
+ {roles
360
+ .sort((a, b) => b.permissions.length - a.permissions.length) // Trier par nombre de permissions (DESC)
361
+ .map((role) => {
362
+ const visiblePermissions = role.permissions.slice(0, 4);
363
+ const remainingCount = role.permissions.length - 4;
364
+
365
+ return (
366
+ <div
367
+ key={role.id}
368
+ className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm"
369
+ >
370
+ <div className="flex items-start justify-between">
371
+ <div className="flex items-start gap-3">
372
+ <div className="rounded-lg bg-green-100 p-2">
373
+ <Shield className="h-5 w-5 text-green-600" />
374
+ </div>
375
+ <div>
376
+ <h3 className="font-semibold text-gray-900">
377
+ {role.name}
378
+ {role.isSystem && (
379
+ <span className="ml-2 inline-block rounded bg-blue-100 px-2 py-0.5 text-xs text-blue-800">
380
+ Système
381
+ </span>
382
+ )}
383
+ </h3>
384
+ <p className="mt-1 text-sm text-gray-600">{role.description}</p>
385
+ </div>
347
386
  </div>
348
- <div>
349
- <h3 className="font-semibold text-gray-900">
350
- {role.name}
351
- {role.isSystem && (
352
- <span className="ml-2 inline-block rounded bg-indigo-100 px-2 py-0.5 text-xs text-indigo-600">
353
- Système
354
- </span>
355
- )}
356
- </h3>
357
- <p className="mt-1 text-sm text-gray-600">{role.description}</p>
387
+ <div className="flex items-center gap-2">
388
+ <button
389
+ onClick={() => handleEditRole(role.id)}
390
+ className="cursor-pointer rounded-lg p-2 text-orange-600 hover:bg-orange-50"
391
+ title="Modifier"
392
+ >
393
+ <Edit className="h-4 w-4" />
394
+ </button>
395
+ <button
396
+ onClick={() => handleDeleteRole(role.id)}
397
+ className="cursor-pointer rounded-lg p-2 text-red-600 hover:bg-red-50"
398
+ title="Supprimer"
399
+ >
400
+ <Trash2 className="h-4 w-4" />
401
+ </button>
358
402
  </div>
359
403
  </div>
360
- <div className="flex items-center gap-2">
361
- <button
362
- onClick={() => handleEditRole(role.id)}
363
- className="cursor-pointer rounded-lg p-2 text-orange-600 hover:bg-orange-50"
364
- title="Modifier"
365
- >
366
- <Edit className="h-4 w-4" />
367
- </button>
368
- <button
369
- onClick={() => handleDeleteRole(role.id)}
370
- className="cursor-pointer rounded-lg p-2 text-red-600 hover:bg-red-50"
371
- title="Supprimer"
372
- >
373
- <Trash2 className="h-4 w-4" />
374
- </button>
375
- </div>
376
- </div>
377
404
 
378
- <div className="mt-4 flex items-center gap-4 text-sm text-gray-600">
379
- <div className="flex items-center gap-1">
380
- <UsersIcon className="h-4 w-4" />
381
- <span>
382
- {role.usersCount} utilisateur{role.usersCount > 1 ? 's' : ''}
383
- </span>
384
- </div>
385
- <div className="flex items-center gap-1">
386
- <Key className="h-4 w-4" />
387
- <span>
388
- {role.permissions.length} permission
389
- {role.permissions.length > 1 ? 's' : ''}
390
- </span>
405
+ <div className="mt-4 flex items-center gap-4 text-sm text-gray-600">
406
+ <div className="flex items-center gap-1">
407
+ <UsersIcon className="h-4 w-4" />
408
+ <span>
409
+ {role.usersCount} utilisateur{role.usersCount > 1 ? 's' : ''}
410
+ </span>
411
+ </div>
412
+ <div className="flex items-center gap-1">
413
+ <Key className="h-4 w-4" />
414
+ <span>
415
+ {role.permissions.length} permission
416
+ {role.permissions.length > 1 ? 's' : ''}
417
+ </span>
418
+ </div>
391
419
  </div>
392
- </div>
393
420
 
394
- <div className="mt-4">
395
- <h4 className="mb-2 text-xs font-medium tracking-wide text-gray-500 uppercase">
396
- Permissions
397
- </h4>
398
- <div className="flex flex-wrap gap-2">
399
- {visiblePermissions.map((permCode) => {
400
- const perm = PERMISSIONS.find((p) => p.code === permCode);
401
- return (
402
- <span
403
- key={permCode}
404
- className="rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-700"
405
- >
406
- {perm?.name || permCode}
421
+ <div className="mt-4">
422
+ <h4 className="mb-2 text-xs font-medium tracking-wide text-gray-500 uppercase">
423
+ Permissions
424
+ </h4>
425
+ <div className="flex flex-wrap gap-2">
426
+ {visiblePermissions.map((permCode) => {
427
+ const perm = PERMISSIONS.find((p) => p.code === permCode);
428
+ return (
429
+ <span
430
+ key={permCode}
431
+ className="rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-700"
432
+ >
433
+ {perm?.name || permCode}
434
+ </span>
435
+ );
436
+ })}
437
+ {remainingCount > 0 && (
438
+ <span className="rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700">
439
+ +{remainingCount} autres
407
440
  </span>
408
- );
409
- })}
410
- {remainingCount > 0 && (
411
- <span className="rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700">
412
- +{remainingCount} autres
413
- </span>
414
- )}
441
+ )}
442
+ </div>
415
443
  </div>
416
444
  </div>
417
- </div>
418
- );
419
- })}
420
- </div>
421
- )}
422
- </div>
445
+ );
446
+ })}
447
+ </div>
448
+ )}
449
+ </div>
423
450
 
424
- <RoleModal
425
- isOpen={showModal}
426
- onClose={handleCloseModal}
427
- onSave={handleSaveRole}
428
- role={selectedRole || undefined}
429
- />
430
- </div>
451
+ <RoleModal
452
+ isOpen={showModal}
453
+ onClose={handleCloseModal}
454
+ onSave={handleSaveRole}
455
+ role={selectedRole || undefined}
456
+ />
457
+ <ConfirmDialog />
458
+ </div>
459
+ </ProtectedPage>
431
460
  );
432
461
  }
@@ -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
+ }
@@ -15,7 +15,7 @@ export async function GET(request: NextRequest) {
15
15
  }
16
16
 
17
17
  // On réutilise la permission de vue utilisateurs pour l’instant
18
- const hasPermission = await checkPermission('users.view');
18
+ const hasPermission = await checkPermission('audit.view_all');
19
19
  if (!hasPermission) {
20
20
  return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
21
21
  }
@@ -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
  }
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
2
2
  import { auth } from '@/lib/auth';
3
3
  import { prisma } from '@/lib/prisma';
4
4
  import { exchangeGoogleCodeForTokens } from '@/lib/google-calendar';
5
+ import { encrypt } from '@/lib/encryption';
5
6
 
6
7
  /**
7
8
  * GET /api/auth/google/callback
@@ -17,6 +18,8 @@ export async function GET(request: NextRequest) {
17
18
  return NextResponse.redirect(new URL('/signin', request.url));
18
19
  }
19
20
 
21
+ // Tous les utilisateurs peuvent maintenant connecter leur compte Google pour Calendar
22
+
20
23
  const { searchParams } = new URL(request.url);
21
24
  const code = searchParams.get('code');
22
25
  const error = searchParams.get('error');
@@ -63,19 +66,19 @@ export async function GET(request: NextRequest) {
63
66
  console.error("Erreur lors de la récupération de l'email Google:", err);
64
67
  }
65
68
 
66
- // Sauvegarder ou mettre à jour les tokens
69
+ // Sauvegarder ou mettre à jour les tokens (chiffrés si ENCRYPTION_KEY est définie)
67
70
  await prisma.userGoogleAccount.upsert({
68
71
  where: { userId: session.user.id },
69
72
  create: {
70
73
  userId: session.user.id,
71
- accessToken: tokens.access_token,
72
- refreshToken: tokens.refresh_token || '',
74
+ accessToken: encrypt(tokens.access_token),
75
+ refreshToken: tokens.refresh_token ? encrypt(tokens.refresh_token) : '',
73
76
  tokenExpiresAt,
74
77
  email: googleEmail,
75
78
  },
76
79
  update: {
77
- accessToken: tokens.access_token,
78
- refreshToken: tokens.refresh_token || undefined,
80
+ accessToken: encrypt(tokens.access_token),
81
+ refreshToken: tokens.refresh_token ? encrypt(tokens.refresh_token) : undefined,
79
82
  tokenExpiresAt,
80
83
  email: googleEmail,
81
84
  },
@@ -4,7 +4,7 @@ import { prisma } from '@/lib/prisma';
4
4
 
5
5
  /**
6
6
  * POST /api/auth/google/disconnect
7
- * Déconnecte le compte Google de l'utilisateur
7
+ * Déconnecte le compte Google de l'administrateur (seuls les admins peuvent déconnecter)
8
8
  */
9
9
  export async function POST(request: NextRequest) {
10
10
  try {
@@ -16,7 +16,7 @@ export async function POST(request: NextRequest) {
16
16
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
17
17
  }
18
18
 
19
- // Supprimer le compte Google
19
+ // Tous les utilisateurs peuvent déconnecter leur compte Google
20
20
  await prisma.userGoogleAccount.deleteMany({
21
21
  where: { userId: session.user.id },
22
22
  });
@@ -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({