create-crm-tmp 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/package.json +1 -1
  2. package/template/.prettierignore +2 -0
  3. package/template/README.md +53 -67
  4. package/template/components.json +22 -0
  5. package/template/exemple-contacts.csv +54 -0
  6. package/template/next.config.ts +27 -1
  7. package/template/package.json +64 -27
  8. package/template/prisma/schema.prisma +821 -72
  9. package/template/skills-lock.json +25 -0
  10. package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
  11. package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
  12. package/template/src/app/(auth)/reset-password/page.tsx +12 -8
  13. package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
  14. package/template/src/app/(auth)/signin/page.tsx +20 -17
  15. package/template/src/app/(dashboard)/agenda/page.tsx +2231 -2188
  16. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
  17. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
  18. package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
  19. package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
  20. package/template/src/app/(dashboard)/closing/page.tsx +500 -468
  21. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5035 -4126
  22. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
  23. package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
  24. package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
  25. package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
  26. package/template/src/app/(dashboard)/error.tsx +37 -0
  27. package/template/src/app/(dashboard)/layout.tsx +1 -1
  28. package/template/src/app/(dashboard)/loading.tsx +5 -0
  29. package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
  30. package/template/src/app/(dashboard)/settings/page.tsx +2685 -2489
  31. package/template/src/app/(dashboard)/templates/page.tsx +500 -300
  32. package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
  33. package/template/src/app/(dashboard)/users/page.tsx +279 -310
  34. package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
  35. package/template/src/app/(dashboard)/users/roles/page.tsx +164 -137
  36. package/template/src/app/api/audit-logs/route.ts +1 -1
  37. package/template/src/app/api/auth/google/callback/route.ts +8 -5
  38. package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
  39. package/template/src/app/api/companies/[id]/activities/route.ts +131 -0
  40. package/template/src/app/api/companies/[id]/route.ts +195 -0
  41. package/template/src/app/api/companies/export/route.ts +206 -0
  42. package/template/src/app/api/companies/route.ts +166 -0
  43. package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
  44. package/template/src/app/api/contact-views/[id]/route.ts +197 -0
  45. package/template/src/app/api/contact-views/route.ts +146 -0
  46. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +77 -0
  47. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
  48. package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
  49. package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
  50. package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
  51. package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
  52. package/template/src/app/api/contacts/[id]/route.ts +111 -20
  53. package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
  54. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
  55. package/template/src/app/api/contacts/export/route.ts +12 -17
  56. package/template/src/app/api/contacts/import/route.ts +22 -19
  57. package/template/src/app/api/contacts/import-preview/route.ts +139 -0
  58. package/template/src/app/api/contacts/route.ts +202 -49
  59. package/template/src/app/api/dashboard/stats/route.ts +9 -292
  60. package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
  61. package/template/src/app/api/invite/complete/route.ts +20 -23
  62. package/template/src/app/api/reminders/route.ts +1 -0
  63. package/template/src/app/api/reset-password/complete/route.ts +11 -13
  64. package/template/src/app/api/send/route.ts +9 -85
  65. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
  66. package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
  67. package/template/src/app/api/settings/company/route.ts +19 -26
  68. package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
  69. package/template/src/app/api/settings/google-ads/route.ts +20 -23
  70. package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
  71. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
  72. package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
  73. package/template/src/app/api/settings/google-sheet/route.ts +20 -23
  74. package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
  75. package/template/src/app/api/settings/meta-leads/route.ts +20 -23
  76. package/template/src/app/api/settings/statuses/[id]/route.ts +33 -23
  77. package/template/src/app/api/settings/statuses/route.ts +24 -22
  78. package/template/src/app/api/statuses/route.ts +2 -5
  79. package/template/src/app/api/tasks/[id]/attendees/route.ts +14 -7
  80. package/template/src/app/api/tasks/[id]/route.ts +161 -137
  81. package/template/src/app/api/tasks/meet/route.ts +11 -8
  82. package/template/src/app/api/tasks/route.ts +155 -95
  83. package/template/src/app/api/templates/[id]/route.ts +22 -13
  84. package/template/src/app/api/templates/route.ts +22 -5
  85. package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
  86. package/template/src/app/api/users/[id]/route.ts +16 -1
  87. package/template/src/app/api/users/commercials/route.ts +38 -0
  88. package/template/src/app/api/users/for-agenda/route.ts +1 -2
  89. package/template/src/app/api/users/route.ts +94 -55
  90. package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
  91. package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
  92. package/template/src/app/api/workflows/[id]/route.ts +33 -6
  93. package/template/src/app/api/workflows/process/route.ts +509 -146
  94. package/template/src/app/api/workflows/route.ts +46 -4
  95. package/template/src/app/globals.css +210 -101
  96. package/template/src/app/layout.tsx +19 -8
  97. package/template/src/app/page.tsx +37 -7
  98. package/template/src/components/address-autocomplete.tsx +232 -0
  99. package/template/src/components/contacts/filter-bar.tsx +181 -0
  100. package/template/src/components/contacts/filter-builder.tsx +589 -0
  101. package/template/src/components/contacts/save-view-dialog.tsx +160 -0
  102. package/template/src/components/contacts/views-tab-bar.tsx +440 -0
  103. package/template/src/components/dashboard/activity-chart.tsx +31 -39
  104. package/template/src/components/dashboard/dashboard-content.tsx +79 -0
  105. package/template/src/components/dashboard/stat-card.tsx +40 -42
  106. package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
  107. package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
  108. package/template/src/components/date-picker.tsx +396 -0
  109. package/template/src/components/editor.tsx +27 -13
  110. package/template/src/components/email-template.tsx +4 -2
  111. package/template/src/components/global-search.tsx +358 -0
  112. package/template/src/components/header.tsx +57 -62
  113. package/template/src/components/invitation-email-template.tsx +4 -2
  114. package/template/src/components/lazy-editor.tsx +11 -0
  115. package/template/src/components/meet-cancellation-email-template.tsx +11 -3
  116. package/template/src/components/meet-confirmation-email-template.tsx +10 -3
  117. package/template/src/components/meet-update-email-template.tsx +10 -3
  118. package/template/src/components/page-header.tsx +19 -15
  119. package/template/src/components/protected-page.tsx +94 -0
  120. package/template/src/components/reset-password-email-template.tsx +4 -2
  121. package/template/src/components/sidebar.tsx +92 -94
  122. package/template/src/components/skeleton.tsx +128 -42
  123. package/template/src/components/ui/accordion.tsx +64 -0
  124. package/template/src/components/ui/alert-dialog.tsx +139 -0
  125. package/template/src/components/ui/button.tsx +60 -0
  126. package/template/src/components/view-as-banner.tsx +1 -1
  127. package/template/src/components/view-as-modal.tsx +21 -16
  128. package/template/src/config/nav-pages.ts +108 -0
  129. package/template/src/contexts/app-toast-context.tsx +174 -0
  130. package/template/src/contexts/sidebar-context.tsx +16 -47
  131. package/template/src/contexts/task-reminder-context.tsx +6 -6
  132. package/template/src/contexts/view-as-context.tsx +11 -16
  133. package/template/src/hooks/use-alert.tsx +65 -0
  134. package/template/src/hooks/use-confirm.tsx +87 -0
  135. package/template/src/hooks/use-contact-views.ts +140 -0
  136. package/template/src/hooks/use-contacts.ts +69 -0
  137. package/template/src/hooks/use-fetch.ts +17 -0
  138. package/template/src/hooks/use-focus-trap.ts +73 -0
  139. package/template/src/hooks/use-statuses.ts +22 -0
  140. package/template/src/lib/address-api.ts +155 -0
  141. package/template/src/lib/cache.ts +73 -0
  142. package/template/src/lib/check-permission.ts +12 -177
  143. package/template/src/lib/contact-interactions.ts +3 -1
  144. package/template/src/lib/contact-view-filters.ts +341 -0
  145. package/template/src/lib/dashboard-stats.ts +224 -0
  146. package/template/src/lib/date-utils.ts +49 -0
  147. package/template/src/lib/get-auth-user.ts +25 -0
  148. package/template/src/lib/google-calendar.ts +54 -12
  149. package/template/src/lib/google-drive.ts +796 -75
  150. package/template/src/lib/google-fetch.ts +63 -0
  151. package/template/src/lib/local-storage.ts +34 -0
  152. package/template/src/lib/permissions.ts +245 -47
  153. package/template/src/lib/prisma.ts +11 -11
  154. package/template/src/lib/roles.ts +14 -39
  155. package/template/src/lib/template-variables.ts +67 -33
  156. package/template/src/lib/utils.ts +26 -2
  157. package/template/src/lib/workflow-executor.ts +445 -229
  158. package/template/src/proxy.ts +34 -73
  159. package/template/src/types/contact-views.ts +351 -0
  160. package/template/src/types/yousign.ts +52 -0
  161. package/template/vercel.json +12 -0
  162. package/template/WORKFLOWS_CRON.md +0 -185
  163. package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
  164. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
  165. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
  166. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
  167. package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
  168. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
  169. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
  170. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
  171. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
  172. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
  173. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
  174. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
  175. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
  176. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
  177. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
  178. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
  179. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
  180. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
  181. package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
  182. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
  183. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
  184. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
  185. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
  186. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
  187. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
  188. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
  189. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
  190. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
  191. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
  192. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
  193. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
  194. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
  195. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
  196. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
  197. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
  198. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
  199. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
  200. package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
  201. package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
  202. package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
  203. package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
  204. package/template/prisma/migrations/migration_lock.toml +0 -3
  205. package/template/src/app/(dashboard)/users/layout.tsx +0 -30
  206. package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
  207. package/template/src/app/api/dashboard/widgets/route.ts +0 -181
  208. package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
  209. package/template/src/components/dashboard/color-picker.tsx +0 -65
  210. package/template/src/components/dashboard/contacts-chart.tsx +0 -69
  211. package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
  212. package/template/src/components/dashboard/recent-activity.tsx +0 -157
  213. package/template/src/components/dashboard/sales-analytics-chart.tsx +0 -77
  214. package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
  215. package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
  216. package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
  217. package/template/src/contexts/dashboard-theme-context.tsx +0 -58
  218. package/template/src/lib/dashboard-themes.ts +0 -140
  219. package/template/src/lib/default-widgets.ts +0 -14
  220. package/template/src/lib/widget-registry.ts +0 -177
