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.
- package/package.json +1 -1
- package/template/.prettierignore +2 -0
- package/template/README.md +53 -67
- package/template/components.json +22 -0
- package/template/exemple-contacts.csv +54 -0
- package/template/next.config.ts +27 -1
- package/template/package.json +64 -27
- package/template/prisma/schema.prisma +821 -72
- package/template/skills-lock.json +25 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
- package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
- package/template/src/app/(auth)/reset-password/page.tsx +12 -8
- package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
- package/template/src/app/(auth)/signin/page.tsx +20 -17
- package/template/src/app/(dashboard)/agenda/page.tsx +2231 -2188
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
- package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
- package/template/src/app/(dashboard)/closing/page.tsx +500 -468
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5035 -4126
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
- package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
- package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
- package/template/src/app/(dashboard)/error.tsx +37 -0
- package/template/src/app/(dashboard)/layout.tsx +1 -1
- package/template/src/app/(dashboard)/loading.tsx +5 -0
- package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
- package/template/src/app/(dashboard)/settings/page.tsx +2685 -2489
- package/template/src/app/(dashboard)/templates/page.tsx +500 -300
- package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
- package/template/src/app/(dashboard)/users/page.tsx +279 -310
- package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
- package/template/src/app/(dashboard)/users/roles/page.tsx +164 -137
- package/template/src/app/api/audit-logs/route.ts +1 -1
- package/template/src/app/api/auth/google/callback/route.ts +8 -5
- package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
- package/template/src/app/api/companies/[id]/activities/route.ts +131 -0
- package/template/src/app/api/companies/[id]/route.ts +195 -0
- package/template/src/app/api/companies/export/route.ts +206 -0
- package/template/src/app/api/companies/route.ts +166 -0
- package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
- package/template/src/app/api/contact-views/[id]/route.ts +197 -0
- package/template/src/app/api/contact-views/route.ts +146 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +77 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
- package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
- package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
- package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
- package/template/src/app/api/contacts/[id]/route.ts +111 -20
- package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
- package/template/src/app/api/contacts/export/route.ts +12 -17
- package/template/src/app/api/contacts/import/route.ts +22 -19
- package/template/src/app/api/contacts/import-preview/route.ts +139 -0
- package/template/src/app/api/contacts/route.ts +202 -49
- package/template/src/app/api/dashboard/stats/route.ts +9 -292
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
- package/template/src/app/api/invite/complete/route.ts +20 -23
- package/template/src/app/api/reminders/route.ts +1 -0
- package/template/src/app/api/reset-password/complete/route.ts +11 -13
- package/template/src/app/api/send/route.ts +9 -85
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
- package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
- package/template/src/app/api/settings/company/route.ts +19 -26
- package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-ads/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
- package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
- package/template/src/app/api/settings/google-sheet/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/route.ts +20 -23
- package/template/src/app/api/settings/statuses/[id]/route.ts +33 -23
- package/template/src/app/api/settings/statuses/route.ts +24 -22
- package/template/src/app/api/statuses/route.ts +2 -5
- package/template/src/app/api/tasks/[id]/attendees/route.ts +14 -7
- package/template/src/app/api/tasks/[id]/route.ts +161 -137
- package/template/src/app/api/tasks/meet/route.ts +11 -8
- package/template/src/app/api/tasks/route.ts +155 -95
- package/template/src/app/api/templates/[id]/route.ts +22 -13
- package/template/src/app/api/templates/route.ts +22 -5
- package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
- package/template/src/app/api/users/[id]/route.ts +16 -1
- package/template/src/app/api/users/commercials/route.ts +38 -0
- package/template/src/app/api/users/for-agenda/route.ts +1 -2
- package/template/src/app/api/users/route.ts +94 -55
- package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
- package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
- package/template/src/app/api/workflows/[id]/route.ts +33 -6
- package/template/src/app/api/workflows/process/route.ts +509 -146
- package/template/src/app/api/workflows/route.ts +46 -4
- package/template/src/app/globals.css +210 -101
- package/template/src/app/layout.tsx +19 -8
- package/template/src/app/page.tsx +37 -7
- package/template/src/components/address-autocomplete.tsx +232 -0
- package/template/src/components/contacts/filter-bar.tsx +181 -0
- package/template/src/components/contacts/filter-builder.tsx +589 -0
- package/template/src/components/contacts/save-view-dialog.tsx +160 -0
- package/template/src/components/contacts/views-tab-bar.tsx +440 -0
- package/template/src/components/dashboard/activity-chart.tsx +31 -39
- package/template/src/components/dashboard/dashboard-content.tsx +79 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -42
- package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
- package/template/src/components/date-picker.tsx +396 -0
- package/template/src/components/editor.tsx +27 -13
- package/template/src/components/email-template.tsx +4 -2
- package/template/src/components/global-search.tsx +358 -0
- package/template/src/components/header.tsx +57 -62
- package/template/src/components/invitation-email-template.tsx +4 -2
- package/template/src/components/lazy-editor.tsx +11 -0
- package/template/src/components/meet-cancellation-email-template.tsx +11 -3
- package/template/src/components/meet-confirmation-email-template.tsx +10 -3
- package/template/src/components/meet-update-email-template.tsx +10 -3
- package/template/src/components/page-header.tsx +19 -15
- package/template/src/components/protected-page.tsx +94 -0
- package/template/src/components/reset-password-email-template.tsx +4 -2
- package/template/src/components/sidebar.tsx +92 -94
- package/template/src/components/skeleton.tsx +128 -42
- package/template/src/components/ui/accordion.tsx +64 -0
- package/template/src/components/ui/alert-dialog.tsx +139 -0
- package/template/src/components/ui/button.tsx +60 -0
- package/template/src/components/view-as-banner.tsx +1 -1
- package/template/src/components/view-as-modal.tsx +21 -16
- package/template/src/config/nav-pages.ts +108 -0
- package/template/src/contexts/app-toast-context.tsx +174 -0
- package/template/src/contexts/sidebar-context.tsx +16 -47
- package/template/src/contexts/task-reminder-context.tsx +6 -6
- package/template/src/contexts/view-as-context.tsx +11 -16
- package/template/src/hooks/use-alert.tsx +65 -0
- package/template/src/hooks/use-confirm.tsx +87 -0
- package/template/src/hooks/use-contact-views.ts +140 -0
- package/template/src/hooks/use-contacts.ts +69 -0
- package/template/src/hooks/use-fetch.ts +17 -0
- package/template/src/hooks/use-focus-trap.ts +73 -0
- package/template/src/hooks/use-statuses.ts +22 -0
- package/template/src/lib/address-api.ts +155 -0
- package/template/src/lib/cache.ts +73 -0
- package/template/src/lib/check-permission.ts +12 -177
- package/template/src/lib/contact-interactions.ts +3 -1
- package/template/src/lib/contact-view-filters.ts +341 -0
- package/template/src/lib/dashboard-stats.ts +224 -0
- package/template/src/lib/date-utils.ts +49 -0
- package/template/src/lib/get-auth-user.ts +25 -0
- package/template/src/lib/google-calendar.ts +54 -12
- package/template/src/lib/google-drive.ts +796 -75
- package/template/src/lib/google-fetch.ts +63 -0
- package/template/src/lib/local-storage.ts +34 -0
- package/template/src/lib/permissions.ts +245 -47
- package/template/src/lib/prisma.ts +11 -11
- package/template/src/lib/roles.ts +14 -39
- package/template/src/lib/template-variables.ts +67 -33
- package/template/src/lib/utils.ts +26 -2
- package/template/src/lib/workflow-executor.ts +445 -229
- package/template/src/proxy.ts +34 -73
- package/template/src/types/contact-views.ts +351 -0
- package/template/src/types/yousign.ts +52 -0
- package/template/vercel.json +12 -0
- package/template/WORKFLOWS_CRON.md +0 -185
- package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
- package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
- package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
- package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
- package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
- package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
- package/template/prisma/migrations/migration_lock.toml +0 -3
- package/template/src/app/(dashboard)/users/layout.tsx +0 -30
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
- package/template/src/app/api/dashboard/widgets/route.ts +0 -181
- package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
- package/template/src/components/dashboard/color-picker.tsx +0 -65
- package/template/src/components/dashboard/contacts-chart.tsx +0 -69
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
- package/template/src/components/dashboard/recent-activity.tsx +0 -157
- package/template/src/components/dashboard/sales-analytics-chart.tsx +0 -77
- package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
- package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
- package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
- package/template/src/contexts/dashboard-theme-context.tsx +0 -58
- package/template/src/lib/dashboard-themes.ts +0 -140
- package/template/src/lib/default-widgets.ts +0 -14
- 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
|
-
|
|
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-
|
|
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 '
|
|
271
|
+
return 'text-emerald-600';
|
|
223
272
|
case 'NOTE':
|
|
224
|
-
return '
|
|
273
|
+
return 'text-violet-600';
|
|
225
274
|
default:
|
|
226
|
-
return '
|
|
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
|
-
<
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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={
|
|
321
|
-
className=
|
|
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
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
className="
|
|
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
|
-
<
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
{
|
|
346
|
-
</
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
{template.content.replace(/<[^>]+>/g, '').
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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-
|
|
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.
|
|
449
|
-
onChange={(e) => setFormData({ ...formData,
|
|
450
|
-
className="mt-
|
|
451
|
-
placeholder="Ex:
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
<
|
|
477
|
-
|
|
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.
|
|
480
|
-
onChange={(e) => setFormData({ ...formData,
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
if (formData.
|
|
503
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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'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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
762
|
+
)}
|
|
763
|
+
</div>
|
|
764
|
+
<ConfirmDialog />
|
|
765
|
+
</ProtectedPage>
|
|
566
766
|
);
|
|
567
767
|
}
|