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,338 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { Calendar, ChevronLeft, ChevronRight, Clock } from 'lucide-react';
|
|
6
|
+
import { cn } from '@/lib/utils';
|
|
7
|
+
|
|
8
|
+
export interface DateTimePickerProps {
|
|
9
|
+
value: string;
|
|
10
|
+
onChange: (value: string) => void;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
required?: boolean;
|
|
14
|
+
id?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DAYS = ['Lu', 'Ma', 'Me', 'Je', 'Ve', 'Sa', 'Di'];
|
|
18
|
+
const MONTHS = [
|
|
19
|
+
'Janvier',
|
|
20
|
+
'Février',
|
|
21
|
+
'Mars',
|
|
22
|
+
'Avril',
|
|
23
|
+
'Mai',
|
|
24
|
+
'Juin',
|
|
25
|
+
'Juillet',
|
|
26
|
+
'Août',
|
|
27
|
+
'Septembre',
|
|
28
|
+
'Octobre',
|
|
29
|
+
'Novembre',
|
|
30
|
+
'Décembre',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function pad(n: number) {
|
|
34
|
+
return String(n).padStart(2, '0');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseValue(value: string): { date: Date | null; hour: number; minute: number } {
|
|
38
|
+
if (!value) return { date: null, hour: 9, minute: 0 };
|
|
39
|
+
const d = new Date(value);
|
|
40
|
+
if (Number.isNaN(d.getTime())) return { date: null, hour: 9, minute: 0 };
|
|
41
|
+
return { date: d, hour: d.getHours(), minute: d.getMinutes() };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatDisplay(value: string): string {
|
|
45
|
+
if (!value) return '';
|
|
46
|
+
const d = new Date(value);
|
|
47
|
+
if (Number.isNaN(d.getTime())) return '';
|
|
48
|
+
return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} à ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getDaysInMonth(date: Date): (Date | null)[] {
|
|
52
|
+
const year = date.getFullYear();
|
|
53
|
+
const month = date.getMonth();
|
|
54
|
+
const firstDay = new Date(year, month, 1);
|
|
55
|
+
const lastDay = new Date(year, month + 1, 0);
|
|
56
|
+
const firstDayOfWeek = firstDay.getDay();
|
|
57
|
+
const adjustedFirstDay = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
|
|
58
|
+
|
|
59
|
+
const days: (Date | null)[] = [];
|
|
60
|
+
for (let i = 0; i < adjustedFirstDay; i++) {
|
|
61
|
+
days.push(null);
|
|
62
|
+
}
|
|
63
|
+
for (let i = 1; i <= lastDay.getDate(); i++) {
|
|
64
|
+
days.push(new Date(year, month, i));
|
|
65
|
+
}
|
|
66
|
+
return days;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isSameDay(a: Date, b: Date): boolean {
|
|
70
|
+
return (
|
|
71
|
+
a.getFullYear() === b.getFullYear() &&
|
|
72
|
+
a.getMonth() === b.getMonth() &&
|
|
73
|
+
a.getDate() === b.getDate()
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function DateTimePicker({
|
|
78
|
+
value,
|
|
79
|
+
onChange,
|
|
80
|
+
placeholder = 'Sélectionner date et heure',
|
|
81
|
+
className,
|
|
82
|
+
required,
|
|
83
|
+
id,
|
|
84
|
+
}: DateTimePickerProps) {
|
|
85
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
86
|
+
const parsed = parseValue(value);
|
|
87
|
+
const [currentMonth, setCurrentMonth] = useState(parsed.date || new Date());
|
|
88
|
+
const [selectedDate, setSelectedDate] = useState<Date | null>(parsed.date);
|
|
89
|
+
const [hour, setHour] = useState(parsed.hour);
|
|
90
|
+
const [minute, setMinute] = useState(parsed.minute);
|
|
91
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
92
|
+
|
|
93
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
94
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
95
|
+
const [position, setPosition] = useState({ top: 0, left: 0 });
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
setIsMounted(true);
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
const p = parseValue(value);
|
|
103
|
+
setSelectedDate(p.date);
|
|
104
|
+
setHour(p.hour);
|
|
105
|
+
setMinute(p.minute);
|
|
106
|
+
if (p.date) setCurrentMonth(p.date);
|
|
107
|
+
}, [value]);
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (isOpen && buttonRef.current) {
|
|
111
|
+
const rect = buttonRef.current.getBoundingClientRect();
|
|
112
|
+
const panelH = 420;
|
|
113
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
114
|
+
const top =
|
|
115
|
+
spaceBelow < panelH && rect.top > panelH
|
|
116
|
+
? rect.top + window.scrollY - panelH - 4
|
|
117
|
+
: rect.bottom + window.scrollY + 4;
|
|
118
|
+
setPosition({
|
|
119
|
+
top,
|
|
120
|
+
left: rect.left + window.scrollX,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}, [isOpen]);
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!isOpen) return;
|
|
127
|
+
const handler = (e: MouseEvent) => {
|
|
128
|
+
if (
|
|
129
|
+
panelRef.current &&
|
|
130
|
+
!panelRef.current.contains(e.target as Node) &&
|
|
131
|
+
buttonRef.current &&
|
|
132
|
+
!buttonRef.current.contains(e.target as Node)
|
|
133
|
+
) {
|
|
134
|
+
setIsOpen(false);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
document.addEventListener('mousedown', handler);
|
|
138
|
+
return () => document.removeEventListener('mousedown', handler);
|
|
139
|
+
}, [isOpen]);
|
|
140
|
+
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (!isOpen) return;
|
|
143
|
+
const handler = (e: KeyboardEvent) => {
|
|
144
|
+
if (e.key === 'Escape') setIsOpen(false);
|
|
145
|
+
};
|
|
146
|
+
document.addEventListener('keydown', handler);
|
|
147
|
+
return () => document.removeEventListener('keydown', handler);
|
|
148
|
+
}, [isOpen]);
|
|
149
|
+
|
|
150
|
+
const emitChange = useCallback(
|
|
151
|
+
(date: Date | null, h: number, m: number) => {
|
|
152
|
+
if (!date) return;
|
|
153
|
+
const d = new Date(date);
|
|
154
|
+
d.setHours(h, m, 0, 0);
|
|
155
|
+
const iso = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(h)}:${pad(m)}`;
|
|
156
|
+
onChange(iso);
|
|
157
|
+
},
|
|
158
|
+
[onChange],
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const handleDateClick = (date: Date) => {
|
|
162
|
+
setSelectedDate(date);
|
|
163
|
+
emitChange(date, hour, minute);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const handleHourChange = (h: number) => {
|
|
167
|
+
setHour(h);
|
|
168
|
+
emitChange(selectedDate, h, minute);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const handleMinuteChange = (m: number) => {
|
|
172
|
+
setMinute(m);
|
|
173
|
+
emitChange(selectedDate, hour, m);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const handleSetNow = () => {
|
|
177
|
+
const now = new Date();
|
|
178
|
+
setSelectedDate(now);
|
|
179
|
+
setHour(now.getHours());
|
|
180
|
+
setMinute(now.getMinutes());
|
|
181
|
+
setCurrentMonth(now);
|
|
182
|
+
emitChange(now, now.getHours(), now.getMinutes());
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const today = new Date();
|
|
186
|
+
today.setHours(0, 0, 0, 0);
|
|
187
|
+
const days = getDaysInMonth(currentMonth);
|
|
188
|
+
|
|
189
|
+
const panel =
|
|
190
|
+
isOpen && isMounted
|
|
191
|
+
? createPortal(
|
|
192
|
+
<div
|
|
193
|
+
ref={panelRef}
|
|
194
|
+
className="ui-scale-in fixed z-[100] w-[320px] rounded-xl border border-border bg-popover text-popover-foreground shadow-xl"
|
|
195
|
+
style={{ top: position.top, left: position.left }}
|
|
196
|
+
>
|
|
197
|
+
<div className="flex items-center justify-between border-b border-border px-4 py-3">
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
onClick={() =>
|
|
201
|
+
setCurrentMonth(
|
|
202
|
+
new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1),
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
className="text-muted-foreground hover:bg-muted rounded-lg p-1.5 transition-colors"
|
|
206
|
+
>
|
|
207
|
+
<ChevronLeft className="h-4 w-4" />
|
|
208
|
+
</button>
|
|
209
|
+
<span className="text-sm font-semibold text-foreground">
|
|
210
|
+
{MONTHS[currentMonth.getMonth()]} {currentMonth.getFullYear()}
|
|
211
|
+
</span>
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
onClick={() =>
|
|
215
|
+
setCurrentMonth(
|
|
216
|
+
new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1),
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
className="text-muted-foreground hover:bg-muted rounded-lg p-1.5 transition-colors"
|
|
220
|
+
>
|
|
221
|
+
<ChevronRight className="h-4 w-4" />
|
|
222
|
+
</button>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<div className="grid grid-cols-7 gap-0.5 px-3 pt-2">
|
|
226
|
+
{DAYS.map((d) => (
|
|
227
|
+
<div key={d} className="text-muted-foreground py-1 text-center text-xs font-medium">
|
|
228
|
+
{d}
|
|
229
|
+
</div>
|
|
230
|
+
))}
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div className="grid grid-cols-7 gap-0.5 px-3 pb-2">
|
|
234
|
+
{days.map((day, i) => {
|
|
235
|
+
if (!day) return <div key={`empty-${i}`} />;
|
|
236
|
+
const isTodayCell = isSameDay(day, today);
|
|
237
|
+
const isSelected = selectedDate && isSameDay(day, selectedDate);
|
|
238
|
+
return (
|
|
239
|
+
<button
|
|
240
|
+
key={day.toISOString()}
|
|
241
|
+
type="button"
|
|
242
|
+
onClick={() => handleDateClick(day)}
|
|
243
|
+
className={cn(
|
|
244
|
+
'flex h-8 w-full items-center justify-center rounded-lg text-sm transition-colors',
|
|
245
|
+
isSelected
|
|
246
|
+
? 'bg-primary text-primary-foreground font-semibold shadow-sm'
|
|
247
|
+
: isTodayCell
|
|
248
|
+
? 'border-primary/40 text-primary hover:bg-primary/10 border font-medium'
|
|
249
|
+
: 'text-foreground hover:bg-muted',
|
|
250
|
+
)}
|
|
251
|
+
>
|
|
252
|
+
{day.getDate()}
|
|
253
|
+
</button>
|
|
254
|
+
);
|
|
255
|
+
})}
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<div className="border-border border-t px-4 py-3">
|
|
259
|
+
<div className="flex items-center gap-3">
|
|
260
|
+
<Clock className="text-muted-foreground h-4 w-4 shrink-0" />
|
|
261
|
+
<div className="flex items-center gap-1">
|
|
262
|
+
<select
|
|
263
|
+
value={hour}
|
|
264
|
+
onChange={(e) => handleHourChange(Number(e.target.value))}
|
|
265
|
+
aria-label="Heure"
|
|
266
|
+
className="border-border bg-muted text-foreground focus-visible:border-ring focus-visible:ring-ring/20 rounded-lg border px-2 py-1.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none"
|
|
267
|
+
>
|
|
268
|
+
{Array.from({ length: 24 }, (_, i) => (
|
|
269
|
+
<option key={i} value={i}>
|
|
270
|
+
{pad(i)}
|
|
271
|
+
</option>
|
|
272
|
+
))}
|
|
273
|
+
</select>
|
|
274
|
+
<span className="text-muted-foreground text-sm font-bold">:</span>
|
|
275
|
+
<select
|
|
276
|
+
value={minute}
|
|
277
|
+
onChange={(e) => handleMinuteChange(Number(e.target.value))}
|
|
278
|
+
aria-label="Minute"
|
|
279
|
+
className="border-border bg-muted text-foreground focus-visible:border-ring focus-visible:ring-ring/20 rounded-lg border px-2 py-1.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none"
|
|
280
|
+
>
|
|
281
|
+
{Array.from({ length: 12 }, (_, i) => i * 5).map((m) => (
|
|
282
|
+
<option key={m} value={m}>
|
|
283
|
+
{pad(m)}
|
|
284
|
+
</option>
|
|
285
|
+
))}
|
|
286
|
+
</select>
|
|
287
|
+
</div>
|
|
288
|
+
<button
|
|
289
|
+
type="button"
|
|
290
|
+
onClick={handleSetNow}
|
|
291
|
+
className="text-primary ml-auto rounded-lg px-2.5 py-1 text-xs font-medium transition-colors hover:bg-primary/10"
|
|
292
|
+
>
|
|
293
|
+
Maintenant
|
|
294
|
+
</button>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
<div className="border-border border-t px-4 py-2.5">
|
|
299
|
+
<button
|
|
300
|
+
type="button"
|
|
301
|
+
onClick={() => setIsOpen(false)}
|
|
302
|
+
disabled={!selectedDate}
|
|
303
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full rounded-lg px-3 py-2 text-sm font-medium shadow-sm transition-colors disabled:opacity-50"
|
|
304
|
+
>
|
|
305
|
+
Confirmer
|
|
306
|
+
</button>
|
|
307
|
+
</div>
|
|
308
|
+
</div>,
|
|
309
|
+
document.body,
|
|
310
|
+
)
|
|
311
|
+
: null;
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<>
|
|
315
|
+
<button
|
|
316
|
+
ref={buttonRef}
|
|
317
|
+
id={id}
|
|
318
|
+
type="button"
|
|
319
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
320
|
+
className={cn(
|
|
321
|
+
'border-input bg-background text-foreground flex w-full items-center gap-2 rounded-xl border px-3 py-2 text-sm shadow-sm',
|
|
322
|
+
'transition-[border-color,box-shadow] duration-200',
|
|
323
|
+
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] focus-visible:outline-none',
|
|
324
|
+
isOpen && 'border-ring ring-ring/50 ring-[3px]',
|
|
325
|
+
!value && 'text-muted-foreground',
|
|
326
|
+
className,
|
|
327
|
+
)}
|
|
328
|
+
>
|
|
329
|
+
<Calendar className="text-muted-foreground h-4 w-4 shrink-0" />
|
|
330
|
+
<span className="flex-1 truncate text-left">
|
|
331
|
+
{value ? formatDisplay(value) : placeholder}
|
|
332
|
+
</span>
|
|
333
|
+
{required && !value && <span className="text-xs text-red-500">*</span>}
|
|
334
|
+
</button>
|
|
335
|
+
{panel}
|
|
336
|
+
</>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { ChevronDown } from 'lucide-react';
|
|
6
|
+
import { motion, AnimatePresence } from 'motion/react';
|
|
7
|
+
import { cn } from '@/lib/utils';
|
|
8
|
+
|
|
9
|
+
export interface StatusOption {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
color: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DROPDOWN_MAX_HEIGHT = 240; // max-h-60
|
|
16
|
+
const DROPDOWN_GAP = 4;
|
|
17
|
+
|
|
18
|
+
interface StatusSelectProps {
|
|
19
|
+
value: string;
|
|
20
|
+
onChange: (statusId: string) => void;
|
|
21
|
+
statuses: StatusOption[];
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
className?: string;
|
|
24
|
+
size?: 'sm' | 'md';
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
id?: string;
|
|
27
|
+
/** Optionnel : callback appelé au changement (ex. pour gérer motif de fermeture) */
|
|
28
|
+
onStatusChange?: (statusId: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function StatusSelect({
|
|
32
|
+
value,
|
|
33
|
+
onChange,
|
|
34
|
+
statuses,
|
|
35
|
+
placeholder = 'Aucun statut',
|
|
36
|
+
className,
|
|
37
|
+
size = 'md',
|
|
38
|
+
disabled = false,
|
|
39
|
+
id,
|
|
40
|
+
onStatusChange,
|
|
41
|
+
}: StatusSelectProps) {
|
|
42
|
+
const [open, setOpen] = useState(false);
|
|
43
|
+
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
44
|
+
const [dropdownStyle, setDropdownStyle] = useState<{ top?: number; bottom?: number; left: number; width: number } | null>(null);
|
|
45
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
46
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
47
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
48
|
+
|
|
49
|
+
const selectedStatus = statuses.find((s) => s.id === value);
|
|
50
|
+
const options = [{ id: '', name: placeholder }, ...statuses];
|
|
51
|
+
|
|
52
|
+
const updateDropdownPosition = useCallback(() => {
|
|
53
|
+
if (!triggerRef.current || typeof document === 'undefined') return;
|
|
54
|
+
const rect = triggerRef.current.getBoundingClientRect();
|
|
55
|
+
const spaceBelow = window.innerHeight - rect.bottom - DROPDOWN_GAP;
|
|
56
|
+
const spaceAbove = rect.top - DROPDOWN_GAP;
|
|
57
|
+
const openAbove = spaceBelow < DROPDOWN_MAX_HEIGHT && spaceAbove > spaceBelow;
|
|
58
|
+
|
|
59
|
+
setDropdownStyle({
|
|
60
|
+
left: rect.left,
|
|
61
|
+
width: rect.width,
|
|
62
|
+
...(openAbove
|
|
63
|
+
? { bottom: window.innerHeight - rect.top + DROPDOWN_GAP }
|
|
64
|
+
: { top: rect.bottom + DROPDOWN_GAP }),
|
|
65
|
+
});
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
useLayoutEffect(() => {
|
|
69
|
+
if (open) {
|
|
70
|
+
updateDropdownPosition();
|
|
71
|
+
} else {
|
|
72
|
+
setDropdownStyle(null);
|
|
73
|
+
}
|
|
74
|
+
}, [open, updateDropdownPosition]);
|
|
75
|
+
|
|
76
|
+
const handleSelect = useCallback(
|
|
77
|
+
(statusId: string) => {
|
|
78
|
+
onChange(statusId);
|
|
79
|
+
onStatusChange?.(statusId);
|
|
80
|
+
setOpen(false);
|
|
81
|
+
setFocusedIndex(-1);
|
|
82
|
+
},
|
|
83
|
+
[onChange, onStatusChange],
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const handleKeyDown = useCallback(
|
|
87
|
+
(e: React.KeyboardEvent) => {
|
|
88
|
+
if (!open) {
|
|
89
|
+
if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
setOpen(true);
|
|
92
|
+
setFocusedIndex(value ? options.findIndex((o) => o.id === value) : 0);
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
switch (e.key) {
|
|
97
|
+
case 'Escape':
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
setOpen(false);
|
|
100
|
+
setFocusedIndex(-1);
|
|
101
|
+
break;
|
|
102
|
+
case 'ArrowDown':
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
setFocusedIndex((i) => (i < options.length - 1 ? i + 1 : 0));
|
|
105
|
+
break;
|
|
106
|
+
case 'ArrowUp':
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
setFocusedIndex((i) => (i > 0 ? i - 1 : options.length - 1));
|
|
109
|
+
break;
|
|
110
|
+
case 'Enter':
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
if (focusedIndex >= 0 && options[focusedIndex]) {
|
|
113
|
+
handleSelect(options[focusedIndex].id);
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
default:
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
[open, value, options, focusedIndex, handleSelect],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
useLayoutEffect(() => {
|
|
124
|
+
if (open) {
|
|
125
|
+
updateDropdownPosition();
|
|
126
|
+
} else {
|
|
127
|
+
setDropdownStyle(null);
|
|
128
|
+
}
|
|
129
|
+
}, [open, updateDropdownPosition]);
|
|
130
|
+
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (!open) return;
|
|
133
|
+
const handleScrollOrResize = () => {
|
|
134
|
+
setOpen(false);
|
|
135
|
+
setFocusedIndex(-1);
|
|
136
|
+
};
|
|
137
|
+
// window.addEventListener('scroll', handleScrollOrResize, true);
|
|
138
|
+
window.addEventListener('resize', handleScrollOrResize, { passive: true });
|
|
139
|
+
return () => {
|
|
140
|
+
// window.removeEventListener('scroll', handleScrollOrResize, true);
|
|
141
|
+
window.removeEventListener('resize', handleScrollOrResize);
|
|
142
|
+
};
|
|
143
|
+
}, [open]);
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
147
|
+
const target = event.target as Node;
|
|
148
|
+
if (containerRef.current?.contains(target)) return;
|
|
149
|
+
if (dropdownRef.current?.contains(target)) return;
|
|
150
|
+
setOpen(false);
|
|
151
|
+
setFocusedIndex(-1);
|
|
152
|
+
};
|
|
153
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
154
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
155
|
+
}, []);
|
|
156
|
+
|
|
157
|
+
const sizeClasses = size === 'sm' ? 'px-2 py-1.5 text-sm' : 'px-4 py-2 text-sm';
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div ref={containerRef} className={cn('relative', className)}>
|
|
161
|
+
<button
|
|
162
|
+
ref={triggerRef}
|
|
163
|
+
type="button"
|
|
164
|
+
id={id}
|
|
165
|
+
role="combobox"
|
|
166
|
+
aria-expanded={open}
|
|
167
|
+
aria-haspopup="listbox"
|
|
168
|
+
aria-label="Sélectionner un statut"
|
|
169
|
+
disabled={disabled}
|
|
170
|
+
onClick={() => !disabled && setOpen(!open)}
|
|
171
|
+
onKeyDown={handleKeyDown}
|
|
172
|
+
className={cn(
|
|
173
|
+
'flex w-full cursor-pointer items-center justify-between gap-2 rounded-lg border text-left transition-colors focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
|
174
|
+
sizeClasses,
|
|
175
|
+
selectedStatus
|
|
176
|
+
? ''
|
|
177
|
+
: 'border-gray-300 bg-white text-gray-900 hover:bg-gray-50',
|
|
178
|
+
)}
|
|
179
|
+
style={
|
|
180
|
+
selectedStatus
|
|
181
|
+
? {
|
|
182
|
+
backgroundColor: `${selectedStatus.color}20`,
|
|
183
|
+
color: selectedStatus.color,
|
|
184
|
+
borderColor: selectedStatus.color,
|
|
185
|
+
}
|
|
186
|
+
: undefined
|
|
187
|
+
}
|
|
188
|
+
>
|
|
189
|
+
<span className="flex min-w-0 items-center gap-2 truncate">
|
|
190
|
+
{selectedStatus ? (
|
|
191
|
+
<>
|
|
192
|
+
<span
|
|
193
|
+
className="h-2.5 w-2.5 shrink-0 rounded-full"
|
|
194
|
+
style={{ backgroundColor: selectedStatus.color }}
|
|
195
|
+
aria-hidden
|
|
196
|
+
/>
|
|
197
|
+
<span className="truncate">{selectedStatus.name}</span>
|
|
198
|
+
</>
|
|
199
|
+
) : (
|
|
200
|
+
<span className="text-gray-500">{placeholder}</span>
|
|
201
|
+
)}
|
|
202
|
+
</span>
|
|
203
|
+
<ChevronDown
|
|
204
|
+
aria-hidden="true"
|
|
205
|
+
className={cn('h-4 w-4 shrink-0 text-gray-400 transition-transform', open && 'rotate-180')}
|
|
206
|
+
/>
|
|
207
|
+
</button>
|
|
208
|
+
|
|
209
|
+
{open &&
|
|
210
|
+
dropdownStyle &&
|
|
211
|
+
typeof document !== 'undefined' &&
|
|
212
|
+
createPortal(
|
|
213
|
+
<AnimatePresence>
|
|
214
|
+
<motion.div
|
|
215
|
+
ref={dropdownRef}
|
|
216
|
+
role="listbox"
|
|
217
|
+
initial={{ opacity: 0, scale: 0.96, y: -4 }}
|
|
218
|
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
219
|
+
exit={{ opacity: 0, scale: 0.96, y: -4 }}
|
|
220
|
+
transition={{ duration: 0.15, ease: [0.25, 0.4, 0.25, 1] }}
|
|
221
|
+
className="fixed z-[9999] max-h-60 min-w-[160px] overflow-auto rounded-lg border border-gray-200 bg-white py-1 shadow-lg"
|
|
222
|
+
style={{
|
|
223
|
+
left: dropdownStyle.left,
|
|
224
|
+
width: dropdownStyle.width,
|
|
225
|
+
transformOrigin: dropdownStyle.top !== undefined ? 'top' : 'bottom',
|
|
226
|
+
...(dropdownStyle.top !== undefined
|
|
227
|
+
? { top: dropdownStyle.top }
|
|
228
|
+
: { bottom: dropdownStyle.bottom }),
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
{options.map((opt, idx) => (
|
|
232
|
+
<motion.button
|
|
233
|
+
key={opt.id || '__empty__'}
|
|
234
|
+
initial={{ opacity: 0, x: -4 }}
|
|
235
|
+
animate={{ opacity: 1, x: 0 }}
|
|
236
|
+
transition={{ delay: idx * 0.02, duration: 0.12 }}
|
|
237
|
+
type="button"
|
|
238
|
+
role="option"
|
|
239
|
+
aria-selected={opt.id === value}
|
|
240
|
+
className={cn(
|
|
241
|
+
'flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left text-sm transition-colors',
|
|
242
|
+
idx === focusedIndex ? 'bg-gray-100' : 'hover:bg-gray-50',
|
|
243
|
+
opt.id === value ? 'bg-blue-50 font-medium text-blue-700' : 'text-gray-900',
|
|
244
|
+
)}
|
|
245
|
+
onClick={() => handleSelect(opt.id)}
|
|
246
|
+
onMouseEnter={() => setFocusedIndex(idx)}
|
|
247
|
+
>
|
|
248
|
+
{opt.id ? (
|
|
249
|
+
<>
|
|
250
|
+
<span
|
|
251
|
+
className="h-2.5 w-2.5 shrink-0 rounded-full"
|
|
252
|
+
style={{
|
|
253
|
+
backgroundColor:
|
|
254
|
+
statuses.find((s) => s.id === opt.id)?.color ?? '#999',
|
|
255
|
+
}}
|
|
256
|
+
aria-hidden
|
|
257
|
+
/>
|
|
258
|
+
<span className="truncate">{opt.name}</span>
|
|
259
|
+
</>
|
|
260
|
+
) : (
|
|
261
|
+
<span className="text-gray-500">{opt.name}</span>
|
|
262
|
+
)}
|
|
263
|
+
</motion.button>
|
|
264
|
+
))}
|
|
265
|
+
</motion.div>
|
|
266
|
+
</AnimatePresence>,
|
|
267
|
+
document.body,
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
5
|
+
|
|
6
|
+
import { cn } from '@/lib/utils';
|
|
7
|
+
|
|
8
|
+
function TooltipProvider({
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
|
11
|
+
return <TooltipPrimitive.Provider {...props} />;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const Tooltip = TooltipPrimitive.Root;
|
|
15
|
+
|
|
16
|
+
const TooltipTrigger = TooltipPrimitive.TooltipTrigger;
|
|
17
|
+
|
|
18
|
+
const TooltipContent = React.forwardRef<
|
|
19
|
+
React.ElementRef<typeof TooltipPrimitive.Content>,
|
|
20
|
+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
21
|
+
>(({ className, sideOffset = 6, ...props }, ref) => (
|
|
22
|
+
<TooltipPrimitive.Portal>
|
|
23
|
+
<TooltipPrimitive.Content
|
|
24
|
+
ref={ref}
|
|
25
|
+
sideOffset={sideOffset}
|
|
26
|
+
className={cn(
|
|
27
|
+
'bg-popover text-popover-foreground border-border z-[200] max-w-[min(280px,calc(100vw-16px))] rounded-md border px-3 py-2 text-xs shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50',
|
|
28
|
+
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
</TooltipPrimitive.Portal>
|
|
34
|
+
));
|
|
35
|
+
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
|
36
|
+
|
|
37
|
+
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
|
|
@@ -76,13 +76,21 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
|
|
|
76
76
|
|
|
77
77
|
return (
|
|
78
78
|
<div className="fixed inset-0 z-50 flex items-center justify-center rounded-lg bg-gray-500/20 p-4 shadow-xl backdrop-blur-sm">
|
|
79
|
-
<div
|
|
79
|
+
<div
|
|
80
|
+
ref={contentRef}
|
|
81
|
+
className="w-full max-w-2xl rounded-lg bg-white shadow-xl overscroll-contain"
|
|
82
|
+
role="dialog"
|
|
83
|
+
aria-modal="true"
|
|
84
|
+
aria-labelledby="view-as-title"
|
|
85
|
+
>
|
|
80
86
|
{/* En-tête */}
|
|
81
87
|
<div className="flex items-center justify-between rounded-t-lg border-b border-gray-200 bg-blue-600 px-6 py-4">
|
|
82
88
|
<div className="flex items-center gap-3 text-white">
|
|
83
89
|
<UserIcon className="h-6 w-6" />
|
|
84
90
|
<div>
|
|
85
|
-
<h2 id="view-as-title" className="text-xl font-bold">
|
|
91
|
+
<h2 id="view-as-title" className="text-xl font-bold">
|
|
92
|
+
Changer de vue
|
|
93
|
+
</h2>
|
|
86
94
|
<p className="text-sm text-white/90">
|
|
87
95
|
Voir l'application avec les permissions d'un profil
|
|
88
96
|
</p>
|
|
@@ -114,7 +122,7 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
|
|
|
114
122
|
router.refresh();
|
|
115
123
|
}}
|
|
116
124
|
className={cn(
|
|
117
|
-
'w-full cursor-pointer rounded-lg border-2 p-4 text-left transition-
|
|
125
|
+
'w-full cursor-pointer rounded-lg border-2 p-4 text-left transition-colors',
|
|
118
126
|
!viewAsUser
|
|
119
127
|
? 'border-blue-500 bg-blue-50'
|
|
120
128
|
: 'border-gray-200 bg-white hover:border-blue-300 hover:bg-blue-50/50',
|
|
@@ -125,9 +133,7 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
|
|
|
125
133
|
<div
|
|
126
134
|
className={cn(
|
|
127
135
|
'flex h-12 w-12 items-center justify-center rounded-full text-lg font-bold',
|
|
128
|
-
!viewAsUser
|
|
129
|
-
? 'bg-blue-600 text-white'
|
|
130
|
-
: 'bg-blue-100 text-blue-800',
|
|
136
|
+
!viewAsUser ? 'bg-blue-600 text-white' : 'bg-blue-100 text-blue-800',
|
|
131
137
|
)}
|
|
132
138
|
>
|
|
133
139
|
{getInitials(session.user.name || session.user.email)}
|
|
@@ -153,7 +159,7 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
|
|
|
153
159
|
key={user.id}
|
|
154
160
|
onClick={() => handleSelectUser(user)}
|
|
155
161
|
className={cn(
|
|
156
|
-
'w-full cursor-pointer rounded-lg border-2 p-4 text-left transition-
|
|
162
|
+
'w-full cursor-pointer rounded-lg border-2 p-4 text-left transition-colors',
|
|
157
163
|
viewAsUser?.id === user.id
|
|
158
164
|
? 'border-blue-500 bg-blue-50'
|
|
159
165
|
: 'border-gray-200 bg-white hover:border-blue-300 hover:bg-blue-50/50',
|