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
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
Eye,
|
|
16
16
|
} from 'lucide-react';
|
|
17
17
|
import { cn } from '@/lib/utils';
|
|
18
|
+
import { PageLoader } from '@/components/skeleton';
|
|
19
|
+
import { useUserRole } from '@/hooks/use-user-role';
|
|
20
|
+
import { ProtectedPage } from '@/components/protected-page';
|
|
21
|
+
import { useFetch } from '@/hooks/use-fetch';
|
|
18
22
|
|
|
19
23
|
interface Status {
|
|
20
24
|
id: string;
|
|
@@ -40,10 +44,8 @@ interface Contact {
|
|
|
40
44
|
city: string | null;
|
|
41
45
|
postalCode: string | null;
|
|
42
46
|
origin: string | null;
|
|
43
|
-
companyName: string | null;
|
|
44
|
-
isCompany: boolean;
|
|
45
47
|
companyId: string | null;
|
|
46
|
-
|
|
48
|
+
company: { id: string; name: string | null } | null;
|
|
47
49
|
statusId: string | null;
|
|
48
50
|
status: Status | null;
|
|
49
51
|
assignedCommercialId: string | null;
|
|
@@ -53,6 +55,10 @@ interface Contact {
|
|
|
53
55
|
updatedAt?: string;
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
interface ContactsResponse {
|
|
59
|
+
contacts: Contact[];
|
|
60
|
+
}
|
|
61
|
+
|
|
56
62
|
interface ClosingColumn {
|
|
57
63
|
id: string;
|
|
58
64
|
statusId: string | null;
|
|
@@ -66,11 +72,11 @@ const MIN_WIDTH = 280;
|
|
|
66
72
|
const MAX_WIDTH = 500;
|
|
67
73
|
const LOCAL_STORAGE_KEY = 'closing_pipeline_columns_v1';
|
|
68
74
|
|
|
75
|
+
const SCROLLBAR_HIDDEN = { scrollbarWidth: 'none' } as const;
|
|
76
|
+
|
|
69
77
|
function formatRelativeDate(dateString: string): string {
|
|
70
78
|
const date = new Date(dateString);
|
|
71
79
|
const now = new Date();
|
|
72
|
-
const diffMs = now.getTime() - date.getTime();
|
|
73
|
-
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
74
80
|
|
|
75
81
|
// Réinitialiser les heures pour comparer uniquement les dates
|
|
76
82
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
@@ -103,11 +109,13 @@ function KanbanContactCard({
|
|
|
103
109
|
onDragStart,
|
|
104
110
|
onDragEnd,
|
|
105
111
|
isDragging,
|
|
112
|
+
canDrag = true,
|
|
106
113
|
}: {
|
|
107
114
|
contact: Contact;
|
|
108
115
|
onDragStart: () => void;
|
|
109
116
|
onDragEnd: () => void;
|
|
110
117
|
isDragging: boolean;
|
|
118
|
+
canDrag?: boolean;
|
|
111
119
|
}) {
|
|
112
120
|
const fullName = `${contact.civility ? `${contact.civility}. ` : ''}${
|
|
113
121
|
contact.firstName || ''
|
|
@@ -117,40 +125,37 @@ function KanbanContactCard({
|
|
|
117
125
|
|
|
118
126
|
return (
|
|
119
127
|
<div
|
|
120
|
-
draggable
|
|
121
|
-
onDragStart={onDragStart}
|
|
122
|
-
onDragEnd={onDragEnd}
|
|
128
|
+
draggable={canDrag}
|
|
129
|
+
onDragStart={canDrag ? onDragStart : undefined}
|
|
130
|
+
onDragEnd={canDrag ? onDragEnd : undefined}
|
|
123
131
|
className={cn(
|
|
124
|
-
'relative rounded-lg border border-
|
|
125
|
-
'cursor-grab active:cursor-grabbing',
|
|
126
|
-
|
|
132
|
+
'relative rounded-lg border border-border bg-card p-4 text-[13px] shadow-(--shadow-card) transition-transform hover:-translate-y-0.5',
|
|
133
|
+
canDrag && 'cursor-grab active:cursor-grabbing',
|
|
134
|
+
!canDrag && 'cursor-default',
|
|
135
|
+
isDragging && 'opacity-80 ring-2 ring-blue-400',
|
|
127
136
|
)}
|
|
128
137
|
>
|
|
129
138
|
{/* Icône Eye en haut à droite */}
|
|
130
139
|
<Link
|
|
131
140
|
href={`/contacts/${contact.id}`}
|
|
132
|
-
className="absolute top-2 right-2 z-10 rounded p-1 transition-colors hover:bg-
|
|
141
|
+
className="absolute top-2 right-2 z-10 rounded p-1 transition-colors duration-200 hover:bg-muted"
|
|
133
142
|
onClick={(e) => e.stopPropagation()}
|
|
134
143
|
>
|
|
135
144
|
<Eye className="h-4 w-4 stroke-gray-400" />
|
|
136
145
|
</Link>
|
|
137
146
|
|
|
138
147
|
<div className="block">
|
|
139
|
-
{contact.
|
|
148
|
+
{contact.company?.name && (
|
|
140
149
|
<p className="mb-1 truncate text-[10px] font-semibold text-gray-400 uppercase">
|
|
141
|
-
{contact.
|
|
150
|
+
{contact.company.name}
|
|
142
151
|
</p>
|
|
143
152
|
)}
|
|
144
153
|
<div className="mb-3 flex items-center gap-3">
|
|
145
|
-
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-
|
|
146
|
-
{contact.
|
|
147
|
-
<span>🏢</span>
|
|
148
|
-
) : (
|
|
149
|
-
(contact.firstName?.[0] || contact.lastName?.[0] || '?').toUpperCase()
|
|
150
|
-
)}
|
|
154
|
+
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-blue-100 text-xs font-semibold text-blue-700">
|
|
155
|
+
{(contact.firstName?.[0] || contact.lastName?.[0] || '?').toUpperCase()}
|
|
151
156
|
</div>
|
|
152
157
|
<div className="min-w-0 flex-1">
|
|
153
|
-
<p className="truncate text-sm font-semibold text-
|
|
158
|
+
<p className="truncate text-sm font-semibold text-foreground">{fullName || 'Sans nom'}</p>
|
|
154
159
|
</div>
|
|
155
160
|
</div>
|
|
156
161
|
|
|
@@ -224,11 +229,21 @@ function createDefaultColumns(statuses: Status[]): ClosingColumn[] {
|
|
|
224
229
|
}
|
|
225
230
|
|
|
226
231
|
export default function ClosingPage() {
|
|
227
|
-
const
|
|
232
|
+
const { hasPermission, isLoading: roleLoading } = useUserRole();
|
|
233
|
+
const {
|
|
234
|
+
data: statuses = [],
|
|
235
|
+
isLoading: statusesLoading,
|
|
236
|
+
error: statusesError,
|
|
237
|
+
} = useFetch<Status[]>('/api/statuses');
|
|
238
|
+
const {
|
|
239
|
+
data: contactsData,
|
|
240
|
+
isLoading: contactsLoading,
|
|
241
|
+
error: contactsError,
|
|
242
|
+
} = useFetch<ContactsResponse>('/api/contacts?limit=500');
|
|
228
243
|
const [contacts, setContacts] = useState<Contact[]>([]);
|
|
229
|
-
const [loading, setLoading] = useState(true);
|
|
230
|
-
const [error, setError] = useState('');
|
|
231
244
|
const [columns, setColumns] = useState<ClosingColumn[]>([]);
|
|
245
|
+
|
|
246
|
+
const canManagePipeline = hasPermission('contacts.manage_pipeline');
|
|
232
247
|
const [draggedCard, setDraggedCard] = useState<{
|
|
233
248
|
contactId: string;
|
|
234
249
|
fromStatusId: string | null;
|
|
@@ -246,64 +261,39 @@ export default function ClosingPage() {
|
|
|
246
261
|
const [draggedColumnId, setDraggedColumnId] = useState<string | null>(null);
|
|
247
262
|
const [dragOverColumnId, setDragOverColumnId] = useState<string | null>(null);
|
|
248
263
|
|
|
249
|
-
const [selectedCompanyId
|
|
264
|
+
const [selectedCompanyId] = useState<string | 'all'>('all');
|
|
250
265
|
const [selectedPeriod, setSelectedPeriod] = useState<string>('all');
|
|
251
266
|
const [selectedCommercialId, setSelectedCommercialId] = useState<string | 'all'>('all');
|
|
252
267
|
const [selectedTeleproId, setSelectedTeleproId] = useState<string | 'all'>('all');
|
|
253
268
|
|
|
254
269
|
useEffect(() => {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
setLoading(true);
|
|
258
|
-
setError('');
|
|
259
|
-
|
|
260
|
-
const [statusRes, contactsRes] = await Promise.all([
|
|
261
|
-
fetch('/api/statuses'),
|
|
262
|
-
fetch('/api/contacts?limit=500'),
|
|
263
|
-
]);
|
|
270
|
+
setContacts(contactsData?.contacts || []);
|
|
271
|
+
}, [contactsData]);
|
|
264
272
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
if (!statuses.length) return;
|
|
275
|
+
if (typeof window === 'undefined') {
|
|
276
|
+
setColumns(createDefaultColumns(statuses));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
271
279
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (typeof window !== 'undefined') {
|
|
279
|
-
const stored = window.localStorage.getItem(LOCAL_STORAGE_KEY);
|
|
280
|
-
if (stored) {
|
|
281
|
-
try {
|
|
282
|
-
const parsed: ClosingColumn[] = JSON.parse(stored);
|
|
283
|
-
setColumns(
|
|
284
|
-
parsed
|
|
285
|
-
.filter((c) => !c.statusId || statusesJson.some((s) => s.id === c.statusId))
|
|
286
|
-
.sort((a, b) => a.order - b.order),
|
|
287
|
-
);
|
|
288
|
-
} catch {
|
|
289
|
-
setColumns(createDefaultColumns(statusesJson));
|
|
290
|
-
}
|
|
291
|
-
} else {
|
|
292
|
-
setColumns(createDefaultColumns(statusesJson));
|
|
293
|
-
}
|
|
294
|
-
} else {
|
|
295
|
-
setColumns(createDefaultColumns(statusesJson));
|
|
296
|
-
}
|
|
297
|
-
} catch (e: any) {
|
|
298
|
-
console.error(e);
|
|
299
|
-
setError(e.message || 'Erreur de chargement');
|
|
300
|
-
} finally {
|
|
301
|
-
setLoading(false);
|
|
302
|
-
}
|
|
303
|
-
};
|
|
280
|
+
const stored = window.localStorage.getItem(LOCAL_STORAGE_KEY);
|
|
281
|
+
if (!stored) {
|
|
282
|
+
setColumns(createDefaultColumns(statuses));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
304
285
|
|
|
305
|
-
|
|
306
|
-
|
|
286
|
+
try {
|
|
287
|
+
const parsed: ClosingColumn[] = JSON.parse(stored);
|
|
288
|
+
setColumns(
|
|
289
|
+
parsed
|
|
290
|
+
.filter((c) => !c.statusId || statuses.some((s) => s.id === c.statusId))
|
|
291
|
+
.sort((a, b) => a.order - b.order),
|
|
292
|
+
);
|
|
293
|
+
} catch {
|
|
294
|
+
setColumns(createDefaultColumns(statuses));
|
|
295
|
+
}
|
|
296
|
+
}, [statuses]);
|
|
307
297
|
|
|
308
298
|
const statusesById = useMemo(() => {
|
|
309
299
|
const map: Record<string, Status> = {};
|
|
@@ -402,7 +392,7 @@ export default function ClosingPage() {
|
|
|
402
392
|
setDraggingContactId(null);
|
|
403
393
|
setDragOverStatusId(null);
|
|
404
394
|
|
|
405
|
-
if (targetStatusId) {
|
|
395
|
+
if (targetStatusId && canManagePipeline) {
|
|
406
396
|
try {
|
|
407
397
|
setIsSavingDrag(true);
|
|
408
398
|
const res = await fetch(`/api/contacts/${contactId}`, {
|
|
@@ -554,447 +544,489 @@ export default function ClosingPage() {
|
|
|
554
544
|
setConfigColumns(defaults);
|
|
555
545
|
};
|
|
556
546
|
|
|
557
|
-
// Récupérer les entreprises uniques pour le filtre
|
|
558
|
-
|
|
547
|
+
// Récupérer les entreprises uniques pour le filtre (réservé pour usage futur)
|
|
548
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- réservé pour filtre entreprise
|
|
549
|
+
const _companies = useMemo(() => {
|
|
559
550
|
const companyMap = new Map<string, { id: string; name: string }>();
|
|
560
551
|
contacts.forEach((c) => {
|
|
561
|
-
if (c.companyId && c.
|
|
562
|
-
companyMap.set(c.companyId, { id: c.companyId, name: c.
|
|
552
|
+
if (c.companyId && c.company?.name) {
|
|
553
|
+
companyMap.set(c.companyId, { id: c.companyId, name: c.company.name });
|
|
563
554
|
}
|
|
564
555
|
});
|
|
565
556
|
return Array.from(companyMap.values());
|
|
566
557
|
}, [contacts]);
|
|
567
558
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
<div className="
|
|
572
|
-
<div>
|
|
573
|
-
<
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
559
|
+
return (
|
|
560
|
+
<ProtectedPage requiredPermission="contacts.view_closing_pipeline">
|
|
561
|
+
{statusesLoading || contactsLoading || roleLoading ? (
|
|
562
|
+
<div className="h-full">
|
|
563
|
+
<div className="border-b border-border bg-background/95 px-4 py-4 backdrop-blur-sm sm:px-6 lg:px-8 lg:py-6">
|
|
564
|
+
<div className="flex items-start gap-3">
|
|
565
|
+
<div className="min-w-0 flex-1">
|
|
566
|
+
<h1 className="text-xl font-bold text-foreground sm:text-2xl">Pipeline de Closing</h1>
|
|
567
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
568
|
+
Visualisez et gérez vos opportunités commerciales
|
|
569
|
+
</p>
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
577
572
|
</div>
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
<div className="rounded-lg bg-white p-8 shadow">
|
|
581
|
-
<p className="text-gray-500">Chargement du pipeline...</p>
|
|
573
|
+
<div className="flex-1">
|
|
574
|
+
<PageLoader text="Chargement du pipeline..." />
|
|
582
575
|
</div>
|
|
583
576
|
</div>
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
577
|
+
) : (
|
|
578
|
+
<div className="h-full">
|
|
579
|
+
{' '}
|
|
580
|
+
{/* md:overflow-y-hidden */}
|
|
581
|
+
{/* En-tête personnalisé avec filtres intégrés */}
|
|
582
|
+
<div className="border-b border-border bg-background/95 px-4 py-4 backdrop-blur-sm sm:px-6 lg:px-8 lg:py-6">
|
|
583
|
+
<div className="flex items-start gap-3">
|
|
584
|
+
<div className="flex min-w-0 flex-1 items-start justify-between gap-4">
|
|
585
|
+
<div className="min-w-0 flex-1">
|
|
586
|
+
<h1 className="text-xl font-bold text-foreground sm:text-2xl">
|
|
587
|
+
Pipeline de Closing
|
|
588
|
+
</h1>
|
|
589
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
590
|
+
Visualisez et gérez vos opportunités commerciales
|
|
591
|
+
</p>
|
|
587
592
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
<div className="relative">
|
|
607
|
-
<Filter className="absolute top-1/2 left-2 h-3.5 w-3.5 -translate-y-1/2 text-gray-400" />
|
|
608
|
-
<select
|
|
609
|
-
className="rounded-lg border border-gray-300 py-1.5 pr-2 pl-7 text-sm text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
610
|
-
value={selectedPeriod}
|
|
611
|
-
onChange={(e) => setSelectedPeriod(e.target.value)}
|
|
612
|
-
>
|
|
613
|
-
<option value="all">Toutes</option>
|
|
614
|
-
<option value="today">Aujourd'hui</option>
|
|
615
|
-
<option value="week">Cette semaine</option>
|
|
616
|
-
<option value="month">Ce mois</option>
|
|
617
|
-
</select>
|
|
618
|
-
</div>
|
|
619
|
-
</div>
|
|
593
|
+
{/* Filtres intégrés dans l'en-tête */}
|
|
594
|
+
<div className="mt-4 flex flex-col gap-3 sm:flex-row">
|
|
595
|
+
<div className="flex items-center gap-2">
|
|
596
|
+
<span className="text-xs font-medium text-muted-foreground">Période:</span>
|
|
597
|
+
<div className="relative">
|
|
598
|
+
<Filter className="absolute top-1/2 left-2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
|
599
|
+
<select
|
|
600
|
+
className="rounded-lg border border-border bg-card py-1.5 pr-2 pl-7 text-sm text-foreground focus:border-primary/50 focus:ring-2 focus:ring-primary/20 focus:outline-none"
|
|
601
|
+
value={selectedPeriod}
|
|
602
|
+
onChange={(e) => setSelectedPeriod(e.target.value)}
|
|
603
|
+
>
|
|
604
|
+
<option value="all">Toutes</option>
|
|
605
|
+
<option value="today">Aujourd'hui</option>
|
|
606
|
+
<option value="week">Cette semaine</option>
|
|
607
|
+
<option value="month">Ce mois</option>
|
|
608
|
+
</select>
|
|
609
|
+
</div>
|
|
610
|
+
</div>
|
|
620
611
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
612
|
+
<div className="flex items-center gap-2">
|
|
613
|
+
<span className="text-xs font-medium text-muted-foreground">Commercial:</span>
|
|
614
|
+
<select
|
|
615
|
+
className="rounded-lg border border-border bg-card px-2 py-1.5 text-sm text-foreground focus:border-primary/50 focus:ring-2 focus:ring-primary/20 focus:outline-none"
|
|
616
|
+
value={selectedCommercialId}
|
|
617
|
+
onChange={(e) =>
|
|
618
|
+
setSelectedCommercialId(e.target.value === 'all' ? 'all' : e.target.value)
|
|
619
|
+
}
|
|
620
|
+
>
|
|
621
|
+
<option value="all">Tous</option>
|
|
622
|
+
{Array.from(
|
|
623
|
+
new Map(
|
|
624
|
+
contacts
|
|
625
|
+
.filter((c) => c.assignedCommercial)
|
|
626
|
+
.map((c) => [c.assignedCommercialId, c.assignedCommercial!]),
|
|
627
|
+
).values(),
|
|
628
|
+
).map((user) => (
|
|
629
|
+
<option key={user.id} value={user.id}>
|
|
630
|
+
{user.name}
|
|
631
|
+
</option>
|
|
632
|
+
))}
|
|
633
|
+
</select>
|
|
634
|
+
</div>
|
|
635
|
+
|
|
636
|
+
<div className="flex items-center gap-2">
|
|
637
|
+
<span className="text-xs font-medium text-muted-foreground">Télépro:</span>
|
|
638
|
+
<select
|
|
639
|
+
className="rounded-lg border border-border bg-card px-2 py-1.5 text-sm text-foreground focus:border-primary/50 focus:ring-2 focus:ring-primary/20 focus:outline-none"
|
|
640
|
+
value={selectedTeleproId}
|
|
641
|
+
onChange={(e) =>
|
|
642
|
+
setSelectedTeleproId(e.target.value === 'all' ? 'all' : e.target.value)
|
|
643
|
+
}
|
|
644
|
+
>
|
|
645
|
+
<option value="all">Tous</option>
|
|
646
|
+
{Array.from(
|
|
647
|
+
new Map(
|
|
648
|
+
contacts
|
|
649
|
+
.filter((c) => c.assignedTelepro)
|
|
650
|
+
.map((c) => [c.assignedTeleproId, c.assignedTelepro!]),
|
|
651
|
+
).values(),
|
|
652
|
+
).map((user) => (
|
|
653
|
+
<option key={user.id} value={user.id}>
|
|
654
|
+
{user.name}
|
|
655
|
+
</option>
|
|
656
|
+
))}
|
|
657
|
+
</select>
|
|
658
|
+
</div>
|
|
659
|
+
|
|
660
|
+
{isSavingDrag && (
|
|
661
|
+
<span className="self-end text-xs text-muted-foreground">
|
|
662
|
+
Sauvegarde des changements…
|
|
663
|
+
</span>
|
|
664
|
+
)}
|
|
665
|
+
</div>
|
|
643
666
|
</div>
|
|
644
667
|
|
|
645
|
-
<div className="
|
|
646
|
-
<
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
onChange={(e) =>
|
|
651
|
-
setSelectedTeleproId(e.target.value === 'all' ? 'all' : e.target.value)
|
|
652
|
-
}
|
|
668
|
+
<div className="shrink-0">
|
|
669
|
+
<button
|
|
670
|
+
type="button"
|
|
671
|
+
onClick={openConfigModal}
|
|
672
|
+
className="inline-flex cursor-pointer items-center gap-2 rounded-lg border border-border bg-card px-3 py-2 text-sm font-semibold text-foreground shadow-sm transition-colors duration-200 hover:bg-muted focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:outline-none"
|
|
653
673
|
>
|
|
654
|
-
<
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
contacts
|
|
658
|
-
.filter((c) => c.assignedTelepro)
|
|
659
|
-
.map((c) => [c.assignedTeleproId, c.assignedTelepro!]),
|
|
660
|
-
).values(),
|
|
661
|
-
).map((user) => (
|
|
662
|
-
<option key={user.id} value={user.id}>
|
|
663
|
-
{user.name}
|
|
664
|
-
</option>
|
|
665
|
-
))}
|
|
666
|
-
</select>
|
|
674
|
+
<SettingsIcon className="h-4 w-4" />
|
|
675
|
+
Configurer
|
|
676
|
+
</button>
|
|
667
677
|
</div>
|
|
668
|
-
|
|
669
|
-
{isSavingDrag && (
|
|
670
|
-
<span className="self-end text-xs text-gray-400">
|
|
671
|
-
Sauvegarde des changements…
|
|
672
|
-
</span>
|
|
673
|
-
)}
|
|
674
678
|
</div>
|
|
675
679
|
</div>
|
|
676
|
-
|
|
677
|
-
<div className="shrink-0">
|
|
678
|
-
<button
|
|
679
|
-
type="button"
|
|
680
|
-
onClick={openConfigModal}
|
|
681
|
-
className="inline-flex cursor-pointer items-center gap-2 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-semibold text-gray-700 shadow-sm transition-colors hover:bg-gray-50 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:outline-none"
|
|
682
|
-
>
|
|
683
|
-
<SettingsIcon className="h-4 w-4" />
|
|
684
|
-
Configurer
|
|
685
|
-
</button>
|
|
686
|
-
</div>
|
|
687
680
|
</div>
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
<div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>
|
|
693
|
-
</div>
|
|
694
|
-
)}
|
|
695
|
-
{/* Board kanban : taille adaptée au contenu avec padding haut/bas */}
|
|
696
|
-
<div
|
|
697
|
-
className="flex gap-4 overflow-x-auto px-4 pt-4 pb-4 sm:px-6 sm:pt-6 sm:pb-6"
|
|
698
|
-
style={{ scrollbarWidth: 'none' as 'none' }} // for Firefox
|
|
699
|
-
>
|
|
700
|
-
<style jsx>{`
|
|
701
|
-
div::-webkit-scrollbar {
|
|
702
|
-
display: none;
|
|
703
|
-
}
|
|
704
|
-
`}</style>
|
|
705
|
-
{sortedColumns.map((column) => {
|
|
706
|
-
const columnContacts = contactsByStatusId.get(column.statusId) || [];
|
|
707
|
-
const status = column.statusId ? statusesById[column.statusId] : null;
|
|
708
|
-
|
|
709
|
-
return (
|
|
710
|
-
<div
|
|
711
|
-
key={column.id}
|
|
712
|
-
className={cn(
|
|
713
|
-
'flex shrink-0 flex-col rounded-xl bg-gray-50 shadow-sm transition-colors',
|
|
714
|
-
'h-auto max-h-[calc(100vh-220px)]',
|
|
715
|
-
dragOverStatusId === column.statusId && 'ring-2 ring-indigo-400 ring-offset-2',
|
|
716
|
-
dragOverColumnId === column.id &&
|
|
717
|
-
draggedColumnId &&
|
|
718
|
-
'ring-2 ring-indigo-500 ring-offset-2',
|
|
719
|
-
)}
|
|
720
|
-
style={{ width: column.width }}
|
|
721
|
-
onDragOver={(e) => {
|
|
722
|
-
// Gérer le drag des colonnes ET des contacts
|
|
723
|
-
if (draggedColumnId) {
|
|
724
|
-
handleColumnHeaderDragOver(e, column.id);
|
|
725
|
-
} else {
|
|
726
|
-
handleColumnDragOver(e, column.statusId);
|
|
727
|
-
}
|
|
728
|
-
}}
|
|
729
|
-
onDrop={(e) => {
|
|
730
|
-
e.preventDefault();
|
|
731
|
-
if (draggedColumnId) {
|
|
732
|
-
handleColumnHeaderDrop(column.id);
|
|
733
|
-
} else {
|
|
734
|
-
handleColumnDrop(column.statusId);
|
|
735
|
-
}
|
|
736
|
-
}}
|
|
737
|
-
onDragLeave={() => {
|
|
738
|
-
if (!draggedColumnId) {
|
|
739
|
-
setDragOverStatusId(null);
|
|
740
|
-
}
|
|
741
|
-
if (draggedColumnId && dragOverColumnId === column.id) {
|
|
742
|
-
setDragOverColumnId(null);
|
|
743
|
-
}
|
|
744
|
-
}}
|
|
745
|
-
>
|
|
746
|
-
<div
|
|
747
|
-
draggable
|
|
748
|
-
onDragStart={(e) => {
|
|
749
|
-
// Ne pas drag si on drag déjà un contact
|
|
750
|
-
if (!draggedCard && !draggingContactId) {
|
|
751
|
-
handleColumnHeaderDragStart(column.id);
|
|
752
|
-
} else {
|
|
753
|
-
e.preventDefault();
|
|
754
|
-
}
|
|
755
|
-
}}
|
|
756
|
-
onDragEnd={() => {
|
|
757
|
-
setDraggedColumnId(null);
|
|
758
|
-
setDragOverColumnId(null);
|
|
759
|
-
}}
|
|
760
|
-
className={cn(
|
|
761
|
-
'flex shrink-0 items-center justify-between rounded-t-xl px-4 py-3 text-sm font-semibold text-white',
|
|
762
|
-
'cursor-grab transition-opacity active:cursor-grabbing',
|
|
763
|
-
draggedColumnId === column.id && 'opacity-50',
|
|
764
|
-
)}
|
|
765
|
-
style={{ backgroundColor: column.color || '#4f46e5' }}
|
|
766
|
-
>
|
|
767
|
-
<div className="flex items-center gap-2">
|
|
768
|
-
<span>{column.title || status?.name || 'Colonne'}</span>
|
|
769
|
-
<span className="rounded-full bg-white/20 px-2 py-0.5 text-xs font-medium">
|
|
770
|
-
{columnContacts.length}
|
|
771
|
-
</span>
|
|
772
|
-
</div>
|
|
773
|
-
</div>
|
|
774
|
-
|
|
775
|
-
<div
|
|
776
|
-
className="min-h-0 flex-1 space-y-3 overflow-y-auto p-3"
|
|
777
|
-
onDragStart={(e) => {
|
|
778
|
-
// Empêcher le drag du header si on drag un contact
|
|
779
|
-
if (draggingContactId || draggedCard) {
|
|
780
|
-
e.stopPropagation();
|
|
781
|
-
}
|
|
782
|
-
}}
|
|
783
|
-
>
|
|
784
|
-
{columnContacts.length === 0 ? (
|
|
785
|
-
<div className="mt-4 rounded-lg border border-dashed border-gray-200 bg-white/40 p-4 text-center text-xs text-gray-400">
|
|
786
|
-
Les contacts avec ce statut apparaîtront ici
|
|
787
|
-
</div>
|
|
788
|
-
) : (
|
|
789
|
-
columnContacts.map((contact) => (
|
|
790
|
-
<KanbanContactCard
|
|
791
|
-
key={contact.id}
|
|
792
|
-
contact={contact}
|
|
793
|
-
isDragging={draggingContactId === contact.id}
|
|
794
|
-
onDragStart={() => handleCardDragStart(contact.id, contact.statusId)}
|
|
795
|
-
onDragEnd={() => {
|
|
796
|
-
setDraggingContactId(null);
|
|
797
|
-
setDragOverStatusId(null);
|
|
798
|
-
}}
|
|
799
|
-
/>
|
|
800
|
-
))
|
|
801
|
-
)}
|
|
802
|
-
</div>
|
|
803
|
-
</div>
|
|
804
|
-
);
|
|
805
|
-
})}
|
|
806
|
-
</div>
|
|
807
|
-
{showConfigModal && (
|
|
808
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/30 p-4 backdrop-blur-sm">
|
|
809
|
-
<div className="flex w-full max-w-7xl flex-col rounded-2xl bg-white shadow-xl">
|
|
810
|
-
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
|
811
|
-
<div>
|
|
812
|
-
<h2 className="text-lg font-semibold text-gray-900">Configuration du Pipeline</h2>
|
|
813
|
-
<p className="mt-1 text-sm text-gray-500">
|
|
814
|
-
Personnalisez les colonnes de votre pipeline de closing.
|
|
815
|
-
</p>
|
|
681
|
+
{(statusesError || contactsError) && (
|
|
682
|
+
<div className="px-4 pt-4 sm:px-6 sm:pt-6">
|
|
683
|
+
<div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">
|
|
684
|
+
Erreur de chargement des donnees du pipeline.
|
|
816
685
|
</div>
|
|
817
|
-
<button
|
|
818
|
-
type="button"
|
|
819
|
-
onClick={() => setShowConfigModal(false)}
|
|
820
|
-
className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100"
|
|
821
|
-
>
|
|
822
|
-
<X className="h-5 w-5" />
|
|
823
|
-
</button>
|
|
824
686
|
</div>
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
687
|
+
)}
|
|
688
|
+
{/* Board kanban : taille adaptée au contenu avec padding haut/bas */}
|
|
689
|
+
<div
|
|
690
|
+
className="flex gap-4 overflow-x-auto px-4 pt-4 pb-4 sm:px-6 sm:pt-6 sm:pb-6"
|
|
691
|
+
style={SCROLLBAR_HIDDEN}
|
|
692
|
+
>
|
|
693
|
+
<style jsx>{`
|
|
694
|
+
div::-webkit-scrollbar {
|
|
695
|
+
display: none;
|
|
696
|
+
}
|
|
697
|
+
`}</style>
|
|
698
|
+
{sortedColumns.length === 0 ? (
|
|
699
|
+
<div className="flex w-full items-center justify-center py-16">
|
|
700
|
+
<div className="max-w-md rounded-xl border-2 border-dashed border-border bg-card p-8 text-center shadow-(--shadow-card)">
|
|
701
|
+
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-primary/15">
|
|
702
|
+
<Filter className="h-8 w-8 text-primary" />
|
|
703
|
+
</div>
|
|
704
|
+
<h3 className="mb-2 text-lg font-semibold text-foreground">
|
|
705
|
+
Votre pipeline est vide
|
|
706
|
+
</h3>
|
|
707
|
+
<p className="mb-6 text-sm text-muted-foreground">
|
|
708
|
+
Commencez par configurer les colonnes de votre pipeline de closing en cliquant
|
|
709
|
+
sur le bouton ci-dessous.
|
|
837
710
|
</p>
|
|
838
|
-
</div>
|
|
839
|
-
<div className="flex items-center gap-2">
|
|
840
|
-
<button
|
|
841
|
-
type="button"
|
|
842
|
-
onClick={resetToDefaultColumns}
|
|
843
|
-
className="inline-flex cursor-pointer items-center gap-1 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50"
|
|
844
|
-
>
|
|
845
|
-
Réinitialiser
|
|
846
|
-
</button>
|
|
847
711
|
<button
|
|
848
712
|
type="button"
|
|
849
|
-
onClick={
|
|
850
|
-
className="inline-flex cursor-pointer items-center gap-2 rounded-lg
|
|
713
|
+
onClick={openConfigModal}
|
|
714
|
+
className="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-primary px-4 py-2.5 text-sm font-semibold text-primary-foreground shadow-(--shadow-card) transition-colors duration-200 hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:outline-none"
|
|
851
715
|
>
|
|
852
|
-
<
|
|
853
|
-
|
|
716
|
+
<SettingsIcon className="h-4 w-4" />
|
|
717
|
+
Configurer ma pipeline
|
|
854
718
|
</button>
|
|
855
719
|
</div>
|
|
856
720
|
</div>
|
|
721
|
+
) : (
|
|
722
|
+
sortedColumns.map((column) => {
|
|
723
|
+
const columnContacts = contactsByStatusId.get(column.statusId) || [];
|
|
724
|
+
const status = column.statusId ? statusesById[column.statusId] : null;
|
|
857
725
|
|
|
858
|
-
|
|
859
|
-
{configColumns.map((column) => (
|
|
726
|
+
return (
|
|
860
727
|
<div
|
|
861
728
|
key={column.id}
|
|
862
|
-
|
|
863
|
-
|
|
729
|
+
className={cn(
|
|
730
|
+
'flex shrink-0 flex-col rounded-xl bg-muted shadow-sm transition-colors',
|
|
731
|
+
'h-auto max-h-[calc(100vh-220px)]',
|
|
732
|
+
dragOverStatusId === column.statusId &&
|
|
733
|
+
'ring-2 ring-blue-400 ring-offset-2',
|
|
734
|
+
dragOverColumnId === column.id &&
|
|
735
|
+
draggedColumnId &&
|
|
736
|
+
'ring-2 ring-blue-500 ring-offset-2',
|
|
737
|
+
)}
|
|
738
|
+
style={{ width: column.width }}
|
|
864
739
|
onDragOver={(e) => {
|
|
865
|
-
|
|
866
|
-
|
|
740
|
+
// Gérer le drag des colonnes ET des contacts
|
|
741
|
+
if (draggedColumnId) {
|
|
742
|
+
handleColumnHeaderDragOver(e, column.id);
|
|
743
|
+
} else {
|
|
744
|
+
handleColumnDragOver(e, column.statusId);
|
|
745
|
+
}
|
|
867
746
|
}}
|
|
868
747
|
onDrop={(e) => {
|
|
869
748
|
e.preventDefault();
|
|
870
|
-
|
|
749
|
+
if (draggedColumnId) {
|
|
750
|
+
handleColumnHeaderDrop(column.id);
|
|
751
|
+
} else {
|
|
752
|
+
handleColumnDrop(column.statusId);
|
|
753
|
+
}
|
|
754
|
+
}}
|
|
755
|
+
onDragLeave={() => {
|
|
756
|
+
if (!draggedColumnId) {
|
|
757
|
+
setDragOverStatusId(null);
|
|
758
|
+
}
|
|
759
|
+
if (draggedColumnId && dragOverColumnId === column.id) {
|
|
760
|
+
setDragOverColumnId(null);
|
|
761
|
+
}
|
|
871
762
|
}}
|
|
872
|
-
onDragLeave={() =>
|
|
873
|
-
setDragOverConfigColumnId((current) =>
|
|
874
|
-
current === column.id ? null : current,
|
|
875
|
-
)
|
|
876
|
-
}
|
|
877
|
-
className={cn(
|
|
878
|
-
'flex w-80 shrink-0 flex-col rounded-xl border border-gray-200 bg-gray-50',
|
|
879
|
-
'cursor-grab transition-all duration-150 active:cursor-grabbing',
|
|
880
|
-
'hover:-translate-y-1 hover:shadow-lg',
|
|
881
|
-
dragOverConfigColumnId === column.id &&
|
|
882
|
-
'ring-2 ring-indigo-400 ring-offset-2',
|
|
883
|
-
)}
|
|
884
763
|
>
|
|
885
|
-
<div
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
/>
|
|
894
|
-
<button
|
|
895
|
-
type="button"
|
|
896
|
-
onClick={() => removeConfigColumn(column.id)}
|
|
897
|
-
className="ml-2 cursor-pointer rounded-lg p-1 text-gray-400 transition-colors hover:bg-gray-200 hover:text-red-600"
|
|
898
|
-
>
|
|
899
|
-
<X className="h-4 w-4" />
|
|
900
|
-
</button>
|
|
901
|
-
</div>
|
|
902
|
-
|
|
903
|
-
<label className="mb-2 block text-xs font-medium text-gray-600">
|
|
904
|
-
Statut associé
|
|
905
|
-
</label>
|
|
906
|
-
<select
|
|
907
|
-
value={column.statusId || ''}
|
|
908
|
-
onChange={(e) =>
|
|
909
|
-
updateConfigColumn(column.id, {
|
|
910
|
-
statusId: e.target.value || null,
|
|
911
|
-
title:
|
|
912
|
-
e.target.value && statusesById[e.target.value]
|
|
913
|
-
? statusesById[e.target.value].name
|
|
914
|
-
: column.title,
|
|
915
|
-
color:
|
|
916
|
-
e.target.value && statusesById[e.target.value]
|
|
917
|
-
? statusesById[e.target.value].color
|
|
918
|
-
: column.color,
|
|
919
|
-
})
|
|
764
|
+
<div
|
|
765
|
+
draggable
|
|
766
|
+
onDragStart={(e) => {
|
|
767
|
+
// Ne pas drag si on drag déjà un contact
|
|
768
|
+
if (!draggedCard && !draggingContactId) {
|
|
769
|
+
handleColumnHeaderDragStart(column.id);
|
|
770
|
+
} else {
|
|
771
|
+
e.preventDefault();
|
|
920
772
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
<
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
onChange={(e) => updateConfigColumn(column.id, { color: e.target.value })}
|
|
939
|
-
className="h-8 w-12 cursor-pointer rounded border border-gray-300 bg-white"
|
|
940
|
-
/>
|
|
941
|
-
<input
|
|
942
|
-
type="text"
|
|
943
|
-
value={column.color}
|
|
944
|
-
onChange={(e) => updateConfigColumn(column.id, { color: e.target.value })}
|
|
945
|
-
className="flex-1 rounded-lg border border-gray-300 px-2 py-1.5 text-xs text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
946
|
-
/>
|
|
773
|
+
}}
|
|
774
|
+
onDragEnd={() => {
|
|
775
|
+
setDraggedColumnId(null);
|
|
776
|
+
setDragOverColumnId(null);
|
|
777
|
+
}}
|
|
778
|
+
className={cn(
|
|
779
|
+
'flex shrink-0 items-center justify-between rounded-t-xl px-4 py-3 text-sm font-semibold text-white',
|
|
780
|
+
'cursor-grab transition-opacity active:cursor-grabbing',
|
|
781
|
+
draggedColumnId === column.id && 'opacity-50',
|
|
782
|
+
)}
|
|
783
|
+
style={{ backgroundColor: column.color || '#4f46e5' }}
|
|
784
|
+
>
|
|
785
|
+
<div className="flex items-center gap-2">
|
|
786
|
+
<span>{column.title || status?.name || 'Colonne'}</span>
|
|
787
|
+
<span className="rounded-full bg-white/20 px-2 py-0.5 text-xs font-medium">
|
|
788
|
+
{columnContacts.length}
|
|
789
|
+
</span>
|
|
947
790
|
</div>
|
|
791
|
+
</div>
|
|
948
792
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
max={MAX_WIDTH}
|
|
956
|
-
value={column.width}
|
|
957
|
-
onChange={(e) =>
|
|
958
|
-
updateConfigColumn(column.id, {
|
|
959
|
-
width: Number(e.target.value),
|
|
960
|
-
})
|
|
793
|
+
<div
|
|
794
|
+
className="min-h-0 flex-1 space-y-3 overflow-y-auto p-3"
|
|
795
|
+
onDragStart={(e) => {
|
|
796
|
+
// Empêcher le drag du header si on drag un contact
|
|
797
|
+
if (draggingContactId || draggedCard) {
|
|
798
|
+
e.stopPropagation();
|
|
961
799
|
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
800
|
+
}}
|
|
801
|
+
>
|
|
802
|
+
{columnContacts.length === 0 ? (
|
|
803
|
+
<div className="mt-4 rounded-lg border border-dashed border-gray-200 bg-white/40 p-4 text-center text-xs text-gray-400">
|
|
804
|
+
Les contacts avec ce statut apparaîtront ici
|
|
805
|
+
</div>
|
|
806
|
+
) : (
|
|
807
|
+
columnContacts.map((contact) => (
|
|
808
|
+
<KanbanContactCard
|
|
809
|
+
key={contact.id}
|
|
810
|
+
contact={contact}
|
|
811
|
+
isDragging={draggingContactId === contact.id}
|
|
812
|
+
canDrag={canManagePipeline}
|
|
813
|
+
onDragStart={() => handleCardDragStart(contact.id, contact.statusId)}
|
|
814
|
+
onDragEnd={() => {
|
|
815
|
+
setDraggingContactId(null);
|
|
816
|
+
setDragOverStatusId(null);
|
|
817
|
+
}}
|
|
818
|
+
/>
|
|
819
|
+
))
|
|
820
|
+
)}
|
|
965
821
|
</div>
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
822
|
+
</div>
|
|
823
|
+
);
|
|
824
|
+
})
|
|
825
|
+
)}
|
|
826
|
+
</div>
|
|
827
|
+
{showConfigModal && (
|
|
828
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/30 p-4 backdrop-blur-sm">
|
|
829
|
+
<div className="flex w-full max-w-7xl flex-col rounded-2xl bg-white shadow-xl">
|
|
830
|
+
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
|
831
|
+
<div>
|
|
832
|
+
<h2 className="text-lg font-semibold text-gray-900">
|
|
833
|
+
Configuration du Pipeline
|
|
834
|
+
</h2>
|
|
835
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
836
|
+
Personnalisez les colonnes de votre pipeline de closing.
|
|
837
|
+
</p>
|
|
838
|
+
</div>
|
|
839
|
+
<button
|
|
840
|
+
type="button"
|
|
841
|
+
onClick={() => setShowConfigModal(false)}
|
|
842
|
+
className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100"
|
|
843
|
+
>
|
|
844
|
+
<X className="h-5 w-5" />
|
|
845
|
+
</button>
|
|
846
|
+
</div>
|
|
847
|
+
|
|
848
|
+
<div className="flex-1 overflow-y-auto px-6 py-4">
|
|
849
|
+
<div className="mb-3 flex items-center justify-between">
|
|
850
|
+
<div>
|
|
851
|
+
<p className="text-sm text-gray-600">
|
|
852
|
+
{configColumns.length} colonne
|
|
853
|
+
{configColumns.length > 1 ? 's' : ''} configurée
|
|
854
|
+
{configColumns.length > 1 ? 's' : ''}.
|
|
855
|
+
</p>
|
|
856
|
+
<p className="mt-1 text-xs text-gray-400">
|
|
857
|
+
Glissez-déposez les colonnes ci-dessous pour organiser l'ordre de votre
|
|
858
|
+
pipeline.
|
|
972
859
|
</p>
|
|
973
860
|
</div>
|
|
861
|
+
<div className="flex items-center gap-2">
|
|
862
|
+
<button
|
|
863
|
+
type="button"
|
|
864
|
+
onClick={resetToDefaultColumns}
|
|
865
|
+
className="inline-flex cursor-pointer items-center gap-1 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50"
|
|
866
|
+
>
|
|
867
|
+
Réinitialiser
|
|
868
|
+
</button>
|
|
869
|
+
<button
|
|
870
|
+
type="button"
|
|
871
|
+
onClick={addConfigColumn}
|
|
872
|
+
className="inline-flex cursor-pointer items-center gap-2 rounded-lg border border-blue-600 bg-white px-3 py-1.5 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-50"
|
|
873
|
+
>
|
|
874
|
+
<Plus className="h-4 w-4" />
|
|
875
|
+
Ajouter une colonne
|
|
876
|
+
</button>
|
|
877
|
+
</div>
|
|
974
878
|
</div>
|
|
975
|
-
))}
|
|
976
|
-
</div>
|
|
977
|
-
</div>
|
|
978
879
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
880
|
+
<div className="flex gap-4 overflow-x-auto p-2">
|
|
881
|
+
{configColumns.map((column) => (
|
|
882
|
+
<div
|
|
883
|
+
key={column.id}
|
|
884
|
+
draggable
|
|
885
|
+
onDragStart={() => setDraggedConfigColumnId(column.id)}
|
|
886
|
+
onDragOver={(e) => {
|
|
887
|
+
e.preventDefault();
|
|
888
|
+
setDragOverConfigColumnId(column.id);
|
|
889
|
+
}}
|
|
890
|
+
onDrop={(e) => {
|
|
891
|
+
e.preventDefault();
|
|
892
|
+
handleConfigColumnDrop(column.id);
|
|
893
|
+
}}
|
|
894
|
+
onDragLeave={() =>
|
|
895
|
+
setDragOverConfigColumnId((current) =>
|
|
896
|
+
current === column.id ? null : current,
|
|
897
|
+
)
|
|
898
|
+
}
|
|
899
|
+
className={cn(
|
|
900
|
+
'flex w-80 shrink-0 flex-col rounded-xl border border-gray-200 bg-gray-50',
|
|
901
|
+
'cursor-grab transition-all duration-150 active:cursor-grabbing',
|
|
902
|
+
'hover:-translate-y-1 hover:shadow-lg',
|
|
903
|
+
dragOverConfigColumnId === column.id &&
|
|
904
|
+
'ring-2 ring-blue-400 ring-offset-2',
|
|
905
|
+
)}
|
|
906
|
+
>
|
|
907
|
+
<div className="p-4">
|
|
908
|
+
<div className="mb-3 flex items-center justify-between">
|
|
909
|
+
<input
|
|
910
|
+
type="text"
|
|
911
|
+
value={column.title}
|
|
912
|
+
onChange={(e) =>
|
|
913
|
+
updateConfigColumn(column.id, { title: e.target.value })
|
|
914
|
+
}
|
|
915
|
+
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
916
|
+
placeholder="Nom de la colonne"
|
|
917
|
+
/>
|
|
918
|
+
<button
|
|
919
|
+
type="button"
|
|
920
|
+
onClick={() => removeConfigColumn(column.id)}
|
|
921
|
+
className="ml-2 cursor-pointer rounded-lg p-1 text-gray-400 transition-colors hover:bg-gray-200 hover:text-red-600"
|
|
922
|
+
>
|
|
923
|
+
<X className="h-4 w-4" />
|
|
924
|
+
</button>
|
|
925
|
+
</div>
|
|
926
|
+
|
|
927
|
+
<label className="mb-2 block text-xs font-medium text-gray-600">
|
|
928
|
+
Statut associé
|
|
929
|
+
</label>
|
|
930
|
+
<select
|
|
931
|
+
value={column.statusId || ''}
|
|
932
|
+
onChange={(e) =>
|
|
933
|
+
updateConfigColumn(column.id, {
|
|
934
|
+
statusId: e.target.value || null,
|
|
935
|
+
title:
|
|
936
|
+
e.target.value && statusesById[e.target.value]
|
|
937
|
+
? statusesById[e.target.value].name
|
|
938
|
+
: column.title,
|
|
939
|
+
color:
|
|
940
|
+
e.target.value && statusesById[e.target.value]
|
|
941
|
+
? statusesById[e.target.value].color
|
|
942
|
+
: column.color,
|
|
943
|
+
})
|
|
944
|
+
}
|
|
945
|
+
className="mb-3 w-full rounded-lg border border-gray-300 px-2 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
946
|
+
>
|
|
947
|
+
<option value="">Sélectionnez un statut</option>
|
|
948
|
+
{statuses.map((status) => (
|
|
949
|
+
<option key={status.id} value={status.id}>
|
|
950
|
+
{status.name}
|
|
951
|
+
</option>
|
|
952
|
+
))}
|
|
953
|
+
</select>
|
|
954
|
+
|
|
955
|
+
<label className="mb-1 block text-xs font-medium text-gray-600">
|
|
956
|
+
Couleur de la colonne
|
|
957
|
+
</label>
|
|
958
|
+
<div className="mb-3 flex items-center gap-2">
|
|
959
|
+
<input
|
|
960
|
+
type="color"
|
|
961
|
+
value={column.color}
|
|
962
|
+
onChange={(e) =>
|
|
963
|
+
updateConfigColumn(column.id, { color: e.target.value })
|
|
964
|
+
}
|
|
965
|
+
className="h-8 w-12 cursor-pointer rounded border border-gray-300 bg-white"
|
|
966
|
+
/>
|
|
967
|
+
<input
|
|
968
|
+
type="text"
|
|
969
|
+
value={column.color}
|
|
970
|
+
onChange={(e) =>
|
|
971
|
+
updateConfigColumn(column.id, { color: e.target.value })
|
|
972
|
+
}
|
|
973
|
+
className="flex-1 rounded-lg border border-gray-300 px-2 py-1.5 text-xs text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
974
|
+
/>
|
|
975
|
+
</div>
|
|
976
|
+
|
|
977
|
+
<label className="mb-1 block text-xs font-medium text-gray-600">
|
|
978
|
+
Largeur de la colonne (px)
|
|
979
|
+
</label>
|
|
980
|
+
<input
|
|
981
|
+
type="range"
|
|
982
|
+
min={MIN_WIDTH}
|
|
983
|
+
max={MAX_WIDTH}
|
|
984
|
+
value={column.width}
|
|
985
|
+
onChange={(e) =>
|
|
986
|
+
updateConfigColumn(column.id, {
|
|
987
|
+
width: Number(e.target.value),
|
|
988
|
+
})
|
|
989
|
+
}
|
|
990
|
+
className="w-full"
|
|
991
|
+
/>
|
|
992
|
+
<div className="mt-1 text-right text-xs text-gray-500">
|
|
993
|
+
{column.width}px
|
|
994
|
+
</div>
|
|
995
|
+
</div>
|
|
996
|
+
<div className="flex h-[250px] flex-col items-center justify-center rounded-b-xl bg-white">
|
|
997
|
+
<div className="mb-4 flex h-20 w-20 items-center justify-center rounded-full bg-gray-100">
|
|
998
|
+
<Filter className="h-8 w-8 text-gray-400" />
|
|
999
|
+
</div>
|
|
1000
|
+
<p className="px-6 text-center text-sm font-medium text-gray-400">
|
|
1001
|
+
Les contacts avec ce statut apparaîtront ici
|
|
1002
|
+
</p>
|
|
1003
|
+
</div>
|
|
1004
|
+
</div>
|
|
1005
|
+
))}
|
|
1006
|
+
</div>
|
|
1007
|
+
</div>
|
|
1008
|
+
|
|
1009
|
+
<div className="flex items-center justify-between border-t border-gray-200 px-6 py-4">
|
|
1010
|
+
<button
|
|
1011
|
+
type="button"
|
|
1012
|
+
onClick={() => setShowConfigModal(false)}
|
|
1013
|
+
className="cursor-pointer rounded-lg px-4 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100"
|
|
1014
|
+
>
|
|
1015
|
+
Annuler
|
|
1016
|
+
</button>
|
|
1017
|
+
<button
|
|
1018
|
+
type="button"
|
|
1019
|
+
onClick={handleSaveConfig}
|
|
1020
|
+
className="cursor-pointer rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground shadow-sm transition-colors hover:bg-primary/90"
|
|
1021
|
+
>
|
|
1022
|
+
Enregistrer la configuration
|
|
1023
|
+
</button>
|
|
1024
|
+
</div>
|
|
1025
|
+
</div>
|
|
994
1026
|
</div>
|
|
995
|
-
|
|
1027
|
+
)}
|
|
996
1028
|
</div>
|
|
997
1029
|
)}
|
|
998
|
-
</
|
|
1030
|
+
</ProtectedPage>
|
|
999
1031
|
);
|
|
1000
1032
|
}
|