create-crm-tmp 2.0.0 → 2.1.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 (190) hide show
  1. package/bin/create-crm-tmp.js +56 -35
  2. package/package.json +1 -1
  3. package/template/README.md +230 -115
  4. package/template/eslint.config.mjs +13 -0
  5. package/template/next.config.ts +14 -0
  6. package/template/package.json +15 -2
  7. package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
  8. package/template/prisma/migrations/migration_lock.toml +3 -0
  9. package/template/prisma/schema.prisma +132 -637
  10. package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
  11. package/template/src/app/(auth)/layout.tsx +1 -1
  12. package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
  13. package/template/src/app/(auth)/reset-password/page.tsx +4 -4
  14. package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
  15. package/template/src/app/(auth)/signin/page.tsx +14 -6
  16. package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
  17. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
  18. package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
  19. package/template/src/app/(dashboard)/closing/page.tsx +78 -62
  20. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
  21. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
  22. package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
  23. package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
  24. package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
  25. package/template/src/app/(dashboard)/layout.tsx +6 -2
  26. package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
  27. package/template/src/app/(dashboard)/templates/page.tsx +55 -54
  28. package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
  29. package/template/src/app/(dashboard)/users/page.tsx +1 -1
  30. package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
  31. package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
  32. package/template/src/app/api/agenda/google-events/route.ts +92 -0
  33. package/template/src/app/api/auth/check-active/route.ts +3 -2
  34. package/template/src/app/api/auth/google/route.ts +2 -1
  35. package/template/src/app/api/auth/google/status/route.ts +7 -31
  36. package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
  37. package/template/src/app/api/companies/[id]/route.ts +1 -2
  38. package/template/src/app/api/companies/route.ts +42 -12
  39. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
  40. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
  41. package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
  42. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
  43. package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
  44. package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
  45. package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
  46. package/template/src/app/api/contacts/[id]/route.ts +106 -34
  47. package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
  48. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
  49. package/template/src/app/api/contacts/export/route.ts +9 -13
  50. package/template/src/app/api/contacts/import/route.ts +55 -25
  51. package/template/src/app/api/contacts/import-preview/route.ts +1 -1
  52. package/template/src/app/api/contacts/origins/route.ts +63 -0
  53. package/template/src/app/api/contacts/route.ts +153 -41
  54. package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
  55. package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
  56. package/template/src/app/api/dashboard/widgets/route.ts +181 -0
  57. package/template/src/app/api/dev/reminders/test/route.ts +114 -0
  58. package/template/src/app/api/editor/upload-image/route.ts +61 -0
  59. package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
  60. package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
  61. package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
  62. package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
  63. package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
  64. package/template/src/app/api/reminders/clear/route.ts +120 -0
  65. package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
  66. package/template/src/app/api/reminders/route.ts +164 -39
  67. package/template/src/app/api/reminders/state/route.ts +164 -0
  68. package/template/src/app/api/reset-password/request/route.ts +1 -1
  69. package/template/src/app/api/reset-password/verify/route.ts +1 -1
  70. package/template/src/app/api/send/route.ts +16 -4
  71. package/template/src/app/api/settings/google-ads/route.ts +14 -0
  72. package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
  73. package/template/src/app/api/settings/google-calendar/route.ts +124 -0
  74. package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
  75. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
  76. package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
  77. package/template/src/app/api/settings/google-sheet/route.ts +14 -0
  78. package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
  79. package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
  80. package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
  81. package/template/src/app/api/settings/meta-leads/route.ts +14 -2
  82. package/template/src/app/api/settings/smtp/route.ts +53 -6
  83. package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
  84. package/template/src/app/api/tasks/[id]/route.ts +234 -58
  85. package/template/src/app/api/tasks/meet/route.ts +27 -19
  86. package/template/src/app/api/tasks/route.ts +62 -17
  87. package/template/src/app/api/users/[id]/route.ts +20 -14
  88. package/template/src/app/api/users/list/route.ts +57 -19
  89. package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
  90. package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
  91. package/template/src/app/api/workflows/[id]/route.ts +0 -4
  92. package/template/src/app/api/workflows/process/route.ts +22 -51
  93. package/template/src/app/api/workflows/route.ts +0 -4
  94. package/template/src/app/globals.css +342 -4
  95. package/template/src/app/layout.tsx +11 -3
  96. package/template/src/app/page.tsx +1 -1
  97. package/template/src/components/address-autocomplete.tsx +7 -6
  98. package/template/src/components/config-error-alert.tsx +46 -0
  99. package/template/src/components/contacts/filter-bar.tsx +12 -3
  100. package/template/src/components/contacts/filter-builder.tsx +28 -43
  101. package/template/src/components/contacts/save-view-dialog.tsx +1 -1
  102. package/template/src/components/contacts/views-tab-bar.tsx +15 -6
  103. package/template/src/components/dashboard/activity-chart.tsx +41 -28
  104. package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
  105. package/template/src/components/dashboard/color-picker.tsx +64 -0
  106. package/template/src/components/dashboard/contacts-chart.tsx +69 -0
  107. package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
  108. package/template/src/components/dashboard/recent-activity.tsx +154 -0
  109. package/template/src/components/dashboard/stat-card.tsx +40 -40
  110. package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
  111. package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
  112. package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
  113. package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
  114. package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
  115. package/template/src/components/date-picker.tsx +9 -6
  116. package/template/src/components/editor/upload-editor-image.ts +42 -0
  117. package/template/src/components/editor.tsx +161 -22
  118. package/template/src/components/email-template.tsx +2 -2
  119. package/template/src/components/global-search.tsx +30 -28
  120. package/template/src/components/header.tsx +178 -80
  121. package/template/src/components/inactive-account-guard.tsx +58 -0
  122. package/template/src/components/integration-notifications-listener.tsx +12 -0
  123. package/template/src/components/invitation-email-template.tsx +2 -2
  124. package/template/src/components/meet-cancellation-email-template.tsx +3 -3
  125. package/template/src/components/meet-confirmation-email-template.tsx +3 -3
  126. package/template/src/components/meet-update-email-template.tsx +3 -3
  127. package/template/src/components/page-header.tsx +5 -5
  128. package/template/src/components/protected-page.tsx +1 -1
  129. package/template/src/components/reset-password-email-template.tsx +2 -2
  130. package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
  131. package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
  132. package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
  133. package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
  134. package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
  135. package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
  136. package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
  137. package/template/src/components/sidebar.tsx +45 -26
  138. package/template/src/components/skeleton.tsx +40 -43
  139. package/template/src/components/ui/accordion.tsx +2 -2
  140. package/template/src/components/ui/alert-dialog.tsx +1 -1
  141. package/template/src/components/ui/button.tsx +20 -9
  142. package/template/src/components/ui/components.tsx +1 -1
  143. package/template/src/components/ui/date-picker.tsx +422 -0
  144. package/template/src/components/ui/datetime-picker.tsx +338 -0
  145. package/template/src/components/ui/status-select.tsx +271 -0
  146. package/template/src/components/ui/tooltip.tsx +37 -0
  147. package/template/src/components/view-as-modal.tsx +13 -7
  148. package/template/src/contexts/app-toast-context.tsx +245 -57
  149. package/template/src/contexts/dashboard-theme-context.tsx +53 -0
  150. package/template/src/contexts/sidebar-context.tsx +22 -17
  151. package/template/src/contexts/task-reminder-context.tsx +134 -160
  152. package/template/src/contexts/view-as-context.tsx +33 -6
  153. package/template/src/hooks/use-focus-trap.ts +2 -2
  154. package/template/src/hooks/useIntegrationNotifications.ts +49 -0
  155. package/template/src/lib/auth.ts +8 -1
  156. package/template/src/lib/config-links.ts +14 -0
  157. package/template/src/lib/contact-duplicate.ts +79 -61
  158. package/template/src/lib/contact-interactions.ts +21 -21
  159. package/template/src/lib/contact-view-filters.ts +24 -64
  160. package/template/src/lib/contacts-list-url.ts +190 -0
  161. package/template/src/lib/dashboard-stats.ts +65 -7
  162. package/template/src/lib/dashboard-themes.ts +135 -0
  163. package/template/src/lib/date-utils.ts +127 -0
  164. package/template/src/lib/default-widgets.ts +12 -0
  165. package/template/src/lib/editor-html-image-dimensions.ts +172 -0
  166. package/template/src/lib/editor-image-limits.ts +19 -0
  167. package/template/src/lib/email-html-sanitize.ts +19 -0
  168. package/template/src/lib/encryption.ts +9 -6
  169. package/template/src/lib/fr-geography.ts +192 -0
  170. package/template/src/lib/google-calendar-agenda.ts +201 -0
  171. package/template/src/lib/google-calendar.ts +255 -5
  172. package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
  173. package/template/src/lib/google-sheet-sync-runner.ts +514 -0
  174. package/template/src/lib/integration-import-log.ts +21 -0
  175. package/template/src/lib/permissions.ts +40 -10
  176. package/template/src/lib/prisma.ts +4 -1
  177. package/template/src/lib/qstash.ts +65 -0
  178. package/template/src/lib/reminder-state-server.ts +80 -0
  179. package/template/src/lib/reminder-state.ts +29 -0
  180. package/template/src/lib/supabase-storage.ts +113 -0
  181. package/template/src/lib/template-variables.ts +164 -23
  182. package/template/src/lib/utils.ts +45 -0
  183. package/template/src/lib/widget-registry.ts +173 -0
  184. package/template/src/lib/workflow-executor.ts +16 -70
  185. package/template/src/proxy.ts +1 -0
  186. package/template/vercel.json +3 -10
  187. package/template/skills-lock.json +0 -25
  188. package/template/src/components/dashboard/dashboard-content.tsx +0 -79
  189. package/template/src/lib/google-drive.ts +0 -1101
  190. package/template/src/types/yousign.ts +0 -52
