create-crm-tmp 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/package.json +1 -1
  2. package/template/.prettierignore +2 -0
  3. package/template/README.md +53 -67
  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 +64 -27
  8. package/template/prisma/schema.prisma +821 -72
  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 +2231 -2188
  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 +5035 -4126
  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 +12 -17
  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 -85
  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 +33 -23
  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 +161 -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 +16 -1
  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 +94 -55
  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 +509 -146
  94. package/template/src/app/api/workflows/route.ts +46 -4
  95. package/template/src/app/globals.css +210 -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 +14 -39
  155. package/template/src/lib/template-variables.ts +67 -33
  156. package/template/src/lib/utils.ts +26 -2
  157. package/template/src/lib/workflow-executor.ts +445 -229
  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/migration_lock.toml +0 -3
  205. package/template/src/app/(dashboard)/users/layout.tsx +0 -30
  206. package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
  207. package/template/src/app/api/dashboard/widgets/route.ts +0 -181
  208. package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
  209. package/template/src/components/dashboard/color-picker.tsx +0 -65
  210. package/template/src/components/dashboard/contacts-chart.tsx +0 -69
  211. package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
  212. package/template/src/components/dashboard/recent-activity.tsx +0 -157
  213. package/template/src/components/dashboard/sales-analytics-chart.tsx +0 -77
  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,358 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import useSWR from 'swr';
