create-crm-tmp 1.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 (187) hide show
  1. package/bin/create-crm-tmp.js +93 -0
  2. package/package.json +25 -0
  3. package/template/.prettierignore +33 -0
  4. package/template/.prettierrc.json +25 -0
  5. package/template/README.md +173 -0
  6. package/template/eslint.config.mjs +18 -0
  7. package/template/exemple-contacts.csv +11 -0
  8. package/template/next.config.ts +8 -0
  9. package/template/package.json +64 -0
  10. package/template/postcss.config.mjs +7 -0
  11. package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
  12. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
  13. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
  14. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
  15. package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
  16. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
  17. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
  18. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
  19. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
  20. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
  21. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
  22. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
  23. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
  24. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
  25. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
  26. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
  27. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
  28. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
  29. package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
  30. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
  31. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
  32. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
  33. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
  34. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
  35. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
  36. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
  37. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
  38. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
  39. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
  40. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
  41. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
  42. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
  43. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
  44. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
  45. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
  46. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
  47. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
  48. package/template/prisma/migrations/migration_lock.toml +3 -0
  49. package/template/prisma/schema.prisma +582 -0
  50. package/template/prisma.config.ts +14 -0
  51. package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
  52. package/template/src/app/(auth)/layout.tsx +3 -0
  53. package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
  54. package/template/src/app/(auth)/reset-password/page.tsx +146 -0
  55. package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
  56. package/template/src/app/(auth)/signin/page.tsx +166 -0
  57. package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
  58. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
  59. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
  60. package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
  61. package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
  62. package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
  63. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
  64. package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
  65. package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
  66. package/template/src/app/(dashboard)/layout.tsx +30 -0
  67. package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
  68. package/template/src/app/(dashboard)/templates/page.tsx +567 -0
  69. package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
  70. package/template/src/app/(dashboard)/users/page.tsx +457 -0
  71. package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
  72. package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
  73. package/template/src/app/api/audit-logs/route.ts +57 -0
  74. package/template/src/app/api/auth/[...all]/route.ts +4 -0
  75. package/template/src/app/api/auth/check-active/route.ts +31 -0
  76. package/template/src/app/api/auth/google/callback/route.ts +94 -0
  77. package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
  78. package/template/src/app/api/auth/google/route.ts +34 -0
  79. package/template/src/app/api/auth/google/status/route.ts +32 -0
  80. package/template/src/app/api/closing-reasons/route.ts +27 -0
  81. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
  82. package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
  83. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
  84. package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
  85. package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
  86. package/template/src/app/api/contacts/[id]/route.ts +322 -0
  87. package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
  88. package/template/src/app/api/contacts/export/route.ts +270 -0
  89. package/template/src/app/api/contacts/import/route.ts +381 -0
  90. package/template/src/app/api/contacts/route.ts +283 -0
  91. package/template/src/app/api/dashboard/stats/route.ts +299 -0
  92. package/template/src/app/api/email/track/[id]/route.ts +68 -0
  93. package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
  94. package/template/src/app/api/invite/complete/route.ts +88 -0
  95. package/template/src/app/api/invite/validate/route.ts +55 -0
  96. package/template/src/app/api/reminders/route.ts +95 -0
  97. package/template/src/app/api/reset-password/complete/route.ts +73 -0
  98. package/template/src/app/api/reset-password/request/route.ts +84 -0
  99. package/template/src/app/api/reset-password/validate/route.ts +49 -0
  100. package/template/src/app/api/reset-password/verify/route.ts +74 -0
  101. package/template/src/app/api/roles/[id]/route.ts +183 -0
  102. package/template/src/app/api/roles/route.ts +140 -0
  103. package/template/src/app/api/send/route.ts +282 -0
  104. package/template/src/app/api/settings/change-password/route.ts +95 -0
  105. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
  106. package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
  107. package/template/src/app/api/settings/company/route.ts +121 -0
  108. package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
  109. package/template/src/app/api/settings/google-ads/route.ts +122 -0
  110. package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
  111. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
  112. package/template/src/app/api/settings/google-sheet/route.ts +254 -0
  113. package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
  114. package/template/src/app/api/settings/meta-leads/route.ts +132 -0
  115. package/template/src/app/api/settings/profile/route.ts +42 -0
  116. package/template/src/app/api/settings/smtp/route.ts +130 -0
  117. package/template/src/app/api/settings/smtp/test/route.ts +121 -0
  118. package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
  119. package/template/src/app/api/settings/statuses/route.ts +83 -0
  120. package/template/src/app/api/statuses/route.ts +25 -0
  121. package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
  122. package/template/src/app/api/tasks/[id]/route.ts +728 -0
  123. package/template/src/app/api/tasks/meet/route.ts +240 -0
  124. package/template/src/app/api/tasks/route.ts +417 -0
  125. package/template/src/app/api/templates/[id]/route.ts +140 -0
  126. package/template/src/app/api/templates/route.ts +91 -0
  127. package/template/src/app/api/users/[id]/route.ts +168 -0
  128. package/template/src/app/api/users/list/route.ts +45 -0
  129. package/template/src/app/api/users/me/route.ts +48 -0
  130. package/template/src/app/api/users/route.ts +250 -0
  131. package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
  132. package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
  133. package/template/src/app/api/workflows/[id]/route.ts +192 -0
  134. package/template/src/app/api/workflows/process/route.ts +293 -0
  135. package/template/src/app/api/workflows/route.ts +124 -0
  136. package/template/src/app/favicon.ico +0 -0
  137. package/template/src/app/globals.css +1416 -0
  138. package/template/src/app/layout.tsx +31 -0
  139. package/template/src/app/page.tsx +32 -0
  140. package/template/src/components/dashboard/activity-chart.tsx +67 -0
  141. package/template/src/components/dashboard/contacts-chart.tsx +63 -0
  142. package/template/src/components/dashboard/recent-activity.tsx +164 -0
  143. package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
  144. package/template/src/components/dashboard/stat-card.tsx +61 -0
  145. package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
  146. package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
  147. package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
  148. package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
  149. package/template/src/components/editor.tsx +856 -0
  150. package/template/src/components/email-template.tsx +35 -0
  151. package/template/src/components/header.tsx +320 -0
  152. package/template/src/components/invitation-email-template.tsx +79 -0
  153. package/template/src/components/meet-cancellation-email-template.tsx +120 -0
  154. package/template/src/components/meet-confirmation-email-template.tsx +156 -0
  155. package/template/src/components/meet-update-email-template.tsx +209 -0
  156. package/template/src/components/page-header.tsx +61 -0
  157. package/template/src/components/reset-password-email-template.tsx +79 -0
  158. package/template/src/components/sidebar.tsx +294 -0
  159. package/template/src/components/skeleton.tsx +380 -0
  160. package/template/src/components/ui/commands.tsx +396 -0
  161. package/template/src/components/ui/components.tsx +150 -0
  162. package/template/src/components/ui/theme.tsx +5 -0
  163. package/template/src/components/view-as-banner.tsx +45 -0
  164. package/template/src/components/view-as-modal.tsx +186 -0
  165. package/template/src/contexts/mobile-menu-context.tsx +31 -0
  166. package/template/src/contexts/sidebar-context.tsx +107 -0
  167. package/template/src/contexts/task-reminder-context.tsx +239 -0
  168. package/template/src/contexts/view-as-context.tsx +84 -0
  169. package/template/src/hooks/use-user-role.ts +82 -0
  170. package/template/src/lib/audit-log.ts +45 -0
  171. package/template/src/lib/auth-client.ts +16 -0
  172. package/template/src/lib/auth.ts +35 -0
  173. package/template/src/lib/check-permission.ts +193 -0
  174. package/template/src/lib/contact-duplicate.ts +112 -0
  175. package/template/src/lib/contact-interactions.ts +371 -0
  176. package/template/src/lib/encryption.ts +99 -0
  177. package/template/src/lib/google-calendar.ts +300 -0
  178. package/template/src/lib/google-drive.ts +372 -0
  179. package/template/src/lib/permissions.ts +412 -0
  180. package/template/src/lib/prisma.ts +32 -0
  181. package/template/src/lib/roles.ts +120 -0
  182. package/template/src/lib/template-variables.ts +76 -0
  183. package/template/src/lib/utils.ts +46 -0
  184. package/template/src/lib/workflow-executor.ts +482 -0
  185. package/template/src/proxy.ts +91 -0
  186. package/template/tsconfig.json +34 -0
  187. package/template/vercel.json +8 -0
