create-crm-tmp 1.1.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/package.json +1 -1
  2. package/template/.prettierignore +2 -0
  3. package/template/README.md +1 -1
  4. package/template/components.json +22 -0
  5. package/template/exemple-contacts.csv +54 -0
  6. package/template/next.config.ts +27 -1
  7. package/template/package.json +51 -16
  8. package/template/prisma/schema.prisma +807 -58
  9. package/template/skills-lock.json +25 -0
  10. package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
  11. package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
  12. package/template/src/app/(auth)/reset-password/page.tsx +12 -8
  13. package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
  14. package/template/src/app/(auth)/signin/page.tsx +20 -17
  15. package/template/src/app/(dashboard)/agenda/page.tsx +2232 -2189
  16. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
  17. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
  18. package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
  19. package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
  20. package/template/src/app/(dashboard)/closing/page.tsx +500 -468
  21. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5049 -4110
  22. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
  23. package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
  24. package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
  25. package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
  26. package/template/src/app/(dashboard)/error.tsx +37 -0
  27. package/template/src/app/(dashboard)/layout.tsx +1 -1
  28. package/template/src/app/(dashboard)/loading.tsx +5 -0
  29. package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
  30. package/template/src/app/(dashboard)/settings/page.tsx +2685 -2489
  31. package/template/src/app/(dashboard)/templates/page.tsx +500 -300
  32. package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
  33. package/template/src/app/(dashboard)/users/page.tsx +279 -310
  34. package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
  35. package/template/src/app/(dashboard)/users/roles/page.tsx +164 -137
  36. package/template/src/app/api/audit-logs/route.ts +1 -1
  37. package/template/src/app/api/auth/google/callback/route.ts +8 -5
  38. package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
  39. package/template/src/app/api/companies/[id]/activities/route.ts +131 -0
  40. package/template/src/app/api/companies/[id]/route.ts +195 -0
  41. package/template/src/app/api/companies/export/route.ts +206 -0
  42. package/template/src/app/api/companies/route.ts +166 -0
  43. package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
  44. package/template/src/app/api/contact-views/[id]/route.ts +197 -0
  45. package/template/src/app/api/contact-views/route.ts +146 -0
  46. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +77 -0
  47. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
  48. package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
  49. package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
  50. package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
  51. package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
  52. package/template/src/app/api/contacts/[id]/route.ts +111 -20
  53. package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
  54. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
  55. package/template/src/app/api/contacts/export/route.ts +13 -18
  56. package/template/src/app/api/contacts/import/route.ts +22 -19
  57. package/template/src/app/api/contacts/import-preview/route.ts +139 -0
  58. package/template/src/app/api/contacts/route.ts +202 -49
  59. package/template/src/app/api/dashboard/stats/route.ts +9 -292
  60. package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
  61. package/template/src/app/api/invite/complete/route.ts +20 -23
  62. package/template/src/app/api/reminders/route.ts +1 -0
  63. package/template/src/app/api/reset-password/complete/route.ts +11 -13
  64. package/template/src/app/api/send/route.ts +9 -43
  65. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
  66. package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
  67. package/template/src/app/api/settings/company/route.ts +19 -26
  68. package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
  69. package/template/src/app/api/settings/google-ads/route.ts +20 -23
  70. package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
  71. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
  72. package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
  73. package/template/src/app/api/settings/google-sheet/route.ts +20 -23
  74. package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
  75. package/template/src/app/api/settings/meta-leads/route.ts +20 -23
  76. package/template/src/app/api/settings/statuses/[id]/route.ts +29 -32
  77. package/template/src/app/api/settings/statuses/route.ts +24 -22
  78. package/template/src/app/api/statuses/route.ts +2 -5
  79. package/template/src/app/api/tasks/[id]/attendees/route.ts +14 -7
  80. package/template/src/app/api/tasks/[id]/route.ts +173 -137
  81. package/template/src/app/api/tasks/meet/route.ts +11 -8
  82. package/template/src/app/api/tasks/route.ts +155 -95
  83. package/template/src/app/api/templates/[id]/route.ts +22 -13
  84. package/template/src/app/api/templates/route.ts +22 -5
  85. package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
  86. package/template/src/app/api/users/[id]/route.ts +2 -2
  87. package/template/src/app/api/users/commercials/route.ts +38 -0
  88. package/template/src/app/api/users/for-agenda/route.ts +1 -2
  89. package/template/src/app/api/users/route.ts +89 -34
  90. package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
  91. package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
  92. package/template/src/app/api/workflows/[id]/route.ts +33 -6
  93. package/template/src/app/api/workflows/process/route.ts +510 -146
  94. package/template/src/app/api/workflows/route.ts +46 -4
  95. package/template/src/app/globals.css +243 -101
  96. package/template/src/app/layout.tsx +19 -8
  97. package/template/src/app/page.tsx +37 -7
  98. package/template/src/components/address-autocomplete.tsx +232 -0
  99. package/template/src/components/contacts/filter-bar.tsx +181 -0
  100. package/template/src/components/contacts/filter-builder.tsx +589 -0
  101. package/template/src/components/contacts/save-view-dialog.tsx +160 -0
  102. package/template/src/components/contacts/views-tab-bar.tsx +440 -0
  103. package/template/src/components/dashboard/activity-chart.tsx +31 -39
  104. package/template/src/components/dashboard/dashboard-content.tsx +79 -0
  105. package/template/src/components/dashboard/stat-card.tsx +40 -42
  106. package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
  107. package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
  108. package/template/src/components/date-picker.tsx +396 -0
  109. package/template/src/components/editor.tsx +27 -13
  110. package/template/src/components/email-template.tsx +4 -2
  111. package/template/src/components/global-search.tsx +358 -0
  112. package/template/src/components/header.tsx +57 -62
  113. package/template/src/components/invitation-email-template.tsx +4 -2
  114. package/template/src/components/lazy-editor.tsx +11 -0
  115. package/template/src/components/meet-cancellation-email-template.tsx +11 -3
  116. package/template/src/components/meet-confirmation-email-template.tsx +10 -3
  117. package/template/src/components/meet-update-email-template.tsx +10 -3
  118. package/template/src/components/page-header.tsx +19 -15
  119. package/template/src/components/protected-page.tsx +94 -0
  120. package/template/src/components/reset-password-email-template.tsx +4 -2
  121. package/template/src/components/sidebar.tsx +92 -94
  122. package/template/src/components/skeleton.tsx +128 -42
  123. package/template/src/components/ui/accordion.tsx +64 -0
  124. package/template/src/components/ui/alert-dialog.tsx +139 -0
  125. package/template/src/components/ui/button.tsx +60 -0
  126. package/template/src/components/view-as-banner.tsx +1 -1
  127. package/template/src/components/view-as-modal.tsx +21 -16
  128. package/template/src/config/nav-pages.ts +108 -0
  129. package/template/src/contexts/app-toast-context.tsx +174 -0
  130. package/template/src/contexts/sidebar-context.tsx +16 -47
  131. package/template/src/contexts/task-reminder-context.tsx +6 -6
  132. package/template/src/contexts/view-as-context.tsx +11 -16
  133. package/template/src/hooks/use-alert.tsx +65 -0
  134. package/template/src/hooks/use-confirm.tsx +87 -0
  135. package/template/src/hooks/use-contact-views.ts +140 -0
  136. package/template/src/hooks/use-contacts.ts +69 -0
  137. package/template/src/hooks/use-fetch.ts +17 -0
  138. package/template/src/hooks/use-focus-trap.ts +73 -0
  139. package/template/src/hooks/use-statuses.ts +22 -0
  140. package/template/src/lib/address-api.ts +155 -0
  141. package/template/src/lib/cache.ts +73 -0
  142. package/template/src/lib/check-permission.ts +12 -177
  143. package/template/src/lib/contact-interactions.ts +3 -1
  144. package/template/src/lib/contact-view-filters.ts +341 -0
  145. package/template/src/lib/dashboard-stats.ts +224 -0
  146. package/template/src/lib/date-utils.ts +49 -0
  147. package/template/src/lib/get-auth-user.ts +25 -0
  148. package/template/src/lib/google-calendar.ts +54 -12
  149. package/template/src/lib/google-drive.ts +796 -75
  150. package/template/src/lib/google-fetch.ts +63 -0
  151. package/template/src/lib/local-storage.ts +34 -0
  152. package/template/src/lib/permissions.ts +245 -47
  153. package/template/src/lib/prisma.ts +11 -11
  154. package/template/src/lib/roles.ts +12 -15
  155. package/template/src/lib/template-variables.ts +67 -33
  156. package/template/src/lib/utils.ts +26 -11
  157. package/template/src/lib/workflow-executor.ts +445 -228
  158. package/template/src/proxy.ts +34 -73
  159. package/template/src/types/contact-views.ts +351 -0
  160. package/template/src/types/yousign.ts +52 -0
  161. package/template/vercel.json +12 -0
  162. package/template/WORKFLOWS_CRON.md +0 -185
  163. package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
  164. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
  165. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
  166. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
  167. package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
  168. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
  169. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
  170. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
  171. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
  172. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
  173. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
  174. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
  175. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
  176. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
  177. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
  178. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
  179. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
  180. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
  181. package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
  182. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
  183. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
  184. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
  185. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
  186. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
  187. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
  188. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
  189. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
  190. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
  191. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
  192. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
  193. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
  194. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
  195. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
  196. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
  197. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
  198. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
  199. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
  200. package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
  201. package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
  202. package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
  203. package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
  204. package/template/prisma/migrations/20260226093949_fix_cascade_on_user_delete/migration.sql +0 -69
  205. package/template/prisma/migrations/migration_lock.toml +0 -3
  206. package/template/src/app/(dashboard)/users/layout.tsx +0 -30
  207. package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
  208. package/template/src/app/api/dashboard/widgets/route.ts +0 -181
  209. package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
  210. package/template/src/components/dashboard/color-picker.tsx +0 -65
  211. package/template/src/components/dashboard/contacts-chart.tsx +0 -69
  212. package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
  213. package/template/src/components/dashboard/recent-activity.tsx +0 -157
  214. package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
  215. package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
  216. package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
  217. package/template/src/contexts/dashboard-theme-context.tsx +0 -58
  218. package/template/src/lib/dashboard-themes.ts +0 -140
  219. package/template/src/lib/default-widgets.ts +0 -14
  220. package/template/src/lib/widget-registry.ts +0 -177
