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,422 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useRef, useEffect, useCallback } 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
+ export interface DatePickerProps {
9
+ startDate: string;
10
+ endDate?: string;
11
+ onDateChange: (startDate: string, endDate?: string) => void;
12
+ label?: string;
13
+ /** Si true, sélection d'une période (début + fin). */
14
+ isPeriod?: boolean;
15
+ /**
16
+ * Affiche le calendrier directement dans le parent (sans bouton déclencheur ni portail).
17
+ * À utiliser dans un popover ou panneau déjà ouvert.
18
+ */
19
+ embedded?: boolean;
20
+ /** Après Annuler, Échap, ou fermeture — ex. fermer le popover parent. */
21
+ onRequestClose?: () => void;
22
+ /** Filtre déjà appliqué : appelé au clic « Effacer » pour vider côté parent (optionnel). */
23
+ onClear?: () => void;
24
+ }
25
+
26
+ export function DatePicker({
27
+ startDate,
28
+ endDate = '',
29
+ onDateChange,
30
+ label = 'Sélectionner la date',
31
+ isPeriod = false,
32
+ embedded = false,
33
+ onRequestClose,
34
+ onClear,
35
+ }: DatePickerProps) {
36
+ const [isOpen, setIsOpen] = useState(false);
37
+ const [currentMonth, setCurrentMonth] = useState(() => new Date());
38
+
39
+ const parseISODate = (dateString: string): Date | null => {
40
+ if (!dateString) return null;
41
+ const [year, month, day] = dateString.split('-').map(Number);
42
+ if (!year || !month || !day) return null;
43
+ return new Date(year, month - 1, day);
44
+ };
45
+
46
+ const [tempStartDate, setTempStartDate] = useState<Date | null>(
47
+ startDate ? parseISODate(startDate) : null,
48
+ );
49
+ const [tempEndDate, setTempEndDate] = useState<Date | null>(
50
+ endDate ? parseISODate(endDate) : null,
51
+ );
52
+ const [isSelectingStart, setIsSelectingStart] = useState(true);
53
+ const [modalPosition, setModalPosition] = useState({ top: 0, left: 0, width: 0 });
54
+ const [isMounted, setIsMounted] = useState(false);
55
+ const [isDesktopView, setIsDesktopView] = useState(false);
56
+ const dropdownRef = useRef<HTMLDivElement>(null);
57
+ const buttonRef = useRef<HTMLButtonElement>(null);
58
+
59
+ useEffect(() => {
60
+ setIsMounted(true);
61
+ setIsDesktopView(typeof window !== 'undefined' && window.innerWidth >= 640);
62
+ }, []);
63
+
64
+ useEffect(() => {
65
+ setTempStartDate(startDate ? parseISODate(startDate) : null);
66
+ setTempEndDate(endDate ? parseISODate(endDate) : null);
67
+ }, [startDate, endDate]);
68
+
69
+ const handleClose = useCallback(() => {
70
+ setTempStartDate(startDate ? parseISODate(startDate) : null);
71
+ setTempEndDate(endDate ? parseISODate(endDate) : null);
72
+ if (!embedded) {
73
+ setIsOpen(false);
74
+ }
75
+ setIsSelectingStart(true);
76
+ onRequestClose?.();
77
+ }, [startDate, endDate, embedded, onRequestClose]);
78
+
79
+ useEffect(() => {
80
+ const handleEscape = (event: KeyboardEvent) => {
81
+ if (event.key === 'Escape' && (embedded || isOpen)) {
82
+ handleClose();
83
+ }
84
+ };
85
+
86
+ if (embedded || isOpen) {
87
+ document.addEventListener('keydown', handleEscape);
88
+ }
89
+
90
+ return () => {
91
+ document.removeEventListener('keydown', handleEscape);
92
+ };
93
+ }, [embedded, isOpen, handleClose]);
94
+
95
+ useEffect(() => {
96
+ if (isOpen && buttonRef.current) {
97
+ const buttonRect = buttonRef.current.getBoundingClientRect();
98
+ const modalHeight = 450;
99
+
100
+ if (typeof window !== 'undefined' && window.innerWidth < 640) {
101
+ const spaceAbove = buttonRect.top;
102
+ const spaceBelow = window.innerHeight - buttonRect.bottom;
103
+
104
+ let top: number;
105
+ if (spaceAbove > modalHeight && spaceBelow < modalHeight) {
106
+ top = buttonRect.top + window.scrollY - modalHeight - 8;
107
+ } else {
108
+ top = buttonRect.bottom + window.scrollY + 8;
109
+ }
110
+
111
+ setModalPosition({
112
+ top,
113
+ left: 0,
114
+ width: 0,
115
+ });
116
+ }
117
+ }
118
+ }, [isOpen]);
119
+
120
+ const getDaysInMonth = (date: Date): (Date | null)[] => {
121
+ const year = date.getFullYear();
122
+ const month = date.getMonth();
123
+ const firstDay = new Date(year, month, 1);
124
+ const lastDay = new Date(year, month + 1, 0);
125
+ const firstDayOfWeek = firstDay.getDay();
126
+ const adjustedFirstDay = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
127
+
128
+ const days: (Date | null)[] = [];
129
+ for (let i = 0; i < adjustedFirstDay; i++) {
130
+ days.push(null);
131
+ }
132
+ for (let i = 1; i <= lastDay.getDate(); i++) {
133
+ days.push(new Date(year, month, i));
134
+ }
135
+
136
+ return days;
137
+ };
138
+
139
+ const handleDateClick = (date: Date) => {
140
+ const clickedDate = new Date(date);
141
+ clickedDate.setHours(0, 0, 0, 0);
142
+
143
+ if (!isPeriod) {
144
+ setTempStartDate(clickedDate);
145
+ setTempEndDate(null);
146
+ } else if (isSelectingStart || !tempStartDate) {
147
+ setTempStartDate(clickedDate);
148
+ setTempEndDate(null);
149
+ setIsSelectingStart(false);
150
+ } else {
151
+ if (clickedDate < tempStartDate) {
152
+ setTempStartDate(clickedDate);
153
+ setTempEndDate(null);
154
+ } else {
155
+ setTempEndDate(clickedDate);
156
+ setIsSelectingStart(true);
157
+ }
158
+ }
159
+ };
160
+
161
+ const handleApply = () => {
162
+ if (tempStartDate) {
163
+ const startISO = `${tempStartDate.getFullYear()}-${String(tempStartDate.getMonth() + 1).padStart(2, '0')}-${String(tempStartDate.getDate()).padStart(2, '0')}`;
164
+
165
+ if (isPeriod && tempEndDate) {
166
+ const endISO = `${tempEndDate.getFullYear()}-${String(tempEndDate.getMonth() + 1).padStart(2, '0')}-${String(tempEndDate.getDate()).padStart(2, '0')}`;
167
+ onDateChange(startISO, endISO);
168
+ } else if (!isPeriod) {
169
+ onDateChange(startISO);
170
+ }
171
+ if (!embedded) {
172
+ setIsOpen(false);
173
+ }
174
+ }
175
+ };
176
+
177
+ const handleClear = () => {
178
+ if (onClear) {
179
+ onClear();
180
+ setIsSelectingStart(true);
181
+ return;
182
+ }
183
+ setTempStartDate(null);
184
+ setTempEndDate(null);
185
+ setIsSelectingStart(true);
186
+ };
187
+
188
+ const isDateInRange = (date: Date): boolean => {
189
+ if (!isPeriod || !tempStartDate || !tempEndDate) return false;
190
+ const current = new Date(date);
191
+ current.setHours(0, 0, 0, 0);
192
+ return current >= tempStartDate && current <= tempEndDate;
193
+ };
194
+
195
+ const isDateSelected = (date: Date): boolean => {
196
+ const current = new Date(date);
197
+ current.setHours(0, 0, 0, 0);
198
+ return !!(
199
+ (tempStartDate && current.getTime() === tempStartDate.getTime()) ||
200
+ (tempEndDate && current.getTime() === tempEndDate.getTime())
201
+ );
202
+ };
203
+
204
+ const isTodayCell = (date: Date): boolean => {
205
+ const today = new Date();
206
+ return (
207
+ date.getDate() === today.getDate() &&
208
+ date.getMonth() === today.getMonth() &&
209
+ date.getFullYear() === today.getFullYear()
210
+ );
211
+ };
212
+
213
+ const isWeekendCell = (date: Date): boolean => {
214
+ const day = date.getDay();
215
+ return day === 0 || day === 6;
216
+ };
217
+
218
+ const formatDisplayDate = (): string => {
219
+ if (!tempStartDate && !startDate) return label;
220
+
221
+ const displayStart = tempStartDate || (startDate ? parseISODate(startDate) : null);
222
+ const displayEnd = tempEndDate || (endDate ? parseISODate(endDate) : null);
223
+
224
+ if (!displayStart) return label;
225
+
226
+ if (isPeriod && displayEnd) {
227
+ return `Du ${displayStart.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })} au ${displayEnd.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' })}`;
228
+ }
229
+
230
+ return displayStart.toLocaleDateString('fr-FR', {
231
+ day: 'numeric',
232
+ month: 'short',
233
+ year: 'numeric',
234
+ });
235
+ };
236
+
237
+ const days = getDaysInMonth(currentMonth);
238
+
239
+ const dayNames = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'];
240
+
241
+ const canApply = isPeriod ? !!(tempStartDate && tempEndDate) : !!tempStartDate;
242
+
243
+ const panelCard = (
244
+ <div
245
+ ref={embedded ? dropdownRef : undefined}
246
+ className={cn(
247
+ 'border-border bg-popover text-popover-foreground max-h-[90vh] overflow-y-auto rounded-xl border p-4 shadow-2xl',
248
+ embedded ? 'w-full max-w-full border-0 p-0 shadow-none' : 'w-[320px] max-w-[95vw] sm:w-[400px]',
249
+ )}
250
+ >
251
+ <div className="border-border mb-3 flex items-center justify-between border-b pb-3">
252
+ <div className="min-w-0 flex-1">
253
+ <h3 className="text-foreground truncate text-sm font-semibold">
254
+ {isPeriod ? 'Sélectionner une période' : 'Sélectionner une date'}
255
+ </h3>
256
+ {isPeriod && (
257
+ <p className="text-muted-foreground mt-0.5 text-xs">
258
+ {isSelectingStart
259
+ ? 'Cliquez pour sélectionner la date de début'
260
+ : 'Cliquez pour sélectionner la date de fin'}
261
+ </p>
262
+ )}
263
+ </div>
264
+ <button
265
+ type="button"
266
+ onClick={handleClose}
267
+ className="text-muted-foreground hover:bg-muted hover:text-foreground ml-2 shrink-0 cursor-pointer rounded-lg p-1 transition-colors"
268
+ >
269
+ <X className="h-4 w-4" />
270
+ </button>
271
+ </div>
272
+
273
+ <div className="space-y-3">
274
+ <div>
275
+ <div className="mb-2 flex items-center justify-between">
276
+ <button
277
+ type="button"
278
+ onClick={() =>
279
+ setCurrentMonth((prev) => new Date(prev.getFullYear(), prev.getMonth() - 1, 1))
280
+ }
281
+ className="text-muted-foreground hover:bg-muted shrink-0 cursor-pointer rounded-lg p-1.5 transition-colors"
282
+ >
283
+ <ChevronLeft className="h-4 w-4" />
284
+ </button>
285
+ <h3 className="text-foreground min-w-0 flex-1 truncate text-center text-xs font-semibold sm:text-sm">
286
+ {currentMonth.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })}
287
+ </h3>
288
+ <button
289
+ type="button"
290
+ onClick={() =>
291
+ setCurrentMonth((prev) => new Date(prev.getFullYear(), prev.getMonth() + 1, 1))
292
+ }
293
+ className="text-muted-foreground hover:bg-muted shrink-0 cursor-pointer rounded-lg p-1.5 transition-colors"
294
+ >
295
+ <ChevronRight className="h-4 w-4" />
296
+ </button>
297
+ </div>
298
+
299
+ <div className="grid grid-cols-7 gap-0.5 sm:gap-1">
300
+ {dayNames.map((day) => (
301
+ <div
302
+ key={day}
303
+ className="text-muted-foreground py-1 text-center text-[9px] font-medium sm:text-[10px]"
304
+ >
305
+ {day}
306
+ </div>
307
+ ))}
308
+ {days.map((date, index) => {
309
+ if (!date) {
310
+ return <div key={`empty-${index}`} className="aspect-square" />;
311
+ }
312
+
313
+ const inRange = isDateInRange(date);
314
+ const selected = isDateSelected(date);
315
+ const today = isTodayCell(date);
316
+ const weekend = isWeekendCell(date);
317
+
318
+ const y = date.getFullYear();
319
+ const m = date.getMonth();
320
+ const d = date.getDate();
321
+ const cellKey = `${y}-${m}-${d}`;
322
+
323
+ return (
324
+ <button
325
+ key={cellKey}
326
+ type="button"
327
+ onClick={() => handleDateClick(date)}
328
+ className={cn(
329
+ 'aspect-square cursor-pointer rounded-md text-[10px] font-medium transition-colors sm:text-xs',
330
+ selected &&
331
+ 'bg-primary text-primary-foreground z-10 shadow-md hover:bg-primary/90',
332
+ inRange && !selected && 'bg-primary/10 text-primary hover:bg-primary/15',
333
+ !inRange && !selected && 'text-foreground hover:bg-muted',
334
+ weekend && !selected && !inRange && 'text-muted-foreground',
335
+ today && !selected && !inRange && 'border-primary ring-primary/30 border',
336
+ )}
337
+ >
338
+ {date.getDate()}
339
+ </button>
340
+ );
341
+ })}
342
+ </div>
343
+ </div>
344
+ </div>
345
+
346
+ <div className="border-border mt-3 flex flex-col gap-2 border-t pt-3 sm:flex-row sm:items-center sm:justify-between">
347
+ <button
348
+ type="button"
349
+ onClick={handleClear}
350
+ className="text-muted-foreground hover:text-foreground cursor-pointer text-center text-xs font-medium transition-colors sm:text-left"
351
+ >
352
+ Effacer
353
+ </button>
354
+ <div className="flex items-center gap-2">
355
+ <button
356
+ type="button"
357
+ onClick={handleClose}
358
+ className="border-border bg-background text-foreground hover:bg-muted flex-1 cursor-pointer rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors sm:flex-initial"
359
+ >
360
+ Annuler
361
+ </button>
362
+ <button
363
+ type="button"
364
+ onClick={handleApply}
365
+ disabled={!canApply}
366
+ className="bg-primary text-primary-foreground hover:bg-primary/90 flex-1 rounded-lg px-3 py-1.5 text-xs font-medium transition-colors enabled:cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 sm:flex-initial"
367
+ >
368
+ Appliquer
369
+ </button>
370
+ </div>
371
+ </div>
372
+ </div>
373
+ );
374
+
375
+ if (embedded) {
376
+ return <div className="w-full min-w-0">{panelCard}</div>;
377
+ }
378
+
379
+ const modalContent = isOpen && isMounted && (
380
+ <>
381
+ <div
382
+ className="fixed inset-0 z-[9998] cursor-pointer bg-black/20 backdrop-blur-sm transition-opacity duration-200"
383
+ aria-hidden
384
+ onClick={handleClose}
385
+ />
386
+ <div
387
+ ref={dropdownRef}
388
+ className={cn(
389
+ 'fixed z-[9999] transition-opacity duration-200',
390
+ 'left-1/2 -translate-x-1/2',
391
+ isDesktopView && 'top-1/2 -translate-y-1/2',
392
+ )}
393
+ style={
394
+ !isDesktopView
395
+ ? {
396
+ top: `${modalPosition.top}px`,
397
+ }
398
+ : undefined
399
+ }
400
+ >
401
+ {panelCard}
402
+ </div>
403
+ </>
404
+ );
405
+
406
+ return (
407
+ <>
408
+ <button
409
+ ref={buttonRef}
410
+ type="button"
411
+ onClick={() => setIsOpen(!isOpen)}
412
+ className="border-input bg-background text-foreground hover:border-ring flex w-full cursor-pointer items-center justify-between rounded-xl border px-4 py-2 text-left text-sm shadow-sm transition-colors focus-visible:ring-ring/50 focus-visible:ring-[3px] focus-visible:outline-none"
413
+ >
414
+ <div className="flex min-w-0 items-center gap-2">
415
+ <Calendar className="text-muted-foreground h-4 w-4 shrink-0" />
416
+ <div className="truncate font-medium">{formatDisplayDate()}</div>
417
+ </div>
418
+ </button>
419
+ {isMounted && modalContent && createPortal(modalContent, document.body)}
420
+ </>
421
+ );
422
+ }