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.
- package/bin/create-crm-tmp.js +93 -0
- package/package.json +25 -0
- package/template/.prettierignore +33 -0
- package/template/.prettierrc.json +25 -0
- package/template/README.md +173 -0
- package/template/eslint.config.mjs +18 -0
- package/template/exemple-contacts.csv +11 -0
- package/template/next.config.ts +8 -0
- package/template/package.json +64 -0
- package/template/postcss.config.mjs +7 -0
- package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
- package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +582 -0
- package/template/prisma.config.ts +14 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
- package/template/src/app/(auth)/layout.tsx +3 -0
- package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
- package/template/src/app/(auth)/reset-password/page.tsx +146 -0
- package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
- package/template/src/app/(auth)/signin/page.tsx +166 -0
- package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
- package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
- package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
- package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
- package/template/src/app/(dashboard)/layout.tsx +30 -0
- package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
- package/template/src/app/(dashboard)/templates/page.tsx +567 -0
- package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
- package/template/src/app/(dashboard)/users/page.tsx +457 -0
- package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
- package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
- package/template/src/app/api/audit-logs/route.ts +57 -0
- package/template/src/app/api/auth/[...all]/route.ts +4 -0
- package/template/src/app/api/auth/check-active/route.ts +31 -0
- package/template/src/app/api/auth/google/callback/route.ts +94 -0
- package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
- package/template/src/app/api/auth/google/route.ts +34 -0
- package/template/src/app/api/auth/google/status/route.ts +32 -0
- package/template/src/app/api/closing-reasons/route.ts +27 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
- package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
- package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
- package/template/src/app/api/contacts/[id]/route.ts +322 -0
- package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
- package/template/src/app/api/contacts/export/route.ts +270 -0
- package/template/src/app/api/contacts/import/route.ts +381 -0
- package/template/src/app/api/contacts/route.ts +283 -0
- package/template/src/app/api/dashboard/stats/route.ts +299 -0
- package/template/src/app/api/email/track/[id]/route.ts +68 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
- package/template/src/app/api/invite/complete/route.ts +88 -0
- package/template/src/app/api/invite/validate/route.ts +55 -0
- package/template/src/app/api/reminders/route.ts +95 -0
- package/template/src/app/api/reset-password/complete/route.ts +73 -0
- package/template/src/app/api/reset-password/request/route.ts +84 -0
- package/template/src/app/api/reset-password/validate/route.ts +49 -0
- package/template/src/app/api/reset-password/verify/route.ts +74 -0
- package/template/src/app/api/roles/[id]/route.ts +183 -0
- package/template/src/app/api/roles/route.ts +140 -0
- package/template/src/app/api/send/route.ts +282 -0
- package/template/src/app/api/settings/change-password/route.ts +95 -0
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
- package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
- package/template/src/app/api/settings/company/route.ts +121 -0
- package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
- package/template/src/app/api/settings/google-ads/route.ts +122 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
- package/template/src/app/api/settings/google-sheet/route.ts +254 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
- package/template/src/app/api/settings/meta-leads/route.ts +132 -0
- package/template/src/app/api/settings/profile/route.ts +42 -0
- package/template/src/app/api/settings/smtp/route.ts +130 -0
- package/template/src/app/api/settings/smtp/test/route.ts +121 -0
- package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
- package/template/src/app/api/settings/statuses/route.ts +83 -0
- package/template/src/app/api/statuses/route.ts +25 -0
- package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
- package/template/src/app/api/tasks/[id]/route.ts +728 -0
- package/template/src/app/api/tasks/meet/route.ts +240 -0
- package/template/src/app/api/tasks/route.ts +417 -0
- package/template/src/app/api/templates/[id]/route.ts +140 -0
- package/template/src/app/api/templates/route.ts +91 -0
- package/template/src/app/api/users/[id]/route.ts +168 -0
- package/template/src/app/api/users/list/route.ts +45 -0
- package/template/src/app/api/users/me/route.ts +48 -0
- package/template/src/app/api/users/route.ts +250 -0
- package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
- package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
- package/template/src/app/api/workflows/[id]/route.ts +192 -0
- package/template/src/app/api/workflows/process/route.ts +293 -0
- package/template/src/app/api/workflows/route.ts +124 -0
- package/template/src/app/favicon.ico +0 -0
- package/template/src/app/globals.css +1416 -0
- package/template/src/app/layout.tsx +31 -0
- package/template/src/app/page.tsx +32 -0
- package/template/src/components/dashboard/activity-chart.tsx +67 -0
- package/template/src/components/dashboard/contacts-chart.tsx +63 -0
- package/template/src/components/dashboard/recent-activity.tsx +164 -0
- package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
- package/template/src/components/dashboard/stat-card.tsx +61 -0
- package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
- package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
- package/template/src/components/editor.tsx +856 -0
- package/template/src/components/email-template.tsx +35 -0
- package/template/src/components/header.tsx +320 -0
- package/template/src/components/invitation-email-template.tsx +79 -0
- package/template/src/components/meet-cancellation-email-template.tsx +120 -0
- package/template/src/components/meet-confirmation-email-template.tsx +156 -0
- package/template/src/components/meet-update-email-template.tsx +209 -0
- package/template/src/components/page-header.tsx +61 -0
- package/template/src/components/reset-password-email-template.tsx +79 -0
- package/template/src/components/sidebar.tsx +294 -0
- package/template/src/components/skeleton.tsx +380 -0
- package/template/src/components/ui/commands.tsx +396 -0
- package/template/src/components/ui/components.tsx +150 -0
- package/template/src/components/ui/theme.tsx +5 -0
- package/template/src/components/view-as-banner.tsx +45 -0
- package/template/src/components/view-as-modal.tsx +186 -0
- package/template/src/contexts/mobile-menu-context.tsx +31 -0
- package/template/src/contexts/sidebar-context.tsx +107 -0
- package/template/src/contexts/task-reminder-context.tsx +239 -0
- package/template/src/contexts/view-as-context.tsx +84 -0
- package/template/src/hooks/use-user-role.ts +82 -0
- package/template/src/lib/audit-log.ts +45 -0
- package/template/src/lib/auth-client.ts +16 -0
- package/template/src/lib/auth.ts +35 -0
- package/template/src/lib/check-permission.ts +193 -0
- package/template/src/lib/contact-duplicate.ts +112 -0
- package/template/src/lib/contact-interactions.ts +371 -0
- package/template/src/lib/encryption.ts +99 -0
- package/template/src/lib/google-calendar.ts +300 -0
- package/template/src/lib/google-drive.ts +372 -0
- package/template/src/lib/permissions.ts +412 -0
- package/template/src/lib/prisma.ts +32 -0
- package/template/src/lib/roles.ts +120 -0
- package/template/src/lib/template-variables.ts +76 -0
- package/template/src/lib/utils.ts +46 -0
- package/template/src/lib/workflow-executor.ts +482 -0
- package/template/src/proxy.ts +91 -0
- package/template/tsconfig.json +34 -0
- 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
|
+
}
|