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.
Files changed (190) hide show
  1. package/bin/create-crm-tmp.js +56 -35
  2. package/package.json +1 -1
  3. package/template/README.md +230 -115
  4. package/template/eslint.config.mjs +13 -0
  5. package/template/next.config.ts +14 -0
  6. package/template/package.json +15 -2
  7. package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
  8. package/template/prisma/migrations/migration_lock.toml +3 -0
  9. package/template/prisma/schema.prisma +132 -637
  10. package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
  11. package/template/src/app/(auth)/layout.tsx +1 -1
  12. package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
  13. package/template/src/app/(auth)/reset-password/page.tsx +4 -4
  14. package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
  15. package/template/src/app/(auth)/signin/page.tsx +14 -6
  16. package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
  17. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
  18. package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
  19. package/template/src/app/(dashboard)/closing/page.tsx +78 -62
  20. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
  21. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
  22. package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
  23. package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
  24. package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
  25. package/template/src/app/(dashboard)/layout.tsx +6 -2
  26. package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
  27. package/template/src/app/(dashboard)/templates/page.tsx +55 -54
  28. package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
  29. package/template/src/app/(dashboard)/users/page.tsx +1 -1
  30. package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
  31. package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
  32. package/template/src/app/api/agenda/google-events/route.ts +92 -0
  33. package/template/src/app/api/auth/check-active/route.ts +3 -2
  34. package/template/src/app/api/auth/google/route.ts +2 -1
  35. package/template/src/app/api/auth/google/status/route.ts +7 -31
  36. package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
  37. package/template/src/app/api/companies/[id]/route.ts +1 -2
  38. package/template/src/app/api/companies/route.ts +42 -12
  39. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
  40. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
  41. package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
  42. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
  43. package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
  44. package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
  45. package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
  46. package/template/src/app/api/contacts/[id]/route.ts +106 -34
  47. package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
  48. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
  49. package/template/src/app/api/contacts/export/route.ts +9 -13
  50. package/template/src/app/api/contacts/import/route.ts +55 -25
  51. package/template/src/app/api/contacts/import-preview/route.ts +1 -1
  52. package/template/src/app/api/contacts/origins/route.ts +63 -0
  53. package/template/src/app/api/contacts/route.ts +153 -41
  54. package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
  55. package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
  56. package/template/src/app/api/dashboard/widgets/route.ts +181 -0
  57. package/template/src/app/api/dev/reminders/test/route.ts +114 -0
  58. package/template/src/app/api/editor/upload-image/route.ts +61 -0
  59. package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
  60. package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
  61. package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
  62. package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
  63. package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
  64. package/template/src/app/api/reminders/clear/route.ts +120 -0
  65. package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
  66. package/template/src/app/api/reminders/route.ts +164 -39
  67. package/template/src/app/api/reminders/state/route.ts +164 -0
  68. package/template/src/app/api/reset-password/request/route.ts +1 -1
  69. package/template/src/app/api/reset-password/verify/route.ts +1 -1
  70. package/template/src/app/api/send/route.ts +16 -4
  71. package/template/src/app/api/settings/google-ads/route.ts +14 -0
  72. package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
  73. package/template/src/app/api/settings/google-calendar/route.ts +124 -0
  74. package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
  75. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
  76. package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
  77. package/template/src/app/api/settings/google-sheet/route.ts +14 -0
  78. package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
  79. package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
  80. package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
  81. package/template/src/app/api/settings/meta-leads/route.ts +14 -2
  82. package/template/src/app/api/settings/smtp/route.ts +53 -6
  83. package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
  84. package/template/src/app/api/tasks/[id]/route.ts +234 -58
  85. package/template/src/app/api/tasks/meet/route.ts +27 -19
  86. package/template/src/app/api/tasks/route.ts +62 -17
  87. package/template/src/app/api/users/[id]/route.ts +20 -14
  88. package/template/src/app/api/users/list/route.ts +57 -19
  89. package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
  90. package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
  91. package/template/src/app/api/workflows/[id]/route.ts +0 -4
  92. package/template/src/app/api/workflows/process/route.ts +22 -51
  93. package/template/src/app/api/workflows/route.ts +0 -4
  94. package/template/src/app/globals.css +342 -4
  95. package/template/src/app/layout.tsx +11 -3
  96. package/template/src/app/page.tsx +1 -1
  97. package/template/src/components/address-autocomplete.tsx +7 -6
  98. package/template/src/components/config-error-alert.tsx +46 -0
  99. package/template/src/components/contacts/filter-bar.tsx +12 -3
  100. package/template/src/components/contacts/filter-builder.tsx +28 -43
  101. package/template/src/components/contacts/save-view-dialog.tsx +1 -1
  102. package/template/src/components/contacts/views-tab-bar.tsx +15 -6
  103. package/template/src/components/dashboard/activity-chart.tsx +41 -28
  104. package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
  105. package/template/src/components/dashboard/color-picker.tsx +64 -0
  106. package/template/src/components/dashboard/contacts-chart.tsx +69 -0
  107. package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
  108. package/template/src/components/dashboard/recent-activity.tsx +154 -0
  109. package/template/src/components/dashboard/stat-card.tsx +40 -40
  110. package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
  111. package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
  112. package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
  113. package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
  114. package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
  115. package/template/src/components/date-picker.tsx +9 -6
  116. package/template/src/components/editor/upload-editor-image.ts +42 -0
  117. package/template/src/components/editor.tsx +161 -22
  118. package/template/src/components/email-template.tsx +2 -2
  119. package/template/src/components/global-search.tsx +30 -28
  120. package/template/src/components/header.tsx +178 -80
  121. package/template/src/components/inactive-account-guard.tsx +58 -0
  122. package/template/src/components/integration-notifications-listener.tsx +12 -0
  123. package/template/src/components/invitation-email-template.tsx +2 -2
  124. package/template/src/components/meet-cancellation-email-template.tsx +3 -3
  125. package/template/src/components/meet-confirmation-email-template.tsx +3 -3
  126. package/template/src/components/meet-update-email-template.tsx +3 -3
  127. package/template/src/components/page-header.tsx +5 -5
  128. package/template/src/components/protected-page.tsx +1 -1
  129. package/template/src/components/reset-password-email-template.tsx +2 -2
  130. package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
  131. package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
  132. package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
  133. package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
  134. package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
  135. package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
  136. package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
  137. package/template/src/components/sidebar.tsx +45 -26
  138. package/template/src/components/skeleton.tsx +40 -43
  139. package/template/src/components/ui/accordion.tsx +2 -2
  140. package/template/src/components/ui/alert-dialog.tsx +1 -1
  141. package/template/src/components/ui/button.tsx +20 -9
  142. package/template/src/components/ui/components.tsx +1 -1
  143. package/template/src/components/ui/date-picker.tsx +422 -0
  144. package/template/src/components/ui/datetime-picker.tsx +338 -0
  145. package/template/src/components/ui/status-select.tsx +271 -0
  146. package/template/src/components/ui/tooltip.tsx +37 -0
  147. package/template/src/components/view-as-modal.tsx +13 -7
  148. package/template/src/contexts/app-toast-context.tsx +245 -57
  149. package/template/src/contexts/dashboard-theme-context.tsx +53 -0
  150. package/template/src/contexts/sidebar-context.tsx +22 -17
  151. package/template/src/contexts/task-reminder-context.tsx +134 -160
  152. package/template/src/contexts/view-as-context.tsx +33 -6
  153. package/template/src/hooks/use-focus-trap.ts +2 -2
  154. package/template/src/hooks/useIntegrationNotifications.ts +49 -0
  155. package/template/src/lib/auth.ts +8 -1
  156. package/template/src/lib/config-links.ts +14 -0
  157. package/template/src/lib/contact-duplicate.ts +79 -61
  158. package/template/src/lib/contact-interactions.ts +21 -21
  159. package/template/src/lib/contact-view-filters.ts +24 -64
  160. package/template/src/lib/contacts-list-url.ts +190 -0
  161. package/template/src/lib/dashboard-stats.ts +65 -7
  162. package/template/src/lib/dashboard-themes.ts +135 -0
  163. package/template/src/lib/date-utils.ts +127 -0
  164. package/template/src/lib/default-widgets.ts +12 -0
  165. package/template/src/lib/editor-html-image-dimensions.ts +172 -0
  166. package/template/src/lib/editor-image-limits.ts +19 -0
  167. package/template/src/lib/email-html-sanitize.ts +19 -0
  168. package/template/src/lib/encryption.ts +9 -6
  169. package/template/src/lib/fr-geography.ts +192 -0
  170. package/template/src/lib/google-calendar-agenda.ts +201 -0
  171. package/template/src/lib/google-calendar.ts +255 -5
  172. package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
  173. package/template/src/lib/google-sheet-sync-runner.ts +514 -0
  174. package/template/src/lib/integration-import-log.ts +21 -0
  175. package/template/src/lib/permissions.ts +40 -10
  176. package/template/src/lib/prisma.ts +4 -1
  177. package/template/src/lib/qstash.ts +65 -0
  178. package/template/src/lib/reminder-state-server.ts +80 -0
  179. package/template/src/lib/reminder-state.ts +29 -0
  180. package/template/src/lib/supabase-storage.ts +113 -0
  181. package/template/src/lib/template-variables.ts +164 -23
  182. package/template/src/lib/utils.ts +45 -0
  183. package/template/src/lib/widget-registry.ts +173 -0
  184. package/template/src/lib/workflow-executor.ts +16 -70
  185. package/template/src/proxy.ts +1 -0
  186. package/template/vercel.json +3 -10
  187. package/template/skills-lock.json +0 -25
  188. package/template/src/components/dashboard/dashboard-content.tsx +0 -79
  189. package/template/src/lib/google-drive.ts +0 -1101
  190. 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 { cn, normalizePhoneNumber, formatDateTime } from '@/lib/utils';
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
- const entity = searchParams.get('entity');
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>>(new Set());
462
- const [originFilter, setOriginFilter] = useState<Set<string>>(new Set());
463
- const [assignedCommercialFilter, setAssignedCommercialFilter] = useState<Set<string>>(new Set());
464
- const [assignedTeleproFilter, setAssignedTeleproFilter] = useState<Set<string>>(new Set());
465
- const [regionFilter, setRegionFilter] = useState<Set<string>>(new Set());
466
- const [departmentFilter, setDepartmentFilter] = useState<Set<string>>(new Set());
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 origins = new Set<string>();
470
- for (const c of contacts) {
471
- if (c.origin) origins.add(c.origin);
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
- const [createdAtEnd, setCreatedAtEnd] = useState('');
521
- const [updatedAtStart, setUpdatedAtStart] = useState('');
522
- const [updatedAtEnd, setUpdatedAtEnd] = useState('');
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
- | 'createdAt'
528
- | 'updatedAt'
529
- | 'status'
530
- | 'commercial'
531
- | 'telepro'
532
- | 'postalCode'
533
- | 'region'
534
- | 'department'
535
- >('');
536
- const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
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 [currentPage, setCurrentPage] = useState(1);
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(25);
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) - Persistée dans localStorage
571
- // Initialiser avec 'table' pour éviter l'erreur d'hydratation
572
- const [viewMode, setViewMode] = useState<'table' | 'cards'>('table');
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
- const hasTriggeredGoogleSheetSyncRef = useRef(false);
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 savedView = localStorage.getItem('contactsViewMode');
660
- if (savedView === 'cards' || savedView === 'table') {
661
- setViewMode(savedView);
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
- const savedLimit = localStorage.getItem('contactsPageLimit');
665
- if (savedLimit && ['25', '50', '100'].includes(savedLimit)) {
666
- setLimit(parseInt(savedLimit, 10));
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
- setError('Erreur lors du chargement des contacts');
892
+ if (fetchGen === listFetchGenerationRef.current) {
893
+ setError(devToast('Erreur lors du chargement des contacts', error));
894
+ }
803
895
  } finally {
804
- setLoading(false);
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
- setError('Erreur lors du chargement des entreprises');
933
+ if (fetchGen === listFetchGenerationRef.current) {
934
+ setError(devToast('Erreur lors du chargement des entreprises', error));
935
+ }
831
936
  } finally {
832
- setLoading(false);
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
- router.replace('/contacts', { scroll: false });
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
- router.replace('/contacts?entity=companies', { scroll: false });
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
- setError('Veuillez renseigner le motif de fermeture pour ce statut.');
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.message);
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.message);
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
- !!sortField
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('asc');
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 contactGeoLookup = useMemo(() => {
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
- if (aValue < bValue) return sortOrder === 'asc' ? -1 : 1;
1343
- if (aValue > bValue) return sortOrder === 'asc' ? 1 : -1;
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
- const filteredDepartmentsForFilter = useMemo(() => {
1350
- if (regionFilter.size === 0) return FRENCH_DEPARTMENTS;
1351
- return FRENCH_DEPARTMENTS.filter((d) => regionFilter.has(d.regionCode));
1352
- }, [regionFilter]);
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(`Erreur: ${err instanceof Error ? err.message : 'Erreur inconnue'}`);
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(`Erreur: ${err instanceof Error ? err.message : 'Erreur inconnue'}`);
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.message);
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(err.message || 'Erreur lors de la mise à jour');
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(err.message || 'Erreur lors de la mise à jour');
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(err.message || 'Erreur lors de la mise à jour');
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(err.message || 'Erreur lors de la suppression');
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(err instanceof Error ? err.message : "Erreur lors de l'export");
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
- {/* Titre et vues unifiés (z-50 quand un dropdown vues ou le menu contextuel est ouvert pour passer au-dessus de la barre de recherche et du tableau) */}
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 justify-between gap-3 px-4 py-2 sm:px-6 lg:px-8">
2185
- <div className="flex min-w-0 flex-1 items-center gap-2 sm:gap-3">
2186
- <div className="relative" ref={entitySelectorRef}>
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={() => setShowEntitySelector(!showEntitySelector)}
2189
- className="flex cursor-pointer items-center gap-1.5 rounded-lg border border-border bg-card px-3 py-1.5 text-lg font-bold text-foreground transition-colors duration-200 hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 sm:text-xl"
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
- {viewEntity === 'contacts' ? (
2192
- <Users className="h-4 w-4 text-blue-600" />
2193
- ) : (
2194
- <Building2 className="h-4 w-4 text-blue-600" />
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
- {viewEntity === 'contacts' ? 'Contacts' : 'Entreprises'}
2197
- <ChevronDown className="h-4 w-4 text-muted-foreground" />
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
- <span className="shrink-0 rounded-full bg-blue-200 px-2 py-0.5 text-xs font-semibold text-blue-900 sm:text-sm">
2244
- {totalContacts}
2245
- </span>
2246
- </div>
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
- <RefreshCw className="h-4 w-4 sm:h-4" />
2253
- </button>
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
- {/* Filter bar */}
2276
- <div className="border-b border-border bg-background/95">
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
- </div>
2290
-
2291
- {/* Barre d'outils avec recherche et filtres */}
2292
- <div className="border-b border-border bg-background/95 px-4 py-3 sm:px-6 lg:px-8">
2293
- <div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
2294
- {/* Recherche et filtres à gauche */}
2295
- <div className="flex flex-1 flex-wrap items-center gap-2">
2296
- <div className="relative min-w-[200px] flex-1">
2297
- <Search className="pointer-events-none absolute top-2.5 left-3 h-4 w-4 text-muted-foreground" />
2298
- <input
2299
- type="text"
2300
- value={search}
2301
- onChange={(e) => setSearch(e.target.value)}
2302
- placeholder="Rechercher"
2303
- className="w-full rounded-lg border border-border bg-muted py-2 pr-3 pl-9 text-base text-foreground placeholder:text-muted-foreground focus:border-primary/50 focus:bg-background focus:ring-2 focus:ring-primary/20 focus:outline-none"
2304
- />
2305
- </div>
2306
- <div className="relative" ref={sortMenuRef}>
2307
- <button
2308
- type="button"
2309
- onClick={() => setShowSortMenu(!showSortMenu)}
2310
- className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-border bg-card px-3 py-2 text-xs font-medium text-muted-foreground transition-colors duration-200 hover:bg-muted hover:text-foreground"
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
- <ChevronsLeft className="h-3.5 w-3.5" />
2313
- <span>Trier par</span>
2314
- </button>
2315
- {showSortMenu && (
2316
- <div
2317
- className="absolute top-full right-0 z-50 mt-2 w-48 rounded-lg border border-border bg-popover shadow-(--shadow-dropdown)"
2318
- style={{ maxWidth: 'calc(100vw - 2rem)', right: '0' }}
2319
- >
2320
- <div className="py-1">
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('postalCode')}
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
- Code postal{' '}
2327
- {sortField === 'postalCode' && (sortOrder === 'asc' ? '↑' : '↓')}
2622
+ Région {sortField === 'region' && (sortOrder === 'asc' ? '↑' : '↓')}
2328
2623
  </button>
2329
- {viewEntity === 'contacts' && (
2330
- <button
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('commercial')}
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
- Commercial {sortField === 'commercial' && (sortOrder === 'asc' ? '↑' : '↓')}
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('telepro')}
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
- Télépro {sortField === 'telepro' && (sortOrder === 'asc' ? '↑' : '↓')}
2641
+ Statut {sortField === 'status' && (sortOrder === 'asc' ? '↑' : '↓')}
2370
2642
  </button>
2643
+ )}
2644
+ {viewEntity === 'contacts' && (
2371
2645
  <button
2372
2646
  type="button"
2373
- onClick={() => handleSortByField('createdAt')}
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
- Date de création{' '}
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={() => handleSortByField('updatedAt')}
2382
- className="w-full cursor-pointer px-4 py-2 text-left text-base text-gray-700 hover:bg-gray-100"
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
- Date de modification{' '}
2385
- {sortField === 'updatedAt' && (sortOrder === 'asc' ? '↑' : '↓')}
2694
+ Réinitialiser
2386
2695
  </button>
2387
- {sortField && (
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
- {(createdAtStart || createdAtEnd || updatedAtStart || updatedAtEnd) && (
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
- {/* Gérer les colonnes */}
2542
- {viewMode === 'table' && (
2702
+ <div className="relative" ref={datePickerRef}>
2543
2703
  <button
2544
- onClick={() => setShowColumnPanel(true)}
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
- Colonnes
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
- {/* Bouton Réinitialiser les filtres */}
2552
- {hasActiveFilters() && (
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
- onClick={handleResetAllFilters}
2555
- 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"
2556
- title="Réinitialiser tous les filtres"
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
- {/* Groupe vue (liste / grille) - uniquement pour contacts */}
2563
- {viewEntity === 'contacts' && (
2564
- <div className="flex items-center rounded-lg border border-gray-200 bg-white p-1">
2565
- <button
2566
- onClick={() => setViewMode('table')}
2567
- className={cn(
2568
- 'cursor-pointer rounded-md px-2 py-1.5 text-gray-600 transition-colors',
2569
- viewMode === 'table' ? 'bg-gray-100 text-gray-900' : 'hover:bg-gray-50',
2570
- )}
2571
- title="Vue liste"
2572
- >
2573
- <List className="h-4 w-4" />
2574
- </button>
2575
- <button
2576
- onClick={() => setViewMode('cards')}
2577
- className={cn(
2578
- 'cursor-pointer rounded-md px-2 py-1.5 text-gray-600 transition-colors',
2579
- viewMode === 'cards' ? 'bg-gray-100 text-gray-900' : 'hover:bg-gray-50',
2580
- )}
2581
- title="Vue grille"
2582
- >
2583
- <LayoutGrid className="h-4 w-4" />
2584
- </button>
2585
- </div>
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
- type="button"
2593
- onClick={() => setShowImportModal(true)}
2594
- 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"
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
- <Upload className="h-4 w-4" />
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
- setExportAll(true);
2606
- setShowExportModal(true);
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
- disabled={exporting}
2609
- 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"
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
- <Download className="h-4 w-4" />
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
- {/* Ajouter un contact / entreprise */}
2617
- {((viewEntity === 'contacts' && canCreate) ||
2618
- (viewEntity === 'companies' && canCreateCompany)) && (
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
- statusFilter.size > 0 ||
2716
- assignedCommercialFilter.size > 0 ||
2717
- assignedTeleproFilter.size > 0 ||
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 = column.id === 'createdAt' || column.id === 'updatedAt';
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
- {sortField === column.id && (
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 && sortField === column.id) ||
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={() => router.push(`/contacts/${contact.id}`)}
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={() => router.push(`/contacts/${contact.id}`)}
3055
- className="relative cursor-pointer rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-all hover:border-blue-300 hover:shadow-md"
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={() => setCurrentPage(1)}
3223
- disabled={currentPage === 1}
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={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
3231
- disabled={currentPage === 1}
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 - currentPage) <= 2;
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={() => setCurrentPage(page)}
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
- currentPage === page
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={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
3266
- disabled={currentPage === totalPages}
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={() => setCurrentPage(totalPages)}
3273
- disabled={currentPage === totalPages}
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
- setLimit(Number(e.target.value));
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:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
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
- <select
3866
+ <StatusSelect
3867
+ statuses={statuses}
3552
3868
  value={formData.statusId}
3553
- onChange={(e) => setFormData({ ...formData, statusId: e.target.value })}
3554
- 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"
3555
- >
3556
- <option value="">Aucun statut</option>
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
- (u) =>
3606
- u.role === 'COMMERCIAL' ||
3607
- u.role === 'ADMIN' ||
3608
- u.role === 'MANAGER',
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
- <select
4611
+ <StatusSelect
4612
+ statuses={statuses}
4303
4613
  value={defaultStatusId}
4304
- onChange={(e) => setDefaultStatusId(e.target.value)}
4305
- 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"
4306
- >
4307
- <option value="">Aucun statut par défaut</option>
4308
- {statuses.map((status) => (
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
- <select
5235
+ <StatusSelect
5236
+ statuses={statuses}
4924
5237
  value={bulkStatusId}
4925
- onChange={(e) => setBulkStatusId(e.target.value)}
4926
- 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"
4927
- >
4928
- <option value="">Sélectionner un statut</option>
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
- <div className="space-y-3">
4982
- <div>
4983
- <label className="mb-1 block text-xs font-medium text-gray-700">Du</label>
4984
- <input
4985
- type="date"
4986
- value={dateFilterModal === 'createdAt' ? createdAtStart : updatedAtStart}
4987
- onChange={(e) => {
4988
- if (dateFilterModal === 'createdAt') {
4989
- setCreatedAtStart(e.target.value);
4990
- } else {
4991
- setUpdatedAtStart(e.target.value);
4992
- }
4993
- }}
4994
- 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"
4995
- />
4996
- </div>
4997
-
4998
- <div>
4999
- <label className="mb-1 block text-xs font-medium text-gray-700">Au</label>
5000
- <input
5001
- type="date"
5002
- value={dateFilterModal === 'createdAt' ? createdAtEnd : updatedAtEnd}
5003
- onChange={(e) => {
5004
- if (dateFilterModal === 'createdAt') {
5005
- setCreatedAtEnd(e.target.value);
5006
- } else {
5007
- setUpdatedAtEnd(e.target.value);
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
- Array.from(
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 === 'commercial' &&
5156
- [
5157
- { id: 'unassigned', name: 'NON ATTRIBUÉ' },
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={item.id}
5441
+ key={r.code}
5167
5442
  type="button"
5168
- onClick={() => toggleFilterValue(setAssignedCommercialFilter, value)}
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">{item.name}</span>
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 === 'telepro' &&
5187
- [
5188
- { id: 'unassigned', name: 'NON ATTRIBUÉ' },
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={item.id}
5466
+ key={d.code}
5198
5467
  type="button"
5199
- onClick={() => toggleFilterValue(setAssignedTeleproFilter, value)}
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">{item.name}</span>
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 === 'region' &&
5218
- FRENCH_REGIONS.map((region) => {
5219
- const checked = regionFilter.has(region.code);
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={region.code}
5499
+ key={item.id}
5223
5500
  type="button"
5224
- onClick={() => toggleFilterValue(setRegionFilter, region.code)}
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">{region.name}</span>
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 === 'department' &&
5243
- filteredDepartmentsForFilter.map((dept) => {
5244
- const checked = departmentFilter.has(dept.code);
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={dept.code}
5530
+ key={item.id}
5248
5531
  type="button"
5249
- onClick={() => toggleFilterValue(setDepartmentFilter, dept.code)}
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-xs font-medium text-gray-500">{dept.code}</span>
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'