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,3 +1,5 @@
1
+ import DOMPurify from 'isomorphic-dompurify';
2
+
1
3
  interface MeetCancellationEmailTemplateProps {
2
4
  contactName: string;
3
5
  title: string;
@@ -50,7 +52,13 @@ export function MeetCancellationEmailTemplate({
50
52
  };
51
53
 
52
54
  return (
53
- <div style={{ fontFamily: 'Arial, sans-serif', lineHeight: '1.6', color: '#333' }}>
55
+ <div
56
+ style={{
57
+ fontFamily: '"Segoe UI", "Helvetica Neue", sans-serif',
58
+ lineHeight: '1.6',
59
+ color: '#333',
60
+ }}
61
+ >
54
62
  <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
55
63
  <h1 style={{ color: '#EF4444', fontSize: '24px', marginBottom: '20px' }}>
56
64
  Annulation de rendez-vous
@@ -93,7 +101,7 @@ export function MeetCancellationEmailTemplate({
93
101
  <strong>Description :</strong>
94
102
  <div
95
103
  style={{ marginTop: '10px' }}
96
- dangerouslySetInnerHTML={{ __html: description }}
104
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(description) }}
97
105
  />
98
106
  </div>
99
107
  )}
@@ -111,7 +119,7 @@ export function MeetCancellationEmailTemplate({
111
119
  borderTop: '1px solid #ddd',
112
120
  fontSize: '14px',
113
121
  }}
114
- dangerouslySetInnerHTML={{ __html: signature }}
122
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(signature) }}
115
123
  />
116
124
  )}
117
125
  </div>
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import DOMPurify from 'isomorphic-dompurify';
2
3
 
3
4
  interface MeetConfirmationEmailTemplateProps {
4
5
  contactName: string;
@@ -52,7 +53,13 @@ export function MeetConfirmationEmailTemplate({
52
53
  };
53
54
 
54
55
  return (
55
- <div style={{ fontFamily: 'Arial, sans-serif', lineHeight: '1.6', color: '#333' }}>
56
+ <div
57
+ style={{
58
+ fontFamily: '"Segoe UI", "Helvetica Neue", sans-serif',
59
+ lineHeight: '1.6',
60
+ color: '#333',
61
+ }}
62
+ >
56
63
  <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
57
64
  <h1 style={{ color: '#1a1a1a', fontSize: '24px', marginBottom: '20px' }}>
58
65
  Confirmation de rendez-vous
@@ -94,7 +101,7 @@ export function MeetConfirmationEmailTemplate({
94
101
  <strong>Description :</strong>
95
102
  <div
96
103
  style={{ marginTop: '10px' }}
97
- dangerouslySetInnerHTML={{ __html: description }}
104
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(description) }}
98
105
  />
99
106
  </div>
100
107
  )}
@@ -147,7 +154,7 @@ export function MeetConfirmationEmailTemplate({
147
154
  borderTop: '1px solid #ddd',
148
155
  fontSize: '14px',
149
156
  }}
150
- dangerouslySetInnerHTML={{ __html: signature }}
157
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(signature) }}
151
158
  />
152
159
  )}
153
160
  </div>
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import DOMPurify from 'isomorphic-dompurify';
2
3
 
3
4
  interface MeetUpdateEmailTemplateProps {
4
5
  contactName: string;
@@ -62,7 +63,13 @@ export function MeetUpdateEmailTemplate({
62
63
  const isGoogleMeet = !!meetLink;
63
64
 
64
65
  return (
65
- <div style={{ fontFamily: 'Arial, sans-serif', lineHeight: '1.6', color: '#333' }}>
66
+ <div
67
+ style={{
68
+ fontFamily: '"Segoe UI", "Helvetica Neue", sans-serif',
69
+ lineHeight: '1.6',
70
+ color: '#333',
71
+ }}
72
+ >
66
73
  <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
67
74
  <h1 style={{ color: '#1a1a1a', fontSize: '24px', marginBottom: '20px' }}>
68
75
  Modification de rendez-vous
@@ -154,7 +161,7 @@ export function MeetUpdateEmailTemplate({
154
161
  <strong>Description :</strong>
155
162
  <div
156
163
  style={{ marginTop: '10px' }}
157
- dangerouslySetInnerHTML={{ __html: description }}
164
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(description) }}
158
165
  />
159
166
  </div>
160
167
  )}
@@ -200,7 +207,7 @@ export function MeetUpdateEmailTemplate({
200
207
  borderTop: '1px solid #ddd',
201
208
  fontSize: '14px',
202
209
  }}
203
- dangerouslySetInnerHTML={{ __html: signature }}
210
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(signature) }}
204
211
  />