@@ -0,0 +1,451 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import { useAppToast } from '@/contexts/app-toast-context';
5
+ import { useConfirm } from '@/hooks/use-confirm';
6
+ import { StatusSelect } from '@/components/ui/status-select';
7
+ import { devToast } from '@/lib/utils';
8
+
9
+ interface MetaLeadConfig {
10
+ id: string;
11
+ name: string;
12
+ pageId: string;
13
+ verifyToken: string;
14
+ active: boolean;
15
+ defaultStatusId: string | null;
16
+ defaultAssignedUserId: string | null;
17
+ defaultStatus: { id: string; name: string; color: string } | null;
18
+ defaultAssignedUser: { id: string; name: string; email: string } | null;
19
+ }
20
+
21
+ interface MetaLeadFormData {
22
+ name: string;
23
+ active: boolean;
24
+ pageId: string;
25
+ accessToken: string;
26
+ verifyToken: string;
27
+ defaultStatusId: string | null;
28
+ defaultAssignedUserId: string | null;
29
+ }
30
+
31
+ const EMPTY_FORM: MetaLeadFormData = {
32
+ name: '',
33
+ active: true,
34
+ pageId: '',
35
+ accessToken: '',
36
+ verifyToken: '',
37
+ defaultStatusId: null,
38
+ defaultAssignedUserId: null,
39
+ };
40
+
41
+ interface MetaLeadIntegrationProps {
42
+ readonly statuses: ReadonlyArray<{ id: string; name: string; color: string }>;
43
+ readonly users: ReadonlyArray<{ id: string; name: string; email: string }>;
44
+ readonly onOpenLogs: (configId: string, configName: string) => void;
45
+ }
46
+
47
+ export function MetaLeadIntegration({ statuses, users, onOpenLogs }: MetaLeadIntegrationProps) {
48
+ const toast = useAppToast();
49
+ const { confirm, ConfirmDialog } = useConfirm();
50
+
51
+ const [loading, setLoading] = useState(true);
52
+ const [saving, setSaving] = useState(false);
53
+ const [configs, setConfigs] = useState<MetaLeadConfig[]>([]);
54
+ const [showModal, setShowModal] = useState(false);
55
+ const [editingConfig, setEditingConfig] = useState<string | null>(null);
56
+ const [formData, setFormData] = useState<MetaLeadFormData>(EMPTY_FORM);
57
+
58
+ const loadConfigs = useCallback(async () => {
59
+ try {
60
+ const res = await fetch('/api/settings/meta-leads');
61
+ if (res.ok) {
62
+ const data = await res.json();
63
+ setConfigs(Array.isArray(data) ? data : []);
64
+ }
65
+ } catch {
66
+ toast.error('Impossible de charger les configurations Meta. Veuillez rafraîchir la page.');
67
+ } finally {
68
+ setLoading(false);
69
+ }
70
+ }, [toast]);
71
+
72
+ useEffect(() => {
73
+ loadConfigs();
74
+ }, [loadConfigs]);
75
+
76
+ const resetForm = () => {
77
+ setShowModal(false);
78
+ setEditingConfig(null);
79
+ setFormData(EMPTY_FORM);
80
+ };
81
+
82
+ const handleOpenAdd = () => {
83
+ setEditingConfig(null);
84
+ setFormData(EMPTY_FORM);
85
+ setShowModal(true);
86
+ };
87
+
88
+ const handleEdit = (config: MetaLeadConfig) => {
89
+ setEditingConfig(config.id);
90
+ setFormData({
91
+ name: config.name,
92
+ active: config.active,
93
+ pageId: config.pageId,
94
+ accessToken: '',
95
+ verifyToken: config.verifyToken,
96
+ defaultStatusId: config.defaultStatusId,
97
+ defaultAssignedUserId: config.defaultAssignedUserId,
98
+ });
99
+ setShowModal(true);
100
+ };
101
+
102
+ const handleSubmit = async (e: React.FormEvent) => {
103
+ e.preventDefault();
104
+ setSaving(true);
105
+
106
+ try {
107
+ const url = editingConfig
108
+ ? `/api/settings/meta-leads/${editingConfig}`
109
+ : '/api/settings/meta-leads';
110
+ const method = editingConfig ? 'PUT' : 'POST';
111
+
112
+ const response = await fetch(url, {
113
+ method,
114
+ headers: { 'Content-Type': 'application/json' },
115
+ body: JSON.stringify(formData),
116
+ });
117
+
118
+ const data = await response.json();
119
+
120
+ if (!response.ok) {
121
+ throw new Error(data.error || 'Erreur lors de la sauvegarde de la configuration Meta');
122
+ }
123
+
124
+ toast.success(
125
+ editingConfig
126
+ ? 'Configuration Meta Lead Ads mise à jour avec succès'
127
+ : 'Configuration Meta Lead Ads créée avec succès',
128
+ );
129
+ resetForm();
130
+ await loadConfigs();
131
+ } catch (error: any) {
132
+ toast.error(devToast('Impossible de sauvegarder la configuration Meta. Veuillez réessayer.', error));
133
+ } finally {
134
+ setSaving(false);
135
+ }
136
+ };
137
+
138
+ const handleDelete = async (id: string) => {
139
+ const confirmed = await confirm({
140
+ title: 'Supprimer la configuration Meta Lead',
141
+ description: 'Êtes-vous sûr de vouloir supprimer cette configuration ?',
142
+ confirmText: 'Supprimer',
143
+ cancelText: 'Annuler',
144
+ variant: 'destructive',
145
+ });
146
+
147
+ if (!confirmed) return;
148
+
149
+ try {
150
+ const response = await fetch(`/api/settings/meta-leads/${id}`, {
151
+ method: 'DELETE',
152
+ });
153
+
154
+ if (!response.ok) {
155
+ const data = await response.json();
156
+ throw new Error(data.error || 'Erreur lors de la suppression');
157
+ }
158
+
159
+ toast.success('Configuration supprimée avec succès');
160
+ await loadConfigs();
161
+ } catch (error: any) {
162
+ toast.error(devToast('Impossible de supprimer la configuration. Veuillez réessayer.', error));
163
+ }
164
+ };
165
+
166
+ const getWebhookUrl = () => {
167
+ const origin = globalThis.window?.location.origin ?? '';
168
+ return `${origin}/api/webhooks/meta-leads`;
169
+ };
170
+
171
+ const handleCopyWebhook = () => {
172
+ const url = getWebhookUrl();
173
+ navigator.clipboard?.writeText(url).then(() => toast.success('Lien webhook copié'));
174
+ };
175
+
176
+ return (
177
+ <>
178
+ <div className="rounded-lg bg-white p-6 shadow-sm">
179
+ <div className="flex items-center justify-between">
180
+ <div>
181
+ <h2 className="text-lg font-bold text-gray-900">Intégration Meta Lead Ads</h2>
182
+ <p className="mt-1 text-sm text-gray-600">
183
+ Recevez automatiquement les leads depuis Facebook Lead Ads.
184
+ </p>
185
+ </div>
186
+ <button
187
+ type="button"
188
+ onClick={handleOpenAdd}
189
+ className="cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
190
+ >
191
+ + Ajouter
192
+ </button>
193
+ </div>
194
+
195
+ {loading && (
196
+ <div className="mt-6 text-center text-gray-500">Chargement...</div>
197
+ )}
198
+
199
+ {!loading && configs.length === 0 && (
200
+ <div className="mt-6 rounded-lg border border-dashed border-gray-300 bg-gray-50 p-8 text-center">
201
+ <p className="text-sm text-gray-600">Aucune configuration Meta Lead Ads</p>
202
+ <p className="mt-1 text-xs text-gray-500">
203
+ Cliquez sur &quot;+ Ajouter&quot; pour créer votre première configuration
204
+ </p>
205
+ </div>
206
+ )}
207
+
208
+ {!loading && configs.length > 0 && (
209
+ <div className="mt-6 space-y-3">
210
+ {configs.map((config) => (
211
+ <div
212
+ key={config.id}
213
+ className="flex items-center justify-between rounded-lg border border-gray-200 bg-gray-50 p-4"
214
+ >
215
+ <div className="flex-1">
216
+ <div className="flex items-center gap-2">
217
+ <h3 className="font-medium text-gray-900">{config.name}</h3>
218
+ {config.active ? (
219
+ <span className="rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
220
+ Actif
221
+ </span>
222
+ ) : (
223
+ <span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-800">
224
+ Inactif
225
+ </span>
226
+ )}
227
+ </div>
228
+ <p className="mt-1 text-xs text-gray-500">Page ID: {config.pageId}</p>
229
+ <p className="mt-0.5 text-xs text-gray-400">Webhook : {getWebhookUrl()}</p>
230
+ </div>
231
+ <div className="flex flex-wrap items-center gap-2">
232
+ <button
233
+ type="button"
234
+ onClick={() => onOpenLogs(config.id, config.name)}
235
+ className="cursor-pointer rounded-lg border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-100"
236
+ >
237
+ Voir les logs
238
+ </button>
239
+ <button
240
+ type="button"
241
+ onClick={handleCopyWebhook}
242
+ className="cursor-pointer rounded-lg border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-100"
243
+ >
244
+ Copier le lien webhook
245
+ </button>
246
+ <button
247
+ type="button"
248
+ onClick={() => handleEdit(config)}
249
+ className="cursor-pointer rounded-lg border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-100"
250
+ >
251
+ Modifier
252
+ </button>
253
+ <button
254
+ type="button"
255
+ onClick={() => handleDelete(config.id)}
256
+ className="cursor-pointer rounded-lg border border-blue-300 px-3 py-1.5 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-50"
257
+ >
258
+ Supprimer
259
+ </button>
260
+ </div>
261
+ </div>
262
+ ))}
263
+ </div>
264
+ )}
265
+ </div>
266
+
267
+ {showModal && (
268
+ <div className="ui-fade-in fixed inset-0 z-50 flex min-h-dvh items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
269
+ <div className="ui-scale-in flex max-h-[90vh] w-full max-w-2xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
270
+ <div className="shrink-0 border-b border-gray-100 pb-4">
271
+ <div className="flex items-center justify-between">
272
+ <h2 className="text-xl font-bold text-gray-900 sm:text-2xl">
273
+ {editingConfig ? 'Modifier' : 'Ajouter'} une configuration Meta Lead Ads
274
+ </h2>
275
+ <button
276
+ type="button"
277
+ onClick={resetForm}
278
+ className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100"
279
+ >
280
+ <svg
281
+ className="h-6 w-6"
282
+ fill="none"
283
+ stroke="currentColor"
284
+ viewBox="0 0 24 24"
285
+ >
286
+ <path
287
+ strokeLinecap="round"
288
+ strokeLinejoin="round"
289
+ strokeWidth={2}
290
+ d="M6 18L18 6M6 6l12 12"
291
+ />
292
+ </svg>
293
+ </button>
294
+ </div>
295
+ </div>
296
+
297
+ <form
298
+ id="meta-lead-form"
299
+ onSubmit={handleSubmit}
300
+ className="flex-1 space-y-4 overflow-y-auto pt-4"
301
+ >
302
+ <div>
303
+ <label htmlFor="meta-lead-name" className="block text-sm font-medium text-gray-700">
304
+ Nom de la configuration *
305
+ </label>
306
+ <input
307
+ id="meta-lead-name"
308
+ type="text"
309
+ required
310
+ value={formData.name}
311
+ onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
312
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
313
+ placeholder="Ex: Facebook Lead Ads"
314
+ />
315
+ </div>
316
+
317
+ <div className="flex items-center">
318
+ <input
319
+ id="meta-lead-active"
320
+ type="checkbox"
321
+ checked={formData.active}
322
+ onChange={(e) =>
323
+ setFormData((prev) => ({ ...prev, active: e.target.checked }))
324
+ }
325
+ className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-gray-400/30"
326
+ />
327
+ <label
328
+ htmlFor="meta-lead-active"
329
+ className="ml-2 text-sm font-medium text-gray-700"
330
+ >
331
+ Activer l&apos;intégration Meta Lead Ads
332
+ </label>
333
+ </div>
334
+
335
+ <div>
336
+ <label htmlFor="meta-lead-page-id" className="block text-sm font-medium text-gray-700">Page ID *</label>
337
+ <input
338
+ id="meta-lead-page-id"
339
+ type="text"
340
+ required
341
+ value={formData.pageId}
342
+ onChange={(e) => setFormData((prev) => ({ ...prev, pageId: e.target.value }))}
343
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
344
+ placeholder="Page ID Facebook"
345
+ />
346
+ </div>
347
+
348
+ <div>
349
+ <label htmlFor="meta-lead-access-token" className="block text-sm font-medium text-gray-700">Access Token *</label>
350
+ <input
351
+ id="meta-lead-access-token"
352
+ type="password"
353
+ autoComplete="off"
354
+ required
355
+ value={formData.accessToken}
356
+ onChange={(e) =>
357
+ setFormData((prev) => ({ ...prev, accessToken: e.target.value }))
358
+ }
359
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
360
+ placeholder="Access Token"
361
+ />
362
+ </div>
363
+
364
+ <div>
365
+ <label htmlFor="meta-lead-verify-token" className="block text-sm font-medium text-gray-700">Verify Token *</label>
366
+ <input
367
+ id="meta-lead-verify-token"
368
+ type="text"
369
+ required
370
+ value={formData.verifyToken}
371
+ onChange={(e) =>
372
+ setFormData((prev) => ({ ...prev, verifyToken: e.target.value }))
373
+ }
374
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
375
+ placeholder="Verify Token"
376
+ />
377
+ </div>
378
+
379
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
380
+ <div>
381
+ <label htmlFor="meta-lead-assigned-user" className="block text-sm font-medium text-gray-700">
382
+ Utilisateur assigné par défaut (optionnel)
383
+ </label>
384
+ <select
385
+ id="meta-lead-assigned-user"
386
+ value={formData.defaultAssignedUserId || ''}
387
+ onChange={(e) =>
388
+ setFormData((prev) => ({
389
+ ...prev,
390
+ defaultAssignedUserId: e.target.value || null,
391
+ }))
392
+ }
393
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
394
+ >
395
+ <option value="">Aucun utilisateur par défaut</option>
396
+ {users.map((user) => (
397
+ <option key={user.id} value={user.id}>
398
+ {user.name} ({user.email})
399
+ </option>
400
+ ))}
401
+ </select>
402
+ </div>
403
+
404
+ <div>
405
+ <label htmlFor="meta-lead-default-status" className="block text-sm font-medium text-gray-700">
406
+ Statut par défaut
407
+ </label>
408
+ <StatusSelect
409
+ id="meta-lead-default-status"
410
+ statuses={[...statuses]}
411
+ value={formData.defaultStatusId || ''}
412
+ onChange={(v) =>
413
+ setFormData((prev) => ({
414
+ ...prev,
415
+ defaultStatusId: v || null,
416
+ }))
417
+ }
418
+ placeholder="Aucun statut par défaut"
419
+ className="mt-1"
420
+ />
421
+ </div>
422
+ </div>
423
+ </form>
424
+
425
+ <div className="shrink-0 border-t border-gray-100 pt-4">
426
+ <div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
427
+ <button
428
+ type="button"
429
+ onClick={resetForm}
430
+ 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"
431
+ >
432
+ Annuler
433
+ </button>
434
+ <button
435
+ type="submit"
436
+ form="meta-lead-form"
437
+ disabled={saving}
438
+ className="w-full cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
439
+ >
440
+ {saving ? 'Enregistrement...' : 'Enregistrer'}
441
+ </button>
442
+ </div>
443
+ </div>
444
+ </div>
445
+ </div>
446
+ )}
447
+
448
+ <ConfirmDialog />
449
+ </>
450
+ );
451
+ }
@@ -9,7 +9,7 @@ import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
9
9
  import { useSidebarContext } from '@/contexts/sidebar-context';