@@ -0,0 +1,396 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useRef, useEffect } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+ import { Calendar, ChevronLeft, ChevronRight, X } from 'lucide-react';
6
+ import { cn } from '@/lib/utils';
7
+
8
+ interface DatePickerProps {
9
+ startDate: string;
10
+ endDate?: string;
11
+ onDateChange: (startDate: string, endDate?: string) => void;
12
+ label?: string;
13
+ isPeriod?: boolean; // Si true, permet de sélectionner une période
14
+ }
15
+
16
+ export const DatePicker: React.FC<DatePickerProps> = ({
17
+ startDate,
18
+ endDate = '',
19
+ onDateChange,
20
+ label = 'Sélectionner la date',
21
+ isPeriod = false,
22
+ }) => {
23
+ const [isOpen, setIsOpen] = useState(false);
24
+ const [currentMonth, setCurrentMonth] = useState(new Date());
25
+
26
+ // Helper pour parser les dates ISO en mode local
27
+ const parseISODate = (dateString: string): Date | null => {
28
+ if (!dateString) return null;
29
+ const [year, month, day] = dateString.split('-').map(Number);
30
+ return new Date(year, month - 1, day);
31
+ };
32
+
33
+ const [tempStartDate, setTempStartDate] = useState<Date | null>(
34
+ startDate ? parseISODate(startDate) : null,
35
+ );
36
+ const [tempEndDate, setTempEndDate] = useState<Date | null>(
37
+ endDate ? parseISODate(endDate) : null,
38
+ );
39
+
40
+ // Mettre à jour les dates temporaires quand les props changent
41
+ useEffect(() => {
42
+ setTempStartDate(startDate ? parseISODate(startDate) : null);
43
+ setTempEndDate(endDate ? parseISODate(endDate) : null);
44
+ }, [startDate, endDate]);
45
+ const [isSelectingStart, setIsSelectingStart] = useState(true);
46
+ const [modalPosition, setModalPosition] = useState({ top: 0, left: 0, width: 0 });
47
+ const [isMounted, setIsMounted] = useState(false);
48
+ const [isDesktopView, setIsDesktopView] = useState(false);
49
+ const dropdownRef = useRef<HTMLDivElement>(null);
50
+ const buttonRef = useRef<HTMLButtonElement>(null);
51
+
52
+ useEffect(() => {
53
+ setIsMounted(true);
54
+ setIsDesktopView(window.innerWidth >= 640);
55
+ }, []);
56
+
57
+ // Gérer la touche Escape
58
+ useEffect(() => {
59
+ const handleEscape = (event: KeyboardEvent) => {
60
+ if (event.key === 'Escape' && isOpen) {
61
+ handleClose();
62
+ }
63
+ };
64
+
65
+ if (isOpen) {
66
+ document.addEventListener('keydown', handleEscape);
67
+ }
68
+
69
+ return () => {
70
+ document.removeEventListener('keydown', handleEscape);
71
+ };
72
+ }, [isOpen]);
73
+
74
+ // Calculer la position de la modale
75
+ useEffect(() => {
76
+ if (isOpen && buttonRef.current) {
77
+ const buttonRect = buttonRef.current.getBoundingClientRect();
78
+ const modalHeight = 450;
79
+ const isDesktop = window.innerWidth >= 640;
80
+
81
+ setIsDesktopView(isDesktop);
82
+
83
+ if (!isDesktop) {
84
+ const spaceAbove = buttonRect.top;
85
+ const spaceBelow = window.innerHeight - buttonRect.bottom;
86
+
87
+ let top: number;
88
+ if (spaceAbove > modalHeight && spaceBelow < modalHeight) {
89
+ top = buttonRect.top + window.scrollY - modalHeight - 8;
90
+ } else {
91
+ top = buttonRect.bottom + window.scrollY + 8;
92
+ }
93
+
94
+ setModalPosition({
95
+ top,
96
+ left: 0,
97
+ width: 0,
98
+ });
99
+ }
100
+ }
101
+ }, [isOpen]);
102
+
103
+ const getDaysInMonth = (date: Date): (Date | null)[] => {
104
+ const year = date.getFullYear();
105
+ const month = date.getMonth();
106
+ const firstDay = new Date(year, month, 1);
107
+ const lastDay = new Date(year, month + 1, 0);
108
+ const firstDayOfWeek = firstDay.getDay();
109
+ const adjustedFirstDay = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
110
+
111
+ const days: (Date | null)[] = [];
112
+ for (let i = 0; i < adjustedFirstDay; i++) {
113
+ days.push(null);
114
+ }
115
+ for (let i = 1; i <= lastDay.getDate(); i++) {
116
+ days.push(new Date(year, month, i));
117
+ }
118
+
119
+ return days;
120
+ };
121
+
122
+ const handleDateClick = (date: Date) => {
123
+ const clickedDate = new Date(date);
124
+ clickedDate.setHours(0, 0, 0, 0);
125
+
126
+ if (!isPeriod) {
127
+ // Mode date unique
128
+ setTempStartDate(clickedDate);
129
+ setTempEndDate(null);
130
+ } else {
131
+ // Mode période
132
+ if (isSelectingStart || !tempStartDate) {
133
+ setTempStartDate(clickedDate);
134
+ setTempEndDate(null);
135
+ setIsSelectingStart(false);
136
+ } else {
137
+ if (clickedDate < tempStartDate) {
138
+ setTempStartDate(clickedDate);
139
+ setTempEndDate(null);
140
+ } else {
141
+ setTempEndDate(clickedDate);
142
+ setIsSelectingStart(true);
143
+ }
144
+ }
145
+ }
146
+ };
147
+
148
+ const handleApply = () => {
149
+ if (tempStartDate) {
150
+ const startISO = `${tempStartDate.getFullYear()}-${String(tempStartDate.getMonth() + 1).padStart(2, '0')}-${String(tempStartDate.getDate()).padStart(2, '0')}`;
151
+
152
+ if (isPeriod && tempEndDate) {
153
+ const endISO = `${tempEndDate.getFullYear()}-${String(tempEndDate.getMonth() + 1).padStart(2, '0')}-${String(tempEndDate.getDate()).padStart(2, '0')}`;
154
+ onDateChange(startISO, endISO);
155
+ } else {
156
+ onDateChange(startISO);
157
+ }
158
+ setIsOpen(false);
159
+ }
160
+ };
161
+
162
+ const handleClose = () => {
163
+ setTempStartDate(startDate ? parseISODate(startDate) : null);
164
+ setTempEndDate(endDate ? parseISODate(endDate) : null);
165
+ setIsOpen(false);
166
+ setIsSelectingStart(true);
167
+ };
168
+
169
+ const handleClear = () => {
170
+ setTempStartDate(null);
171
+ setTempEndDate(null);
172
+ setIsSelectingStart(true);
173
+ };
174
+
175
+ const isDateInRange = (date: Date): boolean => {
176
+ if (!isPeriod || !tempStartDate || !tempEndDate) return false;
177
+ const current = new Date(date);
178
+ current.setHours(0, 0, 0, 0);
179
+ return current >= tempStartDate && current <= tempEndDate;
180
+ };
181
+
182
+ const isDateSelected = (date: Date): boolean => {
183
+ const current = new Date(date);
184
+ current.setHours(0, 0, 0, 0);
185
+ return !!(
186
+ (tempStartDate && current.getTime() === tempStartDate.getTime()) ||
187
+ (tempEndDate && current.getTime() === tempEndDate.getTime())
188
+ );
189
+ };
190
+
191
+ const isToday = (date: Date): boolean => {
192
+ const today = new Date();
193
+ return (
194
+ date.getDate() === today.getDate() &&
195
+ date.getMonth() === today.getMonth() &&
196
+ date.getFullYear() === today.getFullYear()
197
+ );
198
+ };
199
+
200
+ const isWeekend = (date: Date): boolean => {
201
+ const day = date.getDay();
202
+ return day === 0 || day === 6;
203
+ };
204
+
205
+ const formatDisplayDate = (): string => {
206
+ if (!tempStartDate && !startDate) return label;
207
+
208
+ const displayStart = tempStartDate || (startDate ? parseISODate(startDate) : null);
209
+ const displayEnd = tempEndDate || (endDate ? parseISODate(endDate) : null);
210
+
211
+ if (!displayStart) return label;
212
+
213
+ if (isPeriod && displayEnd) {
214
+ return `Du ${displayStart.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })} au ${displayEnd.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' })}`;
215
+ }
216
+
217
+ return displayStart.toLocaleDateString('fr-FR', {
218
+ day: 'numeric',
219
+ month: 'short',
220
+ year: 'numeric',
221
+ });
222
+ };
223
+
224
+ const days = getDaysInMonth(currentMonth);
225
+ const dayNames = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'];
226
+
227
+ const canApply = isPeriod ? tempStartDate && tempEndDate : tempStartDate;
228
+
229
+ const modalContent = isOpen && isMounted && (
230
+ <>
231
+ <div
232
+ className="fixed inset-0 z-[9998] cursor-pointer bg-black/20 backdrop-blur-sm transition-opacity duration-200"
233
+ onClick={handleClose}
234
+ />
235
+ <div
236
+ ref={dropdownRef}
237
+ className={cn(
238
+ 'fixed z-[9999] transition-all duration-200',
239
+ 'left-1/2 -translate-x-1/2',
240
+ isDesktopView && 'top-1/2 -translate-y-1/2',
241
+ )}
242
+ style={
243
+ !isDesktopView
244
+ ? {
245
+ top: `${modalPosition.top}px`,
246
+ }
247
+ : undefined
248
+ }
249
+ >
250
+ <div
251
+ className={cn(
252
+ 'rounded-xl border border-gray-200 bg-white p-4 shadow-2xl',
253
+ 'w-[320px] sm:w-[400px]',
254
+ 'max-h-[90vh] overflow-y-auto',
255
+ )}
256
+ style={{
257
+ maxWidth: '95vw',
258
+ }}
259
+ >
260
+ <div className="mb-3 flex items-center justify-between border-b border-gray-100 pb-3">
261
+ <div className="min-w-0 flex-1">
262
+ <h3 className="truncate text-sm font-semibold text-gray-900">
263
+ {isPeriod ? 'Sélectionner une période' : 'Sélectionner une date'}
264
+ </h3>
265
+ {isPeriod && (
266
+ <p className="mt-0.5 text-xs text-gray-500">
267
+ {isSelectingStart
268
+ ? 'Cliquez pour sélectionner la date de début'
269
+ : 'Cliquez pour sélectionner la date de fin'}
270
+ </p>
271
+ )}
272
+ </div>
273
+ <button
274
+ onClick={handleClose}
275
+ className="ml-2 shrink-0 cursor-pointer rounded-lg p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
276
+ >
277
+ <X className="h-4 w-4" />
278
+ </button>
279
+ </div>
280
+
281
+ <div className="space-y-3">
282
+ <div>
283
+ <div className="mb-2 flex items-center justify-between">
284
+ <button
285
+ type="button"
286
+ onClick={() =>
287
+ setCurrentMonth(new Date(currentMonth.setMonth(currentMonth.getMonth() - 1)))
288
+ }
289
+ className="shrink-0 cursor-pointer rounded-lg p-1.5 text-gray-600 transition-colors hover:bg-gray-100"
290
+ >
291
+ <ChevronLeft className="h-4 w-4" />
292
+ </button>
293
+ <h3 className="min-w-0 flex-1 truncate text-center text-xs font-semibold text-gray-900 sm:text-sm">
294
+ {currentMonth.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })}
295
+ </h3>
296
+ <button
297
+ type="button"
298
+ onClick={() =>
299
+ setCurrentMonth(new Date(currentMonth.setMonth(currentMonth.getMonth() + 1)))
300
+ }
301
+ className="shrink-0 cursor-pointer rounded-lg p-1.5 text-gray-600 transition-colors hover:bg-gray-100"
302
+ >
303
+ <ChevronRight className="h-4 w-4" />
304
+ </button>
305
+ </div>
306
+
307
+ <div className="grid grid-cols-7 gap-0.5 sm:gap-1">
308
+ {dayNames.map((day) => (
309
+ <div
310
+ key={day}
311
+ className="py-1 text-center text-[9px] font-medium text-gray-500 sm:text-[10px]"
312
+ >
313
+ {day}
314
+ </div>
315
+ ))}
316
+ {days.map((date, index) => {
317
+ if (!date) {
318
+ return <div key={`empty-${index}`} className="aspect-square" />;
319
+ }
320
+
321
+ const inRange = isDateInRange(date);
322
+ const selected = isDateSelected(date);
323
+ const today = isToday(date);
324
+ const weekend = isWeekend(date);
325
+
326
+ return (
327
+ <button
328
+ key={date.toISOString()}
329
+ type="button"
330
+ onClick={() => handleDateClick(date)}
331
+ className={cn(
332
+ 'aspect-square cursor-pointer rounded-md text-[10px] font-medium transition-all sm:text-xs',
333
+ selected &&
334
+ 'z-10 bg-blue-600 text-white shadow-md shadow-blue-500/30 hover:bg-blue-700',
335
+ inRange && !selected && 'bg-blue-50 text-blue-700 hover:bg-blue-100',
336
+ !inRange && !selected && 'text-gray-700 hover:bg-gray-100',
337
+ weekend && !selected && !inRange && 'text-gray-400',
338
+ today && !selected && !inRange && 'border border-blue-400',
339
+ )}
340
+ >
341
+ {date.getDate()}
342
+ </button>
343
+ );
344
+ })}
345
+ </div>
346
+ </div>
347
+ </div>
348
+
349
+ <div className="mt-3 flex flex-col gap-2 border-t border-gray-100 pt-3 sm:flex-row sm:items-center sm:justify-between">
350
+ <button
351
+ type="button"
352
+ onClick={handleClear}
353
+ className="cursor-pointer text-center text-xs font-medium text-gray-600 transition-colors hover:text-gray-900 sm:text-left"
354
+ >
355
+ Effacer
356
+ </button>
357
+ <div className="flex items-center gap-2">
358
+ <button
359
+ type="button"
360
+ onClick={handleClose}
361
+ className="flex-1 cursor-pointer rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50 sm:flex-initial"
362
+ >
363
+ Annuler
364
+ </button>
365
+ <button
366
+ type="button"
367
+ onClick={handleApply}
368
+ disabled={!canApply}
369
+ className="flex-1 rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-blue-700 enabled:cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 sm:flex-initial"
370
+ >
371
+ Appliquer
372
+ </button>
373
+ </div>
374
+ </div>
375
+ </div>
376
+ </div>
377
+ </>
378
+ );
379
+
380
+ return (
381
+ <>
382
+ <button
383
+ ref={buttonRef}
384
+ type="button"
385
+ onClick={() => setIsOpen(!isOpen)}
386
+ className="flex w-full cursor-pointer items-center justify-between rounded-xl border border-gray-300 bg-white px-4 py-2.5 text-left text-sm shadow-sm transition-colors hover:border-blue-400 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
387
+ >
388
+ <div className="flex items-center gap-2">
389
+ <Calendar className="h-4 w-4 text-gray-400" />
390
+ <div className="font-medium text-gray-900">{formatDisplayDate()}</div>
391
+ </div>
392
+ </button>
393
+ {isMounted && modalContent && createPortal(modalContent, document.body)}
394
+ </>
395
+ );
396
+ };
@@ -84,7 +84,11 @@ type TableConfig = {
84
84
  };