6
+ import { Search, Users, Building2, FileText, Loader2 } from 'lucide-react';
7
+ import { cn } from '@/lib/utils';
8
+ import { NAV_PAGES } from '@/config/nav-pages';
9
+ import { useUserRole } from '@/hooks/use-user-role';
10
+
11
+ interface ContactResult {
12
+ id: string;
13
+ firstName: string | null;
14
+ lastName: string | null;
15
+ email: string | null;
16
+ phone: string | null;
17
+ }
18
+
19
+ interface CompanyResult {
20
+ id: string;
21
+ name: string;
22
+ email: string | null;
23
+ phone: string | null;
24
+ }
25
+
26
+ interface SearchResults {
27
+ contacts: ContactResult[];
28
+ companies: CompanyResult[];
29
+ }
30
+
31
+ const fetcher = async (url: string): Promise<SearchResults> => {
32
+ const params = new URL(url, globalThis.location?.origin);
33
+ const q = params.searchParams.get('q') || '';
34
+ const searchParam = encodeURIComponent(q);
35
+
36
+ const [contactsRes, companiesRes] = await Promise.all([
37
+ fetch(`/api/contacts?search=${searchParam}&limit=5`),
38
+ fetch(`/api/companies?search=${searchParam}&limit=5`),
39
+ ]);
40
+
41
+ const [contactsData, companiesData] = await Promise.all([
42
+ contactsRes.ok ? contactsRes.json() : { contacts: [] },
43
+ companiesRes.ok ? companiesRes.json() : { companies: [] },
44
+ ]);
45
+
46
+ return {
47
+ contacts: contactsData.contacts ?? [],
48
+ companies: companiesData.companies ?? companiesData ?? [],
49
+ };
50
+ };
51
+
52
+ function useDebounce(value: string, delay: number) {
53
+ const [debouncedValue, setDebouncedValue] = useState(value);
54
+ useEffect(() => {
55
+ const timer = setTimeout(() => setDebouncedValue(value), delay);
56
+ return () => clearTimeout(timer);
57
+ }, [value, delay]);
58
+ return debouncedValue;
59
+ }
60
+
61
+ export function GlobalSearch() {
62
+ const [query, setQuery] = useState('');
63
+ const [isOpen, setIsOpen] = useState(false);
64
+ const containerRef = useRef<HTMLDivElement>(null);
65
+ const inputRef = useRef<HTMLInputElement>(null);
66
+ const router = useRouter();
67
+ const { hasPermission } = useUserRole();
68
+
69
+ const debouncedQuery = useDebounce(query.trim(), 300);
70
+ const shouldFetchCRM = debouncedQuery.length >= 2;
71
+
72
+ const { data: crmResults, isLoading: crmLoading } = useSWR(
73
+ shouldFetchCRM ? `/api/_search?q=${debouncedQuery}` : null,
74
+ fetcher,
75
+ { keepPreviousData: true, revalidateOnFocus: false },
76
+ );
77
+
78
+ const filteredPages = useMemo(() => {
79
+ const q = query.trim().toLowerCase();
80
+ if (q.length === 0) return [];
81
+ return NAV_PAGES.filter(
82
+ (page) =>
83
+ page.permissions.some((p) => hasPermission(p)) &&
84
+ `${page.parentLabel ?? ''} ${page.name}`.toLowerCase().includes(q),
85
+ );
86
+ }, [query, hasPermission]);
87
+
88
+ const flatOptions = useMemo(() => {
89
+ const opts: { href: string }[] = [];
90
+ filteredPages.forEach((p) => opts.push({ href: p.href }));
91
+ if (crmResults) {
92
+ crmResults.contacts.forEach((c) => opts.push({ href: `/contacts/${c.id}` }));
93
+ crmResults.companies.forEach((c) => opts.push({ href: `/contacts/companies/${c.id}` }));
94
+ }
95
+ return opts;
96
+ }, [filteredPages, crmResults]);
97
+
98
+ const [focusedIndex, setFocusedIndex] = useState(0);
99
+ const optionRefs = useRef<(HTMLButtonElement | null)[]>([]);
100
+ const showDropdown = isOpen && query.trim().length > 0;
101
+
102
+ useEffect(() => {
103
+ setFocusedIndex(0);
104
+ }, [flatOptions.length]);
105
+
106
+ useEffect(() => {
107
+ if (!showDropdown || focusedIndex < 0 || focusedIndex >= flatOptions.length) return;
108
+ optionRefs.current[focusedIndex]?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
109
+ }, [showDropdown, focusedIndex, flatOptions.length]);
110
+
111
+ const hasResults =
112
+ filteredPages.length > 0 ||
113
+ (crmResults &&
114
+ (crmResults.contacts.length > 0 ||
115
+ crmResults.companies.length > 0));
116
+
117
+ const navigate = useCallback(
118
+ (href: string) => {
119
+ setIsOpen(false);
120
+ setQuery('');
121
+ router.push(href);
122
+ },
123
+ [router],
124
+ );
125
+
126
+ useEffect(() => {
127
+ const handleClickOutside = (e: MouseEvent) => {
128
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
129
+ setIsOpen(false);
130
+ }
131
+ };
132
+ const handleEscape = (e: KeyboardEvent) => {
133
+ if (e.key === 'Escape') {
134
+ setIsOpen(false);
135
+ inputRef.current?.blur();
136
+ }
137
+ };
138
+ document.addEventListener('mousedown', handleClickOutside);
139
+ document.addEventListener('keydown', handleEscape);
140
+ return () => {
141
+ document.removeEventListener('mousedown', handleClickOutside);
142
+ document.removeEventListener('keydown', handleEscape);
143
+ };
144
+ }, []);
145
+
146
+ const handleKeyDown = useCallback(
147
+ (e: React.KeyboardEvent<HTMLInputElement>) => {
148
+ if (!showDropdown || flatOptions.length === 0) return;
149
+ if (e.key === 'ArrowDown') {
150
+ e.preventDefault();
151
+ setFocusedIndex((i) => Math.min(i + 1, flatOptions.length - 1));
152
+ return;
153
+ }
154
+ if (e.key === 'ArrowUp') {
155
+ e.preventDefault();
156
+ setFocusedIndex((i) => Math.max(i - 1, 0));
157
+ return;
158
+ }
159
+ if (e.key === 'Enter') {
160
+ e.preventDefault();
161
+ navigate(flatOptions[focusedIndex].href);
162
+ }
163
+ },
164
+ [showDropdown, flatOptions, focusedIndex, navigate],
165
+ );
166
+
167
+ let optionIndex = 0;
168
+
169
+ return (
170
+ <div ref={containerRef} className="relative hidden w-full max-w-xl sm:block">
171
+ <div className="relative">
172
+ <Search className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
173
+ <input
174
+ ref={inputRef}
175
+ type="text"
176
+ value={query}
177
+ onChange={(e) => {
178
+ setQuery(e.target.value);
179
+ if (!isOpen) setIsOpen(true);
180
+ }}
181
+ onFocus={() => {
182
+ if (query.trim().length > 0) setIsOpen(true);
183
+ }}
184
+ onKeyDown={handleKeyDown}
185
+ placeholder="Rechercher Gold Blessing"
186
+ className="w-full rounded-lg border border-border bg-muted py-2 pr-4 pl-9 text-sm text-foreground transition-colors outline-none placeholder:text-muted-foreground focus:border-primary/40 focus:bg-background focus:ring-1 focus:ring-primary/40"
187
+ role="combobox"
188
+ aria-expanded={showDropdown}
189
+ aria-controls="global-search-results"
190
+ aria-activedescendant={
191
+ showDropdown && flatOptions.length > 0
192
+ ? `global-search-option-${focusedIndex}`
193
+ : undefined
194
+ }
195
+ autoComplete="off"
196
+ />
197
+ </div>
198
+
199
+ {showDropdown && (
200
+ <div
201
+ id="global-search-results"
202
+ role="listbox"
203
+ className="absolute top-full left-0 z-50 mt-1 max-h-112 w-full overflow-y-auto rounded-xl border border-border bg-popover shadow-(--shadow-dropdown)"
204
+ >
205
+ {/* Pages */}
206
+ {filteredPages.length > 0 && (
207
+ <div>
208
+ <div className="px-3 pt-3 pb-1">
209
+ <span className="text-xs font-semibold tracking-wider text-muted-foreground uppercase">
210
+ Pages
211
+ </span>
212
+ </div>
213
+ {filteredPages.map((page) => {
214
+ const Icon = page.icon;
215
+ const idx = optionIndex++;
216
+ return (
217
+ <button
218
+ key={page.href}
219
+ ref={(el) => {
220
+ optionRefs.current[idx] = el;
221
+ }}
222
+ id={`global-search-option-${idx}`}
223
+ onClick={() => navigate(page.href)}
224
+ className={cn(
225
+ 'flex w-full cursor-pointer items-center gap-3 px-3 py-2 text-left text-sm text-popover-foreground transition-colors duration-200 hover:bg-accent',
226
+ focusedIndex === idx && 'bg-accent',
227
+ )}
228
+ role="option"
229
+ aria-selected={focusedIndex === idx}
230
+ >
231
+ <Icon className="h-4 w-4 shrink-0 text-muted-foreground" />
232
+ <div className="min-w-0 flex-1">
233
+ <span className="block truncate font-medium text-popover-foreground">{page.name}</span>
234
+ {page.parentLabel && (
235
+ <span className="block truncate text-xs text-muted-foreground">
236
+ {page.parentLabel} {'>'} {page.name}
237
+ </span>
238
+ )}
239
+ </div>
240
+ </button>
241
+ );
242
+ })}
243
+ </div>
244
+ )}
245
+
246
+ {/* CRM loading */}
247
+ {shouldFetchCRM && crmLoading && !crmResults && (
248
+ <div className="flex items-center justify-center gap-2 px-3 py-6 text-sm text-muted-foreground">
249
+ <Loader2 className="h-4 w-4 animate-spin" />
250
+ Recherche...
251
+ </div>
252
+ )}
253
+
254
+ {/* Contacts */}
255
+ {crmResults && crmResults.contacts.length > 0 && (
256
+ <div>
257
+ <div
258
+ className={cn(
259
+ 'px-3 pt-3 pb-1',
260
+ filteredPages.length > 0 && 'border-t border-border',
261
+ )}
262
+ >
263
+ <span className="text-xs font-semibold tracking-wider text-muted-foreground uppercase">
264
+ Contacts
265
+ </span>
266
+ </div>
267
+ {crmResults.contacts.map((contact) => {
268
+ const name =
269
+ [contact.firstName, contact.lastName].filter(Boolean).join(' ') ||
270
+ 'Contact sans nom';
271
+ const idx = optionIndex++;
272
+ return (
273
+ <button
274
+ key={contact.id}
275
+ ref={(el) => {
276
+ optionRefs.current[idx] = el;
277
+ }}
278
+ id={`global-search-option-${idx}`}
279
+ onClick={() => navigate(`/contacts/${contact.id}`)}
280
+ className={cn(
281
+ 'flex w-full cursor-pointer items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-200 hover:bg-accent',
282
+ focusedIndex === idx && 'bg-accent',
283
+ )}
284
+ role="option"
285
+ aria-selected={focusedIndex === idx}
286
+ >
287
+ <Users className="h-4 w-4 shrink-0 text-muted-foreground" />
288
+ <div className="min-w-0 flex-1">
289
+ <span className="font-medium text-popover-foreground">{name}</span>
290
+ {(contact.email || contact.phone) && (
291
+ <span className="ml-2 truncate text-xs text-muted-foreground">
292
+ {contact.email || contact.phone}
293
+ </span>
294
+ )}
295
+ </div>
296
+ </button>
297
+ );
298
+ })}
299
+ </div>
300
+ )}
301
+
302
+ {/* Entreprises */}
303
+ {crmResults && crmResults.companies.length > 0 && (
304
+ <div>
305
+ <div className="border-t border-border px-3 pt-3 pb-1">
306
+ <span className="text-xs font-semibold tracking-wider text-muted-foreground uppercase">
307
+ Entreprises
308
+ </span>
309
+ </div>
310
+ {crmResults.companies.map((company) => {
311
+ const idx = optionIndex++;
312
+ return (
313
+ <button
314
+ key={company.id}
315
+ ref={(el) => {
316
+ optionRefs.current[idx] = el;
317
+ }}
318
+ id={`global-search-option-${idx}`}
319
+ onClick={() => navigate(`/contacts/companies/${company.id}`)}
320
+ className={cn(
321
+ 'flex w-full cursor-pointer items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-200 hover:bg-accent',
322
+ focusedIndex === idx && 'bg-accent',
323
+ )}
324
+ role="option"
325
+ aria-selected={focusedIndex === idx}
326
+ >
327
+ <Building2 className="h-4 w-4 shrink-0 text-muted-foreground" />
328
+ <div className="min-w-0 flex-1">
329
+ <span className="font-medium text-popover-foreground">{company.name}</span>
330
+ {company.email && (
331
+ <span className="ml-2 truncate text-xs text-muted-foreground">{company.email}</span>
332
+ )}
333
+ </div>
334
+ </button>
335
+ );
336
+ })}
337
+ </div>
338
+ )}
339
+
340
+ {/* Aucun résultat */}
341
+ {!crmLoading && !hasResults && query.trim().length > 0 && (
342
+ <div className="px-3 py-6 text-center text-sm text-muted-foreground">
343
+ Aucun résultat pour &quot;{query.trim()}&quot;
344
+ </div>
345
+ )}
346
+
347
+ {/* Recherche trop courte pour le CRM */}
348
+ {query.trim().length === 1 && filteredPages.length === 0 && (
349
+ <div className="px-3 py-6 text-center text-sm text-muted-foreground">
350
+ <FileText className="mx-auto mb-1 h-5 w-5 text-muted-foreground/70" />
351
+ Tapez au moins 2 caractères pour rechercher dans le CRM
352
+ </div>
353
+ )}
354
+ </div>
355
+ )}
356
+ </div>
357
+ );
358
+ }
@@ -7,7 +7,8 @@ import { useRouter } from 'next/navigation';
7
7
  import Link from 'next/link';
