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.
- package/package.json +1 -1
- package/template/.prettierignore +2 -0
- package/template/README.md +53 -67
- package/template/components.json +22 -0
- package/template/exemple-contacts.csv +54 -0
- package/template/next.config.ts +27 -1
- package/template/package.json +64 -27
- package/template/prisma/schema.prisma +821 -72
- package/template/skills-lock.json +25 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
- package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
- package/template/src/app/(auth)/reset-password/page.tsx +12 -8
- package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
- package/template/src/app/(auth)/signin/page.tsx +20 -17
- package/template/src/app/(dashboard)/agenda/page.tsx +2231 -2188
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
- package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
- package/template/src/app/(dashboard)/closing/page.tsx +500 -468
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5035 -4126
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
- package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
- package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
- package/template/src/app/(dashboard)/error.tsx +37 -0
- package/template/src/app/(dashboard)/layout.tsx +1 -1
- package/template/src/app/(dashboard)/loading.tsx +5 -0
- package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
- package/template/src/app/(dashboard)/settings/page.tsx +2685 -2489
- package/template/src/app/(dashboard)/templates/page.tsx +500 -300
- package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
- package/template/src/app/(dashboard)/users/page.tsx +279 -310
- package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
- package/template/src/app/(dashboard)/users/roles/page.tsx +164 -137
- package/template/src/app/api/audit-logs/route.ts +1 -1
- package/template/src/app/api/auth/google/callback/route.ts +8 -5
- package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
- package/template/src/app/api/companies/[id]/activities/route.ts +131 -0
- package/template/src/app/api/companies/[id]/route.ts +195 -0
- package/template/src/app/api/companies/export/route.ts +206 -0
- package/template/src/app/api/companies/route.ts +166 -0
- package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
- package/template/src/app/api/contact-views/[id]/route.ts +197 -0
- package/template/src/app/api/contact-views/route.ts +146 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +77 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
- package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
- package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
- package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
- package/template/src/app/api/contacts/[id]/route.ts +111 -20
- package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
- package/template/src/app/api/contacts/export/route.ts +12 -17
- package/template/src/app/api/contacts/import/route.ts +22 -19
- package/template/src/app/api/contacts/import-preview/route.ts +139 -0
- package/template/src/app/api/contacts/route.ts +202 -49
- package/template/src/app/api/dashboard/stats/route.ts +9 -292
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
- package/template/src/app/api/invite/complete/route.ts +20 -23
- package/template/src/app/api/reminders/route.ts +1 -0
- package/template/src/app/api/reset-password/complete/route.ts +11 -13
- package/template/src/app/api/send/route.ts +9 -85
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
- package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
- package/template/src/app/api/settings/company/route.ts +19 -26
- package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-ads/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
- package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
- package/template/src/app/api/settings/google-sheet/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/route.ts +20 -23
- package/template/src/app/api/settings/statuses/[id]/route.ts +33 -23
- package/template/src/app/api/settings/statuses/route.ts +24 -22
- package/template/src/app/api/statuses/route.ts +2 -5
- package/template/src/app/api/tasks/[id]/attendees/route.ts +14 -7
- package/template/src/app/api/tasks/[id]/route.ts +161 -137
- package/template/src/app/api/tasks/meet/route.ts +11 -8
- package/template/src/app/api/tasks/route.ts +155 -95
- package/template/src/app/api/templates/[id]/route.ts +22 -13
- package/template/src/app/api/templates/route.ts +22 -5
- package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
- package/template/src/app/api/users/[id]/route.ts +16 -1
- package/template/src/app/api/users/commercials/route.ts +38 -0
- package/template/src/app/api/users/for-agenda/route.ts +1 -2
- package/template/src/app/api/users/route.ts +94 -55
- package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
- package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
- package/template/src/app/api/workflows/[id]/route.ts +33 -6
- package/template/src/app/api/workflows/process/route.ts +509 -146
- package/template/src/app/api/workflows/route.ts +46 -4
- package/template/src/app/globals.css +210 -101
- package/template/src/app/layout.tsx +19 -8
- package/template/src/app/page.tsx +37 -7
- package/template/src/components/address-autocomplete.tsx +232 -0
- package/template/src/components/contacts/filter-bar.tsx +181 -0
- package/template/src/components/contacts/filter-builder.tsx +589 -0
- package/template/src/components/contacts/save-view-dialog.tsx +160 -0
- package/template/src/components/contacts/views-tab-bar.tsx +440 -0
- package/template/src/components/dashboard/activity-chart.tsx +31 -39
- package/template/src/components/dashboard/dashboard-content.tsx +79 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -42
- package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
- package/template/src/components/date-picker.tsx +396 -0
- package/template/src/components/editor.tsx +27 -13
- package/template/src/components/email-template.tsx +4 -2
- package/template/src/components/global-search.tsx +358 -0
- package/template/src/components/header.tsx +57 -62
- package/template/src/components/invitation-email-template.tsx +4 -2
- package/template/src/components/lazy-editor.tsx +11 -0
- package/template/src/components/meet-cancellation-email-template.tsx +11 -3
- package/template/src/components/meet-confirmation-email-template.tsx +10 -3
- package/template/src/components/meet-update-email-template.tsx +10 -3
- package/template/src/components/page-header.tsx +19 -15
- package/template/src/components/protected-page.tsx +94 -0
- package/template/src/components/reset-password-email-template.tsx +4 -2
- package/template/src/components/sidebar.tsx +92 -94
- package/template/src/components/skeleton.tsx +128 -42
- package/template/src/components/ui/accordion.tsx +64 -0
- package/template/src/components/ui/alert-dialog.tsx +139 -0
- package/template/src/components/ui/button.tsx +60 -0
- package/template/src/components/view-as-banner.tsx +1 -1
- package/template/src/components/view-as-modal.tsx +21 -16
- package/template/src/config/nav-pages.ts +108 -0
- package/template/src/contexts/app-toast-context.tsx +174 -0
- package/template/src/contexts/sidebar-context.tsx +16 -47
- package/template/src/contexts/task-reminder-context.tsx +6 -6
- package/template/src/contexts/view-as-context.tsx +11 -16
- package/template/src/hooks/use-alert.tsx +65 -0
- package/template/src/hooks/use-confirm.tsx +87 -0
- package/template/src/hooks/use-contact-views.ts +140 -0
- package/template/src/hooks/use-contacts.ts +69 -0
- package/template/src/hooks/use-fetch.ts +17 -0
- package/template/src/hooks/use-focus-trap.ts +73 -0
- package/template/src/hooks/use-statuses.ts +22 -0
- package/template/src/lib/address-api.ts +155 -0
- package/template/src/lib/cache.ts +73 -0
- package/template/src/lib/check-permission.ts +12 -177
- package/template/src/lib/contact-interactions.ts +3 -1
- package/template/src/lib/contact-view-filters.ts +341 -0
- package/template/src/lib/dashboard-stats.ts +224 -0
- package/template/src/lib/date-utils.ts +49 -0
- package/template/src/lib/get-auth-user.ts +25 -0
- package/template/src/lib/google-calendar.ts +54 -12
- package/template/src/lib/google-drive.ts +796 -75
- package/template/src/lib/google-fetch.ts +63 -0
- package/template/src/lib/local-storage.ts +34 -0
- package/template/src/lib/permissions.ts +245 -47
- package/template/src/lib/prisma.ts +11 -11
- package/template/src/lib/roles.ts +14 -39
- package/template/src/lib/template-variables.ts +67 -33
- package/template/src/lib/utils.ts +26 -2
- package/template/src/lib/workflow-executor.ts +445 -229
- package/template/src/proxy.ts +34 -73
- package/template/src/types/contact-views.ts +351 -0
- package/template/src/types/yousign.ts +52 -0
- package/template/vercel.json +12 -0
- package/template/WORKFLOWS_CRON.md +0 -185
- package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
- package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
- package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
- package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
- package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
- package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
- package/template/prisma/migrations/migration_lock.toml +0 -3
- package/template/src/app/(dashboard)/users/layout.tsx +0 -30
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
- package/template/src/app/api/dashboard/widgets/route.ts +0 -181
- package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
- package/template/src/components/dashboard/color-picker.tsx +0 -65
- package/template/src/components/dashboard/contacts-chart.tsx +0 -69
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
- package/template/src/components/dashboard/recent-activity.tsx +0 -157
- package/template/src/components/dashboard/sales-analytics-chart.tsx +0 -77
- package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
- package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
- package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
- package/template/src/contexts/dashboard-theme-context.tsx +0 -58
- package/template/src/lib/dashboard-themes.ts +0 -140
- package/template/src/lib/default-widgets.ts +0 -14
- 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
|
-
|
|
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="
|
|
639
|
-
|
|
640
|
-
max="20"
|
|
642
|
+
type="text"
|
|
643
|
+
inputMode="numeric"
|
|
641
644
|
value={tableConfig.rows}
|
|
642
|
-
onChange={(e) =>
|
|
643
|
-
|
|
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="
|
|
653
|
-
|
|
654
|
-
max="20"
|
|
661
|
+
type="text"
|
|
662
|
+
inputMode="numeric"
|
|
655
663
|
value={tableConfig.columns}
|
|
656
|
-
onChange={(e) =>
|
|
657
|
-
|
|
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: '
|
|
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>
|