10
10
  import { useViewAs } from '@/contexts/view-as-context';
11
11
  import { ViewAsModal } from '@/components/view-as-modal';
12
- import { Eye, X, PanelLeftClose, PanelLeftOpen } from 'lucide-react';
12
+ import { Eye, X, PanelLeftClose, PanelLeftOpen, FlaskConical } from 'lucide-react';
13
13
  import { cn } from '@/lib/utils';
14
14
  import { NAV_PAGES } from '@/config/nav-pages';
15
15
 
@@ -56,7 +56,7 @@ export function Sidebar() {
56
56
  {/* Overlay for mobile */}
57
57
  {isMobileMenuOpen && (
58
58
  <div
59
- className="fixed inset-0 z-40 bg-foreground/10 backdrop-blur-sm lg:hidden"
59
+ className="bg-foreground/10 ui-fade-in fixed inset-0 z-40 backdrop-blur-sm lg:hidden"
60
60
  onClick={() => setIsMobileMenuOpen(false)}
61
61
  />
62
62
  )}
@@ -64,12 +64,16 @@ export function Sidebar() {
64
64
  {/* Sidebar */}
65
65
  <div
66
66
  className={cn(
67
- 'group fixed top-0 left-0 z-40 flex h-screen flex-col border-r border-sidebar-border bg-sidebar text-sidebar-foreground shadow-(--shadow-card) transition-all duration-300 ease-(--ease-standard) lg:relative lg:translate-x-0',
67
+ 'group border-sidebar-border bg-sidebar text-sidebar-foreground fixed top-0 left-0 z-40 flex h-screen flex-col border-r shadow-(--shadow-card) transition-[width,transform] duration-300 ease-(--ease-standard) lg:relative lg:translate-x-0',
68
68
  isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0',
69
69
  !isSidebarExpanded ? 'w-64 lg:w-16' : 'w-64 lg:w-64',
70
70
  )}
71
71
  onMouseEnter={() => {
72
- if (typeof globalThis.window !== 'undefined' && globalThis.window.innerWidth >= 1024 && !isPinned) {
72
+ if (
73
+ typeof globalThis.window !== 'undefined' &&
74
+ globalThis.window.innerWidth >= 1024 &&
75
+ !isPinned
76
+ ) {
73
77
  setExpandedByHover(true);
74
78
  }
75
79
  }}
@@ -86,7 +90,7 @@ export function Sidebar() {
86
90
  <div className="flex items-center justify-end">
87
91
  <button
88
92
  onClick={() => setIsMobileMenuOpen(false)}
89
- className="cursor-pointer rounded-lg p-2 text-sidebar-foreground/70 transition-colors duration-200 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground lg:hidden"
93
+ className="text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground cursor-pointer rounded-lg p-2 transition-colors duration-200 lg:hidden"
90
94
  aria-label="Close menu"
91
95
  >
92
96
  <X className="h-5 w-5" />
@@ -103,21 +107,36 @@ export function Sidebar() {
103
107
  href={item.href}
104
108
  onClick={handleLinkClick}
105
109
  className={cn(
106
- 'flex items-center gap-3 rounded-lg py-2 text-sm font-medium transition-colors',
110
+ 'flex items-center gap-3 rounded-lg py-2 text-sm font-medium transition-[color,background-color,box-shadow] duration-(--duration-normal) ease-(--ease-standard)',
107
111
  !isSidebarExpanded ? 'px-3 lg:justify-center lg:px-2' : 'px-3',
108
112
  isActive
109
113
  ? 'bg-sidebar-primary text-sidebar-primary-foreground shadow-sm'
110
- : 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
114
+ : 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:scale-[0.97]',
111
115
  )}
112
116
  title={!isSidebarExpanded ? displayName : undefined}
113
117
  >
114
- <Icon className="h-5 w-5 shrink-0" />
115
- {isSidebarExpanded && (
116
- <span className="whitespace-nowrap">{displayName}</span>
117
- )}
118
+ <Icon aria-hidden="true" className="h-5 w-5 shrink-0" />
119
+ {isSidebarExpanded && <span className="whitespace-nowrap">{displayName}</span>}
118
120
  </Link>
119
121
  );
120
122
  })}
123
+ {process.env.NODE_ENV === 'development' && (
124
+ <Link
125
+ href="/dev"
126
+ onClick={handleLinkClick}
127
+ className={cn(
128
+ 'flex items-center gap-3 rounded-lg py-2 text-sm font-medium transition-[color,background-color,box-shadow] duration-(--duration-normal) ease-(--ease-standard)',
129
+ !isSidebarExpanded ? 'px-3 lg:justify-center lg:px-2' : 'px-3',
130
+ pathname === '/dev'
131
+ ? 'bg-sidebar-primary text-sidebar-primary-foreground shadow-sm'
132
+ : 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:scale-[0.97]',
133
+ )}
134
+ title={!isSidebarExpanded ? 'Dev' : undefined}
135
+ >
136
+ <FlaskConical aria-hidden="true" className="h-5 w-5 shrink-0" />
137
+ {isSidebarExpanded && <span className="whitespace-nowrap">Dev</span>}
138
+ </Link>
139
+ )}
121
140
  </div>
122
141
  </div>
123
142
  </nav>
@@ -126,7 +145,7 @@ export function Sidebar() {
126
145
  {isRealAdmin && (
127
146
  <div
128
147
  className={cn(
129
- 'border-t border-sidebar-border transition-all duration-300',
148
+ 'border-sidebar-border border-t transition-[padding] duration-300',
130
149
  !isSidebarExpanded ? 'p-3 lg:p-2' : 'p-3',
131
150
  )}
132
151
  >
@@ -134,7 +153,7 @@ export function Sidebar() {
134
153
  <button
135
154
  onClick={() => setShowViewAsModal(true)}
136
155
  className={cn(
137
- 'w-full cursor-pointer rounded-lg border-2 p-3 text-left transition-all',
156
+ 'w-full cursor-pointer rounded-lg border-2 p-3 text-left transition-colors',
138
157
  isViewingAsOther
139
158
  ? 'border-sidebar-primary bg-sidebar-primary text-sidebar-primary-foreground hover:opacity-95'
140
159
  : 'border-sidebar-border bg-sidebar text-sidebar-foreground hover:border-sidebar-ring hover:bg-sidebar-accent',
@@ -175,7 +194,7 @@ export function Sidebar() {
175
194
  : session?.user?.name || 'Utilisateur'}
176
195
  </p>
177
196
  </div>
178
- <Eye className="h-5 w-5 shrink-0" />
197
+ <Eye aria-hidden="true" className="h-5 w-5 shrink-0" />
179
198
  </div>
180
199
  </button>
181
200
  ) : (
@@ -191,7 +210,7 @@ export function Sidebar() {
191
210
  aria-label="Changer de vue"
192
211
  >
193
212
  <div className="flex items-center justify-center">
194
- <Eye className="h-5 w-5" />
213
+ <Eye aria-hidden="true" className="h-5 w-5" />
195
214
  </div>
196
215
  </button>
197
216
  )}
@@ -201,14 +220,14 @@ export function Sidebar() {
201
220
  {/* Bouton Réduire / Développer la navigation (desktop uniquement) */}
202
221
  <div
203
222
  className={cn(
204
- 'hidden border-t border-sidebar-border lg:block',
223
+ 'border-sidebar-border hidden border-t lg:block',
205
224
  !isSidebarExpanded ? 'p-3 lg:p-2' : 'p-3',
206
225
  )}
207
226
  >
208
227
  <button
209
228
  onClick={togglePin}
210
229
  className={cn(
211
- 'flex w-full cursor-pointer items-center gap-3 rounded-lg py-2 text-sm font-medium text-sidebar-foreground/70 transition-colors duration-200 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
230
+ 'text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex w-full cursor-pointer items-center gap-3 rounded-lg py-2 text-sm font-medium transition-colors duration-200',
212
231
  !isSidebarExpanded ? 'justify-center px-2' : 'px-3',
213
232
  )}
214
233
  title={isPinned ? 'Réduire la navigation' : 'Développer la navigation'}
@@ -216,12 +235,12 @@ export function Sidebar() {
216
235
  >
217
236
  {isPinned ? (
218
237
  <>
219
- <PanelLeftClose className="h-5 w-5 shrink-0" />
238
+ <PanelLeftClose aria-hidden="true" className="h-5 w-5 shrink-0" />
220
239
  <span className="whitespace-nowrap">Réduire la navigation</span>
221
240
  </>
222
241
  ) : (
223
242
  <div className="flex gap-2">
224
- <PanelLeftOpen className="h-5 w-5 shrink-0" />
243
+ <PanelLeftOpen aria-hidden="true" className="h-5 w-5 shrink-0" />
225
244
  <span className="hidden whitespace-nowrap group-hover:block">
226
245
  Développer la navigation
227
246
  </span>
@@ -233,40 +252,40 @@ export function Sidebar() {
233
252
  {/* User Profile */}
234
253
  <div
235
254
  className={cn(
236
- 'border-t border-sidebar-border transition-all duration-300',
255
+ 'border-sidebar-border border-t transition-[padding] duration-300',
237
256
  !isSidebarExpanded ? 'p-4 lg:p-2' : 'p-4',
238
257
  )}
239
258
  >
240
259
  {isSidebarExpanded ? (
241
260
  <>
242
261
  <div className="flex items-center gap-3">
243
- <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-sidebar-accent text-sidebar-accent-foreground">
262
+ <div className="bg-sidebar-accent text-sidebar-accent-foreground flex h-10 w-10 shrink-0 items-center justify-center rounded-full">
244
263
  {!isMounted ? 'U' : session?.user?.name?.[0]?.toUpperCase() || 'U'}
245
264
  </div>
246
265
  <div className="min-w-0 flex-1">
247
- <p className="truncate text-sm font-medium text-sidebar-foreground">
266
+ <p className="text-sidebar-foreground truncate text-sm font-medium">
248
267
  {!isMounted ? 'Utilisateur' : session?.user?.name || 'Utilisateur'}
249
268
  </p>
250
- <p className="truncate text-xs text-sidebar-foreground/70">
269
+ <p className="text-sidebar-foreground/70 truncate text-xs">
251
270
  {!isMounted ? '' : session?.user?.email}
252
271
  </p>
253
272
  </div>
254
273
  </div>
255
274
  <button
256
275
  onClick={handleSignOut}
257
- className="mt-3 w-full cursor-pointer rounded-lg bg-sidebar-accent px-3 py-2 text-sm font-medium text-sidebar-accent-foreground transition-colors duration-200 hover:bg-sidebar-primary hover:text-sidebar-primary-foreground"
276
+ className="bg-sidebar-accent text-sidebar-accent-foreground hover:bg-sidebar-primary hover:text-sidebar-primary-foreground mt-3 w-full cursor-pointer rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200"
258
277
  >
259
278
  Déconnexion
260
279
  </button>
261
280
  </>
262
281
  ) : (
263
282
  <div className="flex flex-col items-center gap-2">
264
- <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-sidebar-accent text-sidebar-accent-foreground">
283
+ <div className="bg-sidebar-accent text-sidebar-accent-foreground flex h-10 w-10 shrink-0 items-center justify-center rounded-full">
265
284
  {!isMounted ? 'U' : session?.user?.name?.[0]?.toUpperCase() || 'U'}
266
285
  </div>
267
286
  <button
268
287
  onClick={handleSignOut}
269
- className="cursor-pointer rounded-lg p-2 text-sidebar-foreground/70 transition-colors duration-200 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
288
+ className="text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground cursor-pointer rounded-lg p-2 transition-colors duration-200"
270
289
  title="Déconnexion"
271
290
  aria-label="Déconnexion"
272
291
  >