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
|
@@ -8,11 +8,18 @@ import Link from 'next/link';
|
|
|
8
8
|
import { cn } from '@/lib/utils';
|
|
9
9
|
import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
|
|
10
10
|
import { GlobalSearch } from '@/components/global-search';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
REMINDERS_CLEAR_UNDO_WINDOW_MS,
|
|
13
|
+
REMINDERS_POLL_INTERVAL_MS,
|
|
14
|
+
REMINDERS_REFRESH_EVENT,
|
|
15
|
+
requestRemindersRefresh,
|
|
16
|
+
} from '@/lib/reminder-state';
|
|
17
|
+
import { useAppToast } from '@/contexts/app-toast-context';
|
|
12
18
|
|
|
13
19
|
interface Reminder {
|
|
14
20
|
id: string;
|
|
15
21
|
taskId: string;
|
|
22
|
+
kind: 'due' | 'reminder';
|
|
16
23
|
type: string;
|
|
17
24
|
title: string | null;
|
|
18
25
|
description: string;
|
|
@@ -20,6 +27,9 @@ interface Reminder {
|
|
|
20
27
|
scheduledAt: string;
|
|
21
28
|
reminderTime: string;
|
|
22
29
|
reminderMinutesBefore: number | null;
|
|
30
|
+
isRead: boolean;
|
|
31
|
+
isDismissed: boolean;
|
|
32
|
+
isClearedByCutoff: boolean;
|
|
23
33
|
contact: {
|
|
24
34
|
id: string;
|
|
25
35
|
firstName: string | null;
|
|
@@ -42,6 +52,30 @@ const PRIORITY_COLORS: Record<string, string> = {
|
|
|
42
52
|
URGENT: 'bg-red-100 text-red-700',
|
|
43
53
|
};
|
|
44
54
|
|
|
55
|
+
const REMINDER_KIND_BADGE: Record<Reminder['kind'], { label: string; className: string }> = {
|
|
56
|
+
due: {
|
|
57
|
+
label: 'À faire maintenant',
|
|
58
|
+
className: 'bg-amber-100 text-amber-900 ring-1 ring-amber-200/80',
|
|
59
|
+
},
|
|
60
|
+
reminder: {
|
|
61
|
+
label: 'Rappel',
|
|
62
|
+
className: 'bg-sky-100 text-sky-900 ring-1 ring-sky-200/80',
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function priorityLabel(priority: string): string {
|
|
67
|
+
switch (priority) {
|
|
68
|
+
case 'URGENT':
|
|
69
|
+
return 'Urgente';
|
|
70
|
+
case 'HIGH':
|
|
71
|
+
return 'Haute';
|
|
72
|
+
case 'MEDIUM':
|
|
73
|
+
return 'Moyenne';
|
|
74
|
+
default:
|
|
75
|
+
return 'Faible';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
45
79
|
function formatDateTime(dateString: string) {
|
|
46
80
|
const date = new Date(dateString);
|
|
47
81
|
return {
|
|
@@ -58,12 +92,12 @@ function formatDateTime(dateString: string) {
|
|
|
58
92
|
|
|
59
93
|
export function Header() {
|
|
60
94
|
const { data: session } = useSession();
|
|
95
|
+
const { persistent: showPersistentToast } = useAppToast();
|
|
61
96
|
const router = useRouter();
|
|
62
97
|
const { toggle: toggleMobileMenu } = useMobileMenuContext();
|
|
63
98
|
const [showRemindersDropdown, setShowRemindersDropdown] = useState(false);
|
|
64
99
|
const [showUserDropdown, setShowUserDropdown] = useState(false);
|
|
65
100
|
const [reminders, setReminders] = useState<Reminder[]>([]);
|
|
66
|
-
const [readReminders, setReadReminders] = useState<Set<string>>(new Set());
|
|
67
101
|
const [loading, setLoading] = useState(false);
|
|
68
102
|
const remindersRef = useRef<HTMLDivElement>(null);
|
|
69
103
|
const userRef = useRef<HTMLDivElement>(null);
|
|
@@ -72,26 +106,15 @@ export function Header() {
|
|
|
72
106
|
const userEmail = session?.user?.email || '';
|
|
73
107
|
const userInitial = userName?.[0]?.toUpperCase() || 'U';
|
|
74
108
|
|
|
75
|
-
// Charger les rappels
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
const stored = safeLocalStorageGet<string[]>('read-reminders', []);
|
|
78
|
-
setReadReminders(new Set(stored));
|
|
79
|
-
}, []);
|
|
80
|
-
|
|
81
|
-
// Sauvegarder les rappels lus dans localStorage
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
if (readReminders.size > 0) {
|
|
84
|
-
safeLocalStorageSet('read-reminders', Array.from(readReminders));
|
|
85
|
-
}
|
|
86
|
-
}, [readReminders]);
|
|
87
|
-
|
|
88
|
-
// Charger les rappels
|
|
109
|
+
// Charger les rappels (polling + retour sur l’onglet + événement explicite)
|
|
89
110
|
useEffect(() => {
|
|
90
111
|
if (!session) return;
|
|
91
112
|
|
|
92
|
-
const fetchReminders = async () => {
|
|
113
|
+
const fetchReminders = async (opts?: { silent?: boolean }) => {
|
|
93
114
|
try {
|
|
94
|
-
|
|
115
|
+
if (!opts?.silent) {
|
|
116
|
+
setLoading(true);
|
|
117
|
+
}
|
|
95
118
|
const response = await fetch('/api/reminders');
|
|
96
119
|
if (response.ok) {
|
|
97
120
|
const data = await response.json();
|
|
@@ -100,13 +123,29 @@ export function Header() {
|
|
|
100
123
|
} catch (error) {
|
|
101
124
|
console.error('Erreur lors du chargement des rappels:', error);
|
|
102
125
|
} finally {
|
|
103
|
-
|
|
126
|
+
if (!opts?.silent) {
|
|
127
|
+
setLoading(false);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
void fetchReminders();
|
|
133
|
+
const interval = setInterval(() => void fetchReminders({ silent: true }), REMINDERS_POLL_INTERVAL_MS);
|
|
134
|
+
|
|
135
|
+
const onVisible = () => {
|
|
136
|
+
if (document.visibilityState === 'visible') {
|
|
137
|
+
void fetchReminders({ silent: true });
|
|
104
138
|
}
|
|
105
139
|
};
|
|
140
|
+
const onRefresh = () => void fetchReminders({ silent: true });
|
|
106
141
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return () =>
|
|
142
|
+
document.addEventListener('visibilitychange', onVisible);
|
|
143
|
+
globalThis.addEventListener(REMINDERS_REFRESH_EVENT, onRefresh);
|
|
144
|
+
return () => {
|
|
145
|
+
clearInterval(interval);
|
|
146
|
+
document.removeEventListener('visibilitychange', onVisible);
|
|
147
|
+
globalThis.removeEventListener(REMINDERS_REFRESH_EVENT, onRefresh);
|
|
148
|
+
};
|
|
110
149
|
}, [session]);
|
|
111
150
|
|
|
112
151
|
// Fermer les dropdowns en cliquant à l'extérieur
|
|
@@ -124,15 +163,53 @@ export function Header() {
|
|
|
124
163
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
125
164
|
}, []);
|
|
126
165
|
|
|
127
|
-
const unreadCount = reminders.filter((r) => !
|
|
166
|
+
const unreadCount = reminders.filter((r) => !r.isRead && !r.isDismissed).length;
|
|
128
167
|
|
|
129
|
-
const handleMarkAsRead = (reminderId: string) => {
|
|
130
|
-
|
|
168
|
+
const handleMarkAsRead = async (reminderId: string) => {
|
|
169
|
+
setReminders((prev) =>
|
|
170
|
+
prev.map((reminder) =>
|
|
171
|
+
reminder.id === reminderId ? { ...reminder, isRead: true } : reminder,
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
globalThis.dispatchEvent(new CustomEvent('reminders:read', { detail: { reminderId } }));
|
|
175
|
+
try {
|
|
176
|
+
await fetch('/api/reminders/state', {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: { 'Content-Type': 'application/json' },
|
|
179
|
+
body: JSON.stringify({ reminderId, status: 'READ' }),
|
|
180
|
+
});
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error("Erreur lors du marquage d'un rappel comme lu:", error);
|
|
183
|
+
}
|
|
131
184
|
};
|
|
132
185
|
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
186
|
+
const handleClearReminders = async () => {
|
|
187
|
+
try {
|
|
188
|
+
const response = await fetch('/api/reminders/clear', { method: 'POST' });
|
|
189
|
+
const data = (await response.json().catch(() => ({}))) as {
|
|
190
|
+
degraded?: boolean;
|
|
191
|
+
undoUntil?: string;
|
|
192
|
+
};
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
throw new Error('Impossible de vider les rappels.');
|
|
195
|
+
}
|
|
196
|
+
setReminders([]);
|
|
197
|
+
globalThis.dispatchEvent(new CustomEvent('reminders:cleared'));
|
|
198
|
+
if (data.degraded !== true && typeof data.undoUntil === 'string') {
|
|
199
|
+
showPersistentToast('info', 'Rappels vidés.', {
|
|
200
|
+
actionLabel: 'Annuler',
|
|
201
|
+
actionOnClick: async () => {
|
|
202
|
+
const undoRes = await fetch('/api/reminders/clear/undo', { method: 'POST' });
|
|
203
|
+
if (undoRes.ok) {
|
|
204
|
+
requestRemindersRefresh();
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
autoDismissMs: REMINDERS_CLEAR_UNDO_WINDOW_MS,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error('Erreur lors du vidage des rappels:', error);
|
|
212
|
+
}
|
|
136
213
|
};
|
|
137
214
|
|
|
138
215
|
const handleSignOut = async () => {
|
|
@@ -141,7 +218,7 @@ export function Header() {
|
|
|
141
218
|
};
|
|
142
219
|
|
|
143
220
|
return (
|
|
144
|
-
<header className="sticky top-0 z-40 border-b
|
|
221
|
+
<header className="border-border bg-background/95 sticky top-0 z-40 border-b px-4 py-3 backdrop-blur-sm sm:px-6 lg:px-8">
|
|
145
222
|
<div className="flex items-center gap-2 sm:gap-4">
|
|
146
223
|
{/* Left: Logo + Greeting */}
|
|
147
224
|
<div className="flex shrink-0 items-center gap-2 sm:gap-3">
|
|
@@ -149,12 +226,12 @@ export function Header() {
|
|
|
149
226
|
{/* Bouton burger pour mobile */}
|
|
150
227
|
<button
|
|
151
228
|
onClick={toggleMobileMenu}
|
|
152
|
-
className="cursor-pointer rounded-lg p-2
|
|
229
|
+
className="text-foreground/80 hover:bg-muted focus-visible:ring-primary cursor-pointer rounded-lg p-2 transition-colors duration-200 focus-visible:ring-2 focus-visible:outline-none lg:hidden"
|
|
153
230
|
aria-label="Ouvrir ou fermer le menu"
|
|
154
231
|
>
|
|
155
232
|
<Menu className="h-5 w-5" />
|
|
156
233
|
</button>
|
|
157
|
-
<span className="text-base font-bold
|
|
234
|
+
<span className="text-foreground text-base font-bold sm:text-lg">CRM Template</span>
|
|
158
235
|
</div>
|
|
159
236
|
</div>
|
|
160
237
|
|
|
@@ -169,12 +246,21 @@ export function Header() {
|
|
|
169
246
|
<div className="relative" ref={remindersRef}>
|
|
170
247
|
<button
|
|
171
248
|
onClick={() => setShowRemindersDropdown(!showRemindersDropdown)}
|
|
172
|
-
className="relative cursor-pointer rounded-lg p-2
|
|
249
|
+
className="text-muted-foreground hover:bg-muted focus-visible:ring-primary relative cursor-pointer rounded-lg p-2 transition-colors duration-200 focus-visible:ring-2 focus-visible:outline-none"
|
|
173
250
|
aria-label="Notifications"
|
|
174
251
|
>
|
|
175
|
-
<
|
|
252
|
+
<span
|
|
253
|
+
className={cn(
|
|
254
|
+
unreadCount > 0 && !showRemindersDropdown && 'ui-bell-notify',
|
|
255
|
+
)}
|
|
256
|
+
>
|
|
257
|
+
<Bell className="h-5 w-5" />
|
|
258
|
+
</span>
|
|
176
259
|
{unreadCount > 0 && (
|
|
177
|
-
<span
|
|
260
|
+
<span
|
|
261
|
+
key={unreadCount}
|
|
262
|
+
className="bg-primary text-primary-foreground ui-count-pop absolute top-1 right-1 flex h-4 w-4 items-center justify-center rounded-full text-[10px] font-semibold"
|
|
263
|
+
>
|
|
178
264
|
{unreadCount > 9 ? '9+' : unreadCount}
|
|
179
265
|
</span>
|
|
180
266
|
)}
|
|
@@ -182,67 +268,85 @@ export function Header() {
|
|
|
182
268
|
|
|
183
269
|
{/* Dropdown des rappels */}
|
|
184
270
|
{showRemindersDropdown && (
|
|
185
|
-
<div className="absolute right-0 mt-2 w-80 max-w-[calc(100vw-2rem)] rounded-xl border
|
|
186
|
-
<div className="border-
|
|
271
|
+
<div className="border-border bg-popover ui-dropdown-enter absolute right-0 mt-2 w-80 max-w-[calc(100vw-2rem)] rounded-xl border shadow-(--shadow-dropdown)">
|
|
272
|
+
<div className="border-border border-b px-4 py-3">
|
|
187
273
|
<div className="flex items-center justify-between">
|
|
188
|
-
<h3 className="text-sm font-semibold
|
|
189
|
-
{
|
|
274
|
+
<h3 className="text-popover-foreground text-sm font-semibold">Rappels</h3>
|
|
275
|
+
{reminders.length > 0 && (
|
|
190
276
|
<button
|
|
191
|
-
onClick={
|
|
192
|
-
className="
|
|
277
|
+
onClick={handleClearReminders}
|
|
278
|
+
className="text-primary hover:text-primary/80 focus-visible:ring-primary cursor-pointer text-xs focus-visible:ring-2 focus-visible:outline-none"
|
|
193
279
|
>
|
|
194
|
-
|
|
280
|
+
Vider les rappels
|
|
195
281
|
</button>
|
|
196
282
|
)}
|
|
197
283
|
</div>
|
|
198
284
|
</div>
|
|
199
285
|
<div className="max-h-96 overflow-y-auto">
|
|
200
|
-
{loading
|
|
201
|
-
<div className="px-4 py-8 text-center text-sm
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
)
|
|
205
|
-
|
|
286
|
+
{loading && (
|
|
287
|
+
<div className="text-muted-foreground px-4 py-8 text-center text-sm">
|
|
288
|
+
Chargement...
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
{!loading && reminders.length === 0 && (
|
|
292
|
+
<div className="text-muted-foreground px-4 py-8 text-center text-sm">
|
|
293
|
+
Aucun rappel
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
{!loading && reminders.length > 0 && (
|
|
297
|
+
<div className="divide-border divide-y">
|
|
206
298
|
{reminders.map((reminder) => {
|
|
207
|
-
const isRead =
|
|
208
|
-
const { date, time } = formatDateTime(reminder.
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
299
|
+
const isRead = reminder.isRead || reminder.isDismissed;
|
|
300
|
+
const { date, time } = formatDateTime(reminder.reminderTime);
|
|
301
|
+
const kindBadge = REMINDER_KIND_BADGE[reminder.kind] ?? REMINDER_KIND_BADGE.reminder;
|
|
302
|
+
let contactName: string | null = null;
|
|
303
|
+
if (reminder.contact) {
|
|
304
|
+
const full = `${reminder.contact.firstName || ''} ${reminder.contact.lastName || ''}`.trim();
|
|
305
|
+
contactName = full.length > 0 ? full : 'Contact sans nom';
|
|
306
|
+
}
|
|
213
307
|
|
|
214
308
|
return (
|
|
215
309
|
<button
|
|
216
310
|
type="button"
|
|
217
311
|
key={reminder.id}
|
|
218
312
|
className={cn(
|
|
219
|
-
'w-full px-4 py-3 text-left transition-colors duration-200
|
|
220
|
-
|
|
313
|
+
'hover:bg-accent w-full px-4 py-3 text-left transition-colors duration-200',
|
|
314
|
+
isRead ? undefined : 'bg-accent/70',
|
|
221
315
|
)}
|
|
222
|
-
onClick={() => handleMarkAsRead(reminder.id)}
|
|
316
|
+
onClick={() => void handleMarkAsRead(reminder.id)}
|
|
223
317
|
>
|
|
224
318
|
<div className="flex items-start gap-3">
|
|
225
319
|
<div className="mt-0.5 shrink-0">
|
|
226
|
-
<Calendar className="h-4 w-4
|
|
320
|
+
<Calendar aria-hidden="true" className="text-primary h-4 w-4" />
|
|
227
321
|
</div>
|
|
228
322
|
<div className="min-w-0 flex-1">
|
|
229
323
|
<div className="flex items-start justify-between gap-2">
|
|
230
324
|
<div className="min-w-0 flex-1">
|
|
325
|
+
<span
|
|
326
|
+
className={cn(
|
|
327
|
+
'mb-1 inline-flex max-w-full rounded-full px-2 py-0.5 text-[10px] font-semibold tracking-wide uppercase',
|
|
328
|
+
kindBadge.className,
|
|
329
|
+
)}
|
|
330
|
+
>
|
|
331
|
+
{kindBadge.label}
|
|
332
|
+
</span>
|
|
231
333
|
<p
|
|
232
334
|
className={cn(
|
|
233
335
|
'text-sm font-medium',
|
|
234
|
-
|
|
336
|
+
isRead ? 'text-muted-foreground' : 'text-foreground',
|
|
235
337
|
)}
|
|
236
338
|
>
|
|
237
339
|
{reminder.title || TASK_TYPE_LABELS[reminder.type] || 'Tâche'}
|
|
238
340
|
</p>
|
|
239
341
|
{contactName && (
|
|
240
|
-
<p className="mt-0.5 text-xs
|
|
342
|
+
<p className="text-muted-foreground mt-0.5 text-xs">
|
|
343
|
+
{contactName}
|
|
344
|
+
</p>
|
|
241
345
|
)}
|
|
242
|
-
<div className="mt-1 flex items-center gap-2">
|
|
243
|
-
<span className="text-
|
|
244
|
-
<span className="text-
|
|
245
|
-
<span className="text-
|
|
346
|
+
<div className="mt-1 flex flex-wrap items-center gap-2">
|
|
347
|
+
<span className="text-muted-foreground text-xs">{date}</span>
|
|
348
|
+
<span className="text-muted-foreground/70 text-xs">•</span>
|
|
349
|
+
<span className="text-muted-foreground text-xs">{time}</span>
|
|
246
350
|
<span
|
|
247
351
|
className={cn(
|
|
248
352
|
'rounded-full px-1.5 py-0.5 text-[10px] font-medium',
|
|
@@ -250,25 +354,19 @@ export function Header() {
|
|
|
250
354
|
PRIORITY_COLORS.MEDIUM,
|
|
251
355
|
)}
|
|
252
356
|
>
|
|
253
|
-
{reminder.priority
|
|
254
|
-
? 'Urgente'
|
|
255
|
-
: reminder.priority === 'HIGH'
|
|
256
|
-
? 'Haute'
|
|
257
|
-
: reminder.priority === 'MEDIUM'
|
|
258
|
-
? 'Moyenne'
|
|
259
|
-
: 'Faible'}
|
|
357
|
+
{priorityLabel(reminder.priority)}
|
|
260
358
|
</span>
|
|
261
359
|
</div>
|
|
262
360
|
</div>
|
|
263
361
|
{!isRead && (
|
|
264
|
-
<div className="h-2 w-2 shrink-0 rounded-full
|
|
362
|
+
<div className="bg-primary h-2 w-2 shrink-0 rounded-full" />
|
|
265
363
|
)}
|
|
266
364
|
</div>
|
|
267
365
|
{reminder.contact && (
|
|
268
366
|
<Link
|
|
269
367
|
href={`/contacts/${reminder.contact.id}`}
|
|
270
368
|
onClick={(e) => e.stopPropagation()}
|
|
271
|
-
className="mt-2 inline-block text-xs font-medium
|
|
369
|
+
className="text-primary hover:text-primary/80 mt-2 inline-block text-xs font-medium"
|
|
272
370
|
>
|
|
273
371
|
Voir le contact
|
|
274
372
|
</Link>
|
|
@@ -289,26 +387,26 @@ export function Header() {
|
|
|
289
387
|
<div className="relative" ref={userRef}>
|
|
290
388
|
<button
|
|
291
389
|
onClick={() => setShowUserDropdown(!showUserDropdown)}
|
|
292
|
-
className="flex cursor-pointer items-center gap-1.5 rounded-md transition-colors duration-200
|
|
390
|
+
className="hover:bg-accent focus-visible:ring-primary flex cursor-pointer items-center gap-1.5 rounded-md transition-colors duration-200 focus-visible:ring-2 focus-visible:outline-none sm:gap-2"
|
|
293
391
|
aria-label="Menu utilisateur"
|
|
294
392
|
>
|
|
295
|
-
<div className="flex h-8 w-8 items-center justify-center rounded-full
|
|
393
|
+
<div className="bg-primary/15 text-primary flex h-8 w-8 items-center justify-center rounded-full text-xs font-semibold sm:h-9 sm:w-9 sm:text-sm">
|
|
296
394
|
{userInitial}
|
|
297
395
|
</div>
|
|
298
|
-
<ChevronDown className="hidden h-4 w-4
|
|
396
|
+
<ChevronDown className="text-muted-foreground hover:text-foreground hidden h-4 w-4 transition-colors sm:block" />
|
|
299
397
|
</button>
|
|
300
398
|
|
|
301
399
|
{/* Dropdown utilisateur */}
|
|
302
400
|
{showUserDropdown && (
|
|
303
|
-
<div className="absolute right-0 mt-2 w-56 rounded-xl border
|
|
304
|
-
<div className="border-
|
|
305
|
-
<p className="text-sm font-medium
|
|
306
|
-
<p className="mt-0.5 text-xs
|
|
401
|
+
<div className="border-border bg-popover ui-dropdown-enter absolute right-0 mt-2 w-56 rounded-xl border shadow-(--shadow-dropdown)">
|
|
402
|
+
<div className="border-border border-b px-4 py-3">
|
|
403
|
+
<p className="text-popover-foreground text-sm font-medium">{userName}</p>
|
|
404
|
+
<p className="text-muted-foreground mt-0.5 text-xs">{userEmail}</p>
|
|
307
405
|
</div>
|
|
308
406
|
<div className="py-1">
|
|
309
407
|
<button
|
|
310
408
|
onClick={handleSignOut}
|
|
311
|
-
className="flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-sm
|
|
409
|
+
className="text-popover-foreground hover:bg-accent focus-visible:ring-primary flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-sm transition-colors duration-200 focus-visible:ring-2 focus-visible:outline-none"
|
|
312
410
|
>
|
|
313
411
|
<LogOut className="h-4 w-4" />
|
|
314
412
|
<span>Déconnexion</span>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { signOut } from '@/lib/auth-client';
|
|
6
|
+
|
|
7
|
+
const POLL_MS = 25_000;
|
|
8
|
+
|
|
9
|
+
async function fetchActiveStatus(): Promise<boolean | null> {
|
|
10
|
+
try {
|
|
11
|
+
const res = await fetch('/api/auth/check-active', { credentials: 'include' });
|
|
12
|
+
if (!res.ok) return null;
|
|
13
|
+
const data = (await res.json()) as { active?: boolean };
|
|
14
|
+
if (typeof data.active !== 'boolean') return null;
|
|
15
|
+
return data.active;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Déconnecte et redirige vers /signin si le compte courant est inactif ou sans session valide,
|
|
23
|
+
* pour éviter de rester sur le shell du CRM avec une session révoquée côté serveur.
|
|
24
|
+
*/
|
|
25
|
+
export function InactiveAccountGuard() {
|
|
26
|
+
const router = useRouter();
|
|
27
|
+
const signingOutRef = useRef(false);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const run = async () => {
|
|
31
|
+
if (signingOutRef.current) return;
|
|
32
|
+
const active = await fetchActiveStatus();
|
|
33
|
+
if (active === false) {
|
|
34
|
+
signingOutRef.current = true;
|
|
35
|
+
await signOut();
|
|
36
|
+
router.replace('/signin?inactive=1');
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
void run();
|
|
41
|
+
|
|
42
|
+
const interval = setInterval(() => {
|
|
43
|
+
if (document.visibilityState === 'visible') void run();
|
|
44
|
+
}, POLL_MS);
|
|
45
|
+
|
|
46
|
+
const onVisibility = () => {
|
|
47
|
+
if (document.visibilityState === 'visible') void run();
|
|
48
|
+
};
|
|
49
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
clearInterval(interval);
|
|
53
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
54
|
+
};
|
|
55
|
+
}, [router]);
|
|
56
|
+
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useIntegrationNotifications } from '@/hooks/useIntegrationNotifications';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Renders nothing; runs the integration notifications polling hook
|
|
7
|
+
* (toasts disabled to avoid accumulation).
|
|
8
|
+
*/
|
|
9
|
+
export function IntegrationNotificationsListener() {
|
|
10
|
+
useIntegrationNotifications();
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { sanitizeEmailHtml } from '@/lib/email-html-sanitize';
|
|
2
2
|
|
|
3
3
|
interface InvitationEmailProps {
|
|
4
4
|
name: string;
|
|
@@ -73,7 +73,7 @@ export function InvitationEmailTemplate({ name, invitationUrl, signature }: Invi
|
|
|
73
73
|
fontSize: '14px',
|
|
74
74
|
lineHeight: '1.6',
|
|
75
75
|
}}
|
|
76
|
-
dangerouslySetInnerHTML={{ __html:
|
|
76
|
+
dangerouslySetInnerHTML={{ __html: sanitizeEmailHtml(signature) }}
|
|
77
77
|
/>
|
|
78
78
|
)}
|
|
79
79
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { sanitizeEmailHtml } from '@/lib/email-html-sanitize';
|
|
2
2
|
|
|
3
3
|
interface MeetCancellationEmailTemplateProps {
|
|
4
4
|
contactName: string;
|
|
@@ -101,7 +101,7 @@ export function MeetCancellationEmailTemplate({
|
|
|
101
101
|
<strong>Description :</strong>
|
|
102
102
|
<div
|
|
103
103
|
style={{ marginTop: '10px' }}
|
|
104
|
-
dangerouslySetInnerHTML={{ __html:
|
|
104
|
+
dangerouslySetInnerHTML={{ __html: sanitizeEmailHtml(description) }}
|
|
105
105
|
/>
|
|
106
106
|
</div>
|
|
107
107
|
)}
|
|
@@ -119,7 +119,7 @@ export function MeetCancellationEmailTemplate({
|
|
|
119
119
|
borderTop: '1px solid #ddd',
|
|
120
120
|
fontSize: '14px',
|
|
121
121
|
}}
|
|
122
|
-
dangerouslySetInnerHTML={{ __html:
|
|
122
|
+
dangerouslySetInnerHTML={{ __html: sanitizeEmailHtml(signature) }}
|
|
123
123
|
/>
|
|
124
124
|
)}
|
|
125
125
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import
|
|
2
|
+
import { sanitizeEmailHtml } from '@/lib/email-html-sanitize';
|
|
3
3
|
|
|
4
4
|
interface MeetConfirmationEmailTemplateProps {
|
|
5
5
|
contactName: string;
|
|
@@ -101,7 +101,7 @@ export function MeetConfirmationEmailTemplate({
|
|
|
101
101
|
<strong>Description :</strong>
|
|
102
102
|
<div
|
|
103
103
|
style={{ marginTop: '10px' }}
|
|
104
|
-
dangerouslySetInnerHTML={{ __html:
|
|
104
|
+
dangerouslySetInnerHTML={{ __html: sanitizeEmailHtml(description) }}
|
|
105
105
|
/>
|
|
106
106
|
</div>
|
|
107
107
|
)}
|
|
@@ -154,7 +154,7 @@ export function MeetConfirmationEmailTemplate({
|
|
|
154
154
|
borderTop: '1px solid #ddd',
|
|
155
155
|
fontSize: '14px',
|
|
156
156
|
}}
|
|
157
|
-
dangerouslySetInnerHTML={{ __html:
|
|
157
|
+
dangerouslySetInnerHTML={{ __html: sanitizeEmailHtml(signature) }}
|
|
158
158
|
/>
|
|
159
159
|
)}
|
|
160
160
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import
|
|
2
|
+
import { sanitizeEmailHtml } from '@/lib/email-html-sanitize';
|
|
3
3
|
|
|
4
4
|
interface MeetUpdateEmailTemplateProps {
|
|
5
5
|
contactName: string;
|
|
@@ -161,7 +161,7 @@ export function MeetUpdateEmailTemplate({
|
|
|
161
161
|
<strong>Description :</strong>
|
|
162
162
|
<div
|
|
163
163
|
style={{ marginTop: '10px' }}
|
|
164
|
-
dangerouslySetInnerHTML={{ __html:
|
|
164
|
+
dangerouslySetInnerHTML={{ __html: sanitizeEmailHtml(description) }}
|
|
165
165
|
/>
|
|
166
166
|
</div>
|
|
167
167
|
)}
|
|
@@ -207,7 +207,7 @@ export function MeetUpdateEmailTemplate({
|
|
|
207
207
|
borderTop: '1px solid #ddd',
|
|
208
208
|
fontSize: '14px',
|
|
209
209
|
}}
|
|
210
|
-
dangerouslySetInnerHTML={{ __html:
|
|
210
|
+
dangerouslySetInnerHTML={{ __html: sanitizeEmailHtml(signature) }}
|
|
211
211
|
/>
|
|
212
212
|
)}
|
|
213
213
|
</div>
|
|
@@ -8,21 +8,21 @@ interface PageHeaderProps {
|
|
|
8
8
|
|
|
9
9
|
export function PageHeader({ title, description, action }: Readonly<PageHeaderProps>) {
|
|
10
10
|
return (
|
|
11
|
-
<div className="border-
|
|
11
|
+
<div className="border-border bg-background/95 border-b px-4 py-4 backdrop-blur-sm sm:px-6 lg:px-8 lg:py-6">
|
|
12
12
|
<div className="flex items-start gap-3">
|
|
13
13
|
<div className="min-w-0 flex-1">
|
|
14
14
|
{action ? (
|
|
15
15
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
16
16
|
<div className="min-w-0 flex-1">
|
|
17
|
-
<h1 className="text-xl font-bold
|
|
18
|
-
{description && <p className="mt-1 text-sm
|
|
17
|
+
<h1 className="text-foreground text-xl font-bold sm:text-2xl">{title}</h1>
|
|
18
|
+
{description && <p className="text-muted-foreground mt-1 text-sm">{description}</p>}
|
|
19
19
|
</div>
|
|
20
20
|
<div className="shrink-0">{action}</div>
|
|
21
21
|
</div>
|
|
22
22
|
) : (
|
|
23
23
|
<>
|
|
24
|
-
<h1 className="text-xl font-bold
|
|
25
|
-
{description && <p className="mt-1 text-sm
|
|
24
|
+
<h1 className="text-foreground text-xl font-bold sm:text-2xl">{title}</h1>
|
|
25
|
+
{description && <p className="text-muted-foreground mt-1 text-sm">{description}</p>}
|
|
26
26
|
</>
|
|
27
27
|
)}
|
|
28
28
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { sanitizeEmailHtml } from '@/lib/email-html-sanitize';
|
|
2
2
|
|
|
3
3
|
interface ResetPasswordEmailProps {
|
|
4
4
|
code: string;
|
|
@@ -73,7 +73,7 @@ export function ResetPasswordEmailTemplate({ code, signature }: ResetPasswordEma
|
|
|
73
73
|
fontSize: '14px',
|
|
74
74
|
lineHeight: '1.6',
|
|
75
75
|
}}
|
|
76
|
-
dangerouslySetInnerHTML={{ __html:
|
|
76
|
+
dangerouslySetInnerHTML={{ __html: sanitizeEmailHtml(signature) }}
|
|
77
77
|
/>
|
|
78
78
|
)}
|
|
79
79
|
</div>
|