create-crm-tmp 1.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/.prettierignore +2 -0
- package/template/README.md +53 -67
- package/template/components.json +22 -0
- package/template/exemple-contacts.csv +54 -0
- package/template/next.config.ts +27 -1
- package/template/package.json +64 -27
- package/template/prisma/schema.prisma +821 -72
- package/template/skills-lock.json +25 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
- package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
- package/template/src/app/(auth)/reset-password/page.tsx +12 -8
- package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
- package/template/src/app/(auth)/signin/page.tsx +20 -17
- package/template/src/app/(dashboard)/agenda/page.tsx +2231 -2188
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
- package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
- package/template/src/app/(dashboard)/closing/page.tsx +500 -468
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5035 -4126
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
- package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
- package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
- package/template/src/app/(dashboard)/error.tsx +37 -0
- package/template/src/app/(dashboard)/layout.tsx +1 -1
- package/template/src/app/(dashboard)/loading.tsx +5 -0
- package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
- package/template/src/app/(dashboard)/settings/page.tsx +2685 -2489
- package/template/src/app/(dashboard)/templates/page.tsx +500 -300
- package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
- package/template/src/app/(dashboard)/users/page.tsx +279 -310
- package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
- package/template/src/app/(dashboard)/users/roles/page.tsx +164 -137
- package/template/src/app/api/audit-logs/route.ts +1 -1
- package/template/src/app/api/auth/google/callback/route.ts +8 -5
- package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
- package/template/src/app/api/companies/[id]/activities/route.ts +131 -0
- package/template/src/app/api/companies/[id]/route.ts +195 -0
- package/template/src/app/api/companies/export/route.ts +206 -0
- package/template/src/app/api/companies/route.ts +166 -0
- package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
- package/template/src/app/api/contact-views/[id]/route.ts +197 -0
- package/template/src/app/api/contact-views/route.ts +146 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +77 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
- package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
- package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
- package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
- package/template/src/app/api/contacts/[id]/route.ts +111 -20
- package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
- package/template/src/app/api/contacts/export/route.ts +12 -17
- package/template/src/app/api/contacts/import/route.ts +22 -19
- package/template/src/app/api/contacts/import-preview/route.ts +139 -0
- package/template/src/app/api/contacts/route.ts +202 -49
- package/template/src/app/api/dashboard/stats/route.ts +9 -292
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
- package/template/src/app/api/invite/complete/route.ts +20 -23
- package/template/src/app/api/reminders/route.ts +1 -0
- package/template/src/app/api/reset-password/complete/route.ts +11 -13
- package/template/src/app/api/send/route.ts +9 -85
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
- package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
- package/template/src/app/api/settings/company/route.ts +19 -26
- package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-ads/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
- package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
- package/template/src/app/api/settings/google-sheet/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/route.ts +20 -23
- package/template/src/app/api/settings/statuses/[id]/route.ts +33 -23
- package/template/src/app/api/settings/statuses/route.ts +24 -22
- package/template/src/app/api/statuses/route.ts +2 -5
- package/template/src/app/api/tasks/[id]/attendees/route.ts +14 -7
- package/template/src/app/api/tasks/[id]/route.ts +161 -137
- package/template/src/app/api/tasks/meet/route.ts +11 -8
- package/template/src/app/api/tasks/route.ts +155 -95
- package/template/src/app/api/templates/[id]/route.ts +22 -13
- package/template/src/app/api/templates/route.ts +22 -5
- package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
- package/template/src/app/api/users/[id]/route.ts +16 -1
- package/template/src/app/api/users/commercials/route.ts +38 -0
- package/template/src/app/api/users/for-agenda/route.ts +1 -2
- package/template/src/app/api/users/route.ts +94 -55
- package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
- package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
- package/template/src/app/api/workflows/[id]/route.ts +33 -6
- package/template/src/app/api/workflows/process/route.ts +509 -146
- package/template/src/app/api/workflows/route.ts +46 -4
- package/template/src/app/globals.css +210 -101
- package/template/src/app/layout.tsx +19 -8
- package/template/src/app/page.tsx +37 -7
- package/template/src/components/address-autocomplete.tsx +232 -0
- package/template/src/components/contacts/filter-bar.tsx +181 -0
- package/template/src/components/contacts/filter-builder.tsx +589 -0
- package/template/src/components/contacts/save-view-dialog.tsx +160 -0
- package/template/src/components/contacts/views-tab-bar.tsx +440 -0
- package/template/src/components/dashboard/activity-chart.tsx +31 -39
- package/template/src/components/dashboard/dashboard-content.tsx +79 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -42
- package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
- package/template/src/components/date-picker.tsx +396 -0
- package/template/src/components/editor.tsx +27 -13
- package/template/src/components/email-template.tsx +4 -2
- package/template/src/components/global-search.tsx +358 -0
- package/template/src/components/header.tsx +57 -62
- package/template/src/components/invitation-email-template.tsx +4 -2
- package/template/src/components/lazy-editor.tsx +11 -0
- package/template/src/components/meet-cancellation-email-template.tsx +11 -3
- package/template/src/components/meet-confirmation-email-template.tsx +10 -3
- package/template/src/components/meet-update-email-template.tsx +10 -3
- package/template/src/components/page-header.tsx +19 -15
- package/template/src/components/protected-page.tsx +94 -0
- package/template/src/components/reset-password-email-template.tsx +4 -2
- package/template/src/components/sidebar.tsx +92 -94
- package/template/src/components/skeleton.tsx +128 -42
- package/template/src/components/ui/accordion.tsx +64 -0
- package/template/src/components/ui/alert-dialog.tsx +139 -0
- package/template/src/components/ui/button.tsx +60 -0
- package/template/src/components/view-as-banner.tsx +1 -1
- package/template/src/components/view-as-modal.tsx +21 -16
- package/template/src/config/nav-pages.ts +108 -0
- package/template/src/contexts/app-toast-context.tsx +174 -0
- package/template/src/contexts/sidebar-context.tsx +16 -47
- package/template/src/contexts/task-reminder-context.tsx +6 -6
- package/template/src/contexts/view-as-context.tsx +11 -16
- package/template/src/hooks/use-alert.tsx +65 -0
- package/template/src/hooks/use-confirm.tsx +87 -0
- package/template/src/hooks/use-contact-views.ts +140 -0
- package/template/src/hooks/use-contacts.ts +69 -0
- package/template/src/hooks/use-fetch.ts +17 -0
- package/template/src/hooks/use-focus-trap.ts +73 -0
- package/template/src/hooks/use-statuses.ts +22 -0
- package/template/src/lib/address-api.ts +155 -0
- package/template/src/lib/cache.ts +73 -0
- package/template/src/lib/check-permission.ts +12 -177
- package/template/src/lib/contact-interactions.ts +3 -1
- package/template/src/lib/contact-view-filters.ts +341 -0
- package/template/src/lib/dashboard-stats.ts +224 -0
- package/template/src/lib/date-utils.ts +49 -0
- package/template/src/lib/get-auth-user.ts +25 -0
- package/template/src/lib/google-calendar.ts +54 -12
- package/template/src/lib/google-drive.ts +796 -75
- package/template/src/lib/google-fetch.ts +63 -0
- package/template/src/lib/local-storage.ts +34 -0
- package/template/src/lib/permissions.ts +245 -47
- package/template/src/lib/prisma.ts +11 -11
- package/template/src/lib/roles.ts +14 -39
- package/template/src/lib/template-variables.ts +67 -33
- package/template/src/lib/utils.ts +26 -2
- package/template/src/lib/workflow-executor.ts +445 -229
- package/template/src/proxy.ts +34 -73
- package/template/src/types/contact-views.ts +351 -0
- package/template/src/types/yousign.ts +52 -0
- package/template/vercel.json +12 -0
- package/template/WORKFLOWS_CRON.md +0 -185
- package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
- package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
- package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
- package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
- package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
- package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
- package/template/prisma/migrations/migration_lock.toml +0 -3
- package/template/src/app/(dashboard)/users/layout.tsx +0 -30
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
- package/template/src/app/api/dashboard/widgets/route.ts +0 -181
- package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
- package/template/src/components/dashboard/color-picker.tsx +0 -65
- package/template/src/components/dashboard/contacts-chart.tsx +0 -69
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
- package/template/src/components/dashboard/recent-activity.tsx +0 -157
- package/template/src/components/dashboard/sales-analytics-chart.tsx +0 -77
- package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
- package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
- package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
- package/template/src/contexts/dashboard-theme-context.tsx +0 -58
- package/template/src/lib/dashboard-themes.ts +0 -140
- package/template/src/lib/default-widgets.ts +0 -14
- package/template/src/lib/widget-registry.ts +0 -177
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
4
|
import { useSession } from '@/lib/auth-client';
|
|
5
5
|
import { useViewAs } from '@/contexts/view-as-context';
|
|
6
6
|
import { X, Check, User as UserIcon } from 'lucide-react';
|
|
7
|
+
import { Spinner } from '@/components/skeleton';
|
|
7
8
|
import { useRouter } from 'next/navigation';
|
|
8
9
|
import { cn } from '@/lib/utils';
|
|
10
|
+
import { useFocusTrap } from '@/hooks/use-focus-trap';
|
|
9
11
|
|
|
10
12
|
interface User {
|
|
11
13
|
id: string;
|
|
@@ -67,17 +69,20 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
|
|
|
67
69
|
.slice(0, 2);
|
|
68
70
|
};
|
|
69
71
|
|
|
72
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
73
|
+
useFocusTrap(isOpen, contentRef, { onClose });
|
|
74
|
+
|
|
70
75
|
if (!isOpen) return null;
|
|
71
76
|
|
|
72
77
|
return (
|
|
73
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">
|
|
74
|
-
<div className="w-full max-w-2xl rounded-lg bg-white shadow-xl">
|
|
79
|
+
<div ref={contentRef} className="w-full max-w-2xl rounded-lg bg-white shadow-xl" role="dialog" aria-modal="true" aria-labelledby="view-as-title">
|
|
75
80
|
{/* En-tête */}
|
|
76
|
-
<div className="flex items-center justify-between rounded-t-lg border-b border-gray-200 bg-
|
|
81
|
+
<div className="flex items-center justify-between rounded-t-lg border-b border-gray-200 bg-blue-600 px-6 py-4">
|
|
77
82
|
<div className="flex items-center gap-3 text-white">
|
|
78
83
|
<UserIcon className="h-6 w-6" />
|
|
79
84
|
<div>
|
|
80
|
-
<h2 className="text-xl font-bold">Changer de vue</h2>
|
|
85
|
+
<h2 id="view-as-title" className="text-xl font-bold">Changer de vue</h2>
|
|
81
86
|
<p className="text-sm text-white/90">
|
|
82
87
|
Voir l'application avec les permissions d'un profil
|
|
83
88
|
</p>
|
|
@@ -95,8 +100,8 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
|
|
|
95
100
|
{/* Contenu */}
|
|
96
101
|
<div className="max-h-[60vh] overflow-y-auto p-6">
|
|
97
102
|
{loading ? (
|
|
98
|
-
<div className="py-12
|
|
99
|
-
<
|
|
103
|
+
<div className="flex justify-center py-12">
|
|
104
|
+
<Spinner size="lg" />
|
|
100
105
|
</div>
|
|
101
106
|
) : (
|
|
102
107
|
<div className="space-y-2">
|
|
@@ -111,8 +116,8 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
|
|
|
111
116
|
className={cn(
|
|
112
117
|
'w-full cursor-pointer rounded-lg border-2 p-4 text-left transition-all',
|
|
113
118
|
!viewAsUser
|
|
114
|
-
? 'border-
|
|
115
|
-
: 'border-gray-200 bg-white hover:border-
|
|
119
|
+
? 'border-blue-500 bg-blue-50'
|
|
120
|
+
: 'border-gray-200 bg-white hover:border-blue-300 hover:bg-blue-50/50',
|
|
116
121
|
)}
|
|
117
122
|
>
|
|
118
123
|
<div className="flex items-center justify-between">
|
|
@@ -121,8 +126,8 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
|
|
|
121
126
|
className={cn(
|
|
122
127
|
'flex h-12 w-12 items-center justify-center rounded-full text-lg font-bold',
|
|
123
128
|
!viewAsUser
|
|
124
|
-
? 'bg-
|
|
125
|
-
: 'bg-
|
|
129
|
+
? 'bg-blue-600 text-white'
|
|
130
|
+
: 'bg-blue-100 text-blue-800',
|
|
126
131
|
)}
|
|
127
132
|
>
|
|
128
133
|
{getInitials(session.user.name || session.user.email)}
|
|
@@ -130,12 +135,12 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
|
|
|
130
135
|
<div>
|
|
131
136
|
<div className="flex items-center gap-2">
|
|
132
137
|
<span className="font-semibold text-gray-900">Ma vue</span>
|
|
133
|
-
{!viewAsUser && <span className="text-sm text-
|
|
138
|
+
{!viewAsUser && <span className="text-sm text-blue-600">← Retour</span>}
|
|
134
139
|
</div>
|
|
135
140
|
<span className="text-sm text-gray-600">{session.user.name}</span>
|
|
136
141
|
</div>
|
|
137
142
|
</div>
|
|
138
|
-
{!viewAsUser && <Check className="h-6 w-6 text-
|
|
143
|
+
{!viewAsUser && <Check className="h-6 w-6 text-blue-600" />}
|
|
139
144
|
</div>
|
|
140
145
|
</button>
|
|
141
146
|
)}
|
|
@@ -150,8 +155,8 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
|
|
|
150
155
|
className={cn(
|
|
151
156
|
'w-full cursor-pointer rounded-lg border-2 p-4 text-left transition-all',
|
|
152
157
|
viewAsUser?.id === user.id
|
|
153
|
-
? 'border-
|
|
154
|
-
: 'border-gray-200 bg-white hover:border-
|
|
158
|
+
? 'border-blue-500 bg-blue-50'
|
|
159
|
+
: 'border-gray-200 bg-white hover:border-blue-300 hover:bg-blue-50/50',
|
|
155
160
|
)}
|
|
156
161
|
>
|
|
157
162
|
<div className="flex items-center justify-between">
|
|
@@ -160,7 +165,7 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
|
|
|
160
165
|
className={cn(
|
|
161
166
|
'flex h-12 w-12 items-center justify-center rounded-full text-sm font-bold',
|
|
162
167
|
viewAsUser?.id === user.id
|
|
163
|
-
? 'bg-
|
|
168
|
+
? 'bg-blue-600 text-white'
|
|
164
169
|
: 'bg-gray-200 text-gray-600',
|
|
165
170
|
)}
|
|
166
171
|
>
|
|
@@ -173,7 +178,7 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
|
|
|
173
178
|
</div>
|
|
174
179
|
</div>
|
|
175
180
|
</div>
|
|
176
|
-
{viewAsUser?.id === user.id && <Check className="h-6 w-6 text-
|
|
181
|
+
{viewAsUser?.id === user.id && <Check className="h-6 w-6 text-blue-600" />}
|
|
177
182
|
</div>
|
|
178
183
|
</button>
|
|
179
184
|
))}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LayoutDashboard,
|
|
3
|
+
Users,
|
|
4
|
+
Building2,
|
|
5
|
+
CalendarRange,
|
|
6
|
+
Columns3,
|
|
7
|
+
Zap,
|
|
8
|
+
FileText,
|
|
9
|
+
UserCog,
|
|
10
|
+
Shield,
|
|
11
|
+
KeyRound,
|
|
12
|
+
Settings,
|
|
13
|
+
Plug,
|
|
14
|
+
SlidersHorizontal,
|
|
15
|
+
AppWindow,
|
|
16
|
+
MonitorCog,
|
|
17
|
+
type LucideIcon,
|
|
18
|
+
} from 'lucide-react';
|
|
19
|
+
|
|
20
|
+
export interface NavPage {
|
|
21
|
+
name: string;
|
|
22
|
+
href: string;
|
|
23
|
+
icon: LucideIcon;
|
|
24
|
+
permissions: string[];
|
|
25
|
+
parentLabel?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const NAV_PAGES: NavPage[] = [
|
|
29
|
+
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard, permissions: ['dashboard.view'] },
|
|
30
|
+
{
|
|
31
|
+
name: 'Contacts',
|
|
32
|
+
href: '/contacts',
|
|
33
|
+
icon: Users,
|
|
34
|
+
permissions: ['contacts.view_all', 'contacts.view_own'],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'Agenda',
|
|
38
|
+
href: '/agenda',
|
|
39
|
+
icon: CalendarRange,
|
|
40
|
+
permissions: ['tasks.view_all', 'tasks.view_own'],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'Closing',
|
|
44
|
+
href: '/closing',
|
|
45
|
+
icon: Columns3,
|
|
46
|
+
permissions: ['contacts.view_all', 'contacts.view_own'],
|
|
47
|
+
},
|
|
48
|
+
{ name: 'Automatisations', href: '/automatisation', icon: Zap, permissions: ['workflows.view'] },
|
|
49
|
+
{ name: 'Templates', href: '/templates', icon: FileText, permissions: ['templates.view'] },
|
|
50
|
+
{ name: "Droits d'accès", href: '/users', icon: UserCog, permissions: ['users.view'] },
|
|
51
|
+
{
|
|
52
|
+
name: 'Utilisateurs',
|
|
53
|
+
href: '/users/list',
|
|
54
|
+
icon: Users,
|
|
55
|
+
permissions: ['users.view'],
|
|
56
|
+
parentLabel: "Droits d'accès",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'Profils',
|
|
60
|
+
href: '/users/roles',
|
|
61
|
+
icon: Shield,
|
|
62
|
+
permissions: ['users.manage_roles'],
|
|
63
|
+
parentLabel: "Droits d'accès",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'Permissions',
|
|
67
|
+
href: '/users/permissions',
|
|
68
|
+
icon: KeyRound,
|
|
69
|
+
permissions: ['users.manage_roles'],
|
|
70
|
+
parentLabel: "Droits d'accès",
|
|
71
|
+
},
|
|
72
|
+
{ name: 'Paramètres', href: '/settings', icon: Settings, permissions: ['settings.view'] },
|
|
73
|
+
{
|
|
74
|
+
name: 'Paramètres Généraux',
|
|
75
|
+
href: '/settings?section=general',
|
|
76
|
+
icon: SlidersHorizontal,
|
|
77
|
+
permissions: ['settings.view'],
|
|
78
|
+
parentLabel: 'Paramètres',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "Paramètres de l'Application",
|
|
82
|
+
href: '/settings?section=app',
|
|
83
|
+
icon: AppWindow,
|
|
84
|
+
permissions: ['users.manage_roles'],
|
|
85
|
+
parentLabel: 'Paramètres',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'Paramètres Système',
|
|
89
|
+
href: '/settings?section=system',
|
|
90
|
+
icon: MonitorCog,
|
|
91
|
+
permissions: ['settings.view'],
|
|
92
|
+
parentLabel: 'Paramètres',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'Intégrations',
|
|
96
|
+
href: '/settings?section=integrations',
|
|
97
|
+
icon: Plug,
|
|
98
|
+
permissions: ['settings.view'],
|
|
99
|
+
parentLabel: 'Paramètres',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'Entreprises',
|
|
103
|
+
href: '/contacts?entity=companies',
|
|
104
|
+
icon: Building2,
|
|
105
|
+
permissions: ['contacts.view_all', 'contacts.view_own'],
|
|
106
|
+
parentLabel: 'Contacts',
|
|
107
|
+
},
|
|
108
|
+
];
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CheckCircle2, Info, TriangleAlert, X, XCircle } from 'lucide-react';
|
|
4
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
|
|
7
|
+
type ToastTone = 'success' | 'error' | 'info' | 'warning';
|
|
8
|
+
|
|
9
|
+
interface ToastItem {
|
|
10
|
+
id: string;
|
|
11
|
+
message: string;
|
|
12
|
+
tone: ToastTone;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ToastContextValue {
|
|
16
|
+
success: (message: string) => void;
|
|
17
|
+
error: (message: string) => void;
|
|
18
|
+
info: (message: string) => void;
|
|
19
|
+
warning: (message: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ToastContext = createContext<ToastContextValue | null>(null);
|
|
23
|
+
|
|
24
|
+
const TOAST_STYLES: Record<ToastTone, { icon: React.ComponentType<{ className?: string }>; className: string }> =
|
|
25
|
+
{
|
|
26
|
+
success: {
|
|
27
|
+
icon: CheckCircle2,
|
|
28
|
+
className: 'border-primary/30 bg-primary text-primary-foreground',
|
|
29
|
+
},
|
|
30
|
+
error: {
|
|
31
|
+
icon: XCircle,
|
|
32
|
+
className: 'border-destructive/30 bg-destructive text-white',
|
|
33
|
+
},
|
|
34
|
+
info: {
|
|
35
|
+
icon: Info,
|
|
36
|
+
className: 'border-border bg-card text-foreground',
|
|
37
|
+
},
|
|
38
|
+
warning: {
|
|
39
|
+
icon: TriangleAlert,
|
|
40
|
+
className: 'border-blue-400/40 bg-blue-500 text-blue-950',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const TOAST_DURATION_MS: Record<ToastTone, number> = {
|
|
45
|
+
success: 4500,
|
|
46
|
+
info: 5500,
|
|
47
|
+
warning: 6500,
|
|
48
|
+
error: 7000,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function createToastId() {
|
|
52
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function AppToastProvider({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
56
|
+
const [toasts, setToasts] = useState<ToastItem[]>([]);
|
|
57
|
+
const timersRef = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
|
|
58
|
+
const expiresAtRef = useRef<Record<string, number>>({});
|
|
59
|
+
const remainingMsRef = useRef<Record<string, number>>({});
|
|
60
|
+
|
|
61
|
+
const dismiss = useCallback((id: string) => {
|
|
62
|
+
const timeout = timersRef.current[id];
|
|
63
|
+
if (timeout) {
|
|
64
|
+
clearTimeout(timeout);
|
|
65
|
+
delete timersRef.current[id];
|
|
66
|
+
}
|
|
67
|
+
delete expiresAtRef.current[id];
|
|
68
|
+
delete remainingMsRef.current[id];
|
|
69
|
+
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
const startTimer = useCallback(
|
|
73
|
+
(id: string, durationMs: number) => {
|
|
74
|
+
const existing = timersRef.current[id];
|
|
75
|
+
if (existing) clearTimeout(existing);
|
|
76
|
+
expiresAtRef.current[id] = Date.now() + durationMs;
|
|
77
|
+
remainingMsRef.current[id] = durationMs;
|
|
78
|
+
timersRef.current[id] = setTimeout(() => dismiss(id), durationMs);
|
|
79
|
+
},
|
|
80
|
+
[dismiss],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const pauseTimer = useCallback((id: string) => {
|
|
84
|
+
const timeout = timersRef.current[id];
|
|
85
|
+
if (!timeout) return;
|
|
86
|
+
clearTimeout(timeout);
|
|
87
|
+
delete timersRef.current[id];
|
|
88
|
+
const remaining = Math.max(0, (expiresAtRef.current[id] ?? Date.now()) - Date.now());
|
|
89
|
+
remainingMsRef.current[id] = remaining;
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
const resumeTimer = useCallback(
|
|
93
|
+
(id: string) => {
|
|
94
|
+
const remaining = remainingMsRef.current[id];
|
|
95
|
+
if (remaining == null) return;
|
|
96
|
+
if (remaining <= 0) {
|
|
97
|
+
dismiss(id);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
startTimer(id, remaining);
|
|
101
|
+
},
|
|
102
|
+
[dismiss, startTimer],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const push = useCallback((tone: ToastTone, message: string) => {
|
|
106
|
+
const id = createToastId();
|
|
107
|
+
setToasts((prev) => [...prev, { id, tone, message }]);
|
|
108
|
+
startTimer(id, TOAST_DURATION_MS[tone]);
|
|
109
|
+
}, [startTimer]);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
return () => {
|
|
113
|
+
Object.values(timersRef.current).forEach((timeout) => clearTimeout(timeout));
|
|
114
|
+
timersRef.current = {};
|
|
115
|
+
expiresAtRef.current = {};
|
|
116
|
+
remainingMsRef.current = {};
|
|
117
|
+
};
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
const value = useMemo<ToastContextValue>(
|
|
121
|
+
() => ({
|
|
122
|
+
success: (message) => push('success', message),
|
|
123
|
+
error: (message) => push('error', message),
|
|
124
|
+
info: (message) => push('info', message),
|
|
125
|
+
warning: (message) => push('warning', message),
|
|
126
|
+
}),
|
|
127
|
+
[push],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<ToastContext.Provider value={value}>
|
|
132
|
+
{children}
|
|
133
|
+
<div className="pointer-events-none fixed right-4 bottom-4 z-60 space-y-2">
|
|
134
|
+
{toasts.map((toast) => {
|
|
135
|
+
const { className, icon: Icon } = TOAST_STYLES[toast.tone];
|
|
136
|
+
return (
|
|
137
|
+
<div
|
|
138
|
+
key={toast.id}
|
|
139
|
+
className={cn(
|
|
140
|
+
'pointer-events-auto flex min-w-[280px] max-w-sm items-start gap-2 rounded-lg border px-3 py-2 text-sm shadow-(--shadow-dropdown) transition-all duration-200',
|
|
141
|
+
className,
|
|
142
|
+
)}
|
|
143
|
+
role={toast.tone === 'error' || toast.tone === 'warning' ? 'alert' : 'status'}
|
|
144
|
+
aria-live={toast.tone === 'error' || toast.tone === 'warning' ? 'assertive' : 'polite'}
|
|
145
|
+
onMouseEnter={() => pauseTimer(toast.id)}
|
|
146
|
+
onMouseLeave={() => resumeTimer(toast.id)}
|
|
147
|
+
onFocusCapture={() => pauseTimer(toast.id)}
|
|
148
|
+
onBlurCapture={() => resumeTimer(toast.id)}
|
|
149
|
+
>
|
|
150
|
+
<Icon className="mt-0.5 h-4 w-4 shrink-0" />
|
|
151
|
+
<p className="flex-1">{toast.message}</p>
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
onClick={() => dismiss(toast.id)}
|
|
155
|
+
className="cursor-pointer rounded p-1 opacity-75 transition-opacity hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current/40"
|
|
156
|
+
aria-label="Fermer la notification"
|
|
157
|
+
>
|
|
158
|
+
<X className="h-3.5 w-3.5" />
|
|
159
|
+
</button>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
})}
|
|
163
|
+
</div>
|
|
164
|
+
</ToastContext.Provider>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function useAppToast() {
|
|
169
|
+
const context = useContext(ToastContext);
|
|
170
|
+
if (!context) {
|
|
171
|
+
throw new Error('useAppToast must be used within AppToastProvider');
|
|
172
|
+
}
|
|
173
|
+
return context;
|
|
174
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
|
4
|
+
import { safeLocalStorageGet, safeLocalStorageSet } from '@/lib/local-storage';
|
|
4
5
|
|
|
5
6
|
interface SidebarContextType {
|
|
6
7
|
isCollapsed: boolean;
|
|
@@ -13,70 +14,38 @@ interface SidebarContextType {
|
|
|
13
14
|
const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
|
|
14
15
|
|
|
15
16
|
export function SidebarProvider({ children }: { children: ReactNode }) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
// Lecture initiale de la préférence et de la taille d'écran après montage
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
if (typeof window === 'undefined') return;
|
|
27
|
-
|
|
28
|
-
const saved = window.localStorage.getItem('sidebar-pinned');
|
|
29
|
-
const initialPinned = saved === 'true';
|
|
30
|
-
setIsPinnedState(initialPinned);
|
|
31
|
-
|
|
32
|
-
if (window.innerWidth < 1024) {
|
|
33
|
-
// En mobile, jamais de collapse
|
|
34
|
-
setIsCollapsedState(false);
|
|
35
|
-
} else {
|
|
36
|
-
// En desktop, utiliser la préférence
|
|
37
|
-
setIsCollapsedState(!initialPinned);
|
|
38
|
-
}
|
|
39
|
-
}, []);
|
|
17
|
+
const [isPinned, setIsPinnedState] = useState(() =>
|
|
18
|
+
safeLocalStorageGet<boolean>('sidebar-pinned', true),
|
|
19
|
+
);
|
|
20
|
+
const [isMobile, setIsMobile] = useState(() =>
|
|
21
|
+
typeof globalThis.window === 'undefined' ? false : globalThis.window.innerWidth < 1024,
|
|
22
|
+
);
|
|
23
|
+
const isCollapsed = isMobile ? false : !isPinned;
|
|
40
24
|
|
|
41
25
|
// Sauvegarder la préférence dans localStorage et adapter le collapse
|
|
42
26
|
useEffect(() => {
|
|
43
|
-
if (typeof window === 'undefined') return;
|
|
44
|
-
|
|
45
|
-
window.localStorage.setItem('sidebar-pinned', String(isPinned));
|
|
46
|
-
|
|
47
|
-
// Ne réduire la sidebar qu'en desktop (>= 1024px)
|
|
48
|
-
if (window.innerWidth >= 1024) {
|
|
49
|
-
setIsCollapsedState(!isPinned);
|
|
50
|
-
} else {
|
|
51
|
-
// En mobile, toujours false (pas de collapse)
|
|
52
|
-
setIsCollapsedState(false);
|
|
53
|
-
}
|
|
27
|
+
if (typeof globalThis.window === 'undefined') return;
|
|
28
|
+
safeLocalStorageSet('sidebar-pinned', isPinned);
|
|
54
29
|
}, [isPinned]);
|
|
55
30
|
|
|
56
31
|
// Écouter les changements de taille d'écran
|
|
57
32
|
useEffect(() => {
|
|
58
|
-
if (typeof window === 'undefined') return;
|
|
33
|
+
if (typeof globalThis.window === 'undefined') return;
|
|
59
34
|
|
|
60
35
|
const handleResize = () => {
|
|
61
|
-
|
|
62
|
-
if (window.innerWidth < 1024) {
|
|
63
|
-
setIsCollapsedState(false);
|
|
64
|
-
} else {
|
|
65
|
-
// En desktop, utiliser la préférence isPinned
|
|
66
|
-
setIsCollapsedState(!isPinned);
|
|
67
|
-
}
|
|
36
|
+
setIsMobile(globalThis.window.innerWidth < 1024);
|
|
68
37
|
};
|
|
69
38
|
|
|
70
|
-
window.addEventListener('resize', handleResize);
|
|
71
|
-
return () => window.removeEventListener('resize', handleResize);
|
|
72
|
-
}, [
|
|
39
|
+
globalThis.window.addEventListener('resize', handleResize);
|
|
40
|
+
return () => globalThis.window.removeEventListener('resize', handleResize);
|
|
41
|
+
}, []);
|
|
73
42
|
|
|
74
43
|
const setIsPinned = (pinned: boolean) => {
|
|
75
44
|
setIsPinnedState(pinned);
|
|
76
45
|
};
|
|
77
46
|
|
|
78
47
|
const setIsCollapsed = (collapsed: boolean) => {
|
|
79
|
-
|
|
48
|
+
setIsPinnedState(!collapsed);
|
|
80
49
|
};
|
|
81
50
|
|
|
82
51
|
const togglePin = () => {
|
|
@@ -197,18 +197,18 @@ export function TaskReminderProvider({ children }: { children: React.ReactNode }
|
|
|
197
197
|
{notifications.map((notif) => (
|
|
198
198
|
<div
|
|
199
199
|
key={notif.id}
|
|
200
|
-
className="pointer-events-auto flex max-w-sm items-start gap-3 rounded-xl border border-
|
|
200
|
+
className="pointer-events-auto flex max-w-sm items-start gap-3 rounded-xl border border-blue-200 bg-card p-4 shadow-(--shadow-dropdown)"
|
|
201
201
|
>
|
|
202
|
-
<div className="mt-0.5 rounded-full bg-
|
|
202
|
+
<div className="mt-0.5 rounded-full bg-blue-100 p-2 text-blue-700">
|
|
203
203
|
<Bell className="h-4 w-4" />
|
|
204
204
|
</div>
|
|
205
205
|
<div className="flex-1">
|
|
206
|
-
<p className="text-sm font-medium text-
|
|
207
|
-
<p className="mt-1 text-sm text-
|
|
206
|
+
<p className="text-sm font-medium text-foreground">Rappel de tâche</p>
|
|
207
|
+
<p className="mt-1 text-sm text-muted-foreground">{notif.message}</p>
|
|
208
208
|
{notif.link && (
|
|
209
209
|
<Link
|
|
210
210
|
href={notif.link}
|
|
211
|
-
className="mt-2 inline-flex text-xs font-medium text-
|
|
211
|
+
className="mt-2 inline-flex text-xs font-medium text-blue-700 hover:text-blue-800"
|
|
212
212
|
>
|
|
213
213
|
Ouvrir le contact
|
|
214
214
|
</Link>
|
|
@@ -217,7 +217,7 @@ export function TaskReminderProvider({ children }: { children: React.ReactNode }
|
|
|
217
217
|
<button
|
|
218
218
|
type="button"
|
|
219
219
|
onClick={() => dismissNotification(notif.id)}
|
|
220
|
-
className="ml-2 inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-
|
|
220
|
+
className="ml-2 inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
221
221
|
>
|
|
222
222
|
<span className="sr-only">Fermer</span>
|
|
223
223
|
<X />
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { createContext, useContext, useState
|
|
3
|
+
import React, { createContext, useContext, useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
safeLocalStorageGet,
|
|
6
|
+
safeLocalStorageSet,
|
|
7
|
+
safeLocalStorageRemove,
|
|
8
|
+
} from '@/lib/local-storage';
|
|
4
9
|
|
|
5
10
|
interface User {
|
|
6
11
|
id: string;
|
|
@@ -24,19 +29,9 @@ interface ViewAsContextType {
|
|
|
24
29
|
const ViewAsContext = createContext<ViewAsContextType | undefined>(undefined);
|
|
25
30
|
|
|
26
31
|
export function ViewAsProvider({ children }: { children: React.ReactNode }) {
|
|
27
|
-
const [viewAsUser, setViewAsUserState] = useState<User | null>(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
const stored = localStorage.getItem('viewAsUser');
|
|
32
|
-
if (stored) {
|
|
33
|
-
try {
|
|
34
|
-
setViewAsUserState(JSON.parse(stored));
|
|
35
|
-
} catch (e) {
|
|
36
|
-
localStorage.removeItem('viewAsUser');
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}, []);
|
|
32
|
+
const [viewAsUser, setViewAsUserState] = useState<User | null>(() =>
|
|
33
|
+
safeLocalStorageGet<User | null>('viewAsUser', null),
|
|
34
|
+
);
|
|
40
35
|
|
|
41
36
|
const setViewAsUser = (user: User | null) => {
|
|
42
37
|
// Si l'utilisateur a un customRole, copier les permissions dans un champ direct pour faciliter l'accès
|
|
@@ -49,9 +44,9 @@ export function ViewAsProvider({ children }: { children: React.ReactNode }) {
|
|
|
49
44
|
|
|
50
45
|
setViewAsUserState(userWithPermissions);
|
|
51
46
|
if (userWithPermissions) {
|
|
52
|
-
|
|
47
|
+
safeLocalStorageSet('viewAsUser', userWithPermissions);
|
|
53
48
|
} else {
|
|
54
|
-
|
|
49
|
+
safeLocalStorageRemove('viewAsUser');
|
|
55
50
|
}
|
|
56
51
|
};
|
|
57
52
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
AlertDialog,
|
|
6
|
+
AlertDialogAction,
|
|
7
|
+
AlertDialogContent,
|
|
8
|
+
AlertDialogDescription,
|
|
9
|
+
AlertDialogFooter,
|
|
10
|
+
AlertDialogHeader,
|
|
11
|
+
AlertDialogTitle,
|
|
12
|
+
} from '@/components/ui/alert-dialog';
|
|
13
|
+
|
|
14
|
+
interface AlertOptions {
|
|
15
|
+
title: string;
|
|
16
|
+
description: string;
|
|
17
|
+
confirmText?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useAlert() {
|
|
21
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
22
|
+
const [options, setOptions] = useState<AlertOptions>({
|
|
23
|
+
title: '',
|
|
24
|
+
description: '',
|
|
25
|
+
confirmText: 'OK',
|
|
26
|
+
});
|
|
27
|
+
const [resolvePromise, setResolvePromise] = useState<(() => void) | null>(null);
|
|
28
|
+
|
|
29
|
+
const alert = (opts: AlertOptions | string): Promise<void> => {
|
|
30
|
+
const normalizedOpts = typeof opts === 'string' ? { title: 'Alerte', description: opts } : opts;
|
|
31
|
+
|
|
32
|
+
setOptions({
|
|
33
|
+
confirmText: 'OK',
|
|
34
|
+
...normalizedOpts,
|
|
35
|
+
});
|
|
36
|
+
setIsOpen(true);
|
|
37
|
+
|
|
38
|
+
return new Promise<void>((resolve) => {
|
|
39
|
+
setResolvePromise(() => resolve);
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleConfirm = () => {
|
|
44
|
+
if (resolvePromise) {
|
|
45
|
+
resolvePromise();
|
|
46
|
+
}
|
|
47
|
+
setIsOpen(false);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const AlertDialogComponent = () => (
|
|
51
|
+
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
|
|
52
|
+
<AlertDialogContent>
|
|
53
|
+
<AlertDialogHeader>
|
|
54
|
+
<AlertDialogTitle>{options.title}</AlertDialogTitle>
|
|
55
|
+
<AlertDialogDescription>{options.description}</AlertDialogDescription>
|
|
56
|
+
</AlertDialogHeader>
|
|
57
|
+
<AlertDialogFooter>
|
|
58
|
+
<AlertDialogAction onClick={handleConfirm}>{options.confirmText}</AlertDialogAction>
|
|
59
|
+
</AlertDialogFooter>
|
|
60
|
+
</AlertDialogContent>
|
|
61
|
+
</AlertDialog>
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return { alert, AlertDialog: AlertDialogComponent };
|
|
65
|
+
}
|