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
@@ -3,10 +3,14 @@
3
3
  import { useState, useEffect, useRef } from 'react';
4
4
  import { PageHeader } from '@/components/page-header';
5
5
  import { Plus, Edit, Trash2, Mail, MessageSquare, FileText, X } from 'lucide-react';
6
- import { Editor, type DefaultTemplateRef } from '@/components/editor';
7
- import { AVAILABLE_VARIABLES } from '@/lib/template-variables';
6
+ import { LazyEditor as Editor, type DefaultTemplateRef } from '@/components/lazy-editor';
7
+ import { AVAILABLE_VARIABLES, VARIABLE_SECTIONS } from '@/lib/template-variables';
8
8
  import { TemplatesPageSkeleton } from '@/components/skeleton';
9
9
  import { cn } from '@/lib/utils';
10
+ import { ProtectedPage } from '@/components/protected-page';
11
+ import { useConfirm } from '@/hooks/use-confirm';
12
+ import { useUserRole } from '@/hooks/use-user-role';
13
+ import { useAppToast } from '@/contexts/app-toast-context';
10
14
 
11
15
  interface Template {
12
16
  id: string;
@@ -19,6 +23,15 @@ interface Template {
19
23
  }
20
24
 
21
25
  export default function TemplatesPage() {
26
+ const { confirm, ConfirmDialog } = useConfirm();
27
+ const { hasPermission } = useUserRole();
28
+ const toast = useAppToast();
29
+
30
+ // Permissions
31
+ const canCreate = hasPermission('templates.create');
32
+ const canEdit = hasPermission('templates.edit');
33
+ const canDelete = hasPermission('templates.delete');
34
+
22
35
  const [templates, setTemplates] = useState<Template[]>([]);
23
36
  const [loading, setLoading] = useState(true);
24
37
  const [showModal, setShowModal] = useState(false);
@@ -61,6 +74,20 @@ export default function TemplatesPage() {
61
74
  fetchTemplates();
62
75
  }, [filterType]);
63
76
 
77
+ useEffect(() => {
78
+ if (error) {
79
+ toast.error(error);
80
+ setError('');
81
+ }
82
+ }, [error, toast]);
83
+
84
+ useEffect(() => {
85
+ if (success) {
86
+ toast.success(success);
87
+ setSuccess('');
88
+ }
89
+ }, [success, toast]);
90
+
64
91
  const handleSubmit = async (e: React.FormEvent) => {
65
92
  e.preventDefault();
66
93
  setError('');
@@ -131,7 +158,16 @@ export default function TemplatesPage() {
131
158
  };
132
159
 
133
160
  const handleDelete = async (id: string) => {
134
- if (!confirm('Êtes-vous sûr de vouloir supprimer ce template ?')) {
161
+ const confirmed = await confirm({
162
+ title: 'Supprimer le template',
163
+ description:
164
+ 'Êtes-vous sûr de vouloir supprimer ce template ? Cette action est irréversible.',
165
+ confirmText: 'Supprimer',
166
+ cancelText: 'Annuler',
167
+ variant: 'destructive',
168
+ });
169
+
170
+ if (!confirmed) {
135
171
  return;
136
172
  }
137
173
 
@@ -217,13 +253,26 @@ export default function TemplatesPage() {
217
253
  const getTypeColor = (type: string) => {
218
254
  switch (type) {
219
255
  case 'EMAIL':
220
- return 'bg-blue-100 text-blue-800 border-blue-200';
256
+ return 'bg-blue-50 text-blue-700 border-blue-200';
257
+ case 'SMS':
258
+ return 'bg-emerald-50 text-emerald-700 border-emerald-200';
259
+ case 'NOTE':
260
+ return 'bg-violet-50 text-violet-700 border-violet-200';
261
+ default:
262
+ return 'bg-gray-50 text-gray-700 border-gray-200';
263
+ }
264
+ };
265
+
266
+ const getTypeIconColor = (type: string) => {
267
+ switch (type) {
268
+ case 'EMAIL':
269
+ return 'text-blue-600';
221
270
  case 'SMS':
222
- return 'bg-green-100 text-green-800 border-green-200';
271
+ return 'text-emerald-600';
223
272
  case 'NOTE':
224
- return 'bg-purple-100 text-purple-800 border-purple-200';
273
+ return 'text-violet-600';
225
274
  default:
226
- return 'bg-gray-100 text-gray-800 border-gray-200';
275
+ return 'text-gray-600';
227
276
  }
228
277
  };
229
278
 
@@ -234,334 +283,485 @@ export default function TemplatesPage() {
234
283
  }
235
284
 
236
285
  return (
237
- <div className="h-full">
238
- <PageHeader
239
- title="Templates"
240
- description="Gérez vos templates d'emails, SMS et notes"
241
- action={
242
- <button
243
- onClick={handleNewTemplate}
244
- className="cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700"
245
- >
246
- <Plus className="mr-2 inline h-4 w-4" />
247
- Nouveau template
248
- </button>
249
- }
250
- />
251
-
252
- <div className="p-4 sm:p-6 lg:p-8">
253
- {error && <div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>}
254
- {success && (
255
- <div className="mb-4 rounded-lg bg-green-50 p-4 text-sm text-green-600">{success}</div>
256
- )}
257
-
258
- {/* Filtres */}
259
- <div className="mb-6 flex flex-wrap gap-2">
260
- <button
261
- onClick={() => setFilterType('ALL')}
262
- className={cn(
263
- 'rounded-lg px-4 py-2 text-sm font-medium transition-colors',
264
- filterType === 'ALL'
265
- ? 'bg-indigo-600 text-white'
266
- : 'bg-white text-gray-700 hover:bg-gray-50',
267
- )}
268
- >
269
- Tous
270
- </button>
271
- <button
272
- onClick={() => setFilterType('EMAIL')}
273
- className={cn(
274
- 'rounded-lg px-4 py-2 text-sm font-medium transition-colors',
275
- filterType === 'EMAIL'
276
- ? 'bg-indigo-600 text-white'
277
- : 'bg-white text-gray-700 hover:bg-gray-50',
278
- )}
279
- >
280
- <Mail className="mr-2 inline h-4 w-4" />
281
- Emails
282
- </button>
283
- <button
284
- onClick={() => setFilterType('SMS')}
285
- className={cn(
286
- 'rounded-lg px-4 py-2 text-sm font-medium transition-colors',
287
- filterType === 'SMS'
288
- ? 'bg-indigo-600 text-white'
289
- : 'bg-white text-gray-700 hover:bg-gray-50',
290
- )}
291
- >
292
- <MessageSquare className="mr-2 inline h-4 w-4" />
293
- SMS
294
- </button>
295
- <button
296
- onClick={() => setFilterType('NOTE')}
297
- className={cn(
298
- 'rounded-lg px-4 py-2 text-sm font-medium transition-colors',
299
- filterType === 'NOTE'
300
- ? 'bg-indigo-600 text-white'
301
- : 'bg-white text-gray-700 hover:bg-gray-50',
302
- )}
303
- >
304
- <FileText className="mr-2 inline h-4 w-4" />
305
- Notes
306
- </button>
307
- </div>
308
-
309
- {/* Liste des templates */}
310
- {filteredTemplates.length === 0 ? (
311
- <div className="rounded-lg bg-white p-12 text-center shadow">
312
- <div className="text-4xl">📝</div>
313
- <h2 className="mt-4 text-lg font-semibold text-gray-900">Aucun template</h2>
314
- <p className="mt-2 text-sm text-gray-600">
315
- {filterType === 'ALL'
316
- ? 'Commencez par créer votre premier template'
317
- : `Aucun template de type ${getTypeLabel(filterType)}`}
318
- </p>
286
+ <ProtectedPage requiredPermission="templates.view">
287
+ <div className="h-full">
288
+ <PageHeader
289
+ title="Templates"
290
+ description="Gérez vos templates d'emails, SMS et notes"
291
+ action={
292
+ canCreate ? (
293
+ <button
294
+ onClick={handleNewTemplate}
295
+ className="cursor-pointer rounded-xl bg-primary px-5 py-2.5 text-sm font-semibold text-primary-foreground shadow-(--shadow-card) transition-all duration-200 hover:bg-primary/90"
296
+ >
297
+ <Plus className="mr-2 inline h-4 w-4" />
298
+ Nouveau template
299
+ </button>
300
+ ) : undefined
301
+ }
302
+ />
303
+
304
+ <div className="p-4 sm:p-6 lg:p-8">
305
+ {/* Filtres modernes */}
306
+ <div className="mb-8 flex flex-wrap gap-3">
319
307
  <button
320
- onClick={handleNewTemplate}
321
- className="mt-6 cursor-pointer rounded-lg bg-indigo-600 px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700"
308
+ onClick={() => setFilterType('ALL')}
309
+ className={cn(
310
+ 'group cursor-pointer rounded-xl px-5 py-2.5 text-sm font-semibold transition-all duration-200',
311
+ filterType === 'ALL'
312
+ ? 'bg-primary text-primary-foreground shadow-(--shadow-card)'
313
+ : 'border border-border bg-card text-muted-foreground shadow-sm hover:bg-muted hover:text-foreground',
314
+ )}
322
315
  >
323
- Créer un template
316
+ <span className="flex items-center gap-2">
317
+ <span>Tous</span>
318
+ {filterType === 'ALL' && (
319
+ <span className="ml-1 rounded-full bg-white/20 px-2 py-0.5 text-xs">
320
+ {templates.length}
321
+ </span>
322
+ )}
323
+ </span>
324
+ </button>
325
+ <button
326
+ onClick={() => setFilterType('EMAIL')}
327
+ className={cn(
328
+ 'group cursor-pointer rounded-xl px-5 py-2.5 text-sm font-semibold transition-all duration-200',
329
+ filterType === 'EMAIL'
330
+ ? 'bg-linear-to-r from-blue-600 to-blue-700 text-white shadow-md shadow-blue-500/30'
331
+ : 'border border-border bg-card text-muted-foreground shadow-sm hover:bg-muted hover:text-foreground',
332
+ )}
333
+ >
334
+ <span className="flex items-center gap-2">
335
+ <Mail className="h-4 w-4" />
336
+ <span>Emails</span>
337
+ {filterType === 'EMAIL' && (
338
+ <span className="ml-1 rounded-full bg-white/20 px-2 py-0.5 text-xs">
339
+ {templates.filter((t) => t.type === 'EMAIL').length}
340
+ </span>
341
+ )}
342
+ </span>
343
+ </button>
344
+ <button
345
+ onClick={() => setFilterType('SMS')}
346
+ className={cn(
347
+ 'group cursor-pointer rounded-xl px-5 py-2.5 text-sm font-semibold transition-all duration-200',
348
+ filterType === 'SMS'
349
+ ? 'bg-linear-to-r from-emerald-600 to-emerald-700 text-white shadow-md shadow-emerald-500/30'
350
+ : 'border border-border bg-card text-muted-foreground shadow-sm hover:bg-muted hover:text-foreground',
351
+ )}
352
+ >
353
+ <span className="flex items-center gap-2">
354
+ <MessageSquare className="h-4 w-4" />
355
+ <span>SMS</span>
356
+ {filterType === 'SMS' && (
357
+ <span className="ml-1 rounded-full bg-white/20 px-2 py-0.5 text-xs">
358
+ {templates.filter((t) => t.type === 'SMS').length}
359
+ </span>
360
+ )}
361
+ </span>
362
+ </button>
363
+ <button
364
+ onClick={() => setFilterType('NOTE')}
365
+ className={cn(
366
+ 'group cursor-pointer rounded-xl px-5 py-2.5 text-sm font-semibold transition-all duration-200',
367
+ filterType === 'NOTE'
368
+ ? 'bg-linear-to-r from-violet-600 to-violet-700 text-white shadow-md shadow-violet-500/30'
369
+ : 'border border-border bg-card text-muted-foreground shadow-sm hover:bg-muted hover:text-foreground',
370
+ )}
371
+ >
372
+ <span className="flex items-center gap-2">
373
+ <FileText className="h-4 w-4" />
374
+ <span>Notes</span>
375
+ {filterType === 'NOTE' && (
376
+ <span className="ml-1 rounded-full bg-white/20 px-2 py-0.5 text-xs">
377
+ {templates.filter((t) => t.type === 'NOTE').length}
378
+ </span>
379
+ )}
380
+ </span>
324
381
  </button>
325
382
  </div>
326
- ) : (
327
- <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
328
- {filteredTemplates.map((template) => (
329
- <div
330
- key={template.id}
331
- className="rounded-lg border border-gray-200 bg-white p-4 shadow transition-shadow hover:shadow-md"
383
+
384
+ {/* Liste des templates */}
385
+ {filteredTemplates.length === 0 ? (
386
+ <div className="rounded-2xl border border-dashed border-border bg-linear-to-br from-muted to-card p-16 text-center shadow-sm">
387
+ <div className="mx-auto flex h-20 w-20 items-center justify-center rounded-full bg-primary/15">
388
+ <FileText className="h-10 w-10 text-primary" />
389
+ </div>
390
+ <h2 className="mt-6 text-xl font-bold text-foreground">Aucun template</h2>
391
+ <p className="mt-2 text-sm text-muted-foreground">
392
+ {filterType === 'ALL'
393
+ ? 'Commencez par créer votre premier template pour gagner du temps'
394
+ : `Aucun template de type ${getTypeLabel(filterType)} trouvé`}
395
+ </p>
396
+ <button
397
+ onClick={handleNewTemplate}
398
+ className="mt-8 cursor-pointer rounded-xl bg-primary px-6 py-3 text-sm font-semibold text-primary-foreground shadow-(--shadow-card) transition-all duration-200 hover:bg-primary/90"
332
399
  >
333
- <div className="flex items-start justify-between">
334
- <div className="flex-1">
335
- <div className="flex items-center gap-2">
336
- {getTypeIcon(template.type)}
337
- <h3 className="text-lg font-semibold text-gray-900">{template.name}</h3>
400
+ <Plus className="mr-2 inline h-4 w-4" />
401
+ Créer un template
402
+ </button>
403
+ </div>
404
+ ) : (
405
+ <div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
406
+ {filteredTemplates.map((template) => (
407
+ <div
408
+ key={template.id}
409
+ className="group relative overflow-hidden rounded-2xl border border-border bg-card p-6 shadow-(--shadow-card) transition-all duration-300 hover:border-primary/30"
410
+ >
411
+ {/* Badge de type avec icône */}
412
+ <div className="mb-4 flex items-center justify-between">
413
+ <div className="flex items-center gap-3">
414
+ <div
415
+ className={cn(
416
+ 'flex h-10 w-10 items-center justify-center rounded-xl',
417
+ template.type === 'EMAIL'
418
+ ? 'bg-blue-100'
419
+ : template.type === 'SMS'
420
+ ? 'bg-emerald-100'
421
+ : 'bg-violet-100',
422
+ )}
423
+ >
424
+ <span className={cn('h-5 w-5', getTypeIconColor(template.type))}>
425
+ {getTypeIcon(template.type)}
426
+ </span>
427
+ </div>
428
+ <div>
429
+ <h3 className="text-lg font-bold text-foreground transition-colors group-hover:text-primary">
430
+ {template.name}
431
+ </h3>
432
+ <span
433
+ className={cn(
434
+ 'mt-1 inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1 text-xs font-semibold',
435
+ getTypeColor(template.type),
436
+ )}
437
+ >
438
+ {getTypeLabel(template.type)}
439
+ </span>
440
+ </div>
338
441
  </div>
339
- <span
340
- className={cn(
341
- 'mt-2 inline-flex rounded-full border px-2 py-1 text-xs font-medium',
342
- getTypeColor(template.type),
343
- )}
344
- >
345
- {getTypeLabel(template.type)}
346
- </span>
347
- {template.type === 'EMAIL' && template.subject && (
348
- <p className="mt-2 text-sm text-gray-600">
349
- <strong>Sujet:</strong> {template.subject}
350
- </p>
351
- )}
352
- <p className="mt-2 line-clamp-3 text-sm text-gray-500">
353
- {template.content.replace(/<[^>]+>/g, '').substring(0, 100)}
354
- {template.content.length > 100 && '...'}
442
+ </div>
443
+
444
+ {/* Sujet pour les emails */}
445
+ {template.type === 'EMAIL' && template.subject && (
446
+ <div className="mb-3 rounded-lg bg-muted p-3">
447
+ <p className="text-xs font-medium text-muted-foreground">Sujet</p>
448
+ <p className="mt-1 text-sm font-semibold text-foreground">{template.subject}</p>
449
+ </div>
450
+ )}
451
+
452
+ {/* Aperçu du contenu */}
453
+ <div className="mb-4">
454
+ <p className="line-clamp-3 text-sm leading-relaxed text-muted-foreground">
455
+ {template.content.replace(/<[^>]+>/g, '').substring(0, 120)}
456
+ {template.content.replace(/<[^>]+>/g, '').length > 120 && '...'}
355
457
  </p>
356
458
  </div>
459
+
460
+ {/* Actions */}
461
+ {(canEdit || canDelete) && (
462
+ <div className="flex items-center justify-end gap-2 border-t border-border pt-4">
463
+ {canEdit && (
464
+ <button
465
+ onClick={() => handleEdit(template)}
466
+ className="cursor-pointer rounded-lg p-2 text-muted-foreground transition-all duration-200 hover:bg-primary/15 hover:text-primary"
467
+ title="Modifier"
468
+ >
469
+ <Edit className="h-4 w-4" />
470
+ </button>
471
+ )}
472
+ {canDelete && (
473
+ <button
474
+ onClick={() => handleDelete(template.id)}
475
+ className="cursor-pointer rounded-lg p-2 text-gray-600 transition-all hover:bg-red-50 hover:text-red-600"
476
+ title="Supprimer"
477
+ >
478
+ <Trash2 className="h-4 w-4" />
479
+ </button>
480
+ )}
481
+ </div>
482
+ )}
357
483
  </div>
358
- <div className="mt-4 flex items-center justify-end gap-2">
359
- <button
360
- onClick={() => handleEdit(template)}
361
- className="cursor-pointer rounded-lg p-2 text-gray-600 transition-colors hover:bg-gray-100"
362
- title="Modifier"
363
- >
364
- <Edit className="h-4 w-4" />
365
- </button>
484
+ ))}
485
+ </div>
486
+ )}
487
+ </div>
488
+
489
+ {/* Modal de création/édition */}
490
+ {showModal && (
491
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-foreground/30 p-4 backdrop-blur-sm sm:p-6">
492
+ <div className="flex max-h-[90vh] w-full max-w-5xl flex-col overflow-hidden rounded-2xl border border-border bg-card shadow-(--shadow-dropdown)">
493
+ {/* En-tête fixe */}
494
+ <div className="shrink-0 border-b border-border bg-linear-to-r from-muted to-card px-6 py-5 sm:px-8 sm:py-6">
495
+ <div className="flex items-center justify-between">
496
+ <div>
497
+ <h2 className="text-2xl font-bold text-foreground">
498
+ {editingTemplate ? 'Modifier le template' : 'Nouveau template'}
499
+ </h2>
500
+ <p className="mt-1 text-sm text-muted-foreground">
501
+ {editingTemplate
502
+ ? 'Modifiez les informations du template'
503
+ : 'Créez un nouveau template réutilisable'}
504
+ </p>
505
+ </div>
366
506
  <button
367
- onClick={() => handleDelete(template.id)}
368
- className="cursor-pointer rounded-lg p-2 text-red-600 transition-colors hover:bg-red-50"
369
- title="Supprimer"
507
+ type="button"
508
+ onClick={() => {
509
+ setShowModal(false);
510
+ setEditingTemplate(null);
511
+ setError('');
512
+ }}
513
+ className="cursor-pointer rounded-xl p-2 text-muted-foreground transition-all duration-200 hover:bg-muted hover:text-foreground"
370
514
  >
371
- <Trash2 className="h-4 w-4" />
515
+ <X className="h-6 w-6" />
372
516
  </button>
373
517
  </div>
374
518
  </div>
375
- ))}
376
- </div>
377
- )}
378
- </div>
379
519
 
380
- {/* Modal de création/édition */}
381
- {showModal && (
382
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
383
- <div className="flex max-h-[90vh] w-full max-w-5xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
384
- {/* En-tête fixe */}
385
- <div className="shrink-0 border-b border-gray-100 pb-4">
386
- <div className="flex items-center justify-between">
387
- <h2 className="text-xl font-bold text-gray-900 sm:text-2xl">
388
- {editingTemplate ? 'Modifier le template' : 'Nouveau template'}
389
- </h2>
390
- <button
391
- type="button"
392
- onClick={() => {
393
- setShowModal(false);
394
- setEditingTemplate(null);
395
- setError('');
396
- }}
397
- className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100"
398
- >
399
- <X className="h-6 w-6" />
400
- </button>
401
- </div>
402
- </div>
403
-
404
- {/* Contenu scrollable */}
405
- <form
406
- id="template-form"
407
- onSubmit={handleSubmit}
408
- className="flex-1 space-y-6 overflow-y-auto pt-4 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
409
- >
410
- <div>
411
- <label className="block text-sm font-medium text-gray-700">Nom du template *</label>
412
- <input
413
- type="text"
414
- required
415
- value={formData.name}
416
- onChange={(e) => setFormData({ ...formData, name: e.target.value })}
417
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
418
- placeholder="Ex: Email de bienvenue"
419
- />
420
- </div>
421
-
422
- <div>
423
- <label className="block text-sm font-medium text-gray-700">Type *</label>
424
- <select
425
- required
426
- value={formData.type}
427
- onChange={(e) => {
428
- setFormData({
429
- ...formData,
430
- type: e.target.value as 'EMAIL' | 'SMS' | 'NOTE',
431
- subject: e.target.value === 'EMAIL' ? formData.subject : '',
432
- });
433
- }}
434
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
435
- >
436
- <option value="EMAIL">Email</option>
437
- <option value="SMS">SMS</option>
438
- <option value="NOTE">Note</option>
439
- </select>
440
- </div>
441
-
442
- {formData.type === 'EMAIL' && (
520
+ {/* Contenu scrollable */}
521
+ <form
522
+ id="template-form"
523
+ onSubmit={handleSubmit}
524
+ className="flex-1 space-y-6 overflow-y-auto px-6 py-6 [-ms-overflow-style:none] [scrollbar-width:none] sm:px-8 sm:py-8 [&::-webkit-scrollbar]:hidden"
525
+ >
443
526
  <div>
444
- <label className="block text-sm font-medium text-gray-700">Sujet *</label>
527
+ <label className="block text-sm font-semibold text-foreground">
528
+ Nom du template <span className="text-red-500">*</span>
529
+ </label>
445
530
  <input
446
531
  type="text"
447
532
  required
448
- value={formData.subject}
449
- onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
450
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
451
- placeholder="Ex: Bienvenue dans notre CRM"
533
+ value={formData.name}
534
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
535
+ className="mt-2 block w-full rounded-xl border border-border bg-background px-4 py-3 text-foreground shadow-sm transition-all focus:border-primary/50 focus:ring-2 focus:ring-primary/20 focus:outline-none"
536
+ placeholder="Ex: Email de bienvenue"
452
537
  />
453
538
  </div>
454
- )}
455
539
 
456
- <div>
457
- <label className="block text-sm font-medium text-gray-700">Contenu *</label>
458
- {formData.type === 'EMAIL' || formData.type === 'NOTE' ? (
459
- <div className="mt-1">
460
- <Editor
461
- ref={formData.type === 'EMAIL' ? emailEditorRef : noteEditorRef}
462
- onReady={(methods) => {
463
- if (formData.type === 'EMAIL') {
464
- emailEditorRef.current = methods;
465
- } else {
466
- noteEditorRef.current = methods;
467
- }
468
- if (formData.content) {
469
- methods.injectHTML(formData.content);
470
- }
471
- }}
472
- />
473
- </div>
474
- ) : (
540
+ <div>
541
+ <label className="block text-sm font-semibold text-foreground">
542
+ Type <span className="text-red-500">*</span>
543
+ </label>
544
+ <select
545
+ required
546
+ value={formData.type}
547
+ onChange={(e) => {
548
+ setFormData({
549
+ ...formData,
550
+ type: e.target.value as 'EMAIL' | 'SMS' | 'NOTE',
551
+ subject: e.target.value === 'EMAIL' ? formData.subject : '',
552
+ });
553
+ }}
554
+ className="mt-2 block w-full rounded-xl border border-border bg-background px-4 py-3 text-foreground shadow-sm transition-all focus:border-primary/50 focus:ring-2 focus:ring-primary/20 focus:outline-none"
555
+ >
556
+ <option value="EMAIL">Email</option>
557
+ <option value="SMS">SMS</option>
558
+ <option value="NOTE">Note</option>
559
+ </select>
560
+ </div>
561
+
562
+ {formData.type === 'EMAIL' && (
475
563
  <div>
476
- <textarea
477
- ref={smsTextareaRef}
564
+ <label className="block text-sm font-semibold text-foreground">
565
+ Sujet <span className="text-red-500">*</span>
566
+ </label>
567
+ <input
568
+ type="text"
478
569
  required
479
- value={formData.content}
480
- onChange={(e) => setFormData({ ...formData, content: e.target.value })}
481
- rows={6}
482
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
483
- placeholder="Contenu du SMS..."
570
+ value={formData.subject}
571
+ onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
572
+ className="mt-2 block w-full rounded-xl border border-border bg-background px-4 py-3 text-foreground shadow-sm transition-all focus:border-primary/50 focus:ring-2 focus:ring-primary/20 focus:outline-none"
573
+ placeholder="Ex: Bienvenue dans notre CRM"
484
574
  />
485
575
  </div>
486
576
  )}
487
577
 
488
- {/* Section Variables */}
489
- <div className="mt-4 rounded-lg bg-blue-50 p-4">
490
- <p className="mb-3 text-sm font-semibold text-gray-700">
491
- Variables disponibles :
492
- </p>
493
- <p className="mb-3 text-xs text-gray-600">
494
- Cliquez sur une variable pour l'insérer dans le contenu
495
- </p>
496
- <div className="flex flex-wrap gap-2">
497
- {AVAILABLE_VARIABLES.map((variable) => (
498
- <button
499
- key={variable.key}
500
- type="button"
501
- onClick={() => {
502
- if (formData.type === 'EMAIL' && emailEditorRef.current) {
503
- emailEditorRef.current.insertText(variable.key);
504
- } else if (formData.type === 'NOTE' && noteEditorRef.current) {
505
- noteEditorRef.current.insertText(variable.key);
506
- } else if (formData.type === 'SMS' && smsTextareaRef.current) {
507
- const textarea = smsTextareaRef.current;
508
- const start = textarea.selectionStart || 0;
509
- const end = textarea.selectionEnd || 0;
510
- const text = formData.content;
511
- const newText =
512
- text.substring(0, start) + variable.key + text.substring(end);
513
- setFormData({ ...formData, content: newText });
514
- // Repositionner le curseur après la variable insérée
515
- setTimeout(() => {
516
- textarea.focus();
517
- textarea.setSelectionRange(
518
- start + variable.key.length,
519
- start + variable.key.length,
520
- );
521
- }, 0);
578
+ <div>
579
+ <label className="block text-sm font-semibold text-foreground">
580
+ Contenu <span className="text-red-500">*</span>
581
+ </label>
582
+ {formData.type === 'EMAIL' || formData.type === 'NOTE' ? (
583
+ <div className="mt-2 rounded-xl border border-border shadow-sm">
584
+ <Editor
585
+ ref={formData.type === 'EMAIL' ? emailEditorRef : noteEditorRef}
586
+ onReady={(methods) => {
587
+ if (formData.type === 'EMAIL') {
588
+ emailEditorRef.current = methods;
589
+ } else {
590
+ noteEditorRef.current = methods;
591
+ }
592
+ if (formData.content) {
593
+ methods.injectHTML(formData.content);
522
594
  }
523
595
  }}
524
- className="cursor-pointer rounded-lg bg-blue-100 px-3 py-1.5 font-mono text-xs text-blue-800 transition-colors hover:bg-blue-200"
525
- title={variable.description}
526
- >
527
- {variable.key}
528
- </button>
529
- ))}
596
+ />
597
+ </div>
598
+ ) : (
599
+ <div>
600
+ <textarea
601
+ ref={smsTextareaRef}
602
+ required
603
+ value={formData.content}
604
+ onChange={(e) => setFormData({ ...formData, content: e.target.value })}
605
+ rows={6}
606
+ className="mt-2 block w-full rounded-xl border border-border bg-background px-4 py-3 text-foreground shadow-sm transition-all focus:border-primary/50 focus:ring-2 focus:ring-primary/20 focus:outline-none"
607
+ placeholder="Contenu du SMS..."
608
+ />
609
+ </div>
610
+ )}
611
+
612
+ {/* Section Variables */}
613
+ <div className="mt-6 rounded-xl border border-primary/20 bg-linear-to-br from-primary/10 to-card p-5">
614
+ <div className="mb-4 flex items-center gap-2">
615
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/15">
616
+ <FileText className="h-4 w-4 text-primary" />
617
+ </div>
618
+ <div>
619
+ <p className="text-sm font-bold text-foreground">Variables disponibles</p>
620
+ <p className="text-xs text-muted-foreground">
621
+ Cliquez sur une variable pour l&apos;insérer dans le contenu
622
+ </p>
623
+ </div>
624
+ </div>
625
+ {(
626
+ Object.entries(VARIABLE_SECTIONS) as [
627
+ string,
628
+ { label: string; color: string },
629
+ ][]
630
+ ).map(([sectionKey, sectionInfo]) => {
631
+ const sectionVars = AVAILABLE_VARIABLES.filter(
632
+ (v) => v.section === sectionKey,
633
+ );
634
+ if (sectionVars.length === 0) return null;
635
+ const colorMap: Record<
636
+ string,
637
+ {
638
+ border: string;
639
+ bg: string;
640
+ text: string;
641
+ hoverBg: string;
642
+ hoverBorder: string;
643
+ label: string;
644
+ }
645
+ > = {
646
+ indigo: {
647
+ border: 'border-blue-200',
648
+ bg: 'bg-white',
649
+ text: 'text-blue-700',
650
+ hoverBg: 'hover:bg-blue-50',
651
+ hoverBorder: 'hover:border-blue-300',
652
+ label: 'text-blue-800',
653
+ },
654
+ blue: {
655
+ border: 'border-blue-200',
656
+ bg: 'bg-white',
657
+ text: 'text-blue-700',
658
+ hoverBg: 'hover:bg-blue-50',
659
+ hoverBorder: 'hover:border-blue-300',
660
+ label: 'text-blue-800',
661
+ },
662
+ emerald: {
663
+ border: 'border-emerald-200',
664
+ bg: 'bg-white',
665
+ text: 'text-emerald-700',
666
+ hoverBg: 'hover:bg-emerald-50',
667
+ hoverBorder: 'hover:border-emerald-300',
668
+ label: 'text-emerald-800',
669
+ },
670
+ };
671
+ const colors = colorMap[sectionInfo.color] || colorMap.indigo;
672
+ return (
673
+ <div key={sectionKey} className="mb-3 last:mb-0">
674
+ <p
675
+ className={cn(
676
+ 'mb-1.5 text-xs font-semibold tracking-wide uppercase',
677
+ colors.label,
678
+ )}
679
+ >
680
+ {sectionInfo.label}
681
+ </p>
682
+ <div className="flex flex-wrap gap-3">
683
+ {sectionVars.map((variable) => (
684
+ <button
685
+ key={variable.key}
686
+ type="button"
687
+ onClick={() => {
688
+ if (formData.type === 'EMAIL' && emailEditorRef.current) {
689
+ emailEditorRef.current.insertText(variable.key);
690
+ } else if (formData.type === 'NOTE' && noteEditorRef.current) {
691
+ noteEditorRef.current.insertText(variable.key);
692
+ } else if (formData.type === 'SMS' && smsTextareaRef.current) {
693
+ const textarea = smsTextareaRef.current;
694
+ const start = textarea.selectionStart || 0;
695
+ const end = textarea.selectionEnd || 0;
696
+ const text = formData.content;
697
+ const newText =
698
+ text.substring(0, start) + variable.key + text.substring(end);
699
+ setFormData({ ...formData, content: newText });
700
+ setTimeout(() => {
701
+ textarea.focus();
702
+ textarea.setSelectionRange(
703
+ start + variable.key.length,
704
+ start + variable.key.length,
705
+ );
706
+ }, 0);
707
+ }
708
+ }}
709
+ className={cn(
710
+ 'cursor-pointer rounded-lg border px-3 py-2 text-left shadow-sm transition-all hover:shadow-md',
711
+ colors.border,
712
+ colors.bg,
713
+ colors.hoverBg,
714
+ colors.hoverBorder,
715
+ )}
716
+ title="Cliquer pour insérer"
717
+ >
718
+ <span
719
+ className={cn('font-mono text-xs font-semibold', colors.text)}
720
+ >
721
+ {variable.key}
722
+ </span>
723
+ <p className="mt-1 max-w-[200px] text-[11px] leading-tight font-normal text-muted-foreground">
724
+ {variable.description}
725
+ </p>
726
+ </button>
727
+ ))}
728
+ </div>
729
+ </div>
730
+ );
731
+ })}
530
732
  </div>
531
733
  </div>
532
- </div>
533
734
 
534
- {error && (
535
- <div className="rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>
536
- )}
537
- </form>
538
-
539
- {/* Pied de modal fixe */}
540
- <div className="shrink-0 border-t border-gray-100 pt-4">
541
- <div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
542
- <button
543
- type="button"
544
- onClick={() => {
545
- setShowModal(false);
546
- setEditingTemplate(null);
547
- setError('');
548
- }}
549
- className="w-full cursor-pointer rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 sm:w-auto"
550
- >
551
- Annuler
552
- </button>
553
- <button
554
- type="submit"
555
- form="template-form"
556
- className="w-full cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700 sm:w-auto"
557
- >
558
- {editingTemplate ? 'Modifier' : 'Créer'}
559
- </button>
735
+ </form>
736
+
737
+ {/* Pied de modal fixe */}
738
+ <div className="shrink-0 border-t border-border bg-muted px-6 py-5 sm:px-8 sm:py-6">
739
+ <div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
740
+ <button
741
+ type="button"
742
+ onClick={() => {
743
+ setShowModal(false);
744
+ setEditingTemplate(null);
745
+ setError('');
746
+ }}
747
+ className="w-full cursor-pointer rounded-xl border border-border bg-card px-6 py-3 text-sm font-semibold text-foreground shadow-sm transition-all duration-200 hover:bg-background hover:shadow-md sm:w-auto"
748
+ >
749
+ Annuler
750
+ </button>
751
+ <button
752
+ type="submit"
753
+ form="template-form"
754
+ className="w-full cursor-pointer rounded-xl bg-primary px-6 py-3 text-sm font-semibold text-primary-foreground shadow-(--shadow-card) transition-all duration-200 hover:bg-primary/90 sm:w-auto"
755
+ >
756
+ {editingTemplate ? 'Modifier le template' : 'Créer le template'}
757
+ </button>
758
+ </div>
560
759
  </div>
561
760
  </div>
562
761
  </div>
563
- </div>
564
- )}
565
- </div>
762
+ )}
763
+ </div>
764
+ <ConfirmDialog />
765
+ </ProtectedPage>
566
766
  );
567
767
  }