create-crm-tmp 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-crm-tmp.js +56 -35
- package/package.json +1 -1
- package/template/README.md +230 -115
- package/template/eslint.config.mjs +13 -0
- package/template/next.config.ts +14 -0
- package/template/package.json +15 -2
- package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +132 -637
- package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
- package/template/src/app/(auth)/layout.tsx +1 -1
- package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
- package/template/src/app/(auth)/reset-password/page.tsx +4 -4
- package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
- package/template/src/app/(auth)/signin/page.tsx +14 -6
- package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
- package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
- package/template/src/app/(dashboard)/closing/page.tsx +78 -62
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
- package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
- package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
- package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
- package/template/src/app/(dashboard)/layout.tsx +6 -2
- package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
- package/template/src/app/(dashboard)/templates/page.tsx +55 -54
- package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
- package/template/src/app/(dashboard)/users/page.tsx +1 -1
- package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
- package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
- package/template/src/app/api/agenda/google-events/route.ts +92 -0
- package/template/src/app/api/auth/check-active/route.ts +3 -2
- package/template/src/app/api/auth/google/route.ts +2 -1
- package/template/src/app/api/auth/google/status/route.ts +7 -31
- package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
- package/template/src/app/api/companies/[id]/route.ts +1 -2
- package/template/src/app/api/companies/route.ts +42 -12
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
- package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
- package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
- package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
- package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
- package/template/src/app/api/contacts/[id]/route.ts +106 -34
- package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
- package/template/src/app/api/contacts/export/route.ts +9 -13
- package/template/src/app/api/contacts/import/route.ts +55 -25
- package/template/src/app/api/contacts/import-preview/route.ts +1 -1
- package/template/src/app/api/contacts/origins/route.ts +63 -0
- package/template/src/app/api/contacts/route.ts +153 -41
- package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
- package/template/src/app/api/dashboard/widgets/route.ts +181 -0
- package/template/src/app/api/dev/reminders/test/route.ts +114 -0
- package/template/src/app/api/editor/upload-image/route.ts +61 -0
- package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
- package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
- package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
- package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
- package/template/src/app/api/reminders/clear/route.ts +120 -0
- package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
- package/template/src/app/api/reminders/route.ts +164 -39
- package/template/src/app/api/reminders/state/route.ts +164 -0
- package/template/src/app/api/reset-password/request/route.ts +1 -1
- package/template/src/app/api/reset-password/verify/route.ts +1 -1
- package/template/src/app/api/send/route.ts +16 -4
- package/template/src/app/api/settings/google-ads/route.ts +14 -0
- package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
- package/template/src/app/api/settings/google-calendar/route.ts +124 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
- package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
- package/template/src/app/api/settings/google-sheet/route.ts +14 -0
- package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
- package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
- package/template/src/app/api/settings/meta-leads/route.ts +14 -2
- package/template/src/app/api/settings/smtp/route.ts +53 -6
- package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
- package/template/src/app/api/tasks/[id]/route.ts +234 -58
- package/template/src/app/api/tasks/meet/route.ts +27 -19
- package/template/src/app/api/tasks/route.ts +62 -17
- package/template/src/app/api/users/[id]/route.ts +20 -14
- package/template/src/app/api/users/list/route.ts +57 -19
- package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
- package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
- package/template/src/app/api/workflows/[id]/route.ts +0 -4
- package/template/src/app/api/workflows/process/route.ts +22 -51
- package/template/src/app/api/workflows/route.ts +0 -4
- package/template/src/app/globals.css +342 -4
- package/template/src/app/layout.tsx +11 -3
- package/template/src/app/page.tsx +1 -1
- package/template/src/components/address-autocomplete.tsx +7 -6
- package/template/src/components/config-error-alert.tsx +46 -0
- package/template/src/components/contacts/filter-bar.tsx +12 -3
- package/template/src/components/contacts/filter-builder.tsx +28 -43
- package/template/src/components/contacts/save-view-dialog.tsx +1 -1
- package/template/src/components/contacts/views-tab-bar.tsx +15 -6
- package/template/src/components/dashboard/activity-chart.tsx +41 -28
- package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
- package/template/src/components/dashboard/color-picker.tsx +64 -0
- package/template/src/components/dashboard/contacts-chart.tsx +69 -0
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
- package/template/src/components/dashboard/recent-activity.tsx +154 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -40
- package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
- package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
- package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
- package/template/src/components/date-picker.tsx +9 -6
- package/template/src/components/editor/upload-editor-image.ts +42 -0
- package/template/src/components/editor.tsx +161 -22
- package/template/src/components/email-template.tsx +2 -2
- package/template/src/components/global-search.tsx +30 -28
- package/template/src/components/header.tsx +178 -80
- package/template/src/components/inactive-account-guard.tsx +58 -0
- package/template/src/components/integration-notifications-listener.tsx +12 -0
- package/template/src/components/invitation-email-template.tsx +2 -2
- package/template/src/components/meet-cancellation-email-template.tsx +3 -3
- package/template/src/components/meet-confirmation-email-template.tsx +3 -3
- package/template/src/components/meet-update-email-template.tsx +3 -3
- package/template/src/components/page-header.tsx +5 -5
- package/template/src/components/protected-page.tsx +1 -1
- package/template/src/components/reset-password-email-template.tsx +2 -2
- package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
- package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
- package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
- package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
- package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
- package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
- package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
- package/template/src/components/sidebar.tsx +45 -26
- package/template/src/components/skeleton.tsx +40 -43
- package/template/src/components/ui/accordion.tsx +2 -2
- package/template/src/components/ui/alert-dialog.tsx +1 -1
- package/template/src/components/ui/button.tsx +20 -9
- package/template/src/components/ui/components.tsx +1 -1
- package/template/src/components/ui/date-picker.tsx +422 -0
- package/template/src/components/ui/datetime-picker.tsx +338 -0
- package/template/src/components/ui/status-select.tsx +271 -0
- package/template/src/components/ui/tooltip.tsx +37 -0
- package/template/src/components/view-as-modal.tsx +13 -7
- package/template/src/contexts/app-toast-context.tsx +245 -57
- package/template/src/contexts/dashboard-theme-context.tsx +53 -0
- package/template/src/contexts/sidebar-context.tsx +22 -17
- package/template/src/contexts/task-reminder-context.tsx +134 -160
- package/template/src/contexts/view-as-context.tsx +33 -6
- package/template/src/hooks/use-focus-trap.ts +2 -2
- package/template/src/hooks/useIntegrationNotifications.ts +49 -0
- package/template/src/lib/auth.ts +8 -1
- package/template/src/lib/config-links.ts +14 -0
- package/template/src/lib/contact-duplicate.ts +79 -61
- package/template/src/lib/contact-interactions.ts +21 -21
- package/template/src/lib/contact-view-filters.ts +24 -64
- package/template/src/lib/contacts-list-url.ts +190 -0
- package/template/src/lib/dashboard-stats.ts +65 -7
- package/template/src/lib/dashboard-themes.ts +135 -0
- package/template/src/lib/date-utils.ts +127 -0
- package/template/src/lib/default-widgets.ts +12 -0
- package/template/src/lib/editor-html-image-dimensions.ts +172 -0
- package/template/src/lib/editor-image-limits.ts +19 -0
- package/template/src/lib/email-html-sanitize.ts +19 -0
- package/template/src/lib/encryption.ts +9 -6
- package/template/src/lib/fr-geography.ts +192 -0
- package/template/src/lib/google-calendar-agenda.ts +201 -0
- package/template/src/lib/google-calendar.ts +255 -5
- package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
- package/template/src/lib/google-sheet-sync-runner.ts +514 -0
- package/template/src/lib/integration-import-log.ts +21 -0
- package/template/src/lib/permissions.ts +40 -10
- package/template/src/lib/prisma.ts +4 -1
- package/template/src/lib/qstash.ts +65 -0
- package/template/src/lib/reminder-state-server.ts +80 -0
- package/template/src/lib/reminder-state.ts +29 -0
- package/template/src/lib/supabase-storage.ts +113 -0
- package/template/src/lib/template-variables.ts +164 -23
- package/template/src/lib/utils.ts +45 -0
- package/template/src/lib/widget-registry.ts +173 -0
- package/template/src/lib/workflow-executor.ts +16 -70
- package/template/src/proxy.ts +1 -0
- package/template/vercel.json +3 -10
- package/template/skills-lock.json +0 -25
- package/template/src/components/dashboard/dashboard-content.tsx +0 -79
- package/template/src/lib/google-drive.ts +0 -1101
- package/template/src/types/yousign.ts +0 -52
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
|
3
|
+
import { useState, useEffect, useLayoutEffect, useRef, useMemo, useCallback } from 'react';
|
|
4
4
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
5
5
|
import { useUserRole } from '@/hooks/use-user-role';
|
|
6
6
|
import {
|
|
@@ -32,24 +32,31 @@ import {
|
|
|
32
32
|
Target,
|
|
33
33
|
} from 'lucide-react';
|
|
34
34
|
import { ContactTableSkeleton, ContactCardsSkeleton } from '@/components/skeleton';
|
|
35
|
-
import {
|
|
35
|
+
import { DatePicker } from '@/components/ui/date-picker';
|
|
36
|
+
import { cn, normalizePhoneNumber, formatDateTime, devToast } from '@/lib/utils';
|
|
36
37
|
import AddressAutocomplete from '@/components/address-autocomplete';
|
|
37
38
|
import { ProtectedPage } from '@/components/protected-page';
|
|
38
39
|
import { useConfirm } from '@/hooks/use-confirm';
|
|
39
|
-
import {
|
|
40
|
-
getRegionFromPostalCode,
|
|
41
|
-
getDepartmentFromPostalCode,
|
|
42
|
-
FRENCH_REGIONS,
|
|
43
|
-
FRENCH_DEPARTMENTS,
|
|
44
|
-
} from '@/lib/french-regions';
|
|
45
40
|
import { useContactViews } from '@/hooks/use-contact-views';
|
|
46
41
|
import { ViewsTabBar } from '@/components/contacts/views-tab-bar';
|
|
47
42
|
import { FilterBar } from '@/components/contacts/filter-bar';
|
|
48
43
|
import { FilterBuilder } from '@/components/contacts/filter-builder';
|
|
49
44
|
import { SaveViewDialog } from '@/components/contacts/save-view-dialog';
|
|
45
|
+
import { StatusSelect } from '@/components/ui/status-select';
|
|
50
46
|
import type { ViewFilter, ContactViewData } from '@/types/contact-views';
|
|
51
47
|
import { useSession } from '@/lib/auth-client';
|
|
52
48
|
import { useAppToast } from '@/contexts/app-toast-context';
|
|
49
|
+
import {
|
|
50
|
+
buildContactsListSearchParams,
|
|
51
|
+
canonicalSearchParamsString,
|
|
52
|
+
getContactsListUrlSearchParams,
|
|
53
|
+
contactsListColumnShowsActiveSort,
|
|
54
|
+
isContactsListDefaultSort,
|
|
55
|
+
parseContactsListFromSearchParams,
|
|
56
|
+
readContactsListFromLocationOrParams,
|
|
57
|
+
setsEqualToIds,
|
|
58
|
+
} from '@/lib/contacts-list-url';
|
|
59
|
+
import { FR_DEPARTMENTS, FR_REGIONS } from '@/lib/fr-geography';
|
|
53
60
|
|
|
54
61
|
interface Status {
|
|
55
62
|
id: string;
|
|
@@ -78,9 +85,12 @@ interface Contact {
|
|
|
78
85
|
city: string | null;
|
|
79
86
|
postalCode: string | null;
|
|
80
87
|
origin: string | null;
|
|
88
|
+
companyName: string | null;
|
|
81
89
|
companyId: string | null;
|
|
82
90
|
company: { id: string; name: string } | null;
|
|
83
91
|
jobTitle: string | null;
|
|
92
|
+
website: string | null;
|
|
93
|
+
socialNetworks: { platform: string; url: string }[] | null;
|
|
84
94
|
statusId: string | null;
|
|
85
95
|
status: Status | null;
|
|
86
96
|
assignedCommercialId: string | null;
|
|
@@ -123,6 +133,18 @@ interface TableColumn {
|
|
|
123
133
|
order: number;
|
|
124
134
|
}
|
|
125
135
|
|
|
136
|
+
type ContactsSortField =
|
|
137
|
+
| ''
|
|
138
|
+
| 'createdAt'
|
|
139
|
+
| 'updatedAt'
|
|
140
|
+
| 'status'
|
|
141
|
+
| 'commercial'
|
|
142
|
+
| 'telepro'
|
|
143
|
+
| 'postalCode'
|
|
144
|
+
| 'origin'
|
|
145
|
+
| 'region'
|
|
146
|
+
| 'department';
|
|
147
|
+
|
|
126
148
|
const DEFAULT_COLUMNS: TableColumn[] = [
|
|
127
149
|
{ id: 'contact', label: 'Contact', visible: true, order: 0 },
|
|
128
150
|
{ id: 'phone', label: 'Téléphone', visible: true, order: 1 },
|
|
@@ -138,6 +160,9 @@ const DEFAULT_COLUMNS: TableColumn[] = [
|
|
|
138
160
|
{ id: 'updatedAt', label: 'MODIFIÉ LE', visible: true, order: 11 },
|
|
139
161
|
];
|
|
140
162
|
|
|
163
|
+
/** Retour depuis une fiche : restaure le scroll du conteneur liste (sans scroll animé vers la ligne). */
|
|
164
|
+
const CONTACTS_LIST_SCROLL_RESTORE_KEY = 'crm-template:contactsListScrollRestore:v1';
|
|
165
|
+
|
|
141
166
|
const COMPANY_COLUMNS: TableColumn[] = [
|
|
142
167
|
{ id: 'companyName', label: 'Nom', visible: true, order: 0 },
|
|
143
168
|
{ id: 'phone', label: 'Téléphone', visible: true, order: 1 },
|
|
@@ -163,8 +188,7 @@ export default function ContactsPage() {
|
|
|
163
188
|
|
|
164
189
|
// --- Entity selector (Contacts / Entreprises) ---
|
|
165
190
|
const [viewEntity, setViewEntity] = useState<'contacts' | 'companies'>(() => {
|
|
166
|
-
|
|
167
|
-
return entity === 'companies' ? 'companies' : 'contacts';
|
|
191
|
+
return readContactsListFromLocationOrParams(searchParams).viewEntity;
|
|
168
192
|
});
|
|
169
193
|
const [showEntitySelector, setShowEntitySelector] = useState(false);
|
|
170
194
|
const entitySelectorRef = useRef<HTMLDivElement>(null);
|
|
@@ -457,21 +481,48 @@ export default function ContactsPage() {
|
|
|
457
481
|
const [defaultOrigin, setDefaultOrigin] = useState('');
|
|
458
482
|
|
|
459
483
|
// Filtres (multi-sélection via Set)
|
|
460
|
-
const [search, setSearch] = useState(
|
|
461
|
-
const [statusFilter, setStatusFilter] = useState<Set<string>>(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const [
|
|
465
|
-
|
|
466
|
-
|
|
484
|
+
const [search, setSearch] = useState(() => readContactsListFromLocationOrParams(searchParams).search);
|
|
485
|
+
const [statusFilter, setStatusFilter] = useState<Set<string>>(
|
|
486
|
+
() => new Set(readContactsListFromLocationOrParams(searchParams).statusIds),
|
|
487
|
+
);
|
|
488
|
+
const [originFilter, setOriginFilter] = useState<Set<string>>(
|
|
489
|
+
() => new Set(readContactsListFromLocationOrParams(searchParams).origins),
|
|
490
|
+
);
|
|
491
|
+
const [assignedCommercialFilter, setAssignedCommercialFilter] = useState<Set<string>>(
|
|
492
|
+
() => new Set(readContactsListFromLocationOrParams(searchParams).assignedCommercialIds),
|
|
493
|
+
);
|
|
494
|
+
const [assignedTeleproFilter, setAssignedTeleproFilter] = useState<Set<string>>(
|
|
495
|
+
() => new Set(readContactsListFromLocationOrParams(searchParams).assignedTeleproIds),
|
|
496
|
+
);
|
|
497
|
+
const [regionFilter, setRegionFilter] = useState<Set<string>>(
|
|
498
|
+
() => new Set(readContactsListFromLocationOrParams(searchParams).regionCodes),
|
|
499
|
+
);
|
|
500
|
+
const [departmentFilter, setDepartmentFilter] = useState<Set<string>>(
|
|
501
|
+
() => new Set(readContactsListFromLocationOrParams(searchParams).departmentCodes),
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
const [allOrigins, setAllOrigins] = useState<string[]>([]);
|
|
505
|
+
useEffect(() => {
|
|
506
|
+
if (viewEntity !== 'contacts') return;
|
|
507
|
+
let cancelled = false;
|
|
508
|
+
fetch('/api/contacts/origins')
|
|
509
|
+
.then((r) => (r.ok ? r.json() : { origins: [] }))
|
|
510
|
+
.then((d) => {
|
|
511
|
+
if (!cancelled && Array.isArray(d.origins)) setAllOrigins(d.origins);
|
|
512
|
+
})
|
|
513
|
+
.catch(() => {
|
|
514
|
+
if (!cancelled) setAllOrigins([]);
|
|
515
|
+
});
|
|
516
|
+
return () => {
|
|
517
|
+
cancelled = true;
|
|
518
|
+
};
|
|
519
|
+
}, [viewEntity]);
|
|
467
520
|
|
|
468
521
|
const originOptions = useMemo(() => {
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
return Array.from(origins).sort();
|
|
474
|
-
}, [contacts]);
|
|
522
|
+
const s = new Set(allOrigins);
|
|
523
|
+
originFilter.forEach((o) => s.add(o));
|
|
524
|
+
return [...s].sort((a, b) => a.localeCompare(b, 'fr'));
|
|
525
|
+
}, [allOrigins, originFilter]);
|
|
475
526
|
|
|
476
527
|
useEffect(() => {
|
|
477
528
|
if (!success) return;
|
|
@@ -516,39 +567,58 @@ export default function ContactsPage() {
|
|
|
516
567
|
top: number;
|
|
517
568
|
left: number;
|
|
518
569
|
} | null>(null);
|
|
519
|
-
const [createdAtStart, setCreatedAtStart] = useState(
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const [
|
|
570
|
+
const [createdAtStart, setCreatedAtStart] = useState(
|
|
571
|
+
() => readContactsListFromLocationOrParams(searchParams).createdAtStart,
|
|
572
|
+
);
|
|
573
|
+
const [createdAtEnd, setCreatedAtEnd] = useState(
|
|
574
|
+
() => readContactsListFromLocationOrParams(searchParams).createdAtEnd,
|
|
575
|
+
);
|
|
576
|
+
const [updatedAtStart, setUpdatedAtStart] = useState(
|
|
577
|
+
() => readContactsListFromLocationOrParams(searchParams).updatedAtStart,
|
|
578
|
+
);
|
|
579
|
+
const [updatedAtEnd, setUpdatedAtEnd] = useState(
|
|
580
|
+
() => readContactsListFromLocationOrParams(searchParams).updatedAtEnd,
|
|
581
|
+
);
|
|
523
582
|
|
|
524
583
|
// Tri
|
|
525
|
-
const [sortField, setSortField] = useState<
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
584
|
+
const [sortField, setSortField] = useState<ContactsSortField>(() => {
|
|
585
|
+
const raw = readContactsListFromLocationOrParams(searchParams).sortField || 'createdAt';
|
|
586
|
+
const allowed: ContactsSortField[] = [
|
|
587
|
+
'',
|
|
588
|
+
'createdAt',
|
|
589
|
+
'updatedAt',
|
|
590
|
+
'status',
|
|
591
|
+
'commercial',
|
|
592
|
+
'telepro',
|
|
593
|
+
'postalCode',
|
|
594
|
+
'origin',
|
|
595
|
+
'region',
|
|
596
|
+
'department',
|
|
597
|
+
];
|
|
598
|
+
return (allowed.includes(raw as ContactsSortField) ? raw : 'createdAt') as ContactsSortField;
|
|
599
|
+
});
|
|
600
|
+
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(
|
|
601
|
+
() => readContactsListFromLocationOrParams(searchParams).sortOrder,
|
|
602
|
+
);
|
|
537
603
|
const [showSortMenu, setShowSortMenu] = useState(false);
|
|
538
604
|
const [showDatePicker, setShowDatePicker] = useState(false);
|
|
539
|
-
const [dateRangeStart, setDateRangeStart] = useState('');
|
|
540
|
-
const [dateRangeEnd, setDateRangeEnd] = useState('');
|
|
541
605
|
const sortMenuRef = useRef<HTMLDivElement>(null);
|
|
542
606
|
const datePickerRef = useRef<HTMLDivElement>(null);
|
|
607
|
+
const listScrollContainerRef = useRef<HTMLDivElement | null>(null);
|
|
543
608
|
|
|
544
|
-
// Pagination
|
|
545
|
-
const [
|
|
609
|
+
// Pagination (initialisée depuis window.location pour conserver la page au retour depuis une fiche contact)
|
|
610
|
+
const VALID_LIMITS = [25, 50, 100] as const;
|
|
611
|
+
const [currentPage, setCurrentPage] = useState(
|
|
612
|
+
() => readContactsListFromLocationOrParams(searchParams).currentPage,
|
|
613
|
+
);
|
|
546
614
|
const [totalPages, setTotalPages] = useState(1);
|
|
547
615
|
const [totalContacts, setTotalContacts] = useState(0);
|
|
548
|
-
const [limit, setLimit] = useState(
|
|
616
|
+
const [limit, setLimit] = useState(() => readContactsListFromLocationOrParams(searchParams).limit);
|
|
549
617
|
|
|
550
618
|
// Entreprises state
|
|
551
619
|
const [companies, setCompanies] = useState<CompanyItem[]>([]);
|
|
620
|
+
/** Évite d’appliquer une réponse réseau plus lente après une requête plus récente (liste qui « recule »). */
|
|
621
|
+
const listFetchGenerationRef = useRef(0);
|
|
552
622
|
const [allCompanies, setAllCompanies] = useState<{ id: string; name: string }[]>([]);
|
|
553
623
|
const [showCompanyModal, setShowCompanyModal] = useState(false);
|
|
554
624
|
const [editingCompany, setEditingCompany] = useState<CompanyItem | null>(null);
|
|
@@ -567,9 +637,11 @@ export default function ContactsPage() {
|
|
|
567
637
|
assignedTeleproId: '',
|
|
568
638
|
});
|
|
569
639
|
|
|
570
|
-
// Vue (table ou cards) -
|
|
571
|
-
|
|
572
|
-
|
|
640
|
+
// Vue (table ou cards) - Initialisée depuis window.location puis localStorage pour restaurer au retour
|
|
641
|
+
const [viewMode, setViewMode] = useState<'table' | 'cards'>(() => {
|
|
642
|
+
const v = readContactsListFromLocationOrParams(searchParams).viewMode;
|
|
643
|
+
return v === 'cards' || v === 'table' ? v : 'table';
|
|
644
|
+
});
|
|
573
645
|
|
|
574
646
|
// Gestion des colonnes du tableau (avec récupération initiale depuis localStorage)
|
|
575
647
|
const [tableColumns, setTableColumns] = useState<TableColumn[]>(() => {
|
|
@@ -652,19 +724,36 @@ export default function ContactsPage() {
|
|
|
652
724
|
const [showExportModal, setShowExportModal] = useState(false);
|
|
653
725
|
const [exporting, setExporting] = useState(false);
|
|
654
726
|
const [exportAll, setExportAll] = useState(false);
|
|
655
|
-
|
|
727
|
+
/** True pendant l’application de l’URL (retour navigateur) : évite replace + reset page intempestifs */
|
|
728
|
+
const hydratingFromContactsUrlRef = useRef(false);
|
|
729
|
+
const lastContactsListUrlCanonRef = useRef<string | null>(null);
|
|
730
|
+
/** Incrémenté sur popstate pour relancer l’hydratation si useSearchParams() tarde. */
|
|
731
|
+
const [contactsUrlHistoryTick, setContactsUrlHistoryTick] = useState(0);
|
|
656
732
|
|
|
657
|
-
// Charger les préférences depuis localStorage après le montage
|
|
658
733
|
useEffect(() => {
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
}
|
|
734
|
+
const onPopState = () => {
|
|
735
|
+
lastContactsListUrlCanonRef.current = null;
|
|
736
|
+
setContactsUrlHistoryTick((t) => t + 1);
|
|
737
|
+
};
|
|
738
|
+
globalThis.window?.addEventListener('popstate', onPopState);
|
|
739
|
+
return () => globalThis.window?.removeEventListener('popstate', onPopState);
|
|
740
|
+
}, []);
|
|
663
741
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
742
|
+
// Charger les préférences depuis localStorage au premier montage (sans écraser si l'URL a déjà page/limit/view au retour)
|
|
743
|
+
useEffect(() => {
|
|
744
|
+
const urlView = searchParams.get('view');
|
|
745
|
+
const urlLimit = searchParams.get('limit');
|
|
746
|
+
if (!urlView && typeof window !== 'undefined') {
|
|
747
|
+
const savedView = localStorage.getItem('contactsViewMode');
|
|
748
|
+
if (savedView === 'cards' || savedView === 'table') setViewMode(savedView);
|
|
667
749
|
}
|
|
750
|
+
if (!urlLimit && typeof window !== 'undefined') {
|
|
751
|
+
const savedLimit = localStorage.getItem('contactsPageLimit');
|
|
752
|
+
if (savedLimit && ['25', '50', '100'].includes(savedLimit)) {
|
|
753
|
+
setLimit(Number.parseInt(savedLimit, 10));
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- uniquement au montage pour ne pas écraser l'URL au retour
|
|
668
757
|
}, []);
|
|
669
758
|
|
|
670
759
|
// Formulaire
|
|
@@ -681,32 +770,14 @@ export default function ContactsPage() {
|
|
|
681
770
|
origin: '',
|
|
682
771
|
companyId: '',
|
|
683
772
|
jobTitle: '',
|
|
773
|
+
website: '',
|
|
774
|
+
socialNetworks: [] as { platform: string; url: string }[],
|
|
684
775
|
statusId: '',
|
|
685
776
|
closingReason: '',
|
|
686
777
|
assignedCommercialId: '',
|
|
687
778
|
assignedTeleproId: '',
|
|
688
779
|
});
|
|
689
780
|
|
|
690
|
-
// Synchronisation automatique Google Sheets
|
|
691
|
-
useEffect(() => {
|
|
692
|
-
if (hasTriggeredGoogleSheetSyncRef.current) {
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
hasTriggeredGoogleSheetSyncRef.current = true;
|
|
696
|
-
|
|
697
|
-
const syncGoogleSheet = async () => {
|
|
698
|
-
try {
|
|
699
|
-
await fetch('/api/integrations/google-sheet/sync', {
|
|
700
|
-
method: 'POST',
|
|
701
|
-
});
|
|
702
|
-
} catch (err) {
|
|
703
|
-
console.error('Erreur lors de la synchronisation Google Sheets:', err);
|
|
704
|
-
}
|
|
705
|
-
};
|
|
706
|
-
|
|
707
|
-
syncGoogleSheet();
|
|
708
|
-
}, []);
|
|
709
|
-
|
|
710
781
|
// Charger les statuts et utilisateurs
|
|
711
782
|
useEffect(() => {
|
|
712
783
|
const fetchData = async () => {
|
|
@@ -761,6 +832,7 @@ export default function ContactsPage() {
|
|
|
761
832
|
}, [groupContactInfo]);
|
|
762
833
|
|
|
763
834
|
const fetchContacts = async () => {
|
|
835
|
+
const fetchGen = ++listFetchGenerationRef.current;
|
|
764
836
|
try {
|
|
765
837
|
setLoading(true);
|
|
766
838
|
setSelectedContactIds(new Set());
|
|
@@ -783,12 +855,30 @@ export default function ContactsPage() {
|
|
|
783
855
|
if (createdAtEnd) params.append('createdAtEnd', createdAtEnd);
|
|
784
856
|
if (updatedAtStart) params.append('updatedAtStart', updatedAtStart);
|
|
785
857
|
if (updatedAtEnd) params.append('updatedAtEnd', updatedAtEnd);
|
|
858
|
+
if (regionFilter.size > 0) {
|
|
859
|
+
params.append('regionCodes', Array.from(regionFilter).join(','));
|
|
860
|
+
}
|
|
861
|
+
if (departmentFilter.size > 0) {
|
|
862
|
+
params.append('departmentCodes', Array.from(departmentFilter).join(','));
|
|
863
|
+
}
|
|
864
|
+
if (sortField) {
|
|
865
|
+
params.append('sortField', sortField);
|
|
866
|
+
params.append('sortDir', sortOrder);
|
|
867
|
+
}
|
|
786
868
|
params.append('page', currentPage.toString());
|
|
787
869
|
params.append('limit', limit.toString());
|
|
788
870
|
|
|
789
|
-
const response = await fetch(`/api/contacts?${params.toString()}
|
|
871
|
+
const response = await fetch(`/api/contacts?${params.toString()}`, {
|
|
872
|
+
cache: 'no-store',
|
|
873
|
+
});
|
|
874
|
+
if (fetchGen !== listFetchGenerationRef.current) {
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
790
877
|
if (response.ok) {
|
|
791
878
|
const data = await response.json();
|
|
879
|
+
if (fetchGen !== listFetchGenerationRef.current) {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
792
882
|
setContacts(data.contacts || []);
|
|
793
883
|
if (data.pagination) {
|
|
794
884
|
setTotalPages(data.pagination.totalPages);
|
|
@@ -799,13 +889,18 @@ export default function ContactsPage() {
|
|
|
799
889
|
}
|
|
800
890
|
} catch (error) {
|
|
801
891
|
console.error('Erreur:', error);
|
|
802
|
-
|
|
892
|
+
if (fetchGen === listFetchGenerationRef.current) {
|
|
893
|
+
setError(devToast('Erreur lors du chargement des contacts', error));
|
|
894
|
+
}
|
|
803
895
|
} finally {
|
|
804
|
-
|
|
896
|
+
if (fetchGen === listFetchGenerationRef.current) {
|
|
897
|
+
setLoading(false);
|
|
898
|
+
}
|
|
805
899
|
}
|
|
806
900
|
};
|
|
807
901
|
|
|
808
902
|
const fetchCompanies = async () => {
|
|
903
|
+
const fetchGen = ++listFetchGenerationRef.current;
|
|
809
904
|
try {
|
|
810
905
|
setLoading(true);
|
|
811
906
|
setSelectedContactIds(new Set());
|
|
@@ -814,9 +909,17 @@ export default function ContactsPage() {
|
|
|
814
909
|
params.append('page', currentPage.toString());
|
|
815
910
|
params.append('limit', limit.toString());
|
|
816
911
|
|
|
817
|
-
const response = await fetch(`/api/companies?${params.toString()}
|
|
912
|
+
const response = await fetch(`/api/companies?${params.toString()}`, {
|
|
913
|
+
cache: 'no-store',
|
|
914
|
+
});
|
|
915
|
+
if (fetchGen !== listFetchGenerationRef.current) {
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
818
918
|
if (response.ok) {
|
|
819
919
|
const data = await response.json();
|
|
920
|
+
if (fetchGen !== listFetchGenerationRef.current) {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
820
923
|
setCompanies(data.companies || []);
|
|
821
924
|
if (data.pagination) {
|
|
822
925
|
setTotalPages(data.pagination.totalPages);
|
|
@@ -827,12 +930,37 @@ export default function ContactsPage() {
|
|
|
827
930
|
}
|
|
828
931
|
} catch (error) {
|
|
829
932
|
console.error('Erreur:', error);
|
|
830
|
-
|
|
933
|
+
if (fetchGen === listFetchGenerationRef.current) {
|
|
934
|
+
setError(devToast('Erreur lors du chargement des entreprises', error));
|
|
935
|
+
}
|
|
831
936
|
} finally {
|
|
832
|
-
|
|
937
|
+
if (fetchGen === listFetchGenerationRef.current) {
|
|
938
|
+
setLoading(false);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
/** Dernière fonction de rechargement liste (évite une closure périmée dans pageshow). */
|
|
944
|
+
const reloadContactsOrCompaniesRef = useRef<() => void>(() => {});
|
|
945
|
+
reloadContactsOrCompaniesRef.current = () => {
|
|
946
|
+
if (viewEntity === 'companies') {
|
|
947
|
+
void fetchCompanies();
|
|
948
|
+
} else {
|
|
949
|
+
void fetchContacts();
|
|
833
950
|
}
|
|
834
951
|
};
|
|
835
952
|
|
|
953
|
+
// Retour arrière / bfcache : la page peut être restaurée avec l’ancien state React sans remonter les effets.
|
|
954
|
+
useEffect(() => {
|
|
955
|
+
const onPageShow = (e: PageTransitionEvent) => {
|
|
956
|
+
if (e.persisted) {
|
|
957
|
+
reloadContactsOrCompaniesRef.current();
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
window.addEventListener('pageshow', onPageShow);
|
|
961
|
+
return () => window.removeEventListener('pageshow', onPageShow);
|
|
962
|
+
}, []);
|
|
963
|
+
|
|
836
964
|
const fetchAllCompanies = async () => {
|
|
837
965
|
try {
|
|
838
966
|
const res = await fetch('/api/companies?all=true');
|
|
@@ -858,7 +986,10 @@ export default function ContactsPage() {
|
|
|
858
986
|
setFormData((prev) => ({ ...prev, companyId: companyIdParam }));
|
|
859
987
|
setShowModal(true);
|
|
860
988
|
setEditingContact(null);
|
|
861
|
-
|
|
989
|
+
const p = new URLSearchParams(searchParams.toString());
|
|
990
|
+
p.delete('companyId');
|
|
991
|
+
const qs = p.toString();
|
|
992
|
+
router.replace(qs ? `/contacts?${qs}` : '/contacts', { scroll: false });
|
|
862
993
|
} else if (editCompanyParam) {
|
|
863
994
|
setViewEntity('companies');
|
|
864
995
|
fetch(`/api/companies/${editCompanyParam}`)
|
|
@@ -882,19 +1013,39 @@ export default function ContactsPage() {
|
|
|
882
1013
|
setEditingCompany(data);
|
|
883
1014
|
setShowCompanyModal(true);
|
|
884
1015
|
}
|
|
885
|
-
|
|
1016
|
+
const p = new URLSearchParams(searchParams.toString());
|
|
1017
|
+
p.set('entity', 'companies');
|
|
1018
|
+
p.delete('editCompany');
|
|
1019
|
+
router.replace(`/contacts?${p.toString()}`, { scroll: false });
|
|
886
1020
|
});
|
|
887
1021
|
}
|
|
888
1022
|
}, [searchParams, router]);
|
|
889
1023
|
|
|
890
|
-
// Réinitialiser à la page 1 quand les filtres ou le limit changent
|
|
1024
|
+
// Réinitialiser à la page 1 quand les filtres ou le limit changent (mais pas au premier montage)
|
|
1025
|
+
const filtersInitializedRef = useRef(false);
|
|
891
1026
|
useEffect(() => {
|
|
1027
|
+
if (!filtersInitializedRef.current) {
|
|
1028
|
+
filtersInitializedRef.current = true;
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
if (hydratingFromContactsUrlRef.current) {
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
// Au retour navigateur depuis une fiche contact, conserver la page de l'URL
|
|
1035
|
+
// tant que la restauration du scroll liste n'a pas encore été consommée.
|
|
1036
|
+
if (
|
|
1037
|
+
typeof window !== 'undefined' &&
|
|
1038
|
+
window.sessionStorage.getItem(CONTACTS_LIST_SCROLL_RESTORE_KEY)
|
|
1039
|
+
) {
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
892
1042
|
if (currentPage !== 1) {
|
|
893
1043
|
setCurrentPage(1);
|
|
894
1044
|
}
|
|
895
1045
|
}, [
|
|
896
1046
|
search,
|
|
897
1047
|
statusFilter,
|
|
1048
|
+
originFilter,
|
|
898
1049
|
assignedCommercialFilter,
|
|
899
1050
|
assignedTeleproFilter,
|
|
900
1051
|
createdAtStart,
|
|
@@ -904,8 +1055,136 @@ export default function ContactsPage() {
|
|
|
904
1055
|
limit,
|
|
905
1056
|
viewFilters,
|
|
906
1057
|
viewEntity,
|
|
1058
|
+
regionFilter,
|
|
1059
|
+
departmentFilter,
|
|
907
1060
|
]);
|
|
908
1061
|
|
|
1062
|
+
// Hydratation depuis l’URL en useLayoutEffect : le state doit être à jour AVANT les useEffect
|
|
1063
|
+
// (sinon l’effet d’écriture d’URL voit encore filtres vides et écrase statusIds au retour arrière).
|
|
1064
|
+
useLayoutEffect(() => {
|
|
1065
|
+
const sp = getContactsListUrlSearchParams(searchParams);
|
|
1066
|
+
const canon = canonicalSearchParamsString(sp);
|
|
1067
|
+
if (lastContactsListUrlCanonRef.current === canon) {
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
lastContactsListUrlCanonRef.current = canon;
|
|
1071
|
+
|
|
1072
|
+
hydratingFromContactsUrlRef.current = true;
|
|
1073
|
+
try {
|
|
1074
|
+
const p = parseContactsListFromSearchParams(sp);
|
|
1075
|
+
|
|
1076
|
+
setViewEntity((prev) => (prev === p.viewEntity ? prev : p.viewEntity));
|
|
1077
|
+
setCurrentPage((prev) => (prev === p.currentPage ? prev : p.currentPage));
|
|
1078
|
+
setLimit((prev) => (prev === p.limit ? prev : p.limit));
|
|
1079
|
+
setViewMode((prev) => (prev === p.viewMode ? prev : p.viewMode));
|
|
1080
|
+
setSearch((prev) => (prev === p.search ? prev : p.search));
|
|
1081
|
+
setStatusFilter((prev) =>
|
|
1082
|
+
setsEqualToIds(prev, p.statusIds) ? prev : new Set(p.statusIds),
|
|
1083
|
+
);
|
|
1084
|
+
setOriginFilter((prev) => (setsEqualToIds(prev, p.origins) ? prev : new Set(p.origins)));
|
|
1085
|
+
setAssignedCommercialFilter((prev) =>
|
|
1086
|
+
setsEqualToIds(prev, p.assignedCommercialIds) ? prev : new Set(p.assignedCommercialIds),
|
|
1087
|
+
);
|
|
1088
|
+
setAssignedTeleproFilter((prev) =>
|
|
1089
|
+
setsEqualToIds(prev, p.assignedTeleproIds) ? prev : new Set(p.assignedTeleproIds),
|
|
1090
|
+
);
|
|
1091
|
+
setRegionFilter((prev) =>
|
|
1092
|
+
setsEqualToIds(prev, p.regionCodes) ? prev : new Set(p.regionCodes),
|
|
1093
|
+
);
|
|
1094
|
+
setDepartmentFilter((prev) =>
|
|
1095
|
+
setsEqualToIds(prev, p.departmentCodes) ? prev : new Set(p.departmentCodes),
|
|
1096
|
+
);
|
|
1097
|
+
setCreatedAtStart((prev) => (prev === p.createdAtStart ? prev : p.createdAtStart));
|
|
1098
|
+
setCreatedAtEnd((prev) => (prev === p.createdAtEnd ? prev : p.createdAtEnd));
|
|
1099
|
+
setUpdatedAtStart((prev) => (prev === p.updatedAtStart ? prev : p.updatedAtStart));
|
|
1100
|
+
setUpdatedAtEnd((prev) => (prev === p.updatedAtEnd ? prev : p.updatedAtEnd));
|
|
1101
|
+
setSortField((prev) => {
|
|
1102
|
+
const next = (p.sortField || 'createdAt') as typeof prev;
|
|
1103
|
+
return prev === next ? prev : next;
|
|
1104
|
+
});
|
|
1105
|
+
setSortOrder((prev) => (prev === p.sortOrder ? prev : p.sortOrder));
|
|
1106
|
+
} finally {
|
|
1107
|
+
hydratingFromContactsUrlRef.current = false;
|
|
1108
|
+
}
|
|
1109
|
+
}, [searchParams, contactsUrlHistoryTick]);
|
|
1110
|
+
|
|
1111
|
+
// Écrit l’état liste dans l’URL pour que « Retour » depuis une fiche contact restaure filtres + page
|
|
1112
|
+
const desiredContactsListCanon = useMemo(
|
|
1113
|
+
() =>
|
|
1114
|
+
canonicalSearchParamsString(
|
|
1115
|
+
buildContactsListSearchParams({
|
|
1116
|
+
viewEntity,
|
|
1117
|
+
currentPage,
|
|
1118
|
+
limit,
|
|
1119
|
+
viewMode,
|
|
1120
|
+
search,
|
|
1121
|
+
statusFilter,
|
|
1122
|
+
originFilter,
|
|
1123
|
+
assignedCommercialFilter,
|
|
1124
|
+
assignedTeleproFilter,
|
|
1125
|
+
createdAtStart,
|
|
1126
|
+
createdAtEnd,
|
|
1127
|
+
updatedAtStart,
|
|
1128
|
+
updatedAtEnd,
|
|
1129
|
+
regionFilter,
|
|
1130
|
+
departmentFilter,
|
|
1131
|
+
sortField,
|
|
1132
|
+
sortOrder,
|
|
1133
|
+
}),
|
|
1134
|
+
),
|
|
1135
|
+
[
|
|
1136
|
+
viewEntity,
|
|
1137
|
+
currentPage,
|
|
1138
|
+
limit,
|
|
1139
|
+
viewMode,
|
|
1140
|
+
search,
|
|
1141
|
+
statusFilter,
|
|
1142
|
+
originFilter,
|
|
1143
|
+
assignedCommercialFilter,
|
|
1144
|
+
assignedTeleproFilter,
|
|
1145
|
+
createdAtStart,
|
|
1146
|
+
createdAtEnd,
|
|
1147
|
+
updatedAtStart,
|
|
1148
|
+
updatedAtEnd,
|
|
1149
|
+
regionFilter,
|
|
1150
|
+
departmentFilter,
|
|
1151
|
+
sortField,
|
|
1152
|
+
sortOrder,
|
|
1153
|
+
],
|
|
1154
|
+
);
|
|
1155
|
+
|
|
1156
|
+
useEffect(() => {
|
|
1157
|
+
if (typeof window === 'undefined') return;
|
|
1158
|
+
if (hydratingFromContactsUrlRef.current) {
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const cur = canonicalSearchParamsString(new URLSearchParams(window.location.search));
|
|
1162
|
+
if (desiredContactsListCanon === cur) {
|
|
1163
|
+
lastContactsListUrlCanonRef.current = cur;
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
const p = buildContactsListSearchParams({
|
|
1167
|
+
viewEntity,
|
|
1168
|
+
currentPage,
|
|
1169
|
+
limit,
|
|
1170
|
+
viewMode,
|
|
1171
|
+
search,
|
|
1172
|
+
statusFilter,
|
|
1173
|
+
originFilter,
|
|
1174
|
+
assignedCommercialFilter,
|
|
1175
|
+
assignedTeleproFilter,
|
|
1176
|
+
createdAtStart,
|
|
1177
|
+
createdAtEnd,
|
|
1178
|
+
updatedAtStart,
|
|
1179
|
+
updatedAtEnd,
|
|
1180
|
+
regionFilter,
|
|
1181
|
+
departmentFilter,
|
|
1182
|
+
sortField,
|
|
1183
|
+
sortOrder,
|
|
1184
|
+
});
|
|
1185
|
+
router.replace(`/contacts?${p.toString()}`, { scroll: false });
|
|
1186
|
+
}, [desiredContactsListCanon, router, viewEntity, currentPage, limit, viewMode, search, statusFilter, originFilter, assignedCommercialFilter, assignedTeleproFilter, createdAtStart, createdAtEnd, updatedAtStart, updatedAtEnd, regionFilter, departmentFilter, sortField, sortOrder]);
|
|
1187
|
+
|
|
909
1188
|
// Charger les contacts ou entreprises selon le mode
|
|
910
1189
|
useEffect(() => {
|
|
911
1190
|
if (viewEntity === 'companies') {
|
|
@@ -927,8 +1206,66 @@ export default function ContactsPage() {
|
|
|
927
1206
|
limit,
|
|
928
1207
|
viewFilters,
|
|
929
1208
|
viewEntity,
|
|
1209
|
+
sortField,
|
|
1210
|
+
sortOrder,
|
|
1211
|
+
regionFilter,
|
|
1212
|
+
departmentFilter,
|
|
930
1213
|
]);
|
|
931
1214
|
|
|
1215
|
+
const navigateToContactDetail = useCallback(
|
|
1216
|
+
(contactId: string) => {
|
|
1217
|
+
try {
|
|
1218
|
+
sessionStorage.setItem(
|
|
1219
|
+
CONTACTS_LIST_SCROLL_RESTORE_KEY,
|
|
1220
|
+
JSON.stringify({
|
|
1221
|
+
scrollTop: listScrollContainerRef.current?.scrollTop ?? 0,
|
|
1222
|
+
}),
|
|
1223
|
+
);
|
|
1224
|
+
} catch {
|
|
1225
|
+
/* ignore */
|
|
1226
|
+
}
|
|
1227
|
+
router.push(`/contacts/${contactId}`);
|
|
1228
|
+
},
|
|
1229
|
+
[router],
|
|
1230
|
+
);
|
|
1231
|
+
|
|
1232
|
+
// Au retour depuis une fiche contact : réappliquer le scroll du conteneur (pas d’animation)
|
|
1233
|
+
useLayoutEffect(() => {
|
|
1234
|
+
if (viewEntity !== 'contacts') return;
|
|
1235
|
+
if (loading) return;
|
|
1236
|
+
if (contacts.length === 0) return;
|
|
1237
|
+
|
|
1238
|
+
let raw: string | null = null;
|
|
1239
|
+
try {
|
|
1240
|
+
raw = sessionStorage.getItem(CONTACTS_LIST_SCROLL_RESTORE_KEY);
|
|
1241
|
+
} catch {
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
if (!raw) return;
|
|
1245
|
+
|
|
1246
|
+
let data: { scrollTop?: number };
|
|
1247
|
+
try {
|
|
1248
|
+
data = JSON.parse(raw) as { scrollTop?: number };
|
|
1249
|
+
} catch {
|
|
1250
|
+
sessionStorage.removeItem(CONTACTS_LIST_SCROLL_RESTORE_KEY);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
sessionStorage.removeItem(CONTACTS_LIST_SCROLL_RESTORE_KEY);
|
|
1255
|
+
|
|
1256
|
+
const runRestore = () => {
|
|
1257
|
+
const container = listScrollContainerRef.current;
|
|
1258
|
+
if (!container) return;
|
|
1259
|
+
if (typeof data.scrollTop === 'number' && data.scrollTop >= 0) {
|
|
1260
|
+
container.scrollTop = data.scrollTop;
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
requestAnimationFrame(() => {
|
|
1265
|
+
requestAnimationFrame(runRestore);
|
|
1266
|
+
});
|
|
1267
|
+
}, [loading, viewEntity, contacts]);
|
|
1268
|
+
|
|
932
1269
|
// Fermer la modal de filtre de date au clic en dehors
|
|
933
1270
|
useEffect(() => {
|
|
934
1271
|
const handleClickOutside = (event: MouseEvent) => {
|
|
@@ -1030,7 +1367,7 @@ export default function ContactsPage() {
|
|
|
1030
1367
|
const isLostStatus = selectedStatus?.requiresClosingReason === true;
|
|
1031
1368
|
|
|
1032
1369
|
if (isLostStatus && !formData.closingReason) {
|
|
1033
|
-
|
|
1370
|
+
toast.warning('Veuillez renseigner le motif de fermeture pour ce statut.');
|
|
1034
1371
|
return;
|
|
1035
1372
|
}
|
|
1036
1373
|
|
|
@@ -1070,6 +1407,8 @@ export default function ContactsPage() {
|
|
|
1070
1407
|
origin: '',
|
|
1071
1408
|
companyId: '',
|
|
1072
1409
|
jobTitle: '',
|
|
1410
|
+
website: '',
|
|
1411
|
+
socialNetworks: [],
|
|
1073
1412
|
statusId: '',
|
|
1074
1413
|
closingReason: '',
|
|
1075
1414
|
assignedCommercialId: '',
|
|
@@ -1079,7 +1418,7 @@ export default function ContactsPage() {
|
|
|
1079
1418
|
|
|
1080
1419
|
setTimeout(() => setSuccess(''), 5000);
|
|
1081
1420
|
} catch (err: any) {
|
|
1082
|
-
setError(err
|
|
1421
|
+
setError(devToast("Erreur lors de l'enregistrement du contact", err));
|
|
1083
1422
|
}
|
|
1084
1423
|
};
|
|
1085
1424
|
|
|
@@ -1123,7 +1462,7 @@ export default function ContactsPage() {
|
|
|
1123
1462
|
|
|
1124
1463
|
setTimeout(() => setSuccess(''), 5000);
|
|
1125
1464
|
} catch (err: any) {
|
|
1126
|
-
setError(err
|
|
1465
|
+
setError(devToast("Erreur lors de l'enregistrement de l'entreprise", err));
|
|
1127
1466
|
}
|
|
1128
1467
|
};
|
|
1129
1468
|
|
|
@@ -1149,6 +1488,7 @@ export default function ContactsPage() {
|
|
|
1149
1488
|
| 'createdAt'
|
|
1150
1489
|
| 'updatedAt'
|
|
1151
1490
|
| 'postalCode'
|
|
1491
|
+
| 'origin'
|
|
1152
1492
|
| 'region'
|
|
1153
1493
|
| 'department'
|
|
1154
1494
|
| '',
|
|
@@ -1216,12 +1556,6 @@ export default function ContactsPage() {
|
|
|
1216
1556
|
}
|
|
1217
1557
|
};
|
|
1218
1558
|
|
|
1219
|
-
const handleApplyDateFilter = () => {
|
|
1220
|
-
setDateFilterModal(null);
|
|
1221
|
-
setDateFilterPosition(null);
|
|
1222
|
-
setCurrentPage(1);
|
|
1223
|
-
};
|
|
1224
|
-
|
|
1225
1559
|
const handleClearDateFilter = (type: 'createdAt' | 'updatedAt') => {
|
|
1226
1560
|
if (type === 'createdAt') {
|
|
1227
1561
|
setCreatedAtStart('');
|
|
@@ -1249,7 +1583,7 @@ export default function ContactsPage() {
|
|
|
1249
1583
|
!!createdAtEnd ||
|
|
1250
1584
|
!!updatedAtStart ||
|
|
1251
1585
|
!!updatedAtEnd ||
|
|
1252
|
-
|
|
1586
|
+
!isContactsListDefaultSort(sortField, sortOrder)
|
|
1253
1587
|
);
|
|
1254
1588
|
};
|
|
1255
1589
|
|
|
@@ -1266,90 +1600,28 @@ export default function ContactsPage() {
|
|
|
1266
1600
|
setCreatedAtEnd('');
|
|
1267
1601
|
setUpdatedAtStart('');
|
|
1268
1602
|
setUpdatedAtEnd('');
|
|
1269
|
-
setSortField('');
|
|
1270
|
-
setSortOrder('
|
|
1603
|
+
setSortField('createdAt');
|
|
1604
|
+
setSortOrder('desc');
|
|
1271
1605
|
setCurrentPage(1);
|
|
1272
1606
|
setListFilterModal(null);
|
|
1273
1607
|
setDateFilterModal(null);
|
|
1274
1608
|
setDateFilterPosition(null);
|
|
1275
1609
|
setListFilterPosition(null);
|
|
1610
|
+
lastContactsListUrlCanonRef.current = null;
|
|
1276
1611
|
};
|
|
1277
1612
|
|
|
1278
|
-
const
|
|
1279
|
-
const lookup = new Map<
|
|
1280
|
-
string,
|
|
1281
|
-
{ region: string; regionCode: string; department: string; departmentCode: string }
|
|
1282
|
-
>();
|
|
1283
|
-
for (const c of contacts) {
|
|
1284
|
-
if (c.postalCode) {
|
|
1285
|
-
const region = getRegionFromPostalCode(c.postalCode);
|
|
1286
|
-
const dept = getDepartmentFromPostalCode(c.postalCode);
|
|
1287
|
-
lookup.set(c.id, {
|
|
1288
|
-
region: region?.name || '',
|
|
1289
|
-
regionCode: region?.code || '',
|
|
1290
|
-
department: dept?.name || '',
|
|
1291
|
-
departmentCode: dept?.code || '',
|
|
1292
|
-
});
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
return lookup;
|
|
1296
|
-
}, [contacts]);
|
|
1297
|
-
|
|
1298
|
-
const filteredByGeoContacts = useMemo(() => {
|
|
1299
|
-
if (regionFilter.size === 0 && departmentFilter.size === 0) return contacts;
|
|
1300
|
-
return contacts.filter((c) => {
|
|
1301
|
-
const geo = contactGeoLookup.get(c.id);
|
|
1302
|
-
if (regionFilter.size > 0 && !regionFilter.has(geo?.regionCode || '')) return false;
|
|
1303
|
-
if (departmentFilter.size > 0 && !departmentFilter.has(geo?.departmentCode || ''))
|
|
1304
|
-
return false;
|
|
1305
|
-
return true;
|
|
1306
|
-
});
|
|
1307
|
-
}, [contacts, regionFilter, departmentFilter, contactGeoLookup]);
|
|
1308
|
-
|
|
1309
|
-
const sortedContacts = useMemo(() => {
|
|
1310
|
-
if (!sortField) return filteredByGeoContacts;
|
|
1311
|
-
const sorted = [...filteredByGeoContacts].sort((a, b) => {
|
|
1312
|
-
let aValue: any;
|
|
1313
|
-
let bValue: any;
|
|
1314
|
-
|
|
1315
|
-
if (sortField === 'createdAt' || sortField === 'updatedAt') {
|
|
1316
|
-
aValue = new Date(a[sortField]);
|
|
1317
|
-
bValue = new Date(b[sortField]);
|
|
1318
|
-
const diff = aValue.getTime() - bValue.getTime();
|
|
1319
|
-
return sortOrder === 'asc' ? diff : -diff;
|
|
1320
|
-
} else if (sortField === 'status') {
|
|
1321
|
-
aValue = a.status?.name || '';
|
|
1322
|
-
bValue = b.status?.name || '';
|
|
1323
|
-
} else if (sortField === 'commercial') {
|
|
1324
|
-
aValue = a.assignedCommercial?.name || '';
|
|
1325
|
-
bValue = b.assignedCommercial?.name || '';
|
|
1326
|
-
} else if (sortField === 'telepro') {
|
|
1327
|
-
aValue = a.assignedTelepro?.name || '';
|
|
1328
|
-
bValue = b.assignedTelepro?.name || '';
|
|
1329
|
-
} else if (sortField === 'postalCode') {
|
|
1330
|
-
aValue = a.postalCode || '';
|
|
1331
|
-
bValue = b.postalCode || '';
|
|
1332
|
-
} else if (sortField === 'region') {
|
|
1333
|
-
aValue = contactGeoLookup.get(a.id)?.region || '';
|
|
1334
|
-
bValue = contactGeoLookup.get(b.id)?.region || '';
|
|
1335
|
-
} else if (sortField === 'department') {
|
|
1336
|
-
aValue = contactGeoLookup.get(a.id)?.department || '';
|
|
1337
|
-
bValue = contactGeoLookup.get(b.id)?.department || '';
|
|
1338
|
-
} else {
|
|
1339
|
-
return 0;
|
|
1340
|
-
}
|
|
1613
|
+
const filteredByGeoContacts = contacts;
|
|
1341
1614
|
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
return 0;
|
|
1345
|
-
});
|
|
1346
|
-
return sorted;
|
|
1347
|
-
}, [filteredByGeoContacts, sortField, sortOrder, contactGeoLookup]);
|
|
1615
|
+
// Tri global : orderBy côté API (fetchContacts). Pas de re-tri client sur la page courante.
|
|
1616
|
+
const sortedContacts = filteredByGeoContacts;
|
|
1348
1617
|
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1618
|
+
// Source d'affichage pour la pagination : toujours dériver depuis l'URL (searchParams)
|
|
1619
|
+
// pour éviter tout décalage avec le state.
|
|
1620
|
+
const uiCurrentPage = useMemo(() => {
|
|
1621
|
+
const raw = searchParams.get('page');
|
|
1622
|
+
const fromUrl = Number.parseInt(raw || '1', 10);
|
|
1623
|
+
return Number.isFinite(fromUrl) && fromUrl >= 1 ? fromUrl : 1;
|
|
1624
|
+
}, [searchParams]);
|
|
1353
1625
|
|
|
1354
1626
|
const handleNewContact = () => {
|
|
1355
1627
|
setEditingContact(null);
|
|
@@ -1366,6 +1638,8 @@ export default function ContactsPage() {
|
|
|
1366
1638
|
origin: '',
|
|
1367
1639
|
companyId: '',
|
|
1368
1640
|
jobTitle: '',
|
|
1641
|
+
website: '',
|
|
1642
|
+
socialNetworks: [],
|
|
1369
1643
|
statusId: '',
|
|
1370
1644
|
closingReason: '',
|
|
1371
1645
|
assignedCommercialId: '',
|
|
@@ -1429,6 +1703,47 @@ export default function ContactsPage() {
|
|
|
1429
1703
|
city: ['ville', 'city', 'localité', 'locality'],
|
|
1430
1704
|
postalCode: ['code postal', 'postal code', 'cp', 'zip', 'zipcode', 'code_postal'],
|
|
1431
1705
|
origin: ['origine', 'origin', 'source', 'campagne', 'campaign', 'origine de la campagne'],
|
|
1706
|
+
companyName: [
|
|
1707
|
+
'société',
|
|
1708
|
+
'societe',
|
|
1709
|
+
'company',
|
|
1710
|
+
'company name',
|
|
1711
|
+
'company_name',
|
|
1712
|
+
'company-name',
|
|
1713
|
+
'companyname',
|
|
1714
|
+
'organisation',
|
|
1715
|
+
'organization',
|
|
1716
|
+
'organisme',
|
|
1717
|
+
'org',
|
|
1718
|
+
'org name',
|
|
1719
|
+
'org_name',
|
|
1720
|
+
'org-name',
|
|
1721
|
+
'business',
|
|
1722
|
+
'business name',
|
|
1723
|
+
'business_name',
|
|
1724
|
+
'entreprise',
|
|
1725
|
+
'nom entreprise',
|
|
1726
|
+
'nom société',
|
|
1727
|
+
],
|
|
1728
|
+
website: [
|
|
1729
|
+
'site',
|
|
1730
|
+
'site web',
|
|
1731
|
+
'website',
|
|
1732
|
+
'url',
|
|
1733
|
+
'web',
|
|
1734
|
+
'site internet',
|
|
1735
|
+
],
|
|
1736
|
+
linkedin: ['linkedin', 'linkedin url', 'linkedin profile', 'linkedin profil', 'linkedin.com'],
|
|
1737
|
+
facebook: ['facebook', 'facebook url', 'fb', 'facebook profile', 'facebook profil'],
|
|
1738
|
+
twitter: ['twitter', 'twitter url', 'x', 'x.com', 'twitter profile', 'twitter profil'],
|
|
1739
|
+
instagram: [
|
|
1740
|
+
'instagram',
|
|
1741
|
+
'instagram url',
|
|
1742
|
+
'insta',
|
|
1743
|
+
'instagram profile',
|
|
1744
|
+
'instagram profil',
|
|
1745
|
+
'instagram.com',
|
|
1746
|
+
],
|
|
1432
1747
|
};
|
|
1433
1748
|
|
|
1434
1749
|
headers.forEach((header) => {
|
|
@@ -1503,9 +1818,7 @@ export default function ContactsPage() {
|
|
|
1503
1818
|
const autoMappings = autoMapHeaders(headers);
|
|
1504
1819
|
setImportFieldMappings(autoMappings);
|
|
1505
1820
|
} catch (err: unknown) {
|
|
1506
|
-
setError(
|
|
1507
|
-
`Erreur lors de la lecture du fichier: ${err instanceof Error ? err.message : 'Erreur inconnue'}`,
|
|
1508
|
-
);
|
|
1821
|
+
setError(devToast('Erreur lors de la lecture du fichier', err));
|
|
1509
1822
|
}
|
|
1510
1823
|
};
|
|
1511
1824
|
|
|
@@ -1532,7 +1845,7 @@ export default function ContactsPage() {
|
|
|
1532
1845
|
const autoMappings = autoMapHeaders(data.headers || []);
|
|
1533
1846
|
setImportFieldMappings(autoMappings);
|
|
1534
1847
|
} catch (err: unknown) {
|
|
1535
|
-
setError(
|
|
1848
|
+
setError(devToast('Erreur lors de la lecture de la feuille', err));
|
|
1536
1849
|
}
|
|
1537
1850
|
};
|
|
1538
1851
|
|
|
@@ -1558,7 +1871,7 @@ export default function ContactsPage() {
|
|
|
1558
1871
|
const autoMappings = autoMapHeaders(data.headers || []);
|
|
1559
1872
|
setImportFieldMappings(autoMappings);
|
|
1560
1873
|
} catch (err: unknown) {
|
|
1561
|
-
setError(
|
|
1874
|
+
setError(devToast('Erreur lors de la lecture', err));
|
|
1562
1875
|
}
|
|
1563
1876
|
};
|
|
1564
1877
|
|
|
@@ -1634,7 +1947,7 @@ export default function ContactsPage() {
|
|
|
1634
1947
|
// Afficher la modal de résultats
|
|
1635
1948
|
setShowImportResultModal(true);
|
|
1636
1949
|
} catch (err: any) {
|
|
1637
|
-
setError(err
|
|
1950
|
+
setError(devToast("Erreur lors de l'import", err));
|
|
1638
1951
|
} finally {
|
|
1639
1952
|
setImporting(false);
|
|
1640
1953
|
}
|
|
@@ -1745,13 +2058,14 @@ export default function ContactsPage() {
|
|
|
1745
2058
|
await Promise.all(updatePromises);
|
|
1746
2059
|
|
|
1747
2060
|
setSuccess(`${selectedContactIds.size} contact(s) mis à jour avec succès !`);
|
|
2061
|
+
toast.success('Commercial mis à jour pour ' + selectedContactIds.size + ' contact(s)');
|
|
1748
2062
|
setShowBulkCommercialModal(false);
|
|
1749
2063
|
setBulkCommercialId('');
|
|
1750
2064
|
setSelectedContactIds(new Set());
|
|
1751
2065
|
fetchContacts();
|
|
1752
2066
|
setTimeout(() => setSuccess(''), 5000);
|
|
1753
2067
|
} catch (err: any) {
|
|
1754
|
-
setError(
|
|
2068
|
+
setError(devToast('Erreur lors de la mise à jour du commercial', err));
|
|
1755
2069
|
} finally {
|
|
1756
2070
|
setBulkActionLoading(false);
|
|
1757
2071
|
}
|
|
@@ -1787,13 +2101,14 @@ export default function ContactsPage() {
|
|
|
1787
2101
|
await Promise.all(updatePromises);
|
|
1788
2102
|
|
|
1789
2103
|
setSuccess(`${selectedContactIds.size} contact(s) mis à jour avec succès !`);
|
|
2104
|
+
toast.success('Téléprospecteur mis à jour pour ' + selectedContactIds.size + ' contact(s)');
|
|
1790
2105
|
setShowBulkTeleproModal(false);
|
|
1791
2106
|
setBulkTeleproId('');
|
|
1792
2107
|
setSelectedContactIds(new Set());
|
|
1793
2108
|
fetchContacts();
|
|
1794
2109
|
setTimeout(() => setSuccess(''), 5000);
|
|
1795
2110
|
} catch (err: any) {
|
|
1796
|
-
setError(
|
|
2111
|
+
setError(devToast('Erreur lors de la mise à jour du télépro', err));
|
|
1797
2112
|
} finally {
|
|
1798
2113
|
setBulkActionLoading(false);
|
|
1799
2114
|
}
|
|
@@ -1829,13 +2144,14 @@ export default function ContactsPage() {
|
|
|
1829
2144
|
await Promise.all(updatePromises);
|
|
1830
2145
|
|
|
1831
2146
|
setSuccess(`${selectedContactIds.size} contact(s) mis à jour avec succès !`);
|
|
2147
|
+
toast.success('Statut mis à jour pour ' + selectedContactIds.size + ' contact(s)');
|
|
1832
2148
|
setShowBulkStatusModal(false);
|
|
1833
2149
|
setBulkStatusId('');
|
|
1834
2150
|
setSelectedContactIds(new Set());
|
|
1835
2151
|
fetchContacts();
|
|
1836
2152
|
setTimeout(() => setSuccess(''), 5000);
|
|
1837
2153
|
} catch (err: any) {
|
|
1838
|
-
setError(
|
|
2154
|
+
setError(devToast('Erreur lors de la mise à jour du statut', err));
|
|
1839
2155
|
} finally {
|
|
1840
2156
|
setBulkActionLoading(false);
|
|
1841
2157
|
}
|
|
@@ -1881,11 +2197,12 @@ export default function ContactsPage() {
|
|
|
1881
2197
|
await Promise.all(deletePromises);
|
|
1882
2198
|
|
|
1883
2199
|
setSuccess(`${selectedContactIds.size} contact(s) supprimé(s) avec succès !`);
|
|
2200
|
+
toast.success(selectedContactIds.size + ' contact(s) supprimé(s)');
|
|
1884
2201
|
setSelectedContactIds(new Set());
|
|
1885
2202
|
fetchContacts();
|
|
1886
2203
|
setTimeout(() => setSuccess(''), 5000);
|
|
1887
2204
|
} catch (err: any) {
|
|
1888
|
-
setError(
|
|
2205
|
+
setError(devToast('Erreur lors de la suppression', err));
|
|
1889
2206
|
} finally {
|
|
1890
2207
|
setBulkActionLoading(false);
|
|
1891
2208
|
}
|
|
@@ -1936,13 +2253,14 @@ export default function ContactsPage() {
|
|
|
1936
2253
|
? `Tous les contacts ont été exportés en ${format.toUpperCase()} avec succès !`
|
|
1937
2254
|
: `${selectedContactIds.size} contact(s) exporté(s) en ${format.toUpperCase()} avec succès !`,
|
|
1938
2255
|
);
|
|
2256
|
+
toast.success('Export terminé');
|
|
1939
2257
|
setShowExportModal(false);
|
|
1940
2258
|
if (!isCompanies && !exportAll) {
|
|
1941
2259
|
setSelectedContactIds(new Set());
|
|
1942
2260
|
}
|
|
1943
2261
|
setTimeout(() => setSuccess(''), 5000);
|
|
1944
2262
|
} catch (err: unknown) {
|
|
1945
|
-
setError(
|
|
2263
|
+
setError(devToast("Erreur lors de l'export", err));
|
|
1946
2264
|
} finally {
|
|
1947
2265
|
setExporting(false);
|
|
1948
2266
|
}
|
|
@@ -2013,10 +2331,10 @@ export default function ContactsPage() {
|
|
|
2013
2331
|
)}
|
|
2014
2332
|
</>
|
|
2015
2333
|
)}
|
|
2016
|
-
{contact.company && (
|
|
2334
|
+
{(contact.company || contact.companyName) && (
|
|
2017
2335
|
<div className="flex items-center text-sm text-gray-500">
|
|
2018
2336
|
<Building2 className="mr-1 h-3.5 w-3.5 text-gray-400" />
|
|
2019
|
-
{contact.company.name}
|
|
2337
|
+
{contact.company ? contact.company.name : contact.companyName}
|
|
2020
2338
|
</div>
|
|
2021
2339
|
)}
|
|
2022
2340
|
{contact.city && (
|
|
@@ -2117,30 +2435,6 @@ export default function ContactsPage() {
|
|
|
2117
2435
|
)}
|
|
2118
2436
|
</td>
|
|
2119
2437
|
);
|
|
2120
|
-
case 'region': {
|
|
2121
|
-
const geo = contactGeoLookup.get(contact.id);
|
|
2122
|
-
return (
|
|
2123
|
-
<td key={contactId} className="px-3 py-4 text-base whitespace-nowrap sm:px-6">
|
|
2124
|
-
{geo?.region ? (
|
|
2125
|
-
<span className="text-gray-700">{geo.region}</span>
|
|
2126
|
-
) : (
|
|
2127
|
-
<span className="text-gray-400">-</span>
|
|
2128
|
-
)}
|
|
2129
|
-
</td>
|
|
2130
|
-
);
|
|
2131
|
-
}
|
|
2132
|
-
case 'department': {
|
|
2133
|
-
const geo = contactGeoLookup.get(contact.id);
|
|
2134
|
-
return (
|
|
2135
|
-
<td key={contactId} className="px-3 py-4 text-base whitespace-nowrap sm:px-6">
|
|
2136
|
-
{geo?.department ? (
|
|
2137
|
-
<span className="text-gray-700">{geo.department}</span>
|
|
2138
|
-
) : (
|
|
2139
|
-
<span className="text-gray-400">-</span>
|
|
2140
|
-
)}
|
|
2141
|
-
</td>
|
|
2142
|
-
);
|
|
2143
|
-
}
|
|
2144
2438
|
case 'createdAt':
|
|
2145
2439
|
return (
|
|
2146
2440
|
<td
|
|
@@ -2174,83 +2468,73 @@ export default function ContactsPage() {
|
|
|
2174
2468
|
]}
|
|
2175
2469
|
>
|
|
2176
2470
|
<div className="kb-tab-scope bg-surface-page flex h-full flex-col">
|
|
2177
|
-
{/*
|
|
2471
|
+
{/* Ligne 1 : Titre + Onglets + Actualiser */}
|
|
2178
2472
|
<div
|
|
2179
2473
|
className={cn(
|
|
2180
|
-
'bg-background/95 backdrop-blur-sm',
|
|
2181
|
-
(viewsDropdownOpen || viewsContextMenuOpen) && 'relative z-50',
|
|
2474
|
+
'border-border bg-background/95 flex min-w-0 items-center gap-2 border-b backdrop-blur-sm px-4 py-1.5 sm:px-6 lg:px-8',
|
|
2475
|
+
(viewsDropdownOpen || viewsContextMenuOpen || showEntitySelector) && 'relative z-50',
|
|
2182
2476
|
)}
|
|
2183
2477
|
>
|
|
2184
|
-
<div className="flex items-center
|
|
2185
|
-
<
|
|
2186
|
-
|
|
2478
|
+
<div className="relative flex shrink-0 items-center gap-2" ref={entitySelectorRef}>
|
|
2479
|
+
<button
|
|
2480
|
+
onClick={() => setShowEntitySelector(!showEntitySelector)}
|
|
2481
|
+
className="border-border bg-card text-foreground hover:bg-muted focus-visible:ring-primary flex cursor-pointer items-center gap-1.5 rounded-lg border px-2.5 py-1 text-base font-bold transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none sm:px-3 sm:py-1.5 sm:text-lg"
|
|
2482
|
+
>
|
|
2483
|
+
{viewEntity === 'contacts' ? (
|
|
2484
|
+
<Users className="h-4 w-4 text-blue-600" />
|
|
2485
|
+
) : (
|
|
2486
|
+
<Building2 className="h-4 w-4 text-blue-600" />
|
|
2487
|
+
)}
|
|
2488
|
+
{viewEntity === 'contacts' ? 'Contacts' : 'Entreprises'}
|
|
2489
|
+
<ChevronDown className="text-muted-foreground h-4 w-4" />
|
|
2490
|
+
</button>
|
|
2491
|
+
{showEntitySelector && (
|
|
2492
|
+
<div className="border-border bg-popover ui-dropdown-enter absolute top-full left-0 z-50 mt-1 w-52 rounded-lg border py-1 shadow-(--shadow-dropdown)">
|
|
2187
2493
|
<button
|
|
2188
|
-
onClick={() =>
|
|
2189
|
-
|
|
2494
|
+
onClick={() => {
|
|
2495
|
+
setViewEntity('contacts');
|
|
2496
|
+
setSortField('createdAt');
|
|
2497
|
+
setSortOrder('desc');
|
|
2498
|
+
setShowEntitySelector(false);
|
|
2499
|
+
setCurrentPage(1);
|
|
2500
|
+
}}
|
|
2501
|
+
className={cn(
|
|
2502
|
+
'focus-visible:ring-primary/40 flex w-full cursor-pointer items-center gap-2.5 px-3 py-2 text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset',
|
|
2503
|
+
viewEntity === 'contacts'
|
|
2504
|
+
? 'bg-blue-50 font-medium text-blue-700'
|
|
2505
|
+
: 'text-popover-foreground hover:bg-accent',
|
|
2506
|
+
)}
|
|
2190
2507
|
>
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2508
|
+
<Users className="h-4 w-4" />
|
|
2509
|
+
Contacts
|
|
2510
|
+
</button>
|
|
2511
|
+
<button
|
|
2512
|
+
onClick={() => {
|
|
2513
|
+
setViewEntity('companies');
|
|
2514
|
+
setViewMode('table');
|
|
2515
|
+
setSortField('createdAt');
|
|
2516
|
+
setSortOrder('desc');
|
|
2517
|
+
setShowEntitySelector(false);
|
|
2518
|
+
setCurrentPage(1);
|
|
2519
|
+
}}
|
|
2520
|
+
className={cn(
|
|
2521
|
+
'focus-visible:ring-primary/40 flex w-full cursor-pointer items-center gap-2.5 px-3 py-2 text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset',
|
|
2522
|
+
viewEntity === 'companies'
|
|
2523
|
+
? 'bg-blue-50 font-medium text-blue-700'
|
|
2524
|
+
: 'text-popover-foreground hover:bg-accent',
|
|
2195
2525
|
)}
|
|
2196
|
-
|
|
2197
|
-
<
|
|
2526
|
+
>
|
|
2527
|
+
<Building2 className="h-4 w-4" />
|
|
2528
|
+
Entreprises
|
|
2198
2529
|
</button>
|
|
2199
|
-
{showEntitySelector && (
|
|
2200
|
-
<div className="absolute top-full left-0 z-50 mt-1 w-52 rounded-lg border border-border bg-popover py-1 shadow-(--shadow-dropdown)">
|
|
2201
|
-
<button
|
|
2202
|
-
onClick={() => {
|
|
2203
|
-
setViewEntity('contacts');
|
|
2204
|
-
setSortField('');
|
|
2205
|
-
setSortOrder('asc');
|
|
2206
|
-
setShowEntitySelector(false);
|
|
2207
|
-
setCurrentPage(1);
|
|
2208
|
-
router.replace('/contacts', { scroll: false });
|
|
2209
|
-
}}
|
|
2210
|
-
className={cn(
|
|
2211
|
-
'flex w-full cursor-pointer items-center gap-2.5 px-3 py-2 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-inset',
|
|
2212
|
-
viewEntity === 'contacts'
|
|
2213
|
-
? 'bg-blue-50 font-medium text-blue-700'
|
|
2214
|
-
: 'text-popover-foreground hover:bg-accent',
|
|
2215
|
-
)}
|
|
2216
|
-
>
|
|
2217
|
-
<Users className="h-4 w-4" />
|
|
2218
|
-
Contacts
|
|
2219
|
-
</button>
|
|
2220
|
-
<button
|
|
2221
|
-
onClick={() => {
|
|
2222
|
-
setViewEntity('companies');
|
|
2223
|
-
setViewMode('table');
|
|
2224
|
-
setSortField('');
|
|
2225
|
-
setSortOrder('asc');
|
|
2226
|
-
setShowEntitySelector(false);
|
|
2227
|
-
setCurrentPage(1);
|
|
2228
|
-
router.replace('/contacts?entity=companies', { scroll: false });
|
|
2229
|
-
}}
|
|
2230
|
-
className={cn(
|
|
2231
|
-
'flex w-full cursor-pointer items-center gap-2.5 px-3 py-2 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-inset',
|
|
2232
|
-
viewEntity === 'companies'
|
|
2233
|
-
? 'bg-blue-50 font-medium text-blue-700'
|
|
2234
|
-
: 'text-popover-foreground hover:bg-accent',
|
|
2235
|
-
)}
|
|
2236
|
-
>
|
|
2237
|
-
<Building2 className="h-4 w-4" />
|
|
2238
|
-
Entreprises
|
|
2239
|
-
</button>
|
|
2240
|
-
</div>
|
|
2241
|
-
)}
|
|
2242
2530
|
</div>
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
<button
|
|
2248
|
-
onClick={fetchContacts}
|
|
2249
|
-
className="shrink-0 cursor-pointer rounded-lg p-1.5 text-muted-foreground transition-colors duration-200 hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
|
|
2250
|
-
title="Actualiser"
|
|
2531
|
+
)}
|
|
2532
|
+
<span
|
|
2533
|
+
key={totalContacts}
|
|
2534
|
+
className="ui-count-pop shrink-0 rounded-full bg-blue-200 px-2 py-0.5 text-xs font-semibold text-blue-900"
|
|
2251
2535
|
>
|
|
2252
|
-
|
|
2253
|
-
</
|
|
2536
|
+
{totalContacts}
|
|
2537
|
+
</span>
|
|
2254
2538
|
</div>
|
|
2255
2539
|
<ViewsTabBar
|
|
2256
2540
|
views={views}
|
|
@@ -2269,11 +2553,19 @@ export default function ContactsPage() {
|
|
|
2269
2553
|
hasUnsavedChanges={hasUnsavedViewChanges}
|
|
2270
2554
|
activeFilters={viewFilters}
|
|
2271
2555
|
entityType={viewEntity}
|
|
2556
|
+
inline
|
|
2272
2557
|
/>
|
|
2558
|
+
<button
|
|
2559
|
+
onClick={fetchContacts}
|
|
2560
|
+
className="text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:ring-primary shrink-0 cursor-pointer rounded-lg p-1.5 transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
|
2561
|
+
title="Actualiser"
|
|
2562
|
+
>
|
|
2563
|
+
<RefreshCw className="h-4 w-4" />
|
|
2564
|
+
</button>
|
|
2273
2565
|
</div>
|
|
2274
2566
|
|
|
2275
|
-
{/*
|
|
2276
|
-
<div className="border-
|
|
2567
|
+
{/* Ligne 2 : Filtres avancés + Recherche + Trier + Date + Colonnes + Vue + Importer + Exporter + Ajouter */}
|
|
2568
|
+
<div className="border-border bg-background/95 flex flex-wrap items-center gap-2 border-b px-4 py-2 sm:px-6 lg:px-8">
|
|
2277
2569
|
<FilterBar
|
|
2278
2570
|
filters={viewFilters}
|
|
2279
2571
|
onRemoveFilter={handleRemoveViewFilter}
|
|
@@ -2285,337 +2577,299 @@ export default function ContactsPage() {
|
|
|
2285
2577
|
isViewOwner={isViewOwner}
|
|
2286
2578
|
statusOptions={statuses}
|
|
2287
2579
|
userOptions={users}
|
|
2580
|
+
inline
|
|
2288
2581
|
/>
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
<
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2582
|
+
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
|
|
2583
|
+
<div className="relative min-w-[180px] max-w-[280px] flex-1 basis-0">
|
|
2584
|
+
<Search className="text-muted-foreground pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" />
|
|
2585
|
+
<input
|
|
2586
|
+
type="text"
|
|
2587
|
+
value={search}
|
|
2588
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
2589
|
+
placeholder="Rechercher"
|
|
2590
|
+
className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus:border-primary/50 focus:bg-background focus:ring-primary/20 w-full rounded-lg border py-1.5 pr-3 pl-9 text-sm focus:ring-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
2591
|
+
/>
|
|
2592
|
+
</div>
|
|
2593
|
+
<div className="relative" ref={sortMenuRef}>
|
|
2594
|
+
<button
|
|
2595
|
+
type="button"
|
|
2596
|
+
onClick={() => setShowSortMenu(!showSortMenu)}
|
|
2597
|
+
className="border-border bg-card text-muted-foreground hover:bg-muted hover:text-foreground inline-flex cursor-pointer items-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-medium transition-colors duration-200"
|
|
2598
|
+
>
|
|
2599
|
+
<ChevronsLeft className="h-3.5 w-3.5" />
|
|
2600
|
+
<span>Trier par</span>
|
|
2601
|
+
</button>
|
|
2602
|
+
{showSortMenu && (
|
|
2603
|
+
<div
|
|
2604
|
+
className="border-border bg-popover ui-dropdown-enter absolute top-full right-0 z-50 mt-2 w-48 rounded-lg border shadow-(--shadow-dropdown)"
|
|
2605
|
+
style={{ maxWidth: 'calc(100vw - 2rem)', right: '0' }}
|
|
2311
2606
|
>
|
|
2312
|
-
<
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2607
|
+
<div className="py-1">
|
|
2608
|
+
<button
|
|
2609
|
+
type="button"
|
|
2610
|
+
onClick={() => handleSortByField('postalCode')}
|
|
2611
|
+
className="w-full cursor-pointer px-4 py-2 text-left text-base text-gray-700 hover:bg-gray-100"
|
|
2612
|
+
>
|
|
2613
|
+
Code postal{' '}
|
|
2614
|
+
{sortField === 'postalCode' && (sortOrder === 'asc' ? '↑' : '↓')}
|
|
2615
|
+
</button>
|
|
2616
|
+
{viewEntity === 'contacts' && (
|
|
2321
2617
|
<button
|
|
2322
2618
|
type="button"
|
|
2323
|
-
onClick={() => handleSortByField('
|
|
2619
|
+
onClick={() => handleSortByField('region')}
|
|
2324
2620
|
className="w-full cursor-pointer px-4 py-2 text-left text-base text-gray-700 hover:bg-gray-100"
|
|
2325
2621
|
>
|
|
2326
|
-
|
|
2327
|
-
{sortField === 'postalCode' && (sortOrder === 'asc' ? '↑' : '↓')}
|
|
2622
|
+
Région {sortField === 'region' && (sortOrder === 'asc' ? '↑' : '↓')}
|
|
2328
2623
|
</button>
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
type="button"
|
|
2332
|
-
onClick={() => handleSortByField('region')}
|
|
2333
|
-
className="w-full cursor-pointer px-4 py-2 text-left text-base text-gray-700 hover:bg-gray-100"
|
|
2334
|
-
>
|
|
2335
|
-
Région {sortField === 'region' && (sortOrder === 'asc' ? '↑' : '↓')}
|
|
2336
|
-
</button>
|
|
2337
|
-
)}
|
|
2338
|
-
{viewEntity === 'contacts' && (
|
|
2339
|
-
<button
|
|
2340
|
-
type="button"
|
|
2341
|
-
onClick={() => handleSortByField('department')}
|
|
2342
|
-
className="w-full cursor-pointer px-4 py-2 text-left text-base text-gray-700 hover:bg-gray-100"
|
|
2343
|
-
>
|
|
2344
|
-
Département{' '}
|
|
2345
|
-
{sortField === 'department' && (sortOrder === 'asc' ? '↑' : '↓')}
|
|
2346
|
-
</button>
|
|
2347
|
-
)}
|
|
2348
|
-
{viewEntity === 'contacts' && (
|
|
2349
|
-
<button
|
|
2350
|
-
type="button"
|
|
2351
|
-
onClick={() => handleSortByField('status')}
|
|
2352
|
-
className="w-full cursor-pointer px-4 py-2 text-left text-base text-gray-700 hover:bg-gray-100"
|
|
2353
|
-
>
|
|
2354
|
-
Statut {sortField === 'status' && (sortOrder === 'asc' ? '↑' : '↓')}
|
|
2355
|
-
</button>
|
|
2356
|
-
)}
|
|
2624
|
+
)}
|
|
2625
|
+
{viewEntity === 'contacts' && (
|
|
2357
2626
|
<button
|
|
2358
2627
|
type="button"
|
|
2359
|
-
onClick={() => handleSortByField('
|
|
2628
|
+
onClick={() => handleSortByField('department')}
|
|
2360
2629
|
className="w-full cursor-pointer px-4 py-2 text-left text-base text-gray-700 hover:bg-gray-100"
|
|
2361
2630
|
>
|
|
2362
|
-
|
|
2631
|
+
Département{' '}
|
|
2632
|
+
{sortField === 'department' && (sortOrder === 'asc' ? '↑' : '↓')}
|
|
2363
2633
|
</button>
|
|
2634
|
+
)}
|
|
2635
|
+
{viewEntity === 'contacts' && (
|
|
2364
2636
|
<button
|
|
2365
2637
|
type="button"
|
|
2366
|
-
onClick={() => handleSortByField('
|
|
2638
|
+
onClick={() => handleSortByField('status')}
|
|
2367
2639
|
className="w-full cursor-pointer px-4 py-2 text-left text-base text-gray-700 hover:bg-gray-100"
|
|
2368
2640
|
>
|
|
2369
|
-
|
|
2641
|
+
Statut {sortField === 'status' && (sortOrder === 'asc' ? '↑' : '↓')}
|
|
2370
2642
|
</button>
|
|
2643
|
+
)}
|
|
2644
|
+
{viewEntity === 'contacts' && (
|
|
2371
2645
|
<button
|
|
2372
2646
|
type="button"
|
|
2373
|
-
onClick={() => handleSortByField('
|
|
2647
|
+
onClick={() => handleSortByField('origin')}
|
|
2374
2648
|
className="w-full cursor-pointer px-4 py-2 text-left text-base text-gray-700 hover:bg-gray-100"
|
|
2375
2649
|
>
|
|
2376
|
-
|
|
2377
|
-
{sortField === 'createdAt' && (sortOrder === 'asc' ? '↑' : '↓')}
|
|
2650
|
+
Origine {sortField === 'origin' && (sortOrder === 'asc' ? '↑' : '↓')}
|
|
2378
2651
|
</button>
|
|
2652
|
+
)}
|
|
2653
|
+
<button
|
|
2654
|
+
type="button"
|
|
2655
|
+
onClick={() => handleSortByField('commercial')}
|
|
2656
|
+
className="w-full cursor-pointer px-4 py-2 text-left text-base text-gray-700 hover:bg-gray-100"
|
|
2657
|
+
>
|
|
2658
|
+
Commercial {sortField === 'commercial' && (sortOrder === 'asc' ? '↑' : '↓')}
|
|
2659
|
+
</button>
|
|
2660
|
+
<button
|
|
2661
|
+
type="button"
|
|
2662
|
+
onClick={() => handleSortByField('telepro')}
|
|
2663
|
+
className="w-full cursor-pointer px-4 py-2 text-left text-base text-gray-700 hover:bg-gray-100"
|
|
2664
|
+
>
|
|
2665
|
+
Télépro {sortField === 'telepro' && (sortOrder === 'asc' ? '↑' : '↓')}
|
|
2666
|
+
</button>
|
|
2667
|
+
<button
|
|
2668
|
+
type="button"
|
|
2669
|
+
onClick={() => handleSortByField('createdAt')}
|
|
2670
|
+
className="w-full cursor-pointer px-4 py-2 text-left text-base text-gray-700 hover:bg-gray-100"
|
|
2671
|
+
>
|
|
2672
|
+
Date de création{' '}
|
|
2673
|
+
{contactsListColumnShowsActiveSort('createdAt', sortField, sortOrder) &&
|
|
2674
|
+
(sortOrder === 'asc' ? '↑' : '↓')}
|
|
2675
|
+
</button>
|
|
2676
|
+
<button
|
|
2677
|
+
type="button"
|
|
2678
|
+
onClick={() => handleSortByField('updatedAt')}
|
|
2679
|
+
className="w-full cursor-pointer px-4 py-2 text-left text-base text-gray-700 hover:bg-gray-100"
|
|
2680
|
+
>
|
|
2681
|
+
Date de modification{' '}
|
|
2682
|
+
{sortField === 'updatedAt' && (sortOrder === 'asc' ? '↑' : '↓')}
|
|
2683
|
+
</button>
|
|
2684
|
+
{!isContactsListDefaultSort(sortField, sortOrder) && (
|
|
2379
2685
|
<button
|
|
2380
2686
|
type="button"
|
|
2381
|
-
onClick={() =>
|
|
2382
|
-
|
|
2687
|
+
onClick={() => {
|
|
2688
|
+
setSortField('createdAt');
|
|
2689
|
+
setSortOrder('desc');
|
|
2690
|
+
setShowSortMenu(false);
|
|
2691
|
+
}}
|
|
2692
|
+
className="w-full cursor-pointer px-4 py-2 text-left text-base text-gray-500 hover:bg-gray-100"
|
|
2383
2693
|
>
|
|
2384
|
-
|
|
2385
|
-
{sortField === 'updatedAt' && (sortOrder === 'asc' ? '↑' : '↓')}
|
|
2694
|
+
Réinitialiser
|
|
2386
2695
|
</button>
|
|
2387
|
-
|
|
2388
|
-
<button
|
|
2389
|
-
type="button"
|
|
2390
|
-
onClick={() => {
|
|
2391
|
-
setSortField('');
|
|
2392
|
-
setSortOrder('asc');
|
|
2393
|
-
setShowSortMenu(false);
|
|
2394
|
-
}}
|
|
2395
|
-
className="w-full cursor-pointer px-4 py-2 text-left text-base text-gray-500 hover:bg-gray-100"
|
|
2396
|
-
>
|
|
2397
|
-
Réinitialiser
|
|
2398
|
-
</button>
|
|
2399
|
-
)}
|
|
2400
|
-
</div>
|
|
2696
|
+
)}
|
|
2401
2697
|
</div>
|
|
2402
|
-
)}
|
|
2403
|
-
</div>
|
|
2404
|
-
<div className="flex items-center gap-2">
|
|
2405
|
-
<div className="relative" ref={datePickerRef}>
|
|
2406
|
-
<button
|
|
2407
|
-
type="button"
|
|
2408
|
-
onClick={() => setShowDatePicker(!showDatePicker)}
|
|
2409
|
-
className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50"
|
|
2410
|
-
>
|
|
2411
|
-
<Calendar className="h-3.5 w-3.5" />
|
|
2412
|
-
<span className="hidden sm:inline">
|
|
2413
|
-
{dateRangeStart && dateRangeEnd
|
|
2414
|
-
? `${new Date(dateRangeStart + 'T00:00:00').toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' })} - ${new Date(dateRangeEnd + 'T00:00:00').toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' })}`
|
|
2415
|
-
: createdAtStart && createdAtEnd
|
|
2416
|
-
? `${new Date(createdAtStart + 'T00:00:00').toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' })} - ${new Date(createdAtEnd + 'T00:00:00').toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' })}`
|
|
2417
|
-
: 'Sélectionner une date'}
|
|
2418
|
-
</span>
|
|
2419
|
-
<span className="sm:hidden">Date</span>
|
|
2420
|
-
</button>
|
|
2421
|
-
{showDatePicker && (
|
|
2422
|
-
<div
|
|
2423
|
-
className="absolute top-full right-0 z-50 mt-2 w-72 rounded-lg border border-gray-200 bg-white p-4 shadow-lg"
|
|
2424
|
-
style={{ maxWidth: 'calc(100vw - 2rem)', right: '0' }}
|
|
2425
|
-
>
|
|
2426
|
-
<div className="space-y-3">
|
|
2427
|
-
<div>
|
|
2428
|
-
<label className="mb-1 block text-xs font-medium text-gray-700">Du</label>
|
|
2429
|
-
<input
|
|
2430
|
-
type="date"
|
|
2431
|
-
value={dateRangeStart}
|
|
2432
|
-
onChange={(e) => setDateRangeStart(e.target.value)}
|
|
2433
|
-
className="w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
2434
|
-
/>
|
|
2435
|
-
</div>
|
|
2436
|
-
<div>
|
|
2437
|
-
<label className="mb-1 block text-xs font-medium text-gray-700">Au</label>
|
|
2438
|
-
<input
|
|
2439
|
-
type="date"
|
|
2440
|
-
value={dateRangeEnd}
|
|
2441
|
-
onChange={(e) => setDateRangeEnd(e.target.value)}
|
|
2442
|
-
className="w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
2443
|
-
/>
|
|
2444
|
-
</div>
|
|
2445
|
-
<div className="flex justify-between gap-2">
|
|
2446
|
-
<button
|
|
2447
|
-
type="button"
|
|
2448
|
-
onClick={() => {
|
|
2449
|
-
// Réinitialiser tous les filtres de date
|
|
2450
|
-
setDateRangeStart('');
|
|
2451
|
-
setDateRangeEnd('');
|
|
2452
|
-
setCreatedAtStart('');
|
|
2453
|
-
setCreatedAtEnd('');
|
|
2454
|
-
setUpdatedAtStart('');
|
|
2455
|
-
setUpdatedAtEnd('');
|
|
2456
|
-
setCurrentPage(1);
|
|
2457
|
-
setShowDatePicker(false);
|
|
2458
|
-
// Recharger les contacts sans filtres de date
|
|
2459
|
-
setTimeout(() => {
|
|
2460
|
-
fetchContacts();
|
|
2461
|
-
}, 0);
|
|
2462
|
-
}}
|
|
2463
|
-
className="cursor-pointer rounded-md border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50"
|
|
2464
|
-
>
|
|
2465
|
-
Réinitialiser
|
|
2466
|
-
</button>
|
|
2467
|
-
<div className="flex gap-2">
|
|
2468
|
-
<button
|
|
2469
|
-
type="button"
|
|
2470
|
-
onClick={() => {
|
|
2471
|
-
setDateRangeStart('');
|
|
2472
|
-
setDateRangeEnd('');
|
|
2473
|
-
setShowDatePicker(false);
|
|
2474
|
-
}}
|
|
2475
|
-
className="cursor-pointer rounded-md border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50"
|
|
2476
|
-
>
|
|
2477
|
-
Effacer
|
|
2478
|
-
</button>
|
|
2479
|
-
<button
|
|
2480
|
-
type="button"
|
|
2481
|
-
onClick={() => {
|
|
2482
|
-
if (dateRangeStart && dateRangeEnd) {
|
|
2483
|
-
// Valider que les dates sont valides
|
|
2484
|
-
const start = new Date(dateRangeStart + 'T00:00:00');
|
|
2485
|
-
const end = new Date(dateRangeEnd + 'T00:00:00');
|
|
2486
|
-
if (
|
|
2487
|
-
!isNaN(start.getTime()) &&
|
|
2488
|
-
!isNaN(end.getTime()) &&
|
|
2489
|
-
start <= end
|
|
2490
|
-
) {
|
|
2491
|
-
setCreatedAtStart(dateRangeStart);
|
|
2492
|
-
setCreatedAtEnd(dateRangeEnd);
|
|
2493
|
-
setCurrentPage(1);
|
|
2494
|
-
setShowDatePicker(false);
|
|
2495
|
-
} else {
|
|
2496
|
-
setError('Les dates sélectionnées sont invalides');
|
|
2497
|
-
}
|
|
2498
|
-
} else {
|
|
2499
|
-
setShowDatePicker(false);
|
|
2500
|
-
}
|
|
2501
|
-
}}
|
|
2502
|
-
className="cursor-pointer rounded-md bg-blue-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-blue-700"
|
|
2503
|
-
>
|
|
2504
|
-
Appliquer
|
|
2505
|
-
</button>
|
|
2506
|
-
</div>
|
|
2507
|
-
</div>
|
|
2508
|
-
</div>
|
|
2509
|
-
</div>
|
|
2510
|
-
)}
|
|
2511
2698
|
</div>
|
|
2512
|
-
|
|
2513
|
-
<button
|
|
2514
|
-
type="button"
|
|
2515
|
-
onClick={() => {
|
|
2516
|
-
// Réinitialiser tous les filtres de date
|
|
2517
|
-
setDateRangeStart('');
|
|
2518
|
-
setDateRangeEnd('');
|
|
2519
|
-
setCreatedAtStart('');
|
|
2520
|
-
setCreatedAtEnd('');
|
|
2521
|
-
setUpdatedAtStart('');
|
|
2522
|
-
setUpdatedAtEnd('');
|
|
2523
|
-
setCurrentPage(1);
|
|
2524
|
-
// Recharger les contacts sans filtres de date
|
|
2525
|
-
setTimeout(() => {
|
|
2526
|
-
fetchContacts();
|
|
2527
|
-
}, 0);
|
|
2528
|
-
}}
|
|
2529
|
-
className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50"
|
|
2530
|
-
title="Réinitialiser les filtres de date"
|
|
2531
|
-
>
|
|
2532
|
-
<X className="h-3.5 w-3.5" />
|
|
2533
|
-
<span className="hidden sm:inline">Réinitialiser</span>
|
|
2534
|
-
</button>
|
|
2535
|
-
)}
|
|
2536
|
-
</div>
|
|
2699
|
+
)}
|
|
2537
2700
|
</div>
|
|
2538
|
-
|
|
2539
|
-
{/* Actions à droite */}
|
|
2540
2701
|
<div className="flex items-center gap-2">
|
|
2541
|
-
|
|
2542
|
-
{viewMode === 'table' && (
|
|
2702
|
+
<div className="relative" ref={datePickerRef}>
|
|
2543
2703
|
<button
|
|
2544
|
-
|
|
2704
|
+
type="button"
|
|
2705
|
+
onClick={() => setShowDatePicker(!showDatePicker)}
|
|
2545
2706
|
className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50"
|
|
2546
|
-
title="Gérer les colonnes"
|
|
2547
2707
|
>
|
|
2548
|
-
|
|
2708
|
+
<Calendar className="h-3.5 w-3.5" />
|
|
2709
|
+
<span className="hidden sm:inline">
|
|
2710
|
+
{createdAtStart && createdAtEnd
|
|
2711
|
+
? `${new Date(createdAtStart + 'T00:00:00').toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' })} - ${new Date(createdAtEnd + 'T00:00:00').toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' })}`
|
|
2712
|
+
: 'Sélectionner une date'}
|
|
2713
|
+
</span>
|
|
2714
|
+
<span className="sm:hidden">Date</span>
|
|
2549
2715
|
</button>
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2716
|
+
{showDatePicker && (
|
|
2717
|
+
<div
|
|
2718
|
+
className="ui-dropdown-enter absolute top-full right-0 z-50 mt-2 w-[min(22rem,calc(100vw-2rem))] max-w-[calc(100vw-2rem)] rounded-lg border border-gray-200 bg-white p-3 shadow-lg"
|
|
2719
|
+
style={{ right: '0' }}
|
|
2720
|
+
>
|
|
2721
|
+
<DatePicker
|
|
2722
|
+
embedded
|
|
2723
|
+
isPeriod
|
|
2724
|
+
startDate={createdAtStart}
|
|
2725
|
+
endDate={createdAtEnd}
|
|
2726
|
+
label="Période (création)"
|
|
2727
|
+
onDateChange={(start, end) => {
|
|
2728
|
+
if (start && end) {
|
|
2729
|
+
setCreatedAtStart(start);
|
|
2730
|
+
setCreatedAtEnd(end);
|
|
2731
|
+
setCurrentPage(1);
|
|
2732
|
+
setShowDatePicker(false);
|
|
2733
|
+
}
|
|
2734
|
+
}}
|
|
2735
|
+
onRequestClose={() => setShowDatePicker(false)}
|
|
2736
|
+
onClear={() => {
|
|
2737
|
+
setCreatedAtStart('');
|
|
2738
|
+
setCreatedAtEnd('');
|
|
2739
|
+
setCurrentPage(1);
|
|
2740
|
+
}}
|
|
2741
|
+
/>
|
|
2742
|
+
<button
|
|
2743
|
+
type="button"
|
|
2744
|
+
onClick={() => {
|
|
2745
|
+
setCreatedAtStart('');
|
|
2746
|
+
setCreatedAtEnd('');
|
|
2747
|
+
setUpdatedAtStart('');
|
|
2748
|
+
setUpdatedAtEnd('');
|
|
2749
|
+
setCurrentPage(1);
|
|
2750
|
+
setShowDatePicker(false);
|
|
2751
|
+
setTimeout(() => {
|
|
2752
|
+
fetchContacts();
|
|
2753
|
+
}, 0);
|
|
2754
|
+
}}
|
|
2755
|
+
className="mt-3 w-full cursor-pointer rounded-md border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50"
|
|
2756
|
+
>
|
|
2757
|
+
Réinitialiser toutes les dates
|
|
2758
|
+
</button>
|
|
2759
|
+
</div>
|
|
2760
|
+
)}
|
|
2761
|
+
</div>
|
|
2762
|
+
{(createdAtStart || createdAtEnd || updatedAtStart || updatedAtEnd) && (
|
|
2553
2763
|
<button
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2764
|
+
type="button"
|
|
2765
|
+
onClick={() => {
|
|
2766
|
+
// Réinitialiser tous les filtres de date
|
|
2767
|
+
setCreatedAtStart('');
|
|
2768
|
+
setCreatedAtEnd('');
|
|
2769
|
+
setUpdatedAtStart('');
|
|
2770
|
+
setUpdatedAtEnd('');
|
|
2771
|
+
setCurrentPage(1);
|
|
2772
|
+
// Recharger les contacts sans filtres de date
|
|
2773
|
+
setTimeout(() => {
|
|
2774
|
+
fetchContacts();
|
|
2775
|
+
}, 0);
|
|
2776
|
+
}}
|
|
2777
|
+
className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50"
|
|
2778
|
+
title="Réinitialiser les filtres de date"
|
|
2557
2779
|
>
|
|
2558
2780
|
<X className="h-3.5 w-3.5" />
|
|
2559
|
-
Réinitialiser
|
|
2781
|
+
<span className="hidden sm:inline">Réinitialiser</span>
|
|
2560
2782
|
</button>
|
|
2561
2783
|
)}
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
{/* Importer des contacts (CSV / Excel) */}
|
|
2589
|
-
{/* Importer des contacts */}
|
|
2590
|
-
{canImport && (
|
|
2784
|
+
</div>
|
|
2785
|
+
{/* Gérer les colonnes */}
|
|
2786
|
+
{viewMode === 'table' && (
|
|
2787
|
+
<button
|
|
2788
|
+
onClick={() => setShowColumnPanel(true)}
|
|
2789
|
+
className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50"
|
|
2790
|
+
title="Gérer les colonnes"
|
|
2791
|
+
>
|
|
2792
|
+
Colonnes
|
|
2793
|
+
</button>
|
|
2794
|
+
)}
|
|
2795
|
+
{/* Bouton Réinitialiser les filtres */}
|
|
2796
|
+
{hasActiveFilters() && (
|
|
2797
|
+
<button
|
|
2798
|
+
onClick={handleResetAllFilters}
|
|
2799
|
+
className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50"
|
|
2800
|
+
title="Réinitialiser tous les filtres"
|
|
2801
|
+
>
|
|
2802
|
+
<X className="h-3.5 w-3.5" />
|
|
2803
|
+
Réinitialiser
|
|
2804
|
+
</button>
|
|
2805
|
+
)}
|
|
2806
|
+
{/* Groupe vue (liste / grille) - uniquement pour contacts */}
|
|
2807
|
+
{viewEntity === 'contacts' && (
|
|
2808
|
+
<div className="flex items-center rounded-lg border border-gray-200 bg-white p-1">
|
|
2591
2809
|
<button
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2810
|
+
onClick={() => {
|
|
2811
|
+
setViewMode('table');
|
|
2812
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
2813
|
+
params.set('view', 'table');
|
|
2814
|
+
router.replace(`/contacts?${params.toString()}`, { scroll: false });
|
|
2815
|
+
}}
|
|
2816
|
+
className={cn(
|
|
2817
|
+
'cursor-pointer rounded-md px-2 py-1.5 text-gray-600 transition-colors',
|
|
2818
|
+
viewMode === 'table' ? 'bg-gray-100 text-gray-900' : 'hover:bg-gray-50',
|
|
2819
|
+
)}
|
|
2820
|
+
title="Vue liste"
|
|
2595
2821
|
>
|
|
2596
|
-
<
|
|
2597
|
-
Importer
|
|
2822
|
+
<List className="h-4 w-4" />
|
|
2598
2823
|
</button>
|
|
2599
|
-
)}
|
|
2600
|
-
|
|
2601
|
-
{/* Exporter tous les contacts */}
|
|
2602
|
-
{canExport && (
|
|
2603
2824
|
<button
|
|
2604
2825
|
onClick={() => {
|
|
2605
|
-
|
|
2606
|
-
|
|
2826
|
+
setViewMode('cards');
|
|
2827
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
2828
|
+
params.set('view', 'cards');
|
|
2829
|
+
router.replace(`/contacts?${params.toString()}`, { scroll: false });
|
|
2607
2830
|
}}
|
|
2608
|
-
|
|
2609
|
-
|
|
2831
|
+
className={cn(
|
|
2832
|
+
'cursor-pointer rounded-md px-2 py-1.5 text-gray-600 transition-colors',
|
|
2833
|
+
viewMode === 'cards' ? 'bg-gray-100 text-gray-900' : 'hover:bg-gray-50',
|
|
2834
|
+
)}
|
|
2835
|
+
title="Vue grille"
|
|
2610
2836
|
>
|
|
2611
|
-
<
|
|
2612
|
-
Exporter
|
|
2837
|
+
<LayoutGrid className="h-4 w-4" />
|
|
2613
2838
|
</button>
|
|
2614
|
-
|
|
2839
|
+
</div>
|
|
2840
|
+
)}
|
|
2841
|
+
|
|
2842
|
+
{/* Importer des contacts (CSV / Excel) */}
|
|
2843
|
+
{/* Importer des contacts */}
|
|
2844
|
+
{canImport && (
|
|
2845
|
+
<button
|
|
2846
|
+
type="button"
|
|
2847
|
+
onClick={() => setShowImportModal(true)}
|
|
2848
|
+
className="inline-flex cursor-pointer items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50 focus-visible:ring-2 focus-visible:ring-gray-400/30 focus-visible:ring-offset-2 focus-visible:outline-none sm:text-sm"
|
|
2849
|
+
>
|
|
2850
|
+
<Upload className="h-4 w-4" />
|
|
2851
|
+
Importer
|
|
2852
|
+
</button>
|
|
2853
|
+
)}
|
|
2854
|
+
|
|
2855
|
+
{/* Exporter tous les contacts */}
|
|
2856
|
+
{canExport && (
|
|
2857
|
+
<button
|
|
2858
|
+
onClick={() => {
|
|
2859
|
+
setExportAll(true);
|
|
2860
|
+
setShowExportModal(true);
|
|
2861
|
+
}}
|
|
2862
|
+
disabled={exporting}
|
|
2863
|
+
className="inline-flex cursor-pointer items-center gap-2 rounded-lg border border-blue-600 bg-white px-4 py-2 text-xs font-semibold text-blue-600 shadow-sm transition-colors hover:bg-blue-50 focus-visible:ring-2 focus-visible:ring-gray-400/30 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:text-sm"
|
|
2864
|
+
>
|
|
2865
|
+
<Download className="h-4 w-4" />
|
|
2866
|
+
Exporter
|
|
2867
|
+
</button>
|
|
2868
|
+
)}
|
|
2615
2869
|
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2870
|
+
{/* Ajouter un contact / entreprise */}
|
|
2871
|
+
{((viewEntity === 'contacts' && canCreate) ||
|
|
2872
|
+
(viewEntity === 'companies' && canCreateCompany)) && (
|
|
2619
2873
|
<button
|
|
2620
2874
|
onClick={viewEntity === 'companies' ? handleNewCompany : handleNewContact}
|
|
2621
2875
|
className="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-xs font-semibold text-white shadow-sm transition-colors hover:bg-blue-700 focus-visible:ring-2 focus-visible:ring-gray-400/30 focus-visible:ring-offset-2 focus-visible:outline-none sm:text-sm"
|
|
@@ -2624,15 +2878,14 @@ export default function ContactsPage() {
|
|
|
2624
2878
|
{viewEntity === 'companies' ? 'Ajouter une entreprise' : 'Ajouter'}
|
|
2625
2879
|
</button>
|
|
2626
2880
|
)}
|
|
2627
|
-
</div>
|
|
2628
2881
|
</div>
|
|
2629
2882
|
</div>
|
|
2630
2883
|
|
|
2631
2884
|
<div className="flex flex-1 flex-col overflow-hidden">
|
|
2632
|
-
<div className="flex-1 overflow-auto">
|
|
2885
|
+
<div ref={listScrollContainerRef} className="ui-fade-in flex-1 overflow-auto">
|
|
2633
2886
|
{/* Barre d'actions groupées */}
|
|
2634
2887
|
{selectedContactIds.size > 0 && (
|
|
2635
|
-
<div className="mx-4 mt-4 flex items-center justify-between rounded-lg border border-blue-200 bg-blue-50 p-4 shadow-sm sm:mx-6 lg:mx-8">
|
|
2888
|
+
<div className="ui-slide-up mx-4 mt-4 flex items-center justify-between rounded-lg border border-blue-200 bg-blue-50 p-4 shadow-sm sm:mx-6 lg:mx-8">
|
|
2636
2889
|
<div className="flex items-center gap-4">
|
|
2637
2890
|
<span className="text-sm font-medium text-blue-900">
|
|
2638
2891
|
{selectedContactIds.size} contact(s) sélectionné(s)
|
|
@@ -2705,18 +2958,16 @@ export default function ContactsPage() {
|
|
|
2705
2958
|
<ContactCardsSkeleton />
|
|
2706
2959
|
)
|
|
2707
2960
|
) : (viewEntity === 'companies' ? companies.length : contacts.length) === 0 ? (
|
|
2708
|
-
<div className="mx-4 mt-4 rounded-lg bg-white p-12 text-center shadow sm:mx-6 lg:mx-8">
|
|
2961
|
+
<div className="ui-fade-in mx-4 mt-4 rounded-lg bg-white p-12 text-center shadow sm:mx-6 lg:mx-8">
|
|
2709
2962
|
<div className="text-4xl sm:text-6xl">👥</div>
|
|
2710
2963
|
<h2 className="mt-4 text-lg font-semibold text-gray-900 sm:text-xl">
|
|
2711
2964
|
Aucun contact trouvé
|
|
2712
2965
|
</h2>
|
|
2713
2966
|
<p className="mt-2 text-sm text-gray-600 sm:text-base">
|
|
2714
2967
|
{search ||
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
regionFilter.size > 0 ||
|
|
2719
|
-
departmentFilter.size > 0
|
|
2968
|
+
statusFilter.size > 0 ||
|
|
2969
|
+
assignedCommercialFilter.size > 0 ||
|
|
2970
|
+
assignedTeleproFilter.size > 0
|
|
2720
2971
|
? viewEntity === 'companies'
|
|
2721
2972
|
? 'Aucune entreprise ne correspond à vos critères'
|
|
2722
2973
|
: 'Aucun contact ne correspond à vos critères'
|
|
@@ -2728,8 +2979,6 @@ export default function ContactsPage() {
|
|
|
2728
2979
|
statusFilter.size === 0 &&
|
|
2729
2980
|
assignedCommercialFilter.size === 0 &&
|
|
2730
2981
|
assignedTeleproFilter.size === 0 &&
|
|
2731
|
-
regionFilter.size === 0 &&
|
|
2732
|
-
departmentFilter.size === 0 &&
|
|
2733
2982
|
((viewEntity === 'contacts' && canCreate) ||
|
|
2734
2983
|
(viewEntity === 'companies' && canCreateCompany)) && (
|
|
2735
2984
|
<button
|
|
@@ -2770,7 +3019,7 @@ export default function ContactsPage() {
|
|
|
2770
3019
|
key={company.id}
|
|
2771
3020
|
onClick={() => router.push(`/contacts/companies/${company.id}`)}
|
|
2772
3021
|
className={cn(
|
|
2773
|
-
'cursor-pointer transition-colors',
|
|
3022
|
+
'ui-row-hover cursor-pointer transition-colors',
|
|
2774
3023
|
index % 2 === 1
|
|
2775
3024
|
? 'bg-gray-50/60 hover:bg-gray-100/80'
|
|
2776
3025
|
: 'bg-white hover:bg-gray-50',
|
|
@@ -2927,7 +3176,14 @@ export default function ContactsPage() {
|
|
|
2927
3176
|
</th>
|
|
2928
3177
|
{/* En-têtes dynamiques des colonnes */}
|
|
2929
3178
|
{visibleColumns.map((column) => {
|
|
2930
|
-
const isSortable =
|
|
3179
|
+
const isSortable =
|
|
3180
|
+
column.id === 'createdAt' ||
|
|
3181
|
+
column.id === 'updatedAt' ||
|
|
3182
|
+
column.id === 'postalCode' ||
|
|
3183
|
+
column.id === 'status' ||
|
|
3184
|
+
column.id === 'origin' ||
|
|
3185
|
+
column.id === 'commercial' ||
|
|
3186
|
+
column.id === 'telepro';
|
|
2931
3187
|
const showFilterIcon =
|
|
2932
3188
|
column.id === 'status' ||
|
|
2933
3189
|
column.id === 'origin' ||
|
|
@@ -2952,7 +3208,11 @@ export default function ContactsPage() {
|
|
|
2952
3208
|
className="inline-flex cursor-pointer items-center gap-1"
|
|
2953
3209
|
>
|
|
2954
3210
|
<span>{column.label}</span>
|
|
2955
|
-
{
|
|
3211
|
+
{contactsListColumnShowsActiveSort(
|
|
3212
|
+
column.id,
|
|
3213
|
+
sortField,
|
|
3214
|
+
sortOrder,
|
|
3215
|
+
) && (
|
|
2956
3216
|
<span className="text-blue-600">
|
|
2957
3217
|
{sortOrder === 'asc' ? <ChevronDown /> : <ChevronUp />}
|
|
2958
3218
|
</span>
|
|
@@ -2978,20 +3238,24 @@ export default function ContactsPage() {
|
|
|
2978
3238
|
<Filter
|
|
2979
3239
|
className={cn(
|
|
2980
3240
|
'h-3 w-3',
|
|
2981
|
-
(isSortable &&
|
|
3241
|
+
(isSortable &&
|
|
3242
|
+
contactsListColumnShowsActiveSort(
|
|
3243
|
+
column.id,
|
|
3244
|
+
sortField,
|
|
3245
|
+
sortOrder,
|
|
3246
|
+
)) ||
|
|
2982
3247
|
(column.id === 'createdAt' &&
|
|
2983
3248
|
(createdAtStart || createdAtEnd)) ||
|
|
2984
3249
|
(column.id === 'updatedAt' &&
|
|
2985
3250
|
(updatedAtStart || updatedAtEnd)) ||
|
|
2986
|
-
(column.id === 'region' && regionFilter.size > 0) ||
|
|
2987
|
-
(column.id === 'department' &&
|
|
2988
|
-
departmentFilter.size > 0) ||
|
|
2989
3251
|
(column.id === 'status' && statusFilter.size > 0) ||
|
|
2990
3252
|
(column.id === 'origin' && originFilter.size > 0) ||
|
|
2991
3253
|
(column.id === 'commercial' &&
|
|
2992
3254
|
assignedCommercialFilter.size > 0) ||
|
|
2993
3255
|
(column.id === 'telepro' &&
|
|
2994
|
-
assignedTeleproFilter.size > 0)
|
|
3256
|
+
assignedTeleproFilter.size > 0) ||
|
|
3257
|
+
(column.id === 'region' && regionFilter.size > 0) ||
|
|
3258
|
+
(column.id === 'department' && departmentFilter.size > 0)
|
|
2995
3259
|
? 'text-blue-600'
|
|
2996
3260
|
: 'text-gray-400',
|
|
2997
3261
|
)}
|
|
@@ -3012,9 +3276,9 @@ export default function ContactsPage() {
|
|
|
3012
3276
|
{sortedContacts.map((contact, index) => (
|
|
3013
3277
|
<tr
|
|
3014
3278
|
key={contact.id}
|
|
3015
|
-
onClick={() =>
|
|
3279
|
+
onClick={() => navigateToContactDetail(contact.id)}
|
|
3016
3280
|
className={cn(
|
|
3017
|
-
'cursor-pointer transition-colors',
|
|
3281
|
+
'ui-row-hover cursor-pointer transition-colors',
|
|
3018
3282
|
selectedContactIds.has(contact.id)
|
|
3019
3283
|
? 'bg-blue-50 hover:bg-blue-100'
|
|
3020
3284
|
: index % 2 === 1
|
|
@@ -3051,8 +3315,8 @@ export default function ContactsPage() {
|
|
|
3051
3315
|
return (
|
|
3052
3316
|
<div
|
|
3053
3317
|
key={contact.id}
|
|
3054
|
-
onClick={() =>
|
|
3055
|
-
className="relative cursor-pointer rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-
|
|
3318
|
+
onClick={() => navigateToContactDetail(contact.id)}
|
|
3319
|
+
className="ui-lift-hover relative cursor-pointer rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-[border-color,box-shadow] hover:border-blue-300 hover:shadow-md"
|
|
3056
3320
|
>
|
|
3057
3321
|
{/* En-tête avec trois points */}
|
|
3058
3322
|
<div className="mb-4 flex items-start justify-between">
|
|
@@ -3098,10 +3362,10 @@ export default function ContactsPage() {
|
|
|
3098
3362
|
<span className="font-medium">Email :</span>
|
|
3099
3363
|
<span className="ml-1">{contact.email || '-'}</span>
|
|
3100
3364
|
</div>
|
|
3101
|
-
{contact.company && (
|
|
3365
|
+
{(contact.company || contact.companyName) && (
|
|
3102
3366
|
<div className="flex items-center text-base text-gray-900">
|
|
3103
3367
|
<Building2 className="mr-2 h-4 w-4 text-gray-400" />
|
|
3104
|
-
<span>{contact.company.name}</span>
|
|
3368
|
+
<span>{contact.company ? contact.company.name : contact.companyName}</span>
|
|
3105
3369
|
</div>
|
|
3106
3370
|
)}
|
|
3107
3371
|
{contact.city && (
|
|
@@ -3219,16 +3483,29 @@ export default function ContactsPage() {
|
|
|
3219
3483
|
{totalPages > 1 ? (
|
|
3220
3484
|
<div className="flex items-center gap-2">
|
|
3221
3485
|
<button
|
|
3222
|
-
onClick={() =>
|
|
3223
|
-
|
|
3486
|
+
onClick={() => {
|
|
3487
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
3488
|
+
params.set('page', '1');
|
|
3489
|
+
params.set('limit', String(limit));
|
|
3490
|
+
params.set('view', viewMode);
|
|
3491
|
+
router.replace(`/contacts?${params.toString()}`, { scroll: false });
|
|
3492
|
+
}}
|
|
3493
|
+
disabled={uiCurrentPage === 1}
|
|
3224
3494
|
className="cursor-pointer rounded-md border border-gray-300 bg-white px-2 py-1.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
|
|
3225
3495
|
title="Première page"
|
|
3226
3496
|
>
|
|
3227
3497
|
<ChevronsLeft className="h-4 w-4" />
|
|
3228
3498
|
</button>
|
|
3229
3499
|
<button
|
|
3230
|
-
onClick={() =>
|
|
3231
|
-
|
|
3500
|
+
onClick={() => {
|
|
3501
|
+
const p = Math.max(1, uiCurrentPage - 1);
|
|
3502
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
3503
|
+
params.set('page', String(p));
|
|
3504
|
+
params.set('limit', String(limit));
|
|
3505
|
+
params.set('view', viewMode);
|
|
3506
|
+
router.replace(`/contacts?${params.toString()}`, { scroll: false });
|
|
3507
|
+
}}
|
|
3508
|
+
disabled={uiCurrentPage === 1}
|
|
3232
3509
|
className="cursor-pointer rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
|
|
3233
3510
|
>
|
|
3234
3511
|
Précédent
|
|
@@ -3238,7 +3515,7 @@ export default function ContactsPage() {
|
|
|
3238
3515
|
.filter((page) => {
|
|
3239
3516
|
if (totalPages <= 7) return true;
|
|
3240
3517
|
if (page === 1 || page === totalPages) return true;
|
|
3241
|
-
return Math.abs(page -
|
|
3518
|
+
return Math.abs(page - uiCurrentPage) <= 2;
|
|
3242
3519
|
})
|
|
3243
3520
|
.map((page, idx, array) => {
|
|
3244
3521
|
const prevPage = array[idx - 1];
|
|
@@ -3247,10 +3524,18 @@ export default function ContactsPage() {
|
|
|
3247
3524
|
<div key={page} className="flex items-center gap-1">
|
|
3248
3525
|
{showEllipsis && <span className="px-1 text-gray-400">...</span>}
|
|
3249
3526
|
<button
|
|
3250
|
-
onClick={() =>
|
|
3527
|
+
onClick={() => {
|
|
3528
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
3529
|
+
params.set('page', String(page));
|
|
3530
|
+
params.set('limit', String(limit));
|
|
3531
|
+
params.set('view', viewMode);
|
|
3532
|
+
router.replace(`/contacts?${params.toString()}`, {
|
|
3533
|
+
scroll: false,
|
|
3534
|
+
});
|
|
3535
|
+
}}
|
|
3251
3536
|
className={cn(
|
|
3252
3537
|
'cursor-pointer rounded-md border px-2.5 py-1.5 text-sm font-medium transition-colors',
|
|
3253
|
-
|
|
3538
|
+
uiCurrentPage === page
|
|
3254
3539
|
? 'border-blue-600 bg-blue-600 text-white'
|
|
3255
3540
|
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50',
|
|
3256
3541
|
)}
|
|
@@ -3262,15 +3547,28 @@ export default function ContactsPage() {
|
|
|
3262
3547
|
})}
|
|
3263
3548
|
</div>
|
|
3264
3549
|
<button
|
|
3265
|
-
onClick={() =>
|
|
3266
|
-
|
|
3550
|
+
onClick={() => {
|
|
3551
|
+
const p = Math.min(totalPages, uiCurrentPage + 1);
|
|
3552
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
3553
|
+
params.set('page', String(p));
|
|
3554
|
+
params.set('limit', String(limit));
|
|
3555
|
+
params.set('view', viewMode);
|
|
3556
|
+
router.replace(`/contacts?${params.toString()}`, { scroll: false });
|
|
3557
|
+
}}
|
|
3558
|
+
disabled={uiCurrentPage === totalPages}
|
|
3267
3559
|
className="cursor-pointer rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
|
|
3268
3560
|
>
|
|
3269
3561
|
Suivant
|
|
3270
3562
|
</button>
|
|
3271
3563
|
<button
|
|
3272
|
-
onClick={() =>
|
|
3273
|
-
|
|
3564
|
+
onClick={() => {
|
|
3565
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
3566
|
+
params.set('page', String(totalPages));
|
|
3567
|
+
params.set('limit', String(limit));
|
|
3568
|
+
params.set('view', viewMode);
|
|
3569
|
+
router.replace(`/contacts?${params.toString()}`, { scroll: false });
|
|
3570
|
+
}}
|
|
3571
|
+
disabled={uiCurrentPage === totalPages}
|
|
3274
3572
|
className="cursor-pointer rounded-md border border-gray-300 bg-white px-2 py-1.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
|
|
3275
3573
|
title="Dernière page"
|
|
3276
3574
|
>
|
|
@@ -3284,10 +3582,16 @@ export default function ContactsPage() {
|
|
|
3284
3582
|
<select
|
|
3285
3583
|
value={limit}
|
|
3286
3584
|
onChange={(e) => {
|
|
3287
|
-
|
|
3585
|
+
const newLimit = Number(e.target.value);
|
|
3586
|
+
setLimit(newLimit);
|
|
3288
3587
|
setCurrentPage(1);
|
|
3588
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
3589
|
+
params.set('limit', String(newLimit));
|
|
3590
|
+
params.set('page', '1');
|
|
3591
|
+
params.set('view', viewMode);
|
|
3592
|
+
router.replace(`/contacts?${params.toString()}`, { scroll: false });
|
|
3289
3593
|
}}
|
|
3290
|
-
className="rounded-md border border-gray-300 bg-white px-2 py-1.5 text-sm text-gray-700 focus:
|
|
3594
|
+
className="rounded-md border border-gray-300 bg-white px-2 py-1.5 text-sm text-gray-700 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3291
3595
|
>
|
|
3292
3596
|
<option value={25}>25 par page</option>
|
|
3293
3597
|
<option value={50}>50 par page</option>
|
|
@@ -3407,8 +3711,8 @@ export default function ContactsPage() {
|
|
|
3407
3711
|
|
|
3408
3712
|
{/* Modal de création/édition */}
|
|
3409
3713
|
{showModal && (
|
|
3410
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
|
|
3411
|
-
<div className="flex max-h-[90vh] w-full max-w-5xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
|
|
3714
|
+
<div className="ui-fade-in fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
|
|
3715
|
+
<div className="ui-scale-in flex max-h-[90vh] w-full max-w-5xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
|
|
3412
3716
|
{/* En-tête fixe */}
|
|
3413
3717
|
<div className="shrink-0 border-b border-gray-100 pb-4">
|
|
3414
3718
|
<div className="flex items-center justify-between">
|
|
@@ -3453,7 +3757,7 @@ export default function ContactsPage() {
|
|
|
3453
3757
|
<select
|
|
3454
3758
|
value={formData.civility}
|
|
3455
3759
|
onChange={(e) => setFormData({ ...formData, civility: e.target.value })}
|
|
3456
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
3760
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3457
3761
|
>
|
|
3458
3762
|
<option value="">-</option>
|
|
3459
3763
|
<option value="M">M.</option>
|
|
@@ -3468,7 +3772,7 @@ export default function ContactsPage() {
|
|
|
3468
3772
|
type="text"
|
|
3469
3773
|
value={formData.firstName}
|
|
3470
3774
|
onChange={(e) => setFormData({ ...formData, firstName: e.target.value })}
|
|
3471
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
3775
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3472
3776
|
placeholder="Prénom"
|
|
3473
3777
|
/>
|
|
3474
3778
|
</div>
|
|
@@ -3479,7 +3783,7 @@ export default function ContactsPage() {
|
|
|
3479
3783
|
type="text"
|
|
3480
3784
|
value={formData.lastName}
|
|
3481
3785
|
onChange={(e) => setFormData({ ...formData, lastName: e.target.value })}
|
|
3482
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
3786
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3483
3787
|
placeholder="Nom"
|
|
3484
3788
|
/>
|
|
3485
3789
|
</div>
|
|
@@ -3503,7 +3807,7 @@ export default function ContactsPage() {
|
|
|
3503
3807
|
setFormData({ ...formData, phone: normalized });
|
|
3504
3808
|
}
|
|
3505
3809
|
}}
|
|
3506
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
3810
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3507
3811
|
placeholder="06 12 34 56 78"
|
|
3508
3812
|
/>
|
|
3509
3813
|
</div>
|
|
@@ -3524,7 +3828,7 @@ export default function ContactsPage() {
|
|
|
3524
3828
|
setFormData({ ...formData, secondaryPhone: normalized });
|
|
3525
3829
|
}
|
|
3526
3830
|
}}
|
|
3527
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
3831
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3528
3832
|
placeholder="06 12 34 56 78"
|
|
3529
3833
|
/>
|
|
3530
3834
|
</div>
|
|
@@ -3535,10 +3839,21 @@ export default function ContactsPage() {
|
|
|
3535
3839
|
type="email"
|
|
3536
3840
|
value={formData.email}
|
|
3537
3841
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
3538
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
3842
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3539
3843
|
placeholder="email@exemple.com"
|
|
3540
3844
|
/>
|
|
3541
3845
|
</div>
|
|
3846
|
+
|
|
3847
|
+
<div className="md:col-span-2">
|
|
3848
|
+
<label className="block text-sm font-medium text-gray-700">Site internet</label>
|
|
3849
|
+
<input
|
|
3850
|
+
type="url"
|
|
3851
|
+
value={formData.website}
|
|
3852
|
+
onChange={(e) => setFormData({ ...formData, website: e.target.value })}
|
|
3853
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3854
|
+
placeholder="https://example.com"
|
|
3855
|
+
/>
|
|
3856
|
+
</div>
|
|
3542
3857
|
</div>
|
|
3543
3858
|
</div>
|
|
3544
3859
|
|
|
@@ -3548,18 +3863,13 @@ export default function ContactsPage() {
|
|
|
3548
3863
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
3549
3864
|
<div>
|
|
3550
3865
|
<label className="block text-sm font-medium text-gray-700">Statut</label>
|
|
3551
|
-
<
|
|
3866
|
+
<StatusSelect
|
|
3867
|
+
statuses={statuses}
|
|
3552
3868
|
value={formData.statusId}
|
|
3553
|
-
onChange={(
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
{statuses.map((status) => (
|
|
3558
|
-
<option key={status.id} value={status.id}>
|
|
3559
|
-
{status.name}
|
|
3560
|
-
</option>
|
|
3561
|
-
))}
|
|
3562
|
-
</select>
|
|
3869
|
+
onChange={(v) => setFormData({ ...formData, statusId: v })}
|
|
3870
|
+
placeholder="Aucun statut"
|
|
3871
|
+
className="mt-1"
|
|
3872
|
+
/>
|
|
3563
3873
|
</div>
|
|
3564
3874
|
|
|
3565
3875
|
{/* Motif de fermeture (visible si le statut le requiert) */}
|
|
@@ -3576,7 +3886,7 @@ export default function ContactsPage() {
|
|
|
3576
3886
|
onChange={(e) =>
|
|
3577
3887
|
setFormData({ ...formData, closingReason: e.target.value })
|
|
3578
3888
|
}
|
|
3579
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
3889
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3580
3890
|
>
|
|
3581
3891
|
<option value="">Sélectionnez un motif</option>
|
|
3582
3892
|
{closingReasons.map((reason) => (
|
|
@@ -3596,17 +3906,17 @@ export default function ContactsPage() {
|
|
|
3596
3906
|
onChange={(e) =>
|
|
3597
3907
|
setFormData({ ...formData, assignedCommercialId: e.target.value })
|
|
3598
3908
|
}
|
|
3599
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
3909
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3600
3910
|
>
|
|
3601
3911
|
<option value="">Non assigné</option>
|
|
3602
3912
|
{(isAdmin
|
|
3603
3913
|
? users.filter((u) => u.role !== 'USER')
|
|
3604
3914
|
: users.filter(
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3915
|
+
(u) =>
|
|
3916
|
+
u.role === 'COMMERCIAL' ||
|
|
3917
|
+
u.role === 'ADMIN' ||
|
|
3918
|
+
u.role === 'MANAGER',
|
|
3919
|
+
)
|
|
3610
3920
|
).map((user) => (
|
|
3611
3921
|
<option key={user.id} value={user.id}>
|
|
3612
3922
|
{user.name}
|
|
@@ -3622,7 +3932,7 @@ export default function ContactsPage() {
|
|
|
3622
3932
|
onChange={(e) =>
|
|
3623
3933
|
setFormData({ ...formData, assignedTeleproId: e.target.value })
|
|
3624
3934
|
}
|
|
3625
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
3935
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3626
3936
|
>
|
|
3627
3937
|
<option value="">Non assigné</option>
|
|
3628
3938
|
{(isAdmin
|
|
@@ -3667,7 +3977,7 @@ export default function ContactsPage() {
|
|
|
3667
3977
|
type="text"
|
|
3668
3978
|
value={formData.city}
|
|
3669
3979
|
onChange={(e) => setFormData({ ...formData, city: e.target.value })}
|
|
3670
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
3980
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3671
3981
|
placeholder="Paris"
|
|
3672
3982
|
/>
|
|
3673
3983
|
</div>
|
|
@@ -3680,7 +3990,7 @@ export default function ContactsPage() {
|
|
|
3680
3990
|
type="text"
|
|
3681
3991
|
value={formData.postalCode}
|
|
3682
3992
|
onChange={(e) => setFormData({ ...formData, postalCode: e.target.value })}
|
|
3683
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
3993
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3684
3994
|
placeholder="75001"
|
|
3685
3995
|
/>
|
|
3686
3996
|
</div>
|
|
@@ -3697,7 +4007,7 @@ export default function ContactsPage() {
|
|
|
3697
4007
|
<select
|
|
3698
4008
|
value={formData.companyId}
|
|
3699
4009
|
onChange={(e) => setFormData({ ...formData, companyId: e.target.value })}
|
|
3700
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
4010
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3701
4011
|
>
|
|
3702
4012
|
<option value="">Aucune entreprise</option>
|
|
3703
4013
|
{allCompanies.map((company) => (
|
|
@@ -3716,7 +4026,7 @@ export default function ContactsPage() {
|
|
|
3716
4026
|
type="text"
|
|
3717
4027
|
value={formData.jobTitle}
|
|
3718
4028
|
onChange={(e) => setFormData({ ...formData, jobTitle: e.target.value })}
|
|
3719
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
4029
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3720
4030
|
placeholder="Ex. Directeur commercial, Assistant..."
|
|
3721
4031
|
/>
|
|
3722
4032
|
</div>
|
|
@@ -3729,13 +4039,12 @@ export default function ContactsPage() {
|
|
|
3729
4039
|
type="text"
|
|
3730
4040
|
value={formData.origin}
|
|
3731
4041
|
onChange={(e) => setFormData({ ...formData, origin: e.target.value })}
|
|
3732
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
4042
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3733
4043
|
placeholder="Site web, recommandation, etc."
|
|
3734
4044
|
/>
|
|
3735
4045
|
</div>
|
|
3736
4046
|
</div>
|
|
3737
4047
|
</div>
|
|
3738
|
-
|
|
3739
4048
|
</form>
|
|
3740
4049
|
|
|
3741
4050
|
{/* Pied de modal fixe */}
|
|
@@ -3767,8 +4076,8 @@ export default function ContactsPage() {
|
|
|
3767
4076
|
|
|
3768
4077
|
{/* Modal Entreprise */}
|
|
3769
4078
|
{showCompanyModal && (
|
|
3770
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
|
|
3771
|
-
<div className="flex max-h-[90vh] w-full max-w-2xl flex-col rounded-lg bg-white shadow-xl">
|
|
4079
|
+
<div className="ui-fade-in fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
|
|
4080
|
+
<div className="ui-scale-in flex max-h-[90vh] w-full max-w-2xl flex-col rounded-lg bg-white shadow-xl">
|
|
3772
4081
|
<div className="shrink-0 border-b border-gray-100 px-6 py-4">
|
|
3773
4082
|
<div className="flex items-center justify-between">
|
|
3774
4083
|
<h2 className="text-xl font-bold text-gray-900">
|
|
@@ -3798,7 +4107,7 @@ export default function ContactsPage() {
|
|
|
3798
4107
|
onChange={(e) =>
|
|
3799
4108
|
setCompanyFormData({ ...companyFormData, name: e.target.value })
|
|
3800
4109
|
}
|
|
3801
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
4110
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3802
4111
|
required
|
|
3803
4112
|
/>
|
|
3804
4113
|
</div>
|
|
@@ -3810,7 +4119,7 @@ export default function ContactsPage() {
|
|
|
3810
4119
|
onChange={(e) =>
|
|
3811
4120
|
setCompanyFormData({ ...companyFormData, phone: e.target.value })
|
|
3812
4121
|
}
|
|
3813
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
4122
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3814
4123
|
/>
|
|
3815
4124
|
</div>
|
|
3816
4125
|
<div>
|
|
@@ -3821,7 +4130,7 @@ export default function ContactsPage() {
|
|
|
3821
4130
|
onChange={(e) =>
|
|
3822
4131
|
setCompanyFormData({ ...companyFormData, email: e.target.value })
|
|
3823
4132
|
}
|
|
3824
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
4133
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3825
4134
|
/>
|
|
3826
4135
|
</div>
|
|
3827
4136
|
<div className="md:col-span-2">
|
|
@@ -3853,7 +4162,7 @@ export default function ContactsPage() {
|
|
|
3853
4162
|
onChange={(e) =>
|
|
3854
4163
|
setCompanyFormData({ ...companyFormData, city: e.target.value })
|
|
3855
4164
|
}
|
|
3856
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
4165
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3857
4166
|
/>
|
|
3858
4167
|
</div>
|
|
3859
4168
|
<div>
|
|
@@ -3864,7 +4173,7 @@ export default function ContactsPage() {
|
|
|
3864
4173
|
onChange={(e) =>
|
|
3865
4174
|
setCompanyFormData({ ...companyFormData, postalCode: e.target.value })
|
|
3866
4175
|
}
|
|
3867
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
4176
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3868
4177
|
/>
|
|
3869
4178
|
</div>
|
|
3870
4179
|
<div>
|
|
@@ -3875,7 +4184,7 @@ export default function ContactsPage() {
|
|
|
3875
4184
|
onChange={(e) =>
|
|
3876
4185
|
setCompanyFormData({ ...companyFormData, website: e.target.value })
|
|
3877
4186
|
}
|
|
3878
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
4187
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3879
4188
|
placeholder="https://"
|
|
3880
4189
|
/>
|
|
3881
4190
|
</div>
|
|
@@ -3887,7 +4196,7 @@ export default function ContactsPage() {
|
|
|
3887
4196
|
onChange={(e) =>
|
|
3888
4197
|
setCompanyFormData({ ...companyFormData, siret: e.target.value })
|
|
3889
4198
|
}
|
|
3890
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
4199
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3891
4200
|
/>
|
|
3892
4201
|
</div>
|
|
3893
4202
|
<div>
|
|
@@ -3898,7 +4207,7 @@ export default function ContactsPage() {
|
|
|
3898
4207
|
onChange={(e) =>
|
|
3899
4208
|
setCompanyFormData({ ...companyFormData, industry: e.target.value })
|
|
3900
4209
|
}
|
|
3901
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
4210
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3902
4211
|
/>
|
|
3903
4212
|
</div>
|
|
3904
4213
|
<div>
|
|
@@ -3911,7 +4220,7 @@ export default function ContactsPage() {
|
|
|
3911
4220
|
assignedCommercialId: e.target.value,
|
|
3912
4221
|
})
|
|
3913
4222
|
}
|
|
3914
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
4223
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3915
4224
|
>
|
|
3916
4225
|
<option value="">Non attribué</option>
|
|
3917
4226
|
{users.map((u) => (
|
|
@@ -3931,7 +4240,7 @@ export default function ContactsPage() {
|
|
|
3931
4240
|
assignedTeleproId: e.target.value,
|
|
3932
4241
|
})
|
|
3933
4242
|
}
|
|
3934
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
4243
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3935
4244
|
>
|
|
3936
4245
|
<option value="">Non attribué</option>
|
|
3937
4246
|
{users.map((u) => (
|
|
@@ -3949,7 +4258,7 @@ export default function ContactsPage() {
|
|
|
3949
4258
|
setCompanyFormData({ ...companyFormData, notes: e.target.value })
|
|
3950
4259
|
}
|
|
3951
4260
|
rows={3}
|
|
3952
|
-
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
4261
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
3953
4262
|
/>
|
|
3954
4263
|
</div>
|
|
3955
4264
|
</div>
|
|
@@ -3983,8 +4292,8 @@ export default function ContactsPage() {
|
|
|
3983
4292
|
|
|
3984
4293
|
{/* Modal d'import */}
|
|
3985
4294
|
{showImportModal && (
|
|
3986
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
|
|
3987
|
-
<div className="flex max-h-[90vh] w-full max-w-5xl flex-col rounded-lg bg-white shadow-xl">
|
|
4295
|
+
<div className="ui-fade-in fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
|
|
4296
|
+
<div className="ui-scale-in flex max-h-[90vh] w-full max-w-5xl flex-col rounded-lg bg-white shadow-xl">
|
|
3988
4297
|
{/* En-tête */}
|
|
3989
4298
|
<div className="shrink-0 border-b border-gray-100 px-6 py-4">
|
|
3990
4299
|
<div className="flex items-center justify-between">
|
|
@@ -4213,7 +4522,7 @@ export default function ContactsPage() {
|
|
|
4213
4522
|
key={rowIdx}
|
|
4214
4523
|
onClick={() => setImportHeaderRow(rowIdx)}
|
|
4215
4524
|
className={cn(
|
|
4216
|
-
'cursor-pointer transition-colors',
|
|
4525
|
+
'ui-row-hover cursor-pointer transition-colors',
|
|
4217
4526
|
importHeaderRow === rowIdx
|
|
4218
4527
|
? 'bg-blue-50 ring-2 ring-blue-500 ring-inset'
|
|
4219
4528
|
: rowIdx < importHeaderRow
|
|
@@ -4299,18 +4608,14 @@ export default function ContactsPage() {
|
|
|
4299
4608
|
<label className="mb-1 block text-sm font-medium text-gray-700">
|
|
4300
4609
|
Statut par défaut
|
|
4301
4610
|
</label>
|
|
4302
|
-
<
|
|
4611
|
+
<StatusSelect
|
|
4612
|
+
statuses={statuses}
|
|
4303
4613
|
value={defaultStatusId}
|
|
4304
|
-
onChange={
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
<option key={status.id} value={status.id}>
|
|
4310
|
-
{status.name}
|
|
4311
|
-
</option>
|
|
4312
|
-
))}
|
|
4313
|
-
</select>
|
|
4614
|
+
onChange={setDefaultStatusId}
|
|
4615
|
+
placeholder="Aucun statut par défaut"
|
|
4616
|
+
className="w-full"
|
|
4617
|
+
size="sm"
|
|
4618
|
+
/>
|
|
4314
4619
|
</div>
|
|
4315
4620
|
|
|
4316
4621
|
<div>
|
|
@@ -4320,7 +4625,7 @@ export default function ContactsPage() {
|
|
|
4320
4625
|
<select
|
|
4321
4626
|
value={defaultCommercialId}
|
|
4322
4627
|
onChange={(e) => setDefaultCommercialId(e.target.value)}
|
|
4323
|
-
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
4628
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
4324
4629
|
>
|
|
4325
4630
|
<option value="">Aucun commercial par défaut</option>
|
|
4326
4631
|
{users
|
|
@@ -4342,7 +4647,7 @@ export default function ContactsPage() {
|
|
|
4342
4647
|
value={defaultOrigin}
|
|
4343
4648
|
onChange={(e) => setDefaultOrigin(e.target.value)}
|
|
4344
4649
|
placeholder="Ex: Facebook Ads, Google Ads, etc."
|
|
4345
|
-
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
4650
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
4346
4651
|
/>
|
|
4347
4652
|
</div>
|
|
4348
4653
|
</div>
|
|
@@ -4420,7 +4725,7 @@ export default function ContactsPage() {
|
|
|
4420
4725
|
},
|
|
4421
4726
|
});
|
|
4422
4727
|
}}
|
|
4423
|
-
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
4728
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
4424
4729
|
>
|
|
4425
4730
|
<option value="map">Mapper vers un champ</option>
|
|
4426
4731
|
<option value="note">Ajouter comme note</option>
|
|
@@ -4452,7 +4757,7 @@ export default function ContactsPage() {
|
|
|
4452
4757
|
},
|
|
4453
4758
|
});
|
|
4454
4759
|
}}
|
|
4455
|
-
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
4760
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
4456
4761
|
>
|
|
4457
4762
|
<option value="">Sélectionnez un champ</option>
|
|
4458
4763
|
<option value="phone">Téléphone *</option>
|
|
@@ -4464,7 +4769,15 @@ export default function ContactsPage() {
|
|
|
4464
4769
|
<option value="address">Adresse</option>
|
|
4465
4770
|
<option value="city">Ville</option>
|
|
4466
4771
|
<option value="postalCode">Code postal</option>
|
|
4772
|
+
<option value="companyName">Société</option>
|
|
4467
4773
|
<option value="origin">Origine</option>
|
|
4774
|
+
<option value="website">Site internet</option>
|
|
4775
|
+
<option value="jobTitle">Intitulé du poste</option>
|
|
4776
|
+
<option value="createdAt">Date de création</option>
|
|
4777
|
+
<option value="linkedin">LinkedIn</option>
|
|
4778
|
+
<option value="facebook">Facebook</option>
|
|
4779
|
+
<option value="twitter">Twitter</option>
|
|
4780
|
+
<option value="instagram">Instagram</option>
|
|
4468
4781
|
</select>
|
|
4469
4782
|
</div>
|
|
4470
4783
|
)}
|
|
@@ -4539,7 +4852,6 @@ export default function ContactsPage() {
|
|
|
4539
4852
|
</ul>
|
|
4540
4853
|
</div>
|
|
4541
4854
|
)}
|
|
4542
|
-
|
|
4543
4855
|
</div>
|
|
4544
4856
|
)}
|
|
4545
4857
|
</div>
|
|
@@ -4622,8 +4934,8 @@ export default function ContactsPage() {
|
|
|
4622
4934
|
|
|
4623
4935
|
{/* Modal de résultats d'import */}
|
|
4624
4936
|
{showImportResultModal && importResultData && (
|
|
4625
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm">
|
|
4626
|
-
<div className="w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
|
|
4937
|
+
<div className="ui-fade-in fixed inset-0 z-50 flex min-h-dvh items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm">
|
|
4938
|
+
<div className="ui-scale-in w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
|
|
4627
4939
|
<div className="mb-4 flex items-center justify-between">
|
|
4628
4940
|
<h3 className="text-lg font-semibold text-gray-900">Résultat de l'import</h3>
|
|
4629
4941
|
<button
|
|
@@ -4771,8 +5083,8 @@ export default function ContactsPage() {
|
|
|
4771
5083
|
|
|
4772
5084
|
{/* Modal - Changer commercial */}
|
|
4773
5085
|
{showBulkCommercialModal && (
|
|
4774
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm">
|
|
4775
|
-
<div className="w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
|
|
5086
|
+
<div className="ui-fade-in fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm">
|
|
5087
|
+
<div className="ui-scale-in w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
|
|
4776
5088
|
<div className="mb-4 flex items-center justify-between">
|
|
4777
5089
|
<h3 className="text-lg font-semibold text-gray-900">Changer le commercial</h3>
|
|
4778
5090
|
<button
|
|
@@ -4797,7 +5109,7 @@ export default function ContactsPage() {
|
|
|
4797
5109
|
<select
|
|
4798
5110
|
value={bulkCommercialId}
|
|
4799
5111
|
onChange={(e) => setBulkCommercialId(e.target.value)}
|
|
4800
|
-
className="w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
5112
|
+
className="w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
4801
5113
|
>
|
|
4802
5114
|
<option value="">Sélectionner un commercial</option>
|
|
4803
5115
|
{users
|
|
@@ -4834,8 +5146,8 @@ export default function ContactsPage() {
|
|
|
4834
5146
|
|
|
4835
5147
|
{/* Modal - Changer télépro */}
|
|
4836
5148
|
{showBulkTeleproModal && (
|
|
4837
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm">
|
|
4838
|
-
<div className="w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
|
|
5149
|
+
<div className="ui-fade-in fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm">
|
|
5150
|
+
<div className="ui-scale-in w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
|
|
4839
5151
|
<div className="mb-4 flex items-center justify-between">
|
|
4840
5152
|
<h3 className="text-lg font-semibold text-gray-900">Changer le télépro</h3>
|
|
4841
5153
|
<button
|
|
@@ -4860,7 +5172,7 @@ export default function ContactsPage() {
|
|
|
4860
5172
|
<select
|
|
4861
5173
|
value={bulkTeleproId}
|
|
4862
5174
|
onChange={(e) => setBulkTeleproId(e.target.value)}
|
|
4863
|
-
className="w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
5175
|
+
className="w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
4864
5176
|
>
|
|
4865
5177
|
<option value="">Sélectionner un télépro</option>
|
|
4866
5178
|
{users
|
|
@@ -4897,8 +5209,8 @@ export default function ContactsPage() {
|
|
|
4897
5209
|
|
|
4898
5210
|
{/* Modal - Changer statut */}
|
|
4899
5211
|
{showBulkStatusModal && (
|
|
4900
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm">
|
|
4901
|
-
<div className="w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
|
|
5212
|
+
<div className="ui-fade-in fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm">
|
|
5213
|
+
<div className="ui-scale-in w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
|
|
4902
5214
|
<div className="mb-4 flex items-center justify-between">
|
|
4903
5215
|
<h3 className="text-lg font-semibold text-gray-900">Changer le statut</h3>
|
|
4904
5216
|
<button
|
|
@@ -4920,18 +5232,13 @@ export default function ContactsPage() {
|
|
|
4920
5232
|
<label className="mb-2 block text-sm font-medium text-gray-700">
|
|
4921
5233
|
Nouveau statut
|
|
4922
5234
|
</label>
|
|
4923
|
-
<
|
|
5235
|
+
<StatusSelect
|
|
5236
|
+
statuses={statuses}
|
|
4924
5237
|
value={bulkStatusId}
|
|
4925
|
-
onChange={
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
{statuses.map((status) => (
|
|
4930
|
-
<option key={status.id} value={status.id}>
|
|
4931
|
-
{status.name}
|
|
4932
|
-
</option>
|
|
4933
|
-
))}
|
|
4934
|
-
</select>
|
|
5238
|
+
onChange={setBulkStatusId}
|
|
5239
|
+
placeholder="Sélectionner un statut"
|
|
5240
|
+
className="w-full"
|
|
5241
|
+
/>
|
|
4935
5242
|
</div>
|
|
4936
5243
|
|
|
4937
5244
|
<div className="flex justify-end gap-3">
|
|
@@ -4960,7 +5267,7 @@ export default function ContactsPage() {
|
|
|
4960
5267
|
{dateFilterModal && dateFilterPosition && (
|
|
4961
5268
|
<div
|
|
4962
5269
|
ref={dateFilterModalRef}
|
|
4963
|
-
className="fixed z-50 w-72 rounded-lg border border-gray-200 bg-white p-4 shadow-xl"
|
|
5270
|
+
className="ui-dropdown-enter fixed z-50 w-72 rounded-lg border border-gray-200 bg-white p-4 shadow-xl"
|
|
4964
5271
|
style={{ top: dateFilterPosition.top, left: dateFilterPosition.left }}
|
|
4965
5272
|
>
|
|
4966
5273
|
<div className="relative">
|
|
@@ -4978,54 +5285,35 @@ export default function ContactsPage() {
|
|
|
4978
5285
|
{dateFilterModal === 'createdAt' ? 'Date de création' : 'Date de modification'}
|
|
4979
5286
|
</h3>
|
|
4980
5287
|
|
|
4981
|
-
<
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
className="w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
5011
|
-
/>
|
|
5012
|
-
</div>
|
|
5013
|
-
</div>
|
|
5014
|
-
|
|
5015
|
-
<div className="mt-4 flex justify-end gap-2">
|
|
5016
|
-
<button
|
|
5017
|
-
onClick={() => handleClearDateFilter(dateFilterModal)}
|
|
5018
|
-
className="cursor-pointer rounded-md border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50"
|
|
5019
|
-
>
|
|
5020
|
-
Effacer
|
|
5021
|
-
</button>
|
|
5022
|
-
<button
|
|
5023
|
-
onClick={handleApplyDateFilter}
|
|
5024
|
-
className="cursor-pointer rounded-md bg-blue-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-blue-700"
|
|
5025
|
-
>
|
|
5026
|
-
Filtrer
|
|
5027
|
-
</button>
|
|
5028
|
-
</div>
|
|
5288
|
+
<DatePicker
|
|
5289
|
+
embedded
|
|
5290
|
+
isPeriod
|
|
5291
|
+
startDate={
|
|
5292
|
+
dateFilterModal === 'createdAt' ? createdAtStart : updatedAtStart
|
|
5293
|
+
}
|
|
5294
|
+
endDate={
|
|
5295
|
+
dateFilterModal === 'createdAt' ? createdAtEnd : updatedAtEnd
|
|
5296
|
+
}
|
|
5297
|
+
label="Période"
|
|
5298
|
+
onDateChange={(start, end) => {
|
|
5299
|
+
if (!start || !end) return;
|
|
5300
|
+
if (dateFilterModal === 'createdAt') {
|
|
5301
|
+
setCreatedAtStart(start);
|
|
5302
|
+
setCreatedAtEnd(end);
|
|
5303
|
+
} else {
|
|
5304
|
+
setUpdatedAtStart(start);
|
|
5305
|
+
setUpdatedAtEnd(end);
|
|
5306
|
+
}
|
|
5307
|
+
setCurrentPage(1);
|
|
5308
|
+
setDateFilterModal(null);
|
|
5309
|
+
setDateFilterPosition(null);
|
|
5310
|
+
}}
|
|
5311
|
+
onRequestClose={() => {
|
|
5312
|
+
setDateFilterModal(null);
|
|
5313
|
+
setDateFilterPosition(null);
|
|
5314
|
+
}}
|
|
5315
|
+
onClear={() => handleClearDateFilter(dateFilterModal)}
|
|
5316
|
+
/>
|
|
5029
5317
|
</div>
|
|
5030
5318
|
</div>
|
|
5031
5319
|
)}
|
|
@@ -5034,7 +5322,7 @@ export default function ContactsPage() {
|
|
|
5034
5322
|
{listFilterModal && listFilterPosition && (
|
|
5035
5323
|
<div
|
|
5036
5324
|
ref={listFilterModalRef}
|
|
5037
|
-
className="fixed z-50 w-80 rounded-lg border border-gray-200 bg-white p-4 shadow-xl"
|
|
5325
|
+
className="ui-dropdown-enter fixed z-50 w-80 rounded-lg border border-gray-200 bg-white p-4 shadow-xl"
|
|
5038
5326
|
style={{ top: listFilterPosition.top, left: listFilterPosition.left }}
|
|
5039
5327
|
>
|
|
5040
5328
|
<div className="relative">
|
|
@@ -5078,7 +5366,6 @@ export default function ContactsPage() {
|
|
|
5078
5366
|
setAssignedTeleproFilter(new Set());
|
|
5079
5367
|
} else if (listFilterModal === 'region') {
|
|
5080
5368
|
setRegionFilter(new Set());
|
|
5081
|
-
setDepartmentFilter(new Set());
|
|
5082
5369
|
} else if (listFilterModal === 'department') {
|
|
5083
5370
|
setDepartmentFilter(new Set());
|
|
5084
5371
|
}
|
|
@@ -5122,13 +5409,7 @@ export default function ContactsPage() {
|
|
|
5122
5409
|
})}
|
|
5123
5410
|
|
|
5124
5411
|
{listFilterModal === 'origin' &&
|
|
5125
|
-
|
|
5126
|
-
new Set(
|
|
5127
|
-
contacts
|
|
5128
|
-
.map((c) => c.origin)
|
|
5129
|
-
.filter((o): o is string => Boolean(o && o.trim())),
|
|
5130
|
-
),
|
|
5131
|
-
).map((origin) => {
|
|
5412
|
+
originOptions.map((origin) => {
|
|
5132
5413
|
const checked = originFilter.has(origin);
|
|
5133
5414
|
return (
|
|
5134
5415
|
<button
|
|
@@ -5152,20 +5433,14 @@ export default function ContactsPage() {
|
|
|
5152
5433
|
);
|
|
5153
5434
|
})}
|
|
5154
5435
|
|
|
5155
|
-
{listFilterModal === '
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
...users
|
|
5159
|
-
.filter((u) => u.role !== 'USER')
|
|
5160
|
-
.map((u) => ({ id: u.id, name: u.name || u.email || 'Utilisateur' })),
|
|
5161
|
-
].map((item) => {
|
|
5162
|
-
const value = item.id === 'unassigned' ? 'UNASSIGNED' : item.id;
|
|
5163
|
-
const checked = assignedCommercialFilter.has(value);
|
|
5436
|
+
{listFilterModal === 'region' &&
|
|
5437
|
+
FR_REGIONS.map((r) => {
|
|
5438
|
+
const checked = regionFilter.has(r.code);
|
|
5164
5439
|
return (
|
|
5165
5440
|
<button
|
|
5166
|
-
key={
|
|
5441
|
+
key={r.code}
|
|
5167
5442
|
type="button"
|
|
5168
|
-
onClick={() => toggleFilterValue(
|
|
5443
|
+
onClick={() => toggleFilterValue(setRegionFilter, r.code)}
|
|
5169
5444
|
className="flex w-full cursor-pointer items-center justify-between rounded-md px-2 py-1.5 text-left text-sm hover:bg-gray-50"
|
|
5170
5445
|
>
|
|
5171
5446
|
<div className="flex items-center gap-2">
|
|
@@ -5177,26 +5452,20 @@ export default function ContactsPage() {
|
|
|
5177
5452
|
>
|
|
5178
5453
|
{checked && <span className="text-[10px] text-white">✓</span>}
|
|
5179
5454
|
</div>
|
|
5180
|
-
<span className="text-sm text-gray-800">{
|
|
5455
|
+
<span className="text-sm text-gray-800">{r.name}</span>
|
|
5181
5456
|
</div>
|
|
5182
5457
|
</button>
|
|
5183
5458
|
);
|
|
5184
5459
|
})}
|
|
5185
5460
|
|
|
5186
|
-
{listFilterModal === '
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
...users
|
|
5190
|
-
.filter((u) => u.role !== 'USER')
|
|
5191
|
-
.map((u) => ({ id: u.id, name: u.name || u.email || 'Utilisateur' })),
|
|
5192
|
-
].map((item) => {
|
|
5193
|
-
const value = item.id === 'unassigned' ? 'UNASSIGNED' : item.id;
|
|
5194
|
-
const checked = assignedTeleproFilter.has(value);
|
|
5461
|
+
{listFilterModal === 'department' &&
|
|
5462
|
+
FR_DEPARTMENTS.map((d) => {
|
|
5463
|
+
const checked = departmentFilter.has(d.code);
|
|
5195
5464
|
return (
|
|
5196
5465
|
<button
|
|
5197
|
-
key={
|
|
5466
|
+
key={d.code}
|
|
5198
5467
|
type="button"
|
|
5199
|
-
onClick={() => toggleFilterValue(
|
|
5468
|
+
onClick={() => toggleFilterValue(setDepartmentFilter, d.code)}
|
|
5200
5469
|
className="flex w-full cursor-pointer items-center justify-between rounded-md px-2 py-1.5 text-left text-sm hover:bg-gray-50"
|
|
5201
5470
|
>
|
|
5202
5471
|
<div className="flex items-center gap-2">
|
|
@@ -5208,20 +5477,28 @@ export default function ContactsPage() {
|
|
|
5208
5477
|
>
|
|
5209
5478
|
{checked && <span className="text-[10px] text-white">✓</span>}
|
|
5210
5479
|
</div>
|
|
5211
|
-
<span className="text-sm text-gray-800">
|
|
5480
|
+
<span className="text-sm text-gray-800">
|
|
5481
|
+
{d.code} — {d.name}
|
|
5482
|
+
</span>
|
|
5212
5483
|
</div>
|
|
5213
5484
|
</button>
|
|
5214
5485
|
);
|
|
5215
5486
|
})}
|
|
5216
5487
|
|
|
5217
|
-
{listFilterModal === '
|
|
5218
|
-
|
|
5219
|
-
|
|
5488
|
+
{listFilterModal === 'commercial' &&
|
|
5489
|
+
[
|
|
5490
|
+
{ id: 'unassigned', name: 'NON ATTRIBUÉ' },
|
|
5491
|
+
...users
|
|
5492
|
+
.filter((u) => u.role !== 'USER')
|
|
5493
|
+
.map((u) => ({ id: u.id, name: u.name || u.email || 'Utilisateur' })),
|
|
5494
|
+
].map((item) => {
|
|
5495
|
+
const value = item.id === 'unassigned' ? 'UNASSIGNED' : item.id;
|
|
5496
|
+
const checked = assignedCommercialFilter.has(value);
|
|
5220
5497
|
return (
|
|
5221
5498
|
<button
|
|
5222
|
-
key={
|
|
5499
|
+
key={item.id}
|
|
5223
5500
|
type="button"
|
|
5224
|
-
onClick={() => toggleFilterValue(
|
|
5501
|
+
onClick={() => toggleFilterValue(setAssignedCommercialFilter, value)}
|
|
5225
5502
|
className="flex w-full cursor-pointer items-center justify-between rounded-md px-2 py-1.5 text-left text-sm hover:bg-gray-50"
|
|
5226
5503
|
>
|
|
5227
5504
|
<div className="flex items-center gap-2">
|
|
@@ -5233,20 +5510,26 @@ export default function ContactsPage() {
|
|
|
5233
5510
|
>
|
|
5234
5511
|
{checked && <span className="text-[10px] text-white">✓</span>}
|
|
5235
5512
|
</div>
|
|
5236
|
-
<span className="text-sm text-gray-800">{
|
|
5513
|
+
<span className="text-sm text-gray-800">{item.name}</span>
|
|
5237
5514
|
</div>
|
|
5238
5515
|
</button>
|
|
5239
5516
|
);
|
|
5240
5517
|
})}
|
|
5241
5518
|
|
|
5242
|
-
{listFilterModal === '
|
|
5243
|
-
|
|
5244
|
-
|
|
5519
|
+
{listFilterModal === 'telepro' &&
|
|
5520
|
+
[
|
|
5521
|
+
{ id: 'unassigned', name: 'NON ATTRIBUÉ' },
|
|
5522
|
+
...users
|
|
5523
|
+
.filter((u) => u.role !== 'USER')
|
|
5524
|
+
.map((u) => ({ id: u.id, name: u.name || u.email || 'Utilisateur' })),
|
|
5525
|
+
].map((item) => {
|
|
5526
|
+
const value = item.id === 'unassigned' ? 'UNASSIGNED' : item.id;
|
|
5527
|
+
const checked = assignedTeleproFilter.has(value);
|
|
5245
5528
|
return (
|
|
5246
5529
|
<button
|
|
5247
|
-
key={
|
|
5530
|
+
key={item.id}
|
|
5248
5531
|
type="button"
|
|
5249
|
-
onClick={() => toggleFilterValue(
|
|
5532
|
+
onClick={() => toggleFilterValue(setAssignedTeleproFilter, value)}
|
|
5250
5533
|
className="flex w-full cursor-pointer items-center justify-between rounded-md px-2 py-1.5 text-left text-sm hover:bg-gray-50"
|
|
5251
5534
|
>
|
|
5252
5535
|
<div className="flex items-center gap-2">
|
|
@@ -5258,8 +5541,7 @@ export default function ContactsPage() {
|
|
|
5258
5541
|
>
|
|
5259
5542
|
{checked && <span className="text-[10px] text-white">✓</span>}
|
|
5260
5543
|
</div>
|
|
5261
|
-
<span className="text-
|
|
5262
|
-
<span className="text-sm text-gray-800">{dept.name}</span>
|
|
5544
|
+
<span className="text-sm text-gray-800">{item.name}</span>
|
|
5263
5545
|
</div>
|
|
5264
5546
|
</button>
|
|
5265
5547
|
);
|
|
@@ -5271,8 +5553,8 @@ export default function ContactsPage() {
|
|
|
5271
5553
|
|
|
5272
5554
|
{/* Modal d'export */}
|
|
5273
5555
|
{showExportModal && (
|
|
5274
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm">
|
|
5275
|
-
<div className="w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
|
|
5556
|
+
<div className="ui-fade-in fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm">
|
|
5557
|
+
<div className="ui-scale-in w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
|
|
5276
5558
|
<div className="mb-4 flex items-center justify-between">
|
|
5277
5559
|
<h3 className="text-lg font-semibold text-gray-900">
|
|
5278
5560
|
{viewEntity === 'companies'
|