8
8
  import { cn } from '@/lib/utils';
9
9
  import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
10
- import { useViewAs } from '@/contexts/view-as-context';
10
+ import { GlobalSearch } from '@/components/global-search';
11
+ import { safeLocalStorageGet, safeLocalStorageSet } from '@/lib/local-storage';
11
12
 
12
13
  interface Reminder {
13
14
  id: string;
@@ -35,7 +36,7 @@ const TASK_TYPE_LABELS: Record<string, string> = {
35
36
  };
36
37
 
37
38
  const PRIORITY_COLORS: Record<string, string> = {
38
- LOW: 'bg-gray-100 text-gray-700',
39
+ LOW: 'bg-muted text-muted-foreground',
39
40
  MEDIUM: 'bg-yellow-100 text-yellow-700',
40
41
  HIGH: 'bg-orange-100 text-orange-700',
41
42
  URGENT: 'bg-red-100 text-red-700',
@@ -59,7 +60,6 @@ export function Header() {
59
60
  const { data: session } = useSession();
60
61
  const router = useRouter();
61
62
  const { toggle: toggleMobileMenu } = useMobileMenuContext();
62
- const { viewAsUser, isViewingAsOther } = useViewAs();
63
63
  const [showRemindersDropdown, setShowRemindersDropdown] = useState(false);
64
64
  const [showUserDropdown, setShowUserDropdown] = useState(false);
65
65
  const [reminders, setReminders] = useState<Reminder[]>([]);
@@ -68,29 +68,20 @@ export function Header() {
68
68
  const remindersRef = useRef<HTMLDivElement>(null);
69
69
  const userRef = useRef<HTMLDivElement>(null);
70
70
 
71
- const realUserName = session?.user?.name || 'Utilisateur';
72
- const userName = isViewingAsOther ? viewAsUser?.name || 'Utilisateur' : realUserName;
71
+ const userName = session?.user?.name || 'Utilisateur';
73
72
  const userEmail = session?.user?.email || '';
74
73
  const userInitial = userName?.[0]?.toUpperCase() || 'U';
75
74
 
76
75
  // Charger les rappels lus depuis localStorage
77
76
  useEffect(() => {
78
- if (typeof window !== 'undefined') {
79
- const stored = localStorage.getItem('readReminders');
80
- if (stored) {
81
- try {
82
- setReadReminders(new Set(JSON.parse(stored)));
83
- } catch (e) {
84
- console.error('Erreur lors du chargement des rappels lus:', e);
85
- }
86
- }
87
- }
77
+ const stored = safeLocalStorageGet<string[]>('read-reminders', []);
78
+ setReadReminders(new Set(stored));
88
79
  }, []);
89
80
 
90
81
  // Sauvegarder les rappels lus dans localStorage
91
82
  useEffect(() => {
92
- if (typeof window !== 'undefined' && readReminders.size > 0) {
93
- localStorage.setItem('readReminders', JSON.stringify(Array.from(readReminders)));
83
+ if (readReminders.size > 0) {
84
+ safeLocalStorageSet('read-reminders', Array.from(readReminders));
94
85
  }
95
86
  }, [readReminders]);
96
87
 
@@ -150,37 +141,40 @@ export function Header() {
150
141
  };
151
142
 
152
143
  return (
153
- <header className="sticky top-0 z-30 border-b border-gray-200 bg-white px-4 py-3 sm:px-6 lg:px-8">
154
- <div className="flex items-center justify-between gap-2 sm:gap-4">
155
- {/* Left: Burger + Logo + Greeting */}
156
- <div className="flex items-center gap-2 sm:gap-3">
157
- {/* Bouton burger - visible uniquement sur mobile/tablette */}
158
- <button
159
- onClick={toggleMobileMenu}
160
- className="cursor-pointer rounded-lg p-2 text-gray-600 transition-colors hover:bg-gray-100 lg:hidden"
161
- aria-label="Ouvrir le menu"
162
- >
163
- <Menu className="h-5 w-5" />
164
- </button>
144
+ <header className="sticky top-0 z-40 border-b border-border bg-background/95 px-4 py-3 backdrop-blur-sm sm:px-6 lg:px-8">
145
+ <div className="flex items-center gap-2 sm:gap-4">
146
+ {/* Left: Logo + Greeting */}
147
+ <div className="flex shrink-0 items-center gap-2 sm:gap-3">
165
148
  <div className="flex items-center gap-1.5 sm:gap-2">
166
- {/* <Rocket className="h-5 w-5 text-indigo-600 sm:h-6 sm:w-6" /> */}
167
- <span className="text-base font-bold text-gray-900 sm:text-lg">CRM Template</span>
149
+ {/* Bouton burger pour mobile */}
150
+ <button
151
+ onClick={toggleMobileMenu}
152
+ className="cursor-pointer rounded-lg p-2 text-foreground/80 transition-colors duration-200 hover:bg-muted focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none lg:hidden"
153
+ aria-label="Ouvrir ou fermer le menu"
154
+ >
155
+ <Menu className="h-5 w-5" />
156
+ </button>
157
+ <span className="text-base font-bold text-foreground sm:text-lg">Gold Blessing</span>
168
158
  </div>
169
- <span className="hidden text-sm text-gray-600 sm:inline">👋 Salut {userName} !</span>
159
+ </div>
160
+
161
+ {/* Center: Global Search */}
162
+ <div className="flex min-w-0 flex-1 justify-center">
163
+ <GlobalSearch />
170
164
  </div>
171
165
 
172
166
  {/* Right: Notifications + User Avatar */}
173
- <div className="flex items-center gap-2 sm:gap-3">
167
+ <div className="flex shrink-0 items-center gap-2 sm:gap-3">
174
168
  {/* Notifications Dropdown */}
175
169
  <div className="relative" ref={remindersRef}>
176
170
  <button
177
171
  onClick={() => setShowRemindersDropdown(!showRemindersDropdown)}
178
- className="relative cursor-pointer rounded-lg p-2 text-gray-600 transition-colors hover:bg-gray-100"
172
+ className="relative cursor-pointer rounded-lg p-2 text-muted-foreground transition-colors duration-200 hover:bg-muted focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
179
173
  aria-label="Notifications"
180
174
  >
181
175
  <Bell className="h-5 w-5" />
182
176
  {unreadCount > 0 && (
183
- <span className="absolute top-1 right-1 flex h-4 w-4 items-center justify-center rounded-full bg-blue-600 text-[10px] font-semibold text-white">
177
+ <span className="absolute top-1 right-1 flex h-4 w-4 items-center justify-center rounded-full bg-primary text-[10px] font-semibold text-primary-foreground">
184
178
  {unreadCount > 9 ? '9+' : unreadCount}
185
179
  </span>
186
180
  )}
@@ -188,14 +182,14 @@ export function Header() {
188
182
 
189
183
  {/* Dropdown des rappels */}
190
184
  {showRemindersDropdown && (
191
- <div className="absolute right-0 mt-2 w-80 max-w-[calc(100vw-2rem)] rounded-lg border border-gray-200 bg-white shadow-xl">
192
- <div className="border-b border-gray-200 px-4 py-3">
185
+ <div className="absolute right-0 mt-2 w-80 max-w-[calc(100vw-2rem)] rounded-xl border border-border bg-popover shadow-(--shadow-dropdown)">
186
+ <div className="border-b border-border px-4 py-3">
193
187
  <div className="flex items-center justify-between">
194
- <h3 className="text-sm font-semibold text-gray-900">Rappels</h3>
188
+ <h3 className="text-sm font-semibold text-popover-foreground">Rappels</h3>
195
189
  {unreadCount > 0 && (
196
190
  <button
197
191
  onClick={handleMarkAllAsRead}
198
- className="cursor-pointer text-xs text-indigo-600 hover:text-indigo-700"
192
+ className="cursor-pointer text-xs text-primary hover:text-primary/80 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
199
193
  >
200
194
  Tout marquer comme lu
201
195
  </button>
@@ -204,11 +198,11 @@ export function Header() {
204
198
  </div>
205
199
  <div className="max-h-96 overflow-y-auto">
206
200
  {loading ? (
207
- <div className="px-4 py-8 text-center text-sm text-gray-500">Chargement...</div>
201
+ <div className="px-4 py-8 text-center text-sm text-muted-foreground">Chargement...</div>
208
202
  ) : reminders.length === 0 ? (
209
- <div className="px-4 py-8 text-center text-sm text-gray-500">Aucun rappel</div>
203
+ <div className="px-4 py-8 text-center text-sm text-muted-foreground">Aucun rappel</div>
210
204
  ) : (
211
- <div className="divide-y divide-gray-100">
205
+ <div className="divide-y divide-border">
212
206
  {reminders.map((reminder) => {
213
207
  const isRead = readReminders.has(reminder.id);
214
208
  const { date, time } = formatDateTime(reminder.scheduledAt);
@@ -218,17 +212,18 @@ export function Header() {
218
212
  : null;
219
213
 
220
214
  return (
221
- <div
215
+ <button
216
+ type="button"
222
217
  key={reminder.id}
223
218
  className={cn(
224
- 'px-4 py-3 transition-colors hover:bg-gray-50',
225
- !isRead && 'bg-blue-50/50',
219
+ 'w-full px-4 py-3 text-left transition-colors duration-200 hover:bg-accent',
220
+ !isRead && 'bg-accent/70',
226
221
  )}
227
222
  onClick={() => handleMarkAsRead(reminder.id)}
228
223
  >
229
224
  <div className="flex items-start gap-3">
230
225
  <div className="mt-0.5 shrink-0">
231
- <Calendar className="h-4 w-4 text-indigo-600" />
226
+ <Calendar className="h-4 w-4 text-primary" />
232
227
  </div>
233
228
  <div className="min-w-0 flex-1">
234
229
  <div className="flex items-start justify-between gap-2">
@@ -236,18 +231,18 @@ export function Header() {
236
231
  <p
237
232
  className={cn(
238
233
  'text-sm font-medium',
239
- !isRead ? 'text-gray-900' : 'text-gray-700',
234
+ !isRead ? 'text-foreground' : 'text-muted-foreground',
240
235
  )}
241
236
  >
242
237
  {reminder.title || TASK_TYPE_LABELS[reminder.type] || 'Tâche'}
243
238
  </p>
244
239
  {contactName && (
245
- <p className="mt-0.5 text-xs text-gray-500">{contactName}</p>
240
+ <p className="mt-0.5 text-xs text-muted-foreground">{contactName}</p>
246
241
  )}
247
242
  <div className="mt-1 flex items-center gap-2">
248
- <span className="text-xs text-gray-500">{date}</span>
249
- <span className="text-xs text-gray-400">•</span>
250
- <span className="text-xs text-gray-500">{time}</span>
243
+ <span className="text-xs text-muted-foreground">{date}</span>
244
+ <span className="text-xs text-muted-foreground/70">•</span>
245
+ <span className="text-xs text-muted-foreground">{time}</span>
251
246
  <span
252
247
  className={cn(
253
248
  'rounded-full px-1.5 py-0.5 text-[10px] font-medium',
@@ -266,21 +261,21 @@ export function Header() {
266
261
  </div>
267
262
  </div>
268
263
  {!isRead && (
269
- <div className="h-2 w-2 shrink-0 rounded-full bg-blue-600" />
264
+ <div className="h-2 w-2 shrink-0 rounded-full bg-primary" />
270
265
  )}
271
266
  </div>
272
267
  {reminder.contact && (
273
268
  <Link
274
269
  href={`/contacts/${reminder.contact.id}`}
275
270
  onClick={(e) => e.stopPropagation()}
276
- className="mt-2 inline-block text-xs font-medium text-indigo-600 hover:text-indigo-700"
271
+ className="mt-2 inline-block text-xs font-medium text-primary hover:text-primary/80"
277
272
  >
278
273
  Voir le contact
279
274
  </Link>
280
275
  )}
281
276
  </div>
282
277
  </div>
283
- </div>
278
+ </button>
284
279
  );
285
280
  })}
286
281
  </div>
@@ -294,26 +289,26 @@ export function Header() {
294
289
  <div className="relative" ref={userRef}>
295
290
  <button
296
291
  onClick={() => setShowUserDropdown(!showUserDropdown)}
297
- className="flex cursor-pointer items-center gap-1.5 sm:gap-2"
292
+ className="flex cursor-pointer items-center gap-1.5 rounded-md transition-colors duration-200 hover:bg-accent focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none sm:gap-2"
298
293
  aria-label="Menu utilisateur"
299
294
  >
300
- <div className="flex h-8 w-8 items-center justify-center rounded-full bg-indigo-100 text-xs font-semibold text-indigo-600 sm:h-9 sm:w-9 sm:text-sm">
295
+ <div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/15 text-xs font-semibold text-primary sm:h-9 sm:w-9 sm:text-sm">
301
296
  {userInitial}
302
297
  </div>
303
- <ChevronDown className="hidden h-4 w-4 text-gray-400 transition-colors hover:text-gray-600 sm:block" />
298
+ <ChevronDown className="hidden h-4 w-4 text-muted-foreground transition-colors hover:text-foreground sm:block" />
304
299
  </button>
305
300
 
306
301
  {/* Dropdown utilisateur */}
307
302
  {showUserDropdown && (
308
- <div className="absolute right-0 mt-2 w-56 rounded-lg border border-gray-200 bg-white shadow-xl">
309
- <div className="border-b border-gray-200 px-4 py-3">
310
- <p className="text-sm font-medium text-gray-900">{realUserName}</p>
311
- <p className="mt-0.5 text-xs text-gray-500">{userEmail}</p>
303
+ <div className="absolute right-0 mt-2 w-56 rounded-xl border border-border bg-popover shadow-(--shadow-dropdown)">
304
+ <div className="border-b border-border px-4 py-3">
305
+ <p className="text-sm font-medium text-popover-foreground">{userName}</p>
306
+ <p className="mt-0.5 text-xs text-muted-foreground">{userEmail}</p>
312
307
  </div>
313
308
  <div className="py-1">
314
309
  <button
315
310
  onClick={handleSignOut}
316
- className="flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-50"
311
+ className="flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-sm text-popover-foreground transition-colors duration-200 hover:bg-accent focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
317
312
  >
318
313
  <LogOut className="h-4 w-4" />
319
314
  <span>Déconnexion</span>
@@ -1,3 +1,5 @@
1
+ import DOMPurify from 'isomorphic-dompurify';
2
+
1
3
  interface InvitationEmailProps {
2
4
  name: string;
3
5
  invitationUrl: string;
@@ -8,7 +10,7 @@ export function InvitationEmailTemplate({ name, invitationUrl, signature }: Invi
8
10
  return (
9
11
  <div
10
12
  style={{
11
- fontFamily: 'Arial, sans-serif',
13
+ fontFamily: '"Segoe UI", "Helvetica Neue", sans-serif',
12
14
  padding: '20px',
13
15
  maxWidth: '600px',
14
16
  margin: '0 auto',
@@ -71,7 +73,7 @@ export function InvitationEmailTemplate({ name, invitationUrl, signature }: Invi
71
73
  fontSize: '14px',
72
74
  lineHeight: '1.6',
73
75
  }}
74
- dangerouslySetInnerHTML={{ __html: signature }}
76
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(signature) }}
75
77
  />
76
78
  )}
77
79
  </div>
@@ -0,0 +1,11 @@
1
+ 'use client';
2
+
3
+ import dynamic from 'next/dynamic';
4
+ import { Skeleton } from '@/components/skeleton';
5
+
6
+ export type { DefaultTemplateRef } from '@/components/editor';
7
+
8
+ export const LazyEditor = dynamic(() => import('@/components/editor').then((m) => m.Editor), {
9
+ ssr: false,
10
+ loading: () => <Skeleton className="h-64 rounded-lg" />,
11
+ });