create-crm-tmp 1.1.3 → 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/.prettierignore +2 -0
- package/template/README.md +230 -115
- package/template/components.json +22 -0
- package/template/eslint.config.mjs +13 -0
- package/template/exemple-contacts.csv +54 -0
- package/template/next.config.ts +41 -1
- package/template/package.json +63 -15
- package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
- package/template/prisma/schema.prisma +311 -67
- package/template/src/app/(auth)/invite/[token]/page.tsx +28 -29
- package/template/src/app/(auth)/layout.tsx +1 -1
- package/template/src/app/(auth)/reset-password/complete/page.tsx +21 -27
- package/template/src/app/(auth)/reset-password/page.tsx +14 -10
- package/template/src/app/(auth)/reset-password/verify/page.tsx +14 -10
- package/template/src/app/(auth)/signin/page.tsx +34 -23
- package/template/src/app/(dashboard)/agenda/page.tsx +3655 -2357
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +609 -338
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
- package/template/src/app/(dashboard)/automatisation/page.tsx +463 -186
- package/template/src/app/(dashboard)/closing/page.tsx +517 -469
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6151 -4210
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1702 -0
- package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +4124 -2130
- package/template/src/app/(dashboard)/dashboard/page.tsx +119 -105
- package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
- package/template/src/app/(dashboard)/error.tsx +37 -0
- package/template/src/app/(dashboard)/layout.tsx +6 -2
- 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 +1773 -3362
- package/template/src/app/(dashboard)/templates/page.tsx +504 -303
- package/template/src/app/(dashboard)/users/list/page.tsx +364 -355
- 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 +169 -140
- package/template/src/app/api/agenda/google-events/route.ts +92 -0
- package/template/src/app/api/audit-logs/route.ts +1 -1
- package/template/src/app/api/auth/check-active/route.ts +3 -2
- 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/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 +129 -0
- package/template/src/app/api/companies/[id]/route.ts +194 -0
- package/template/src/app/api/companies/export/route.ts +206 -0
- package/template/src/app/api/companies/route.ts +196 -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 +55 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +20 -48
- package/template/src/app/api/contacts/[id]/files/route.ts +125 -186
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
- package/template/src/app/api/contacts/[id]/interactions/route.ts +45 -8
- package/template/src/app/api/contacts/[id]/kyc/route.ts +81 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +55 -29
- package/template/src/app/api/contacts/[id]/route.ts +184 -21
- package/template/src/app/api/contacts/[id]/send-email/route.ts +33 -11
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +67 -0
- package/template/src/app/api/contacts/export/route.ts +22 -31
- package/template/src/app/api/contacts/import/route.ts +77 -44
- package/template/src/app/api/contacts/import-preview/route.ts +139 -0
- package/template/src/app/api/contacts/origins/route.ts +63 -0
- package/template/src/app/api/contacts/route.ts +322 -57
- package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
- package/template/src/app/api/dashboard/stats/route.ts +9 -292
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -3
- package/template/src/app/api/dashboard/widgets/route.ts +19 -19
- 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 +28 -542
- package/template/src/app/api/invite/complete/route.ts +20 -23
- 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 +165 -39
- package/template/src/app/api/reminders/state/route.ts +164 -0
- package/template/src/app/api/reset-password/complete/route.ts +11 -13
- 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 +25 -47
- 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 +34 -23
- 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 +48 -23
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +56 -32
- package/template/src/app/api/settings/google-sheet/preview/route.ts +110 -0
- package/template/src/app/api/settings/google-sheet/route.ts +34 -23
- 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 +20 -24
- package/template/src/app/api/settings/meta-leads/route.ts +34 -25
- package/template/src/app/api/settings/smtp/route.ts +53 -6
- package/template/src/app/api/settings/statuses/[id]/route.ts +29 -32
- 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 +36 -13
- package/template/src/app/api/tasks/[id]/route.ts +357 -145
- package/template/src/app/api/tasks/meet/route.ts +37 -26
- package/template/src/app/api/tasks/route.ts +201 -96
- 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 +22 -16
- 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/list/route.ts +57 -19
- package/template/src/app/api/users/route.ts +89 -34
- package/template/src/app/api/webhooks/google-ads/route.ts +40 -1
- package/template/src/app/api/webhooks/meta-leads/route.ts +38 -1
- package/template/src/app/api/workflows/[id]/route.ts +29 -6
- package/template/src/app/api/workflows/process/route.ts +505 -170
- package/template/src/app/api/workflows/route.ts +42 -4
- package/template/src/app/globals.css +512 -32
- package/template/src/app/layout.tsx +28 -9
- package/template/src/app/page.tsx +37 -7
- package/template/src/components/address-autocomplete.tsx +233 -0
- package/template/src/components/config-error-alert.tsx +46 -0
- package/template/src/components/contacts/filter-bar.tsx +190 -0
- package/template/src/components/contacts/filter-builder.tsx +574 -0
- package/template/src/components/contacts/save-view-dialog.tsx +160 -0
- package/template/src/components/contacts/views-tab-bar.tsx +449 -0
- package/template/src/components/dashboard/activity-chart.tsx +6 -1
- package/template/src/components/dashboard/add-widget-dialog.tsx +13 -17
- package/template/src/components/dashboard/color-picker.tsx +7 -8
- package/template/src/components/dashboard/recent-activity.tsx +2 -5
- package/template/src/components/dashboard/stat-card.tsx +1 -3
- package/template/src/components/dashboard/status-distribution-chart.tsx +0 -1
- package/template/src/components/dashboard/top-contacts-list.tsx +7 -13
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +2 -5
- package/template/src/components/dashboard/widget-wrapper.tsx +3 -6
- package/template/src/components/date-picker.tsx +399 -0
- package/template/src/components/editor/upload-editor-image.ts +42 -0
- package/template/src/components/editor.tsx +188 -35
- package/template/src/components/email-template.tsx +4 -2
- package/template/src/components/global-search.tsx +360 -0
- package/template/src/components/header.tsx +200 -107
- 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 +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/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 +117 -100
- package/template/src/components/skeleton.tsx +128 -45
- 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 +71 -0
- 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-banner.tsx +1 -1
- package/template/src/components/view-as-modal.tsx +30 -19
- package/template/src/config/nav-pages.ts +108 -0
- package/template/src/contexts/app-toast-context.tsx +362 -0
- package/template/src/contexts/dashboard-theme-context.tsx +2 -7
- package/template/src/contexts/sidebar-context.tsx +27 -53
- package/template/src/contexts/task-reminder-context.tsx +134 -160
- package/template/src/contexts/view-as-context.tsx +32 -10
- 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/hooks/useIntegrationNotifications.ts +49 -0
- package/template/src/lib/address-api.ts +155 -0
- package/template/src/lib/auth.ts +8 -1
- package/template/src/lib/cache.ts +73 -0
- package/template/src/lib/check-permission.ts +12 -177
- 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 +24 -22
- package/template/src/lib/contact-view-filters.ts +301 -0
- package/template/src/lib/contacts-list-url.ts +190 -0
- package/template/src/lib/dashboard-stats.ts +282 -0
- package/template/src/lib/dashboard-themes.ts +0 -5
- package/template/src/lib/date-utils.ts +176 -0
- package/template/src/lib/default-widgets.ts +0 -2
- 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/get-auth-user.ts +25 -0
- package/template/src/lib/google-calendar-agenda.ts +201 -0
- package/template/src/lib/google-calendar.ts +309 -17
- package/template/src/lib/google-fetch.ts +63 -0
- 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/local-storage.ts +34 -0
- package/template/src/lib/permissions.ts +268 -40
- package/template/src/lib/prisma.ts +15 -12
- 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/roles.ts +12 -15
- package/template/src/lib/supabase-storage.ts +113 -0
- package/template/src/lib/template-variables.ts +204 -29
- package/template/src/lib/utils.ts +71 -11
- package/template/src/lib/widget-registry.ts +0 -4
- package/template/src/lib/workflow-executor.ts +391 -228
- package/template/src/proxy.ts +35 -73
- package/template/src/types/contact-views.ts +351 -0
- package/template/vercel.json +5 -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/20260226093949_fix_cascade_on_user_delete/migration.sql +0 -69
- package/template/src/app/(dashboard)/users/layout.tsx +0 -30
- package/template/src/lib/google-drive.ts +0 -380
|
@@ -7,11 +7,19 @@ import { useRouter } from 'next/navigation';
|
|
|
7
7
|
import Link from 'next/link';
|
|
8
8
|
import { cn } from '@/lib/utils';
|
|
9
9
|
import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
|
|
10
|
-
import {
|
|
10
|
+
import { GlobalSearch } from '@/components/global-search';
|
|
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';
|
|
11
18
|
|
|
12
19
|
interface Reminder {
|
|
13
20
|
id: string;
|
|
14
21
|
taskId: string;
|
|
22
|
+
kind: 'due' | 'reminder';
|
|
15
23
|
type: string;
|
|
16
24
|
title: string | null;
|
|
17
25
|
description: string;
|
|
@@ -19,6 +27,9 @@ interface Reminder {
|
|
|
19
27
|
scheduledAt: string;
|
|
20
28
|
reminderTime: string;
|
|
21
29
|
reminderMinutesBefore: number | null;
|
|
30
|
+
isRead: boolean;
|
|
31
|
+
isDismissed: boolean;
|
|
32
|
+
isClearedByCutoff: boolean;
|
|
22
33
|
contact: {
|
|
23
34
|
id: string;
|
|
24
35
|
firstName: string | null;
|
|
@@ -35,12 +46,36 @@ const TASK_TYPE_LABELS: Record<string, string> = {
|
|
|
35
46
|
};
|
|
36
47
|
|
|
37
48
|
const PRIORITY_COLORS: Record<string, string> = {
|
|
38
|
-
LOW: 'bg-
|
|
49
|
+
LOW: 'bg-muted text-muted-foreground',
|
|
39
50
|
MEDIUM: 'bg-yellow-100 text-yellow-700',
|
|
40
51
|
HIGH: 'bg-orange-100 text-orange-700',
|
|
41
52
|
URGENT: 'bg-red-100 text-red-700',
|
|
42
53
|
};
|
|
43
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
|
+
|
|
44
79
|
function formatDateTime(dateString: string) {
|
|
45
80
|
const date = new Date(dateString);
|
|
46
81
|
return {
|
|
@@ -57,50 +92,29 @@ function formatDateTime(dateString: string) {
|
|
|
57
92
|
|
|
58
93
|
export function Header() {
|
|
59
94
|
const { data: session } = useSession();
|
|
95
|
+
const { persistent: showPersistentToast } = useAppToast();
|
|
60
96
|
const router = useRouter();
|
|
61
97
|
const { toggle: toggleMobileMenu } = useMobileMenuContext();
|
|
62
|
-
const { viewAsUser, isViewingAsOther } = useViewAs();
|
|
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);
|
|
70
104
|
|
|
71
|
-
const
|
|
72
|
-
const userName = isViewingAsOther ? viewAsUser?.name || 'Utilisateur' : realUserName;
|
|
105
|
+
const userName = session?.user?.name || 'Utilisateur';
|
|
73
106
|
const userEmail = session?.user?.email || '';
|
|
74
107
|
const userInitial = userName?.[0]?.toUpperCase() || 'U';
|
|
75
108
|
|
|
76
|
-
// Charger les rappels
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
if (typeof window !== 'undefined') {
|
|
79
|
-
const stored = localStorage.getItem('readReminders');
|
|
80
|
-
if (stored) {
|
|
81
|
-
try {
|
|
82
|
-
setReadReminders(new Set(JSON.parse(stored)));
|
|
83
|
-
} catch (e) {
|
|
84
|
-
console.error('Erreur lors du chargement des rappels lus:', e);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}, []);
|
|
89
|
-
|
|
90
|
-
// Sauvegarder les rappels lus dans localStorage
|
|
91
|
-
useEffect(() => {
|
|
92
|
-
if (typeof window !== 'undefined' && readReminders.size > 0) {
|
|
93
|
-
localStorage.setItem('readReminders', JSON.stringify(Array.from(readReminders)));
|
|
94
|
-
}
|
|
95
|
-
}, [readReminders]);
|
|
96
|
-
|
|
97
|
-
// Charger les rappels
|
|
109
|
+
// Charger les rappels (polling + retour sur l’onglet + événement explicite)
|
|
98
110
|
useEffect(() => {
|
|
99
111
|
if (!session) return;
|
|
100
112
|
|
|
101
|
-
const fetchReminders = async () => {
|
|
113
|
+
const fetchReminders = async (opts?: { silent?: boolean }) => {
|
|
102
114
|
try {
|
|
103
|
-
|
|
115
|
+
if (!opts?.silent) {
|
|
116
|
+
setLoading(true);
|
|
117
|
+
}
|
|
104
118
|
const response = await fetch('/api/reminders');
|
|
105
119
|
if (response.ok) {
|
|
106
120
|
const data = await response.json();
|
|
@@ -109,13 +123,29 @@ export function Header() {
|
|
|
109
123
|
} catch (error) {
|
|
110
124
|
console.error('Erreur lors du chargement des rappels:', error);
|
|
111
125
|
} finally {
|
|
112
|
-
|
|
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 });
|
|
113
138
|
}
|
|
114
139
|
};
|
|
140
|
+
const onRefresh = () => void fetchReminders({ silent: true });
|
|
115
141
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
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
|
+
};
|
|
119
149
|
}, [session]);
|
|
120
150
|
|
|
121
151
|
// Fermer les dropdowns en cliquant à l'extérieur
|
|
@@ -133,15 +163,53 @@ export function Header() {
|
|
|
133
163
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
134
164
|
}, []);
|
|
135
165
|
|
|
136
|
-
const unreadCount = reminders.filter((r) => !
|
|
166
|
+
const unreadCount = reminders.filter((r) => !r.isRead && !r.isDismissed).length;
|
|
137
167
|
|
|
138
|
-
const handleMarkAsRead = (reminderId: string) => {
|
|
139
|
-
|
|
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
|
+
}
|
|
140
184
|
};
|
|
141
185
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
}
|
|
145
213
|
};
|
|
146
214
|
|
|
147
215
|
const handleSignOut = async () => {
|
|
@@ -150,37 +218,49 @@ export function Header() {
|
|
|
150
218
|
};
|
|
151
219
|
|
|
152
220
|
return (
|
|
153
|
-
<header className="sticky top-0 z-
|
|
154
|
-
<div className="flex items-center
|
|
155
|
-
{/* Left:
|
|
156
|
-
<div className="flex items-center gap-2 sm:gap-3">
|
|
157
|
-
{/* Bouton burger - visible uniquement sur mobile/tablette */}
|
|
158
|
-
<button
|
|
159
|
-
onClick={toggleMobileMenu}
|
|
160
|
-
className="cursor-pointer rounded-lg p-2 text-gray-600 transition-colors hover:bg-gray-100 lg:hidden"
|
|
161
|
-
aria-label="Ouvrir le menu"
|
|
162
|
-
>
|
|
163
|
-
<Menu className="h-5 w-5" />
|
|
164
|
-
</button>
|
|
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">
|
|
222
|
+
<div className="flex items-center gap-2 sm:gap-4">
|
|
223
|
+
{/* Left: Logo + Greeting */}
|
|
224
|
+
<div className="flex shrink-0 items-center gap-2 sm:gap-3">
|
|
165
225
|
<div className="flex items-center gap-1.5 sm:gap-2">
|
|
166
|
-
{/*
|
|
167
|
-
<
|
|
226
|
+
{/* Bouton burger pour mobile */}
|
|
227
|
+
<button
|
|
228
|
+
onClick={toggleMobileMenu}
|
|
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"
|
|
230
|
+
aria-label="Ouvrir ou fermer le menu"
|
|
231
|
+
>
|
|
232
|
+
<Menu className="h-5 w-5" />
|
|
233
|
+
</button>
|
|
234
|
+
<span className="text-foreground text-base font-bold sm:text-lg">CRM Template</span>
|
|
168
235
|
</div>
|
|
169
|
-
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
{/* Center: Global Search */}
|
|
239
|
+
<div className="flex min-w-0 flex-1 justify-center">
|
|
240
|
+
<GlobalSearch />
|
|
170
241
|
</div>
|
|
171
242
|
|
|
172
243
|
{/* Right: Notifications + User Avatar */}
|
|
173
|
-
<div className="flex items-center gap-2 sm:gap-3">
|
|
244
|
+
<div className="flex shrink-0 items-center gap-2 sm:gap-3">
|
|
174
245
|
{/* Notifications Dropdown */}
|
|
175
246
|
<div className="relative" ref={remindersRef}>
|
|
176
247
|
<button
|
|
177
248
|
onClick={() => setShowRemindersDropdown(!showRemindersDropdown)}
|
|
178
|
-
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"
|
|
179
250
|
aria-label="Notifications"
|
|
180
251
|
>
|
|
181
|
-
<
|
|
252
|
+
<span
|
|
253
|
+
className={cn(
|
|
254
|
+
unreadCount > 0 && !showRemindersDropdown && 'ui-bell-notify',
|
|
255
|
+
)}
|
|
256
|
+
>
|
|
257
|
+
<Bell className="h-5 w-5" />
|
|
258
|
+
</span>
|
|
182
259
|
{unreadCount > 0 && (
|
|
183
|
-
<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
|
+
>
|
|
184
264
|
{unreadCount > 9 ? '9+' : unreadCount}
|
|
185
265
|
</span>
|
|
186
266
|
)}
|
|
@@ -188,66 +268,85 @@ export function Header() {
|
|
|
188
268
|
|
|
189
269
|
{/* Dropdown des rappels */}
|
|
190
270
|
{showRemindersDropdown && (
|
|
191
|
-
<div className="absolute right-0 mt-2 w-80 max-w-[calc(100vw-2rem)] rounded-
|
|
192
|
-
<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">
|
|
193
273
|
<div className="flex items-center justify-between">
|
|
194
|
-
<h3 className="text-sm font-semibold
|
|
195
|
-
{
|
|
274
|
+
<h3 className="text-popover-foreground text-sm font-semibold">Rappels</h3>
|
|
275
|
+
{reminders.length > 0 && (
|
|
196
276
|
<button
|
|
197
|
-
onClick={
|
|
198
|
-
className="cursor-pointer text-xs
|
|
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"
|
|
199
279
|
>
|
|
200
|
-
|
|
280
|
+
Vider les rappels
|
|
201
281
|
</button>
|
|
202
282
|
)}
|
|
203
283
|
</div>
|
|
204
284
|
</div>
|
|
205
285
|
<div className="max-h-96 overflow-y-auto">
|
|
206
|
-
{loading
|
|
207
|
-
<div className="px-4 py-8 text-center text-sm
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
)
|
|
211
|
-
|
|
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">
|
|
212
298
|
{reminders.map((reminder) => {
|
|
213
|
-
const isRead =
|
|
214
|
-
const { date, time } = formatDateTime(reminder.
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
+
}
|
|
219
307
|
|
|
220
308
|
return (
|
|
221
|
-
<
|
|
309
|
+
<button
|
|
310
|
+
type="button"
|
|
222
311
|
key={reminder.id}
|
|
223
312
|
className={cn(
|
|
224
|
-
'px-4 py-3 transition-colors
|
|
225
|
-
|
|
313
|
+
'hover:bg-accent w-full px-4 py-3 text-left transition-colors duration-200',
|
|
314
|
+
isRead ? undefined : 'bg-accent/70',
|
|
226
315
|
)}
|
|
227
|
-
onClick={() => handleMarkAsRead(reminder.id)}
|
|
316
|
+
onClick={() => void handleMarkAsRead(reminder.id)}
|
|
228
317
|
>
|
|
229
318
|
<div className="flex items-start gap-3">
|
|
230
319
|
<div className="mt-0.5 shrink-0">
|
|
231
|
-
<Calendar className="h-4 w-4
|
|
320
|
+
<Calendar aria-hidden="true" className="text-primary h-4 w-4" />
|
|
232
321
|
</div>
|
|
233
322
|
<div className="min-w-0 flex-1">
|
|
234
323
|
<div className="flex items-start justify-between gap-2">
|
|
235
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>
|
|
236
333
|
<p
|
|
237
334
|
className={cn(
|
|
238
335
|
'text-sm font-medium',
|
|
239
|
-
|
|
336
|
+
isRead ? 'text-muted-foreground' : 'text-foreground',
|
|
240
337
|
)}
|
|
241
338
|
>
|
|
242
339
|
{reminder.title || TASK_TYPE_LABELS[reminder.type] || 'Tâche'}
|
|
243
340
|
</p>
|
|
244
341
|
{contactName && (
|
|
245
|
-
<p className="mt-0.5 text-xs
|
|
342
|
+
<p className="text-muted-foreground mt-0.5 text-xs">
|
|
343
|
+
{contactName}
|
|
344
|
+
</p>
|
|
246
345
|
)}
|
|
247
|
-
<div className="mt-1 flex items-center gap-2">
|
|
248
|
-
<span className="text-
|
|
249
|
-
<span className="text-
|
|
250
|
-
<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>
|
|
251
350
|
<span
|
|
252
351
|
className={cn(
|
|
253
352
|
'rounded-full px-1.5 py-0.5 text-[10px] font-medium',
|
|
@@ -255,32 +354,26 @@ export function Header() {
|
|
|
255
354
|
PRIORITY_COLORS.MEDIUM,
|
|
256
355
|
)}
|
|
257
356
|
>
|
|
258
|
-
{reminder.priority
|
|
259
|
-
? 'Urgente'
|
|
260
|
-
: reminder.priority === 'HIGH'
|
|
261
|
-
? 'Haute'
|
|
262
|
-
: reminder.priority === 'MEDIUM'
|
|
263
|
-
? 'Moyenne'
|
|
264
|
-
: 'Faible'}
|
|
357
|
+
{priorityLabel(reminder.priority)}
|
|
265
358
|
</span>
|
|
266
359
|
</div>
|
|
267
360
|
</div>
|
|
268
361
|
{!isRead && (
|
|
269
|
-
<div className="h-2 w-2 shrink-0 rounded-full
|
|
362
|
+
<div className="bg-primary h-2 w-2 shrink-0 rounded-full" />
|
|
270
363
|
)}
|
|
271
364
|
</div>
|
|
272
365
|
{reminder.contact && (
|
|
273
366
|
<Link
|
|
274
367
|
href={`/contacts/${reminder.contact.id}`}
|
|
275
368
|
onClick={(e) => e.stopPropagation()}
|
|
276
|
-
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"
|
|
277
370
|
>
|
|
278
371
|
Voir le contact
|
|
279
372
|
</Link>
|
|
280
373
|
)}
|
|
281
374
|
</div>
|
|
282
375
|
</div>
|
|
283
|
-
</
|
|
376
|
+
</button>
|
|
284
377
|
);
|
|
285
378
|
})}
|
|
286
379
|
</div>
|
|
@@ -294,26 +387,26 @@ export function Header() {
|
|
|
294
387
|
<div className="relative" ref={userRef}>
|
|
295
388
|
<button
|
|
296
389
|
onClick={() => setShowUserDropdown(!showUserDropdown)}
|
|
297
|
-
className="flex cursor-pointer items-center gap-1.5 sm:gap-2"
|
|
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"
|
|
298
391
|
aria-label="Menu utilisateur"
|
|
299
392
|
>
|
|
300
|
-
<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">
|
|
301
394
|
{userInitial}
|
|
302
395
|
</div>
|
|
303
|
-
<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" />
|
|
304
397
|
</button>
|
|
305
398
|
|
|
306
399
|
{/* Dropdown utilisateur */}
|
|
307
400
|
{showUserDropdown && (
|
|
308
|
-
<div className="absolute right-0 mt-2 w-56 rounded-
|
|
309
|
-
<div className="border-
|
|
310
|
-
<p className="text-sm font-medium
|
|
311
|
-
<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>
|
|
312
405
|
</div>
|
|
313
406
|
<div className="py-1">
|
|
314
407
|
<button
|
|
315
408
|
onClick={handleSignOut}
|
|
316
|
-
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"
|
|
317
410
|
>
|
|
318
411
|
<LogOut className="h-4 w-4" />
|
|
319
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,3 +1,5 @@
|
|
|
1
|
+
import { sanitizeEmailHtml } from '@/lib/email-html-sanitize';
|
|
2
|
+
|
|
1
3
|
interface InvitationEmailProps {
|
|
2
4
|
name: string;
|
|
3
5
|
invitationUrl: string;
|
|
@@ -8,7 +10,7 @@ export function InvitationEmailTemplate({ name, invitationUrl, signature }: Invi
|
|
|
8
10
|
return (
|
|
9
11
|
<div
|
|
10
12
|
style={{
|
|
11
|
-
fontFamily: '
|
|
13
|
+
fontFamily: '"Segoe UI", "Helvetica Neue", sans-serif',
|
|
12
14
|
padding: '20px',
|
|
13
15
|
maxWidth: '600px',
|
|
14
16
|
margin: '0 auto',
|
|
@@ -71,7 +73,7 @@ export function InvitationEmailTemplate({ name, invitationUrl, signature }: Invi
|
|
|
71
73
|
fontSize: '14px',
|
|
72
74
|
lineHeight: '1.6',
|
|
73
75
|
}}
|
|
74
|
-
dangerouslySetInnerHTML={{ __html: signature }}
|
|
76
|
+
dangerouslySetInnerHTML={{ __html: sanitizeEmailHtml(signature) }}
|
|
75
77
|
/>
|
|
76
78
|
)}
|
|
77
79
|
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import dynamic from 'next/dynamic';
|
|
4
|
+
import { Skeleton } from '@/components/skeleton';
|
|
5
|
+
|
|
6
|
+
export type { DefaultTemplateRef } from '@/components/editor';
|
|
7
|
+
|
|
8
|
+
export const LazyEditor = dynamic(() => import('@/components/editor').then((m) => m.Editor), {
|
|
9
|
+
ssr: false,
|
|
10
|
+
loading: () => <Skeleton className="h-64 rounded-lg" />,
|
|
11
|
+
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { sanitizeEmailHtml } from '@/lib/email-html-sanitize';
|
|
2
|
+
|
|
1
3
|
interface MeetCancellationEmailTemplateProps {
|
|
2
4
|
contactName: string;
|
|
3
5
|
title: string;
|
|
@@ -50,7 +52,13 @@ export function MeetCancellationEmailTemplate({
|
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
return (
|
|
53
|
-
<div
|
|
55
|
+
<div
|
|
56
|
+
style={{
|
|
57
|
+
fontFamily: '"Segoe UI", "Helvetica Neue", sans-serif',
|
|
58
|
+
lineHeight: '1.6',
|
|
59
|
+
color: '#333',
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
54
62
|
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
|
|
55
63
|
<h1 style={{ color: '#EF4444', fontSize: '24px', marginBottom: '20px' }}>
|
|
56
64
|
Annulation de rendez-vous
|
|
@@ -93,7 +101,7 @@ export function MeetCancellationEmailTemplate({
|
|
|
93
101
|
<strong>Description :</strong>
|
|
94
102
|
<div
|
|
95
103
|
style={{ marginTop: '10px' }}
|
|
96
|
-
dangerouslySetInnerHTML={{ __html: description }}
|
|
104
|
+
dangerouslySetInnerHTML={{ __html: sanitizeEmailHtml(description) }}
|
|
97
105
|
/>
|
|
98
106
|
</div>
|
|
99
107
|
)}
|
|
@@ -111,7 +119,7 @@ export function MeetCancellationEmailTemplate({
|
|
|
111
119
|
borderTop: '1px solid #ddd',
|
|
112
120
|
fontSize: '14px',
|
|
113
121
|
}}
|
|
114
|
-
dangerouslySetInnerHTML={{ __html: signature }}
|
|
122
|
+
dangerouslySetInnerHTML={{ __html: sanitizeEmailHtml(signature) }}
|
|
115
123
|
/>
|
|
116
124
|
)}
|
|
117
125
|
</div>
|