85
85
 
86
86
  // Create markdown extension instance
87
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
88
+ // @ts-ignore - Lexical duplicate types between packages
87
89
  const markdownExt = new MarkdownExtension().configure({
90
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
91
+ // @ts-ignore - Lexical duplicate types between packages
88
92
  customTransformers: ALL_MARKDOWN_TRANSFORMERS,
89
93
  });
90
94
 
@@ -160,7 +164,7 @@ function useImageHandlers(commands: EditorCommands, editor: LexicalEditor | null
160
164
  try {
161
165
  src = await imageExtension.config.uploadHandler(file);
162
166
  } catch (error) {
163
- alert('Failed to upload image');
167
+ console.error('Failed to upload image:', error);
164
168
  return;
165
169
  }
166
170
  } else {
@@ -635,13 +639,18 @@ function Toolbar({
635
639
  <label htmlFor="table-rows">Rows:</label>
636
640
  <input
637
641
  id="table-rows"
638
- type="number"
639
- min="1"
640
- max="20"
642
+ type="text"
643
+ inputMode="numeric"
641
644
  value={tableConfig.rows}
642
- onChange={(e) =>
643
- setTableConfig((prev) => ({ ...prev, rows: parseInt(e.target.value) || 1 }))
644
- }
645
+ onChange={(e) => {
646
+ const value = e.target.value;
647
+ if (value === '' || /^\d+$/.test(value)) {
648
+ const num = parseInt(value) || 1;
649
+ if (num >= 1 && num <= 20) {
650
+ setTableConfig((prev) => ({ ...prev, rows: num }));
651
+ }
652
+ }
653
+ }}
645
654
  className="lexkit-input"
646
655
  />
647
656
  </div>
@@ -649,13 +658,18 @@ function Toolbar({
649
658
  <label htmlFor="table-columns">Columns:</label>
650
659
  <input
651
660
  id="table-columns"
652
- type="number"
653
- min="1"
654
- max="20"
661
+ type="text"
662
+ inputMode="numeric"
655
663
  value={tableConfig.columns}
656
- onChange={(e) =>
657
- setTableConfig((prev) => ({ ...prev, columns: parseInt(e.target.value) || 1 }))
658
- }
664
+ onChange={(e) => {
665
+ const value = e.target.value;
666
+ if (value === '' || /^\d+$/.test(value)) {
667
+ const num = parseInt(value) || 1;
668
+ if (num >= 1 && num <= 20) {
669
+ setTableConfig((prev) => ({ ...prev, columns: num }));
670
+ }
671
+ }
672
+ }}
659
673
  className="lexkit-input"
660
674
  />
661
675
  </div>
@@ -1,3 +1,5 @@
1
+ import DOMPurify from 'isomorphic-dompurify';
2
+
1
3
  interface EmailTemplateProps {
2
4
  firstName: string;
3
5
  signature?: string | null;
@@ -7,7 +9,7 @@ export function EmailTemplate({ firstName, signature }: EmailTemplateProps) {
7
9
  return (
8
10
  <div
9
11
  style={{
10
- fontFamily: 'Arial, sans-serif',
12
+ fontFamily: '"Segoe UI", "Helvetica Neue", sans-serif',
11
13
  padding: '20px',
12
14
  maxWidth: '600px',
13
15
  margin: '0 auto',
@@ -27,7 +29,7 @@ export function EmailTemplate({ firstName, signature }: EmailTemplateProps) {
27
29
  fontSize: '14px',
28
30
  lineHeight: '1.6',
29
31
  }}
30
- dangerouslySetInnerHTML={{ __html: signature }}
32
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(signature) }}
31
33
  />
32
34
  )}
33
35
  </div>