@@ -0,0 +1,567 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef } from 'react';
4
+ import { PageHeader } from '@/components/page-header';
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';
8
+ import { TemplatesPageSkeleton } from '@/components/skeleton';
9
+ import { cn } from '@/lib/utils';
10
+
11
+ interface Template {
12
+ id: string;
13
+ name: string;
14
+ type: 'EMAIL' | 'SMS' | 'NOTE';
15
+ subject: string | null;
16
+ content: string;
17
+ createdAt: string;
18
+ updatedAt: string;
19
+ }
20
+
21
+ export default function TemplatesPage() {
22
+ const [templates, setTemplates] = useState<Template[]>([]);
23
+ const [loading, setLoading] = useState(true);
24
+ const [showModal, setShowModal] = useState(false);
25
+ const [editingTemplate, setEditingTemplate] = useState<Template | null>(null);
26
+ const [error, setError] = useState('');
27
+ const [success, setSuccess] = useState('');
28
+ const [filterType, setFilterType] = useState<'ALL' | 'EMAIL' | 'SMS' | 'NOTE'>('ALL');
29
+
30
+ const [formData, setFormData] = useState({
31
+ name: '',
32
+ type: 'EMAIL' as 'EMAIL' | 'SMS' | 'NOTE',
33
+ subject: '',
34
+ content: '',
35
+ });
36
+
37
+ const emailEditorRef = useRef<DefaultTemplateRef | null>(null);
38
+ const noteEditorRef = useRef<DefaultTemplateRef | null>(null);
39
+ const smsTextareaRef = useRef<HTMLTextAreaElement | null>(null);
40
+
41
+ const fetchTemplates = async () => {
42
+ try {
43
+ setLoading(true);
44
+ const url = filterType === 'ALL' ? '/api/templates' : `/api/templates?type=${filterType}`;
45
+ const response = await fetch(url);
46
+ if (response.ok) {
47
+ const data = await response.json();
48
+ setTemplates(data);
49
+ } else {
50
+ setError('Erreur lors du chargement des templates');
51
+ }
52
+ } catch (error) {
53
+ console.error('Erreur:', error);
54
+ setError('Erreur lors du chargement des templates');
55
+ } finally {
56
+ setLoading(false);
57
+ }
58
+ };
59
+
60
+ useEffect(() => {
61
+ fetchTemplates();
62
+ }, [filterType]);
63
+
64
+ const handleSubmit = async (e: React.FormEvent) => {
65
+ e.preventDefault();
66
+ setError('');
67
+ setSuccess('');
68
+
69
+ if (!formData.name) {
70
+ setError('Le nom est requis');
71
+ return;
72
+ }
73
+
74
+ // Récupérer le contenu depuis l'éditeur si c'est EMAIL ou NOTE
75
+ let content = formData.content;
76
+ if (formData.type === 'EMAIL' && emailEditorRef.current) {
77
+ content = emailEditorRef.current.getHTML() || '';
78
+ } else if (formData.type === 'NOTE' && noteEditorRef.current) {
79
+ content = noteEditorRef.current.getHTML() || '';
80
+ }
81
+
82
+ // Validation du contenu
83
+ if (!content || (typeof content === 'string' && content.trim() === '')) {
84
+ setError('Le contenu est requis');
85
+ return;
86
+ }
87
+
88
+ if (formData.type === 'EMAIL' && !formData.subject) {
89
+ setError('Le sujet est requis pour les templates EMAIL');
90
+ return;
91
+ }
92
+
93
+ try {
94
+ const url = editingTemplate ? `/api/templates/${editingTemplate.id}` : '/api/templates';
95
+ const method = editingTemplate ? 'PUT' : 'POST';
96
+
97
+ const response = await fetch(url, {
98
+ method,
99
+ headers: { 'Content-Type': 'application/json' },
100
+ body: JSON.stringify({
101
+ ...formData,
102
+ content,
103
+ }),
104
+ });
105
+
106
+ const data = await response.json();
107
+
108
+ if (!response.ok) {
109
+ throw new Error(data.error || 'Erreur lors de la sauvegarde');
110
+ }
111
+
112
+ setSuccess(
113
+ editingTemplate ? 'Template modifié avec succès !' : 'Template créé avec succès !',
114
+ );
115
+ setShowModal(false);
116
+ setEditingTemplate(null);
117
+ setFormData({
118
+ name: '',
119
+ type: 'EMAIL',
120
+ subject: '',
121
+ content: '',
122
+ });
123
+ emailEditorRef.current?.injectHTML('');
124
+ noteEditorRef.current?.injectHTML('');
125
+ fetchTemplates();
126
+
127
+ setTimeout(() => setSuccess(''), 5000);
128
+ } catch (err: any) {
129
+ setError(err.message);
130
+ }
131
+ };
132
+
133
+ const handleDelete = async (id: string) => {
134
+ if (!confirm('Êtes-vous sûr de vouloir supprimer ce template ?')) {
135
+ return;
136
+ }
137
+
138
+ try {
139
+ const response = await fetch(`/api/templates/${id}`, {
140
+ method: 'DELETE',
141
+ });
142
+
143
+ if (!response.ok) {
144
+ throw new Error('Erreur lors de la suppression');
145
+ }
146
+
147
+ setSuccess('Template supprimé avec succès !');
148
+ fetchTemplates();
149
+ setTimeout(() => setSuccess(''), 5000);
150
+ } catch (err: any) {
151
+ setError(err.message);
152
+ }
153
+ };
154
+
155
+ const handleEdit = (template: Template) => {
156
+ setEditingTemplate(template);
157
+ setFormData({
158
+ name: template.name,
159
+ type: template.type,
160
+ subject: template.subject || '',
161
+ content: template.content,
162
+ });
163
+ setShowModal(true);
164
+ setError('');
165
+
166
+ // Injecter le contenu dans l'éditeur après un court délai
167
+ setTimeout(() => {
168
+ if (template.type === 'EMAIL' && emailEditorRef.current) {
169
+ emailEditorRef.current.injectHTML(template.content);
170
+ } else if (template.type === 'NOTE' && noteEditorRef.current) {
171
+ noteEditorRef.current.injectHTML(template.content);
172
+ }
173
+ }, 100);
174
+ };
175
+
176
+ const handleNewTemplate = () => {
177
+ setEditingTemplate(null);
178
+ setFormData({
179
+ name: '',
180
+ type: 'EMAIL',
181
+ subject: '',
182
+ content: '',
183
+ });
184
+ setShowModal(true);
185
+ setError('');
186
+ setSuccess('');
187
+ emailEditorRef.current?.injectHTML('');
188
+ noteEditorRef.current?.injectHTML('');
189
+ };
190
+
191
+ const getTypeIcon = (type: string) => {
192
+ switch (type) {
193
+ case 'EMAIL':
194
+ return <Mail className="h-5 w-5" />;
195
+ case 'SMS':
196
+ return <MessageSquare className="h-5 w-5" />;
197
+ case 'NOTE':
198
+ return <FileText className="h-5 w-5" />;
199
+ default:
200
+ return <FileText className="h-5 w-5" />;
201
+ }
202
+ };
203
+
204
+ const getTypeLabel = (type: string) => {
205
+ switch (type) {
206
+ case 'EMAIL':
207
+ return 'Email';
208
+ case 'SMS':
209
+ return 'SMS';
210
+ case 'NOTE':
211
+ return 'Note';
212
+ default:
213
+ return type;
214
+ }
215
+ };
216
+
217
+ const getTypeColor = (type: string) => {
218
+ switch (type) {
219
+ case 'EMAIL':
220
+ return 'bg-blue-100 text-blue-800 border-blue-200';
221
+ case 'SMS':
222
+ return 'bg-green-100 text-green-800 border-green-200';
223
+ case 'NOTE':
224
+ return 'bg-purple-100 text-purple-800 border-purple-200';
225
+ default:
226
+ return 'bg-gray-100 text-gray-800 border-gray-200';
227
+ }
228
+ };
229
+
230
+ const filteredTemplates = templates.filter((t) => filterType === 'ALL' || t.type === filterType);
231
+
232
+ if (loading) {
233
+ return <TemplatesPageSkeleton />;
234
+ }
235
+
236
+ 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>
319
+ <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"
322
+ >
323
+ Créer un template
324
+ </button>
325
+ </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"
332
+ >
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>
338
+ </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 && '...'}
355
+ </p>
356
+ </div>
357
+ </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>
366
+ <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"
370
+ >
371
+ <Trash2 className="h-4 w-4" />
372
+ </button>
373
+ </div>
374
+ </div>
375
+ ))}
376
+ </div>
377
+ )}
378
+ </div>
379
+
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' && (
443
+ <div>
444
+ <label className="block text-sm font-medium text-gray-700">Sujet *</label>
445
+ <input
446
+ type="text"
447
+ 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"
452
+ />
453
+ </div>
454
+ )}
455
+
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
+ ) : (
475
+ <div>
476
+ <textarea
477
+ ref={smsTextareaRef}
478
+ 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..."
484
+ />
485
+ </div>
486
+ )}
487
+
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);
522
+ }
523
+ }}
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
+ ))}
530
+ </div>
531
+ </div>
532
+ </div>
533
+
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>
560
+ </div>
561
+ </div>
562
+ </div>
563
+ </div>
564
+ )}
565
+ </div>
566
+ );
567
+ }