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.
- package/bin/create-crm-tmp.js +56 -35
- package/package.json +1 -1
- package/template/README.md +230 -115
- package/template/eslint.config.mjs +13 -0
- package/template/next.config.ts +14 -0
- package/template/package.json +15 -2
- package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +132 -637
- package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
- package/template/src/app/(auth)/layout.tsx +1 -1
- package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
- package/template/src/app/(auth)/reset-password/page.tsx +4 -4
- package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
- package/template/src/app/(auth)/signin/page.tsx +14 -6
- package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
- package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
- package/template/src/app/(dashboard)/closing/page.tsx +78 -62
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
- package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
- package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
- package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
- package/template/src/app/(dashboard)/layout.tsx +6 -2
- package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
- package/template/src/app/(dashboard)/templates/page.tsx +55 -54
- package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
- package/template/src/app/(dashboard)/users/page.tsx +1 -1
- package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
- package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
- package/template/src/app/api/agenda/google-events/route.ts +92 -0
- package/template/src/app/api/auth/check-active/route.ts +3 -2
- package/template/src/app/api/auth/google/route.ts +2 -1
- package/template/src/app/api/auth/google/status/route.ts +7 -31
- package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
- package/template/src/app/api/companies/[id]/route.ts +1 -2
- package/template/src/app/api/companies/route.ts +42 -12
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
- package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
- package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
- package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
- package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
- package/template/src/app/api/contacts/[id]/route.ts +106 -34
- package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
- package/template/src/app/api/contacts/export/route.ts +9 -13
- package/template/src/app/api/contacts/import/route.ts +55 -25
- package/template/src/app/api/contacts/import-preview/route.ts +1 -1
- package/template/src/app/api/contacts/origins/route.ts +63 -0
- package/template/src/app/api/contacts/route.ts +153 -41
- package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
- package/template/src/app/api/dashboard/widgets/route.ts +181 -0
- package/template/src/app/api/dev/reminders/test/route.ts +114 -0
- package/template/src/app/api/editor/upload-image/route.ts +61 -0
- package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
- package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
- package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
- package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
- package/template/src/app/api/reminders/clear/route.ts +120 -0
- package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
- package/template/src/app/api/reminders/route.ts +164 -39
- package/template/src/app/api/reminders/state/route.ts +164 -0
- package/template/src/app/api/reset-password/request/route.ts +1 -1
- package/template/src/app/api/reset-password/verify/route.ts +1 -1
- package/template/src/app/api/send/route.ts +16 -4
- package/template/src/app/api/settings/google-ads/route.ts +14 -0
- package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
- package/template/src/app/api/settings/google-calendar/route.ts +124 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
- package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
- package/template/src/app/api/settings/google-sheet/route.ts +14 -0
- package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
- package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
- package/template/src/app/api/settings/meta-leads/route.ts +14 -2
- package/template/src/app/api/settings/smtp/route.ts +53 -6
- package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
- package/template/src/app/api/tasks/[id]/route.ts +234 -58
- package/template/src/app/api/tasks/meet/route.ts +27 -19
- package/template/src/app/api/tasks/route.ts +62 -17
- package/template/src/app/api/users/[id]/route.ts +20 -14
- package/template/src/app/api/users/list/route.ts +57 -19
- package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
- package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
- package/template/src/app/api/workflows/[id]/route.ts +0 -4
- package/template/src/app/api/workflows/process/route.ts +22 -51
- package/template/src/app/api/workflows/route.ts +0 -4
- package/template/src/app/globals.css +342 -4
- package/template/src/app/layout.tsx +11 -3
- package/template/src/app/page.tsx +1 -1
- package/template/src/components/address-autocomplete.tsx +7 -6
- package/template/src/components/config-error-alert.tsx +46 -0
- package/template/src/components/contacts/filter-bar.tsx +12 -3
- package/template/src/components/contacts/filter-builder.tsx +28 -43
- package/template/src/components/contacts/save-view-dialog.tsx +1 -1
- package/template/src/components/contacts/views-tab-bar.tsx +15 -6
- package/template/src/components/dashboard/activity-chart.tsx +41 -28
- package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
- package/template/src/components/dashboard/color-picker.tsx +64 -0
- package/template/src/components/dashboard/contacts-chart.tsx +69 -0
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
- package/template/src/components/dashboard/recent-activity.tsx +154 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -40
- package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
- package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
- package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
- package/template/src/components/date-picker.tsx +9 -6
- package/template/src/components/editor/upload-editor-image.ts +42 -0
- package/template/src/components/editor.tsx +161 -22
- package/template/src/components/email-template.tsx +2 -2
- package/template/src/components/global-search.tsx +30 -28
- package/template/src/components/header.tsx +178 -80
- package/template/src/components/inactive-account-guard.tsx +58 -0
- package/template/src/components/integration-notifications-listener.tsx +12 -0
- package/template/src/components/invitation-email-template.tsx +2 -2
- package/template/src/components/meet-cancellation-email-template.tsx +3 -3
- package/template/src/components/meet-confirmation-email-template.tsx +3 -3
- package/template/src/components/meet-update-email-template.tsx +3 -3
- package/template/src/components/page-header.tsx +5 -5
- package/template/src/components/protected-page.tsx +1 -1
- package/template/src/components/reset-password-email-template.tsx +2 -2
- package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
- package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
- package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
- package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
- package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
- package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
- package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
- package/template/src/components/sidebar.tsx +45 -26
- package/template/src/components/skeleton.tsx +40 -43
- package/template/src/components/ui/accordion.tsx +2 -2
- package/template/src/components/ui/alert-dialog.tsx +1 -1
- package/template/src/components/ui/button.tsx +20 -9
- package/template/src/components/ui/components.tsx +1 -1
- package/template/src/components/ui/date-picker.tsx +422 -0
- package/template/src/components/ui/datetime-picker.tsx +338 -0
- package/template/src/components/ui/status-select.tsx +271 -0
- package/template/src/components/ui/tooltip.tsx +37 -0
- package/template/src/components/view-as-modal.tsx +13 -7
- package/template/src/contexts/app-toast-context.tsx +245 -57
- package/template/src/contexts/dashboard-theme-context.tsx +53 -0
- package/template/src/contexts/sidebar-context.tsx +22 -17
- package/template/src/contexts/task-reminder-context.tsx +134 -160
- package/template/src/contexts/view-as-context.tsx +33 -6
- package/template/src/hooks/use-focus-trap.ts +2 -2
- package/template/src/hooks/useIntegrationNotifications.ts +49 -0
- package/template/src/lib/auth.ts +8 -1
- package/template/src/lib/config-links.ts +14 -0
- package/template/src/lib/contact-duplicate.ts +79 -61
- package/template/src/lib/contact-interactions.ts +21 -21
- package/template/src/lib/contact-view-filters.ts +24 -64
- package/template/src/lib/contacts-list-url.ts +190 -0
- package/template/src/lib/dashboard-stats.ts +65 -7
- package/template/src/lib/dashboard-themes.ts +135 -0
- package/template/src/lib/date-utils.ts +127 -0
- package/template/src/lib/default-widgets.ts +12 -0
- package/template/src/lib/editor-html-image-dimensions.ts +172 -0
- package/template/src/lib/editor-image-limits.ts +19 -0
- package/template/src/lib/email-html-sanitize.ts +19 -0
- package/template/src/lib/encryption.ts +9 -6
- package/template/src/lib/fr-geography.ts +192 -0
- package/template/src/lib/google-calendar-agenda.ts +201 -0
- package/template/src/lib/google-calendar.ts +255 -5
- package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
- package/template/src/lib/google-sheet-sync-runner.ts +514 -0
- package/template/src/lib/integration-import-log.ts +21 -0
- package/template/src/lib/permissions.ts +40 -10
- package/template/src/lib/prisma.ts +4 -1
- package/template/src/lib/qstash.ts +65 -0
- package/template/src/lib/reminder-state-server.ts +80 -0
- package/template/src/lib/reminder-state.ts +29 -0
- package/template/src/lib/supabase-storage.ts +113 -0
- package/template/src/lib/template-variables.ts +164 -23
- package/template/src/lib/utils.ts +45 -0
- package/template/src/lib/widget-registry.ts +173 -0
- package/template/src/lib/workflow-executor.ts +16 -70
- package/template/src/proxy.ts +1 -0
- package/template/vercel.json +3 -10
- package/template/skills-lock.json +0 -25
- package/template/src/components/dashboard/dashboard-content.tsx +0 -79
- package/template/src/lib/google-drive.ts +0 -1101
- 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
|
+
}
|