@@ -0,0 +1,232 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef } from 'react';
4
+ import { MapPin, X } from 'lucide-react';
5
+ import { Spinner } from '@/components/skeleton';
6
+ import { searchAddress, type AddressFeature, extractAddressComponents } from '@/lib/address-api';
7
+ import { cn } from '@/lib/utils';
8
+ import { useAppToast } from '@/contexts/app-toast-context';
9
+
10
+ interface AddressAutocompleteProps {
11
+ value: string;
12
+ onChange: (
13
+ address: string,
14
+ components?: {
15
+ street: string;
16
+ city: string;
17
+ postalCode: string;
18
+ fullAddress: string;
19
+ citycode: string;
20
+ },
21
+ ) => void;
22
+ placeholder?: string;
23
+ disabled?: boolean;
24
+ className?: string;
25
+ }
26
+
27
+ export default function AddressAutocomplete({
28
+ value,
29
+ onChange,
30
+ placeholder = 'Rechercher une adresse...',
31
+ disabled = false,
32
+ className,
33
+ }: Readonly<AddressAutocompleteProps>) {
34
+ const toast = useAppToast();
35
+ const [inputValue, setInputValue] = useState(value);
36
+ const [suggestions, setSuggestions] = useState<AddressFeature[]>([]);
37
+ const [loading, setLoading] = useState(false);
38
+ const [showSuggestions, setShowSuggestions] = useState(false);
39
+ const [searchError, setSearchError] = useState<string | null>(null);
40
+ const [selectedIndex, setSelectedIndex] = useState(-1);
41
+ const inputRef = useRef<HTMLInputElement>(null);
42
+ const suggestionsRef = useRef<HTMLDivElement>(null);
43
+ const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
44
+ const lastSearchErrorRef = useRef<string | null>(null);
45
+
46
+ useEffect(() => {
47
+ setInputValue(value);
48
+ }, [value]);
49
+
50
+ useEffect(() => {
51
+ // Nettoyer le timer au démontage
52
+ return () => {
53
+ if (debounceTimerRef.current) {
54
+ clearTimeout(debounceTimerRef.current);
55
+ }
56
+ };
57
+ }, []);
58
+
59
+ useEffect(() => {
60
+ if (!searchError) {
61
+ lastSearchErrorRef.current = null;
62
+ return;
63
+ }
64
+ if (lastSearchErrorRef.current === searchError) {
65
+ return;
66
+ }
67
+ toast.error(searchError);
68
+ lastSearchErrorRef.current = searchError;
69
+ }, [searchError, toast]);
70
+
71
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
72
+ const newValue = e.target.value;
73
+ setInputValue(newValue);
74
+ setSelectedIndex(-1);
75
+
76
+ // Annuler le timer précédent
77
+ if (debounceTimerRef.current) {
78
+ clearTimeout(debounceTimerRef.current);
79
+ }
80
+
81
+ if (newValue.trim().length < 3) {
82
+ setSuggestions([]);
83
+ setShowSuggestions(false);
84
+ setSearchError(null);
85
+ onChange(newValue);
86
+ return;
87
+ }
88
+
89
+ // Débouncer la recherche (300ms)
90
+ debounceTimerRef.current = setTimeout(async () => {
91
+ setLoading(true);
92
+ setSearchError(null);
93
+ try {
94
+ const result = await searchAddress(newValue, 5);
95
+ setSuggestions(result.features);
96
+ setShowSuggestions(true);
97
+ } catch (error) {
98
+ const message = error instanceof Error ? error.message : 'Erreur lors de la recherche.';
99
+ setSearchError(message);
100
+ setSuggestions([]);
101
+ } finally {
102
+ setLoading(false);
103
+ }
104
+ }, 300);
105
+ };
106
+
107
+ const handleSelectSuggestion = (feature: AddressFeature) => {
108
+ const components = extractAddressComponents(feature);
109
+ setInputValue(components.fullAddress);
110
+ onChange(components.fullAddress, components);
111
+ setSuggestions([]);
112
+ setShowSuggestions(false);
113
+ setSelectedIndex(-1);
114
+ setSearchError(null);
115
+ };
116
+
117
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
118
+ if (!showSuggestions || suggestions.length === 0) return;
119
+
120
+ switch (e.key) {
121
+ case 'ArrowDown':
122
+ e.preventDefault();
123
+ setSelectedIndex((prev) => (prev < suggestions.length - 1 ? prev + 1 : prev));
124
+ break;
125
+ case 'ArrowUp':
126
+ e.preventDefault();
127
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : -1));
128
+ break;
129
+ case 'Enter':
130
+ e.preventDefault();
131
+ if (selectedIndex >= 0 && selectedIndex < suggestions.length) {
132
+ handleSelectSuggestion(suggestions[selectedIndex]);
133
+ }
134
+ break;
135
+ case 'Escape':
136
+ e.preventDefault();
137
+ setShowSuggestions(false);
138
+ setSelectedIndex(-1);
139
+ break;
140
+ }
141
+ };
142
+
143
+ const handleClear = () => {
144
+ setInputValue('');
145
+ onChange('');
146
+ setSuggestions([]);
147
+ setShowSuggestions(false);
148
+ setSelectedIndex(-1);
149
+ inputRef.current?.focus();
150
+ };
151
+
152
+ const handleBlur = () => {
153
+ // Délai pour permettre le clic sur une suggestion
154
+ setTimeout(() => {
155
+ setShowSuggestions(false);
156
+ setSelectedIndex(-1);
157
+ }, 200);
158
+ };
159
+
160
+ return (
161
+ <div className="relative">
162
+ <div className="relative">
163
+ <MapPin className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
164
+ <input
165
+ ref={inputRef}
166
+ data-ui-skip-token="true"
167
+ type="text"
168
+ value={inputValue}
169
+ onChange={handleInputChange}
170
+ onKeyDown={handleKeyDown}
171
+ onFocus={() => {
172
+ if (suggestions.length > 0) {
173
+ setShowSuggestions(true);
174
+ }
175
+ }}
176
+ onBlur={handleBlur}
177
+ placeholder={placeholder}
178
+ disabled={disabled}
179
+ className={cn(
180
+ 'w-full rounded-lg border border-gray-300 py-2 pr-10 pl-10 text-sm transition-colors',
181
+ 'focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500',
182
+ 'disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500',
183
+ className,
184
+ )}
185
+ />
186
+ <div className="absolute top-1/2 right-3 flex -translate-y-1/2 items-center gap-2">
187
+ {loading && <Spinner size="sm" className="text-gray-400" />}
188
+ {inputValue && !loading && !disabled && (
189
+ <button
190
+ type="button"
191
+ onClick={handleClear}
192
+ className="cursor-pointer text-gray-400 transition-colors hover:text-gray-600"
193
+ >
194
+ <X className="h-4 w-4" />
195
+ </button>
196
+ )}
197
+ </div>
198
+ </div>
199
+ {/* Suggestions */}
200
+ {showSuggestions && suggestions.length > 0 && (
201
+ <div
202
+ ref={suggestionsRef}
203
+ className="absolute z-50 mt-1 w-full rounded-lg border border-gray-200 bg-white shadow-lg"
204
+ >
205
+ <ul className="max-h-60 overflow-y-auto py-1">
206
+ {suggestions.map((suggestion, index) => (
207
+ <li key={suggestion.properties.id}>
208
+ <button
209
+ type="button"
210
+ onClick={() => handleSelectSuggestion(suggestion)}
211
+ className={cn(
212
+ 'flex w-full cursor-pointer items-start gap-3 px-4 py-2 text-left transition-colors',
213
+ 'hover:bg-blue-50',
214
+ selectedIndex === index && 'bg-blue-50',
215
+ )}
216
+ >
217
+ <MapPin className="mt-1 h-4 w-4 shrink-0 text-gray-400" />
218
+ <div className="flex-1">
219
+ <p className="text-sm font-medium text-gray-900">
220
+ {suggestion.properties.name}
221
+ </p>
222
+ <p className="text-xs text-gray-500">{suggestion.properties.context}</p>
223
+ </div>
224
+ </button>
225
+ </li>
226
+ ))}
227
+ </ul>
228
+ </div>
229
+ )}
230
+ </div>
231
+ );
232
+ }
@@ -0,0 +1,181 @@
1
+ 'use client';
2
+
3
+ import { X, Plus, Filter, Save, RotateCcw } from 'lucide-react';
4
+ import {
5
+ FILTER_FIELD_DEFINITIONS,
6
+ OPERATOR_LABELS,
7
+ DATE_PRESET_LABELS,
8
+ type ViewFilter,
9
+ type DatePreset,
10
+ } from '@/types/contact-views';
11
+
12
+ interface FilterBarProps {
13
+ filters: ViewFilter[];
14
+ onRemoveFilter: (index: number) => void;
15
+ onClearFilters: () => void;
16
+ onAddFilter: () => void;
17
+ onSaveView?: () => void;
18
+ onResetFilters?: () => void;
19
+ hasUnsavedChanges?: boolean;
20
+ isViewOwner?: boolean;
21
+ statusOptions?: { id: string; name: string; color: string }[];
22
+ userOptions?: { id: string; name: string }[];
23
+ }
24
+
25
+ function getFieldLabel(field: string): string {
26
+ const def = FILTER_FIELD_DEFINITIONS.find((d) => d.field === field);
27
+ return def?.label || field;
28
+ }
29
+
30
+ function formatFilterValue(
31
+ filter: ViewFilter,
32
+ statusOptions?: { id: string; name: string; color: string }[],
33
+ userOptions?: { id: string; name: string }[],
34
+ ): string {
35
+ const { field, operator, value, preset } = filter;
36
+
37
+ if (operator === 'is_known') return 'est connu';
38
+ if (operator === 'is_unknown') return 'est inconnu';
39
+
40
+ if (operator === 'date_preset' && preset) {
41
+ return DATE_PRESET_LABELS[preset as DatePreset] || preset;
42
+ }
43
+
44
+ if ((operator === 'is_any_of' || operator === 'is_none_of') && Array.isArray(value)) {
45
+ const labels = value.map((v) => {
46
+ if (v === 'UNASSIGNED') return 'Non assigné';
47
+ if (field === 'statusId' && statusOptions) {
48
+ return statusOptions.find((s) => s.id === v)?.name || v;
49
+ }
50
+ if (
51
+ (field === 'assignedCommercialId' ||
52
+ field === 'assignedTeleproId' ||
53
+ field === 'createdById') &&
54
+ userOptions
55
+ ) {
56
+ return userOptions.find((u) => u.id === v)?.name || v;
57
+ }
58
+ if (field === 'civility') {
59
+ const map: Record<string, string> = {
60
+ M: 'M.',
61
+ MME: 'Mme',
62
+ MLLE: 'Mlle',
63
+ };
64
+ return map[v] || v;
65
+ }
66
+ return v;
67
+ });
68
+
69
+ if (labels.length <= 2) return labels.join(', ');
70
+ return `${labels[0]} +${labels.length - 1}`;
71
+ }
72
+
73
+ if (operator === 'between' && Array.isArray(value) && value.length === 2) {
74
+ return `${value[0]} - ${value[1]}`;
75
+ }
76
+
77
+ if (typeof value === 'string') return value;
78
+ return '';
79
+ }
80
+
81
+ export function FilterBar({
82
+ filters,
83
+ onRemoveFilter,
84
+ onClearFilters,
85
+ onAddFilter,
86
+ onSaveView,
87
+ onResetFilters,
88
+ hasUnsavedChanges,
89
+ isViewOwner,
90
+ statusOptions,
91
+ userOptions,
92
+ }: FilterBarProps) {
93
+ if (filters.length === 0 && !hasUnsavedChanges) {
94
+ return (
95
+ <div className="flex items-center gap-2 px-4 py-2 sm:px-6 lg:px-8">
96
+ <button
97
+ onClick={onAddFilter}
98
+ className="flex cursor-pointer items-center gap-1.5 rounded-md border border-gray-200 px-3 py-1.5 text-sm text-gray-600 transition-colors hover:bg-gray-50"
99
+ >
100
+ <Filter className="h-3.5 w-3.5" />
101
+ Filtres avancés
102
+ </button>
103
+ </div>
104
+ );
105
+ }
106
+
107
+ return (
108
+ <div className="flex flex-wrap items-center gap-2 px-4 py-2 sm:px-6 lg:px-8">
109
+ <Filter className="h-3.5 w-3.5 shrink-0 text-gray-400" />
110
+
111
+ {filters.map((filter, index) => (
112
+ <div
113
+ key={`${filter.field}-${index}`}
114
+ className="flex items-center gap-1 rounded-md border border-blue-200 bg-blue-50 px-2 py-1 text-sm"
115
+ >
116
+ <span className="font-medium text-blue-700">{getFieldLabel(filter.field)}</span>
117
+ <span className="text-blue-500">{OPERATOR_LABELS[filter.operator]}</span>
118
+ <span className="max-w-40 truncate text-blue-700">
119
+ {formatFilterValue(filter, statusOptions, userOptions)}
120
+ </span>
121
+ <button
122
+ onClick={() => onRemoveFilter(index)}
123
+ className="ml-0.5 cursor-pointer rounded p-0.5 text-blue-400 hover:bg-blue-100 hover:text-blue-600"
124
+ >
125
+ <X className="h-3 w-3" />
126
+ </button>
127
+ </div>
128
+ ))}
129
+
130
+ <button
131
+ onClick={onAddFilter}
132
+ className="flex cursor-pointer items-center gap-1 rounded-md border border-dashed border-gray-300 px-2 py-1 text-sm text-gray-500 transition-colors hover:border-gray-400 hover:bg-gray-50"
133
+ >
134
+ <Plus className="h-3 w-3" />
135
+ Ajouter un filtre
136
+ </button>
137
+
138
+ {filters.length > 0 && (
139
+ <button
140
+ onClick={onClearFilters}
141
+ className="flex cursor-pointer items-center gap-1 rounded-md px-2 py-1 text-sm text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700"
142
+ >
143
+ <X className="h-3 w-3" />
144
+ Tout effacer
145
+ </button>
146
+ )}
147
+
148
+ {hasUnsavedChanges && (
149
+ <div className="ml-auto flex items-center gap-2">
150
+ {onResetFilters && (
151
+ <button
152
+ onClick={onResetFilters}
153
+ className="flex cursor-pointer items-center gap-1 rounded-md px-2.5 py-1 text-sm text-gray-500 transition-colors hover:bg-gray-100"
154
+ >
155
+ <RotateCcw className="h-3.5 w-3.5" />
156
+ Annuler
157
+ </button>
158
+ )}
159
+ {onSaveView && isViewOwner && (
160
+ <button
161
+ onClick={onSaveView}
162
+ className="flex cursor-pointer items-center gap-1 rounded-md bg-blue-600 px-2.5 py-1 text-sm font-medium text-white transition-colors hover:bg-blue-700"
163
+ >
164
+ <Save className="h-3.5 w-3.5" />
165
+ Enregistrer
166
+ </button>
167
+ )}
168
+ {onSaveView && !isViewOwner && (
169
+ <button
170
+ onClick={onSaveView}
171
+ className="flex cursor-pointer items-center gap-1 rounded-md bg-blue-600 px-2.5 py-1 text-sm font-medium text-white transition-colors hover:bg-blue-700"
172
+ >
173
+ <Save className="h-3.5 w-3.5" />
174
+ Enregistrer sous
175
+ </button>
176
+ )}
177
+ </div>
178
+ )}
179
+ </div>
180
+ );
181
+ }