205
212
  )}
206
213
  </div>
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  interface PageHeaderProps {
2
4
  title: string;
3
5
  description?: string;
@@ -6,22 +8,24 @@ interface PageHeaderProps {
6
8
 
7
9
  export function PageHeader({ title, description, action }: Readonly<PageHeaderProps>) {
8
10
  return (
9
- <div className="border-b border-gray-200 bg-white px-4 py-4 sm:px-6 lg:px-8 lg:py-6">
10
- <div>
11
- {action ? (
12
- <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
13
- <div className="min-w-0 flex-1">
14
- <h1 className="text-xl font-bold text-gray-900 sm:text-2xl">{title}</h1>
15
- {description && <p className="mt-1 text-sm text-gray-600">{description}</p>}
11
+ <div className="border-b border-border bg-background/95 px-4 py-4 backdrop-blur-sm sm:px-6 lg:px-8 lg:py-6">
12
+ <div className="flex items-start gap-3">
13
+ <div className="min-w-0 flex-1">
14
+ {action ? (
15
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
16
+ <div className="min-w-0 flex-1">
17
+ <h1 className="text-xl font-bold text-foreground sm:text-2xl">{title}</h1>
18
+ {description && <p className="mt-1 text-sm text-muted-foreground">{description}</p>}
19
+ </div>
20
+ <div className="shrink-0">{action}</div>
16
21
  </div>
17
- <div className="shrink-0">{action}</div>
18
- </div>
19
- ) : (
20
- <>
21
- <h1 className="text-xl font-bold text-gray-900 sm:text-2xl">{title}</h1>
22
- {description && <p className="mt-1 text-sm text-gray-600">{description}</p>}
23
- </>
24
- )}
22
+ ) : (
23
+ <>
24
+ <h1 className="text-xl font-bold text-foreground sm:text-2xl">{title}</h1>
25
+ {description && <p className="mt-1 text-sm text-muted-foreground">{description}</p>}
26
+ </>
27
+ )}
28
+ </div>
25
29
  </div>
26
30
  </div>
27
31
  );
@@ -0,0 +1,94 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { useSession } from '@/lib/auth-client';
6
+ import { useUserRole } from '@/hooks/use-user-role';
7
+ import { PageLoader } from '@/components/skeleton';
8
+
9
+ const FALLBACK_ROUTES: { permission: string | string[]; href: string }[] = [
10
+ { permission: 'dashboard.view', href: '/dashboard' },
11
+ { permission: 'contacts.view_own', href: '/contacts' },
12
+ { permission: 'contacts.view_all', href: '/contacts' },
13
+ { permission: ['tasks.view_all', 'tasks.view_own'], href: '/agenda' },
14
+ ];
15
+
16
+ function getFirstAccessibleRoute(hasPermission: (p: string) => boolean): string {
17
+ for (const route of FALLBACK_ROUTES) {
18
+ const perms = Array.isArray(route.permission) ? route.permission : [route.permission];
19
+ if (perms.some((p) => hasPermission(p))) {
20
+ return route.href;
21
+ }
22
+ }
23
+ return '/contacts';
24
+ }
25
+
26
+ interface ProtectedPageProps {
27
+ children: React.ReactNode;
28
+ requiredPermission?: string | string[];
29
+ requireAll?: boolean;
30
+ redirectTo?: string;
31
+ fallback?: React.ReactNode;
32
+ }
33
+
34
+ export function ProtectedPage({
35
+ children,
36
+ requiredPermission,
37
+ requireAll = false,
38
+ redirectTo,
39
+ fallback,
40
+ }: ProtectedPageProps) {
41
+ const router = useRouter();
42
+ const { data: session, isPending: isSessionPending } = useSession();
43
+ const { hasPermission, isLoading } = useUserRole();
44
+
45
+ const checkAccess = (perms: string[]) => {
46
+ if (requireAll) return perms.every((p) => hasPermission(p));
47
+ return perms.some((p) => hasPermission(p));
48
+ };
49
+
50
+ useEffect(() => {
51
+ // Sans session (déconnecté), rediriger vers la page de connexion pour éviter la boucle
52
+ if (!isSessionPending && !session?.user) {
53
+ const signinUrl = `/signin?callbackUrl=${encodeURIComponent(typeof globalThis.window !== 'undefined' ? globalThis.window.location.pathname : '/')}`;
54
+ router.push(signinUrl);
55
+ return;
56
+ }
57
+ if (isLoading || !requiredPermission) return;
58
+
59
+ const permissions = Array.isArray(requiredPermission)
60
+ ? requiredPermission
61
+ : [requiredPermission];
62
+
63
+ if (!checkAccess(permissions)) {
64
+ router.push(redirectTo ?? getFirstAccessibleRoute(hasPermission));
65
+ }
66
+ }, [
67
+ requiredPermission,
68
+ requireAll,
69
+ redirectTo,
70
+ hasPermission,
71
+ isLoading,
72
+ isSessionPending,
73
+ session?.user,
74
+ router,
75
+ ]);
76
+
77
+ if (isSessionPending || !session?.user || isLoading) {
78
+ return (
79
+ fallback || <PageLoader text="Vérification des permissions..." className="min-h-screen" />
80
+ );
81
+ }
82
+
83
+ if (!requiredPermission) {
84
+ return <>{children}</>;
85
+ }
86
+
87
+ const permissions = Array.isArray(requiredPermission) ? requiredPermission : [requiredPermission];
88
+
89
+ if (!checkAccess(permissions)) {
90
+ return null;
91
+ }
92
+
93
+ return <>{children}</>;
94
+ }
@@ -1,3 +1,5 @@
1
+ import DOMPurify from 'isomorphic-dompurify';
2
+
1
3
  interface ResetPasswordEmailProps {
2
4
  code: string;
3
5
  signature?: string | null;
@@ -7,7 +9,7 @@ export function ResetPasswordEmailTemplate({ code, signature }: ResetPasswordEma
7
9
  return (
8
10
  <div
9
11
  style={{
10
- fontFamily: 'Arial, sans-serif',
12
+ fontFamily: '"Segoe UI", "Helvetica Neue", sans-serif',
11
13
  padding: '20px',
12
14
  maxWidth: '600px',
13
15
  margin: '0 auto',
@@ -71,7 +73,7 @@ export function ResetPasswordEmailTemplate({ code, signature }: ResetPasswordEma
71
73
  fontSize: '14px',
72
74
  lineHeight: '1.6',
73
75
  }}
74
- dangerouslySetInnerHTML={{ __html: signature }}
76
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(signature) }}
75
77
  />
76
78
  )}
77
79
  </div>
@@ -9,29 +9,22 @@ import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
9
9
  import { useSidebarContext } from '@/contexts/sidebar-context';
10
10
  import { useViewAs } from '@/contexts/view-as-context';
11
11
  import { ViewAsModal } from '@/components/view-as-modal';
12
- import {
13
- LayoutDashboard,
14
- Users,
15
- UserCog,
16
- Settings,
17
- Calendar as CalendarIcon,
18
- FileText,
19
- Eye,
20
- Zap,
21
- Columns3,
22
- X,
23
- } from 'lucide-react';
12
+ import { Eye, X, PanelLeftClose, PanelLeftOpen } from 'lucide-react';
24
13
  import { cn } from '@/lib/utils';
14
+ import { NAV_PAGES } from '@/config/nav-pages';
25
15
 
26
16
  export function Sidebar() {
27
17
  const pathname = usePathname();
28
18
  const { data: session } = useSession();
29
19
  const router = useRouter();
30
20
  const { isOpen: isMobileMenuOpen, setIsOpen: setIsMobileMenuOpen } = useMobileMenuContext();
31
- const { isCollapsed, isPinned, setIsCollapsed } = useSidebarContext();
21
+ const { isPinned, togglePin } = useSidebarContext();
32
22
  const { viewAsUser, isViewingAsOther } = useViewAs();
33
23
  const [showViewAsModal, setShowViewAsModal] = useState(false);
34
24
  const [isMounted, setIsMounted] = useState(false);
25
+ /** Ouvert au survol uniquement (desktop, sans modifier le pin) */
26
+ const [expandedByHover, setExpandedByHover] = useState(false);
27
+ const isSidebarExpanded = isPinned || expandedByHover;
35
28
 
36
29
  // Éviter l'erreur d'hydratation
37
30
  useEffect(() => {
@@ -39,36 +32,15 @@ export function Sidebar() {
39
32
  }, []);
40
33
 
41
34
  // Obtenir le rôle de l'utilisateur via le hook personnalisé
42
- const { isAdmin, isRealAdmin } = useUserRole();
35
+ const { isRealAdmin, hasPermission } = useUserRole();
43
36
 
44
- // Navigation principale (Dashboard section)
45
- const dashboardNav = useMemo(() => {
46
- const baseNav = [
47
- { name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
48
- { name: 'Contacts', href: '/contacts', icon: Users },
49
- { name: 'Agenda', href: '/agenda', icon: CalendarIcon },
50
- { name: 'Closing', href: '/closing', icon: Columns3 },
51
- { name: 'Automatisations', href: '/automatisation', icon: Zap },
52
- { name: 'Templates', href: '/templates', icon: FileText },
53
- ];
54
-
55
- // Ajouter la gestion des droits d'accès seulement pour les admins
56
- if (isAdmin) {
57
- baseNav.push({
58
- name: "Droits d'accès",
59
- href: '/users',
60
- icon: UserCog,
61
- });
62
- }
63
-
64
- baseNav.push({
65
- name: 'Paramètres',
66
- href: '/settings',
67
- icon: Settings,
68
- });
69
-
70
- return baseNav;
71
- }, [isAdmin]);
37
+ const dashboardNav = useMemo(
38
+ () =>
39
+ NAV_PAGES.filter(
40
+ (page) => !page.parentLabel && page.permissions.some((p) => hasPermission(p)),
41
+ ),
42
+ [hasPermission],
43
+ );
72
44
 
73
45
  const handleSignOut = async () => {
74
46
  await signOut();
@@ -84,7 +56,7 @@ export function Sidebar() {
84
56
  {/* Overlay for mobile */}
85
57
  {isMobileMenuOpen && (
86
58
  <div
87
- className="fixed inset-0 z-40 bg-gray-500/20 backdrop-blur-sm lg:hidden"
59
+ className="fixed inset-0 z-40 bg-foreground/10 backdrop-blur-sm lg:hidden"
88
60
  onClick={() => setIsMobileMenuOpen(false)}
89
61
  />
90
62
  )}
@@ -92,49 +64,39 @@ export function Sidebar() {
92
64
  {/* Sidebar */}
93
65
  <div
94
66
  className={cn(
95
- 'fixed top-0 left-0 z-40 flex h-screen flex-col border-r border-gray-200 bg-white transition-all duration-300 ease-in-out lg:relative lg:translate-x-0',
67
+ 'group fixed top-0 left-0 z-40 flex h-screen flex-col border-r border-sidebar-border bg-sidebar text-sidebar-foreground shadow-(--shadow-card) transition-all duration-300 ease-(--ease-standard) lg:relative lg:translate-x-0',
96
68
  isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0',
97
- isCollapsed && !isPinned ? 'w-64 lg:w-16' : 'w-64 lg:w-64',
69
+ !isSidebarExpanded ? 'w-64 lg:w-16' : 'w-64 lg:w-64',
98
70
  )}
99
71
  onMouseEnter={() => {
100
- if (typeof window !== 'undefined' && window.innerWidth >= 1024) {
101
- if (!isPinned && isCollapsed) {
102
- setIsCollapsed(false);
103
- }
72
+ if (typeof globalThis.window !== 'undefined' && globalThis.window.innerWidth >= 1024 && !isPinned) {
73
+ setExpandedByHover(true);
104
74
  }
105
75
  }}
106
76
  onMouseLeave={() => {
107
- if (typeof window !== 'undefined' && window.innerWidth >= 1024) {
108
- if (!isPinned && !isCollapsed) {
109
- setIsCollapsed(true);
110
- }
77
+ if (typeof globalThis.window !== 'undefined' && globalThis.window.innerWidth >= 1024) {
78
+ setExpandedByHover(false);
111
79
  }
112
80
  }}
113
81
  >
114
- {/* Bouton fermer - Mobile seulement */}
115
- <div className="flex h-16 items-center justify-end border-b border-gray-200 px-4 lg:hidden">
116
- <button
117
- onClick={() => setIsMobileMenuOpen(false)}
118
- className="cursor-pointer rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100"
119
- aria-label="Close menu"
120
- >
121
- <X className="h-5 w-5" />
122
- </button>
123
- </div>
124
-
125
82
  {/* Navigation principale */}
126
83
  <nav className="flex-1 space-y-6 overflow-y-auto py-4">
127
84
  {/* Section Dashboard */}
128
- <div className={cn('px-3', isCollapsed && !isPinned && 'lg:px-2')}>
129
- {(!isCollapsed || isPinned) && (
130
- <h2 className="mb-2 px-3 text-xs font-semibold tracking-wider text-nowrap text-gray-500 uppercase">
131
- CRM Template
132
- </h2>
133
- )}
85
+ <div className={cn('px-3', !isSidebarExpanded && 'lg:px-2')}>
86
+ <div className="flex items-center justify-end">
87
+ <button
88
+ onClick={() => setIsMobileMenuOpen(false)}
89
+ className="cursor-pointer rounded-lg p-2 text-sidebar-foreground/70 transition-colors duration-200 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground lg:hidden"
90
+ aria-label="Close menu"
91
+ >
92
+ <X className="h-5 w-5" />
93
+ </button>
94
+ </div>
134
95
  <div className="space-y-1">
135
96
  {dashboardNav.map((item) => {
136
97
  const isActive = pathname === item.href;
137
98
  const Icon = item.icon;
99
+ const displayName = item.name;
138
100
  return (
139
101
  <Link
140
102
  key={item.name}
@@ -142,16 +104,16 @@ export function Sidebar() {
142
104
  onClick={handleLinkClick}
143
105
  className={cn(
144
106
  'flex items-center gap-3 rounded-lg py-2 text-sm font-medium transition-colors',
145
- isCollapsed && !isPinned ? 'px-3 lg:justify-center lg:px-2' : 'px-3',
107
+ !isSidebarExpanded ? 'px-3 lg:justify-center lg:px-2' : 'px-3',
146
108
  isActive
147
- ? 'bg-indigo-50 text-indigo-600'
148
- : 'text-gray-700 hover:bg-gray-50 hover:text-gray-900',
109
+ ? 'bg-sidebar-primary text-sidebar-primary-foreground shadow-sm'
110
+ : 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
149
111
  )}
150
- title={isCollapsed && !isPinned ? item.name : undefined}
112
+ title={!isSidebarExpanded ? displayName : undefined}
151
113
  >
152
114
  <Icon className="h-5 w-5 shrink-0" />
153
- {(!isCollapsed || isPinned) && (
154
- <span className="whitespace-nowrap">{item.name}</span>
115
+ {isSidebarExpanded && (
116
+ <span className="whitespace-nowrap">{displayName}</span>
155
117
  )}
156
118
  </Link>
157
119
  );
@@ -164,18 +126,18 @@ export function Sidebar() {
164
126
  {isRealAdmin && (
165
127
  <div
166
128
  className={cn(
167
- 'border-t border-gray-200 transition-all duration-300',
168
- isCollapsed && !isPinned ? 'p-3 lg:p-2' : 'p-3',
129
+ 'border-t border-sidebar-border transition-all duration-300',
130
+ !isSidebarExpanded ? 'p-3 lg:p-2' : 'p-3',
169
131
  )}
170
132
  >
171
- {!isCollapsed || isPinned ? (
133
+ {isSidebarExpanded ? (
172
134
  <button
173
135
  onClick={() => setShowViewAsModal(true)}
174
136
  className={cn(
175
137
  'w-full cursor-pointer rounded-lg border-2 p-3 text-left transition-all',
176
138
  isViewingAsOther
177
- ? 'border-indigo-600 bg-indigo-600 text-white hover:border-indigo-700 hover:bg-indigo-700'
178
- : 'border-gray-300 bg-white text-gray-900 hover:border-indigo-300 hover:bg-indigo-50',
139
+ ? 'border-sidebar-primary bg-sidebar-primary text-sidebar-primary-foreground hover:opacity-95'
140
+ : 'border-sidebar-border bg-sidebar text-sidebar-foreground hover:border-sidebar-ring hover:bg-sidebar-accent',
179
141
  )}
180
142
  aria-label="Changer de vue"
181
143
  >
@@ -183,7 +145,9 @@ export function Sidebar() {
183
145
  <div
184
146
  className={cn(
185
147
  'flex h-10 w-10 shrink-0 items-center justify-center rounded-full',
186
- isViewingAsOther ? 'bg-white/20 text-white' : 'bg-indigo-100 text-indigo-600',
148
+ isViewingAsOther
149
+ ? 'bg-sidebar-primary-foreground/20 text-sidebar-primary-foreground'
150
+ : 'bg-sidebar-accent text-sidebar-accent-foreground',
187
151
  )}
188
152
  >
189
153
  {!isMounted
@@ -196,7 +160,9 @@ export function Sidebar() {
196
160
  <p
197
161
  className={cn(
198
162
  'text-xs font-medium',
199
- isViewingAsOther ? 'text-white/80' : 'text-gray-500',
163
+ isViewingAsOther
164
+ ? 'text-sidebar-primary-foreground/80'
165
+ : 'text-sidebar-foreground/70',
200
166
  )}
201
167
  >
202
168
  {isViewingAsOther ? 'Vue:' : 'Ma vue'}
@@ -218,8 +184,8 @@ export function Sidebar() {
218
184
  className={cn(
219
185
  'w-full cursor-pointer rounded-lg p-2 transition-colors',
220
186
  isViewingAsOther
221
- ? 'bg-indigo-600 text-white hover:bg-indigo-700'
222
- : 'text-gray-500 hover:bg-gray-100',
187
+ ? 'bg-sidebar-primary text-sidebar-primary-foreground hover:opacity-95'
188
+ : 'text-sidebar-foreground/70 hover:bg-sidebar-accent',
223
189
  )}
224
190
  title="Changer de vue"
225
191
  aria-label="Changer de vue"
@@ -232,43 +198,75 @@ export function Sidebar() {
232
198
  </div>
233
199
  )}
234
200
 
201
+ {/* Bouton Réduire / Développer la navigation (desktop uniquement) */}
202
+ <div
203
+ className={cn(
204
+ 'hidden border-t border-sidebar-border lg:block',
205
+ !isSidebarExpanded ? 'p-3 lg:p-2' : 'p-3',
206
+ )}
207
+ >
208
+ <button
209
+ onClick={togglePin}
210
+ className={cn(
211
+ 'flex w-full cursor-pointer items-center gap-3 rounded-lg py-2 text-sm font-medium text-sidebar-foreground/70 transition-colors duration-200 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
212
+ !isSidebarExpanded ? 'justify-center px-2' : 'px-3',
213
+ )}
214
+ title={isPinned ? 'Réduire la navigation' : 'Développer la navigation'}
215
+ aria-label={isPinned ? 'Réduire la navigation' : 'Développer la navigation'}
216
+ >
217
+ {isPinned ? (
218
+ <>
219
+ <PanelLeftClose className="h-5 w-5 shrink-0" />
220
+ <span className="whitespace-nowrap">Réduire la navigation</span>
221
+ </>
222
+ ) : (
223
+ <div className="flex gap-2">
224
+ <PanelLeftOpen className="h-5 w-5 shrink-0" />
225
+ <span className="hidden whitespace-nowrap group-hover:block">
226
+ Développer la navigation
227
+ </span>
228
+ </div>
229
+ )}
230
+ </button>
231
+ </div>
232
+
235
233
  {/* User Profile */}
236
234
  <div
237
235
  className={cn(
238
- 'border-t border-gray-200 transition-all duration-300',
239
- isCollapsed && !isPinned ? 'p-4 lg:p-2' : 'p-4',
236
+ 'border-t border-sidebar-border transition-all duration-300',
237
+ !isSidebarExpanded ? 'p-4 lg:p-2' : 'p-4',
240
238
  )}
241
239
  >
242
- {!isCollapsed || isPinned ? (
240
+ {isSidebarExpanded ? (
243
241
  <>
244
242
  <div className="flex items-center gap-3">
245
- <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-indigo-100 text-indigo-600">
243
+ <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-sidebar-accent text-sidebar-accent-foreground">
246
244
  {!isMounted ? 'U' : session?.user?.name?.[0]?.toUpperCase() || 'U'}
247
245
  </div>
248
246
  <div className="min-w-0 flex-1">
249
- <p className="truncate text-sm font-medium text-gray-900">
247
+ <p className="truncate text-sm font-medium text-sidebar-foreground">
250
248
  {!isMounted ? 'Utilisateur' : session?.user?.name || 'Utilisateur'}
251
249
  </p>
252
- <p className="truncate text-xs text-gray-500">
250
+ <p className="truncate text-xs text-sidebar-foreground/70">
253
251
  {!isMounted ? '' : session?.user?.email}
254
252
  </p>
255
253
  </div>
256
254
  </div>
257
255
  <button
258
256
  onClick={handleSignOut}
259
- className="mt-3 w-full cursor-pointer rounded-lg bg-gray-100 px-3 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200"
257
+ className="mt-3 w-full cursor-pointer rounded-lg bg-sidebar-accent px-3 py-2 text-sm font-medium text-sidebar-accent-foreground transition-colors duration-200 hover:bg-sidebar-primary hover:text-sidebar-primary-foreground"
260
258
  >
261
259
  Déconnexion
262
260
  </button>
263
261
  </>
264
262
  ) : (
265
263
  <div className="flex flex-col items-center gap-2">
266
- <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-indigo-100 text-indigo-600">
264
+ <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-sidebar-accent text-sidebar-accent-foreground">
267
265
  {!isMounted ? 'U' : session?.user?.name?.[0]?.toUpperCase() || 'U'}
268
266
  </div>
269
267
  <button
270
268
  onClick={handleSignOut}
271
- className="cursor-pointer rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100"
269
+ className="cursor-pointer rounded-lg p-2 text-sidebar-foreground/70 transition-colors duration-200 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
272
270
  title="Déconnexion"
273
271
  aria-label="Déconnexion"
274
272
  >