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,7 @@
1
1
  'use client';
2
2
 
3
3
  import { X, Plus, Filter, Save, RotateCcw } from 'lucide-react';
4
+ import { cn } from '@/lib/utils';
4
5
  import {
5
6
  FILTER_FIELD_DEFINITIONS,
6
7
  OPERATOR_LABELS,
@@ -20,6 +21,8 @@ interface FilterBarProps {
20
21
  isViewOwner?: boolean;
21
22
  statusOptions?: { id: string; name: string; color: string }[];
22
23
  userOptions?: { id: string; name: string }[];
24
+ /** Quand true, pas de padding (intégré sur une ligne avec la barre d'outils) */
25
+ inline?: boolean;
23
26
  }
24
27
 
25
28
  function getFieldLabel(field: string): string {
@@ -89,10 +92,16 @@ export function FilterBar({
89
92
  isViewOwner,
90
93
  statusOptions,
91
94
  userOptions,
95
+ inline = false,
92
96
  }: FilterBarProps) {
97
+ const rootClass = cn(
98
+ 'flex flex-wrap items-center gap-2',
99
+ !inline && 'px-4 py-2 sm:px-6 lg:px-8',
100
+ inline && 'shrink-0',
101
+ );
93
102
  if (filters.length === 0 && !hasUnsavedChanges) {
94
103
  return (
95
- <div className="flex items-center gap-2 px-4 py-2 sm:px-6 lg:px-8">
104
+ <div className={rootClass}>
96
105
  <button
97
106
  onClick={onAddFilter}
98
107
  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"
@@ -105,7 +114,7 @@ export function FilterBar({
105
114
  }
106
115
 
107
116
  return (
108
- <div className="flex flex-wrap items-center gap-2 px-4 py-2 sm:px-6 lg:px-8">
117
+ <div className={rootClass}>
109
118
  <Filter className="h-3.5 w-3.5 shrink-0 text-gray-400" />
110
119
 
111
120
  {filters.map((filter, index) => (
@@ -120,7 +129,7 @@ export function FilterBar({
120
129
  </span>
121
130
  <button
122
131
  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"
132
+ className="ml-0.5 cursor-pointer rounded p-0.5 text-blue-400 hover:bg-blue-100 hover:text-blue-600"
124
133
  >
125
134
  <X className="h-3 w-3" />
126
135
  </button>
@@ -14,9 +14,9 @@ import {
14
14
  OPERATOR_LABELS,
15
15
  DATE_PRESET_LABELS,
16
16
  } from '@/types/contact-views';
17
- import type { Region, Department } from '@/lib/french-regions';
18
- import { FRENCH_REGIONS, FRENCH_DEPARTMENTS } from '@/lib/french-regions';
19
17
  import { useFocusTrap } from '@/hooks/use-focus-trap';
18
+ import { FR_DEPARTMENTS, FR_REGIONS } from '@/lib/fr-geography';
19
+ import { DatePicker } from '@/components/ui/date-picker';
20
20
 
21
21
  type Step = 'field' | 'operator' | 'value';
22
22
 
@@ -226,16 +226,10 @@ export function FilterBuilder({
226
226
  return originOptions.map((o) => ({ value: o, label: o }));
227
227
  }
228
228
  if (selectedField.field === 'region') {
229
- return (FRENCH_REGIONS as Region[]).map((r) => ({
230
- value: r.code,
231
- label: r.name,
232
- }));
229
+ return FR_REGIONS.map((r) => ({ value: r.code, label: r.name }));
233
230
  }
234
231
  if (selectedField.field === 'department') {
235
- return (FRENCH_DEPARTMENTS as Department[]).map((d) => ({
236
- value: d.code,
237
- label: `${d.code} - ${d.name}`,
238
- }));
232
+ return FR_DEPARTMENTS.map((d) => ({ value: d.code, label: `${d.code} — ${d.name}` }));
239
233
  }
240
234
  return [];
241
235
  }
@@ -308,7 +302,7 @@ export function FilterBuilder({
308
302
  placeholder="Rechercher une propriété..."
309
303
  value={fieldSearch}
310
304
  onChange={(e) => setFieldSearch(e.target.value)}
311
- className="w-full rounded-md border border-gray-200 py-2 pr-3 pl-9 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
305
+ className="w-full rounded-md border border-gray-200 py-2 pr-3 pl-9 text-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
312
306
  autoFocus
313
307
  />
314
308
  </div>
@@ -369,8 +363,8 @@ export function FilterBuilder({
369
363
  <button
370
364
  key={preset}
371
365
  onClick={() => applyFilter('date_preset', null, preset)}
372
- className={cn(
373
- 'flex w-full cursor-pointer items-center justify-between px-4 py-2 text-sm hover:bg-gray-50',
366
+ className={cn(
367
+ 'flex w-full cursor-pointer items-center justify-between px-4 py-2 text-sm hover:bg-gray-50',
374
368
  selectedPreset === preset ? 'font-medium text-blue-600' : 'text-gray-700',
375
369
  )}
376
370
  >
@@ -394,7 +388,7 @@ export function FilterBuilder({
394
388
  placeholder="Rechercher..."
395
389
  value={selectSearch}
396
390
  onChange={(e) => setSelectSearch(e.target.value)}
397
- className="w-full rounded-md border border-gray-200 py-2 pr-3 pl-9 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
391
+ className="w-full rounded-md border border-gray-200 py-2 pr-3 pl-9 text-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
398
392
  autoFocus
399
393
  />
400
394
  </div>
@@ -409,8 +403,8 @@ export function FilterBuilder({
409
403
  <button
410
404
  key={option.value}
411
405
  onClick={() => handleToggleSelectValue(option.value)}
412
- className={cn(
413
- 'flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-sm hover:bg-gray-50',
406
+ className={cn(
407
+ 'flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-sm hover:bg-gray-50',
414
408
  isSelected ? 'text-blue-600' : 'text-gray-700',
415
409
  )}
416
410
  >
@@ -469,7 +463,7 @@ export function FilterBuilder({
469
463
  applyFilter(undefined, textInput.trim(), null);
470
464
  }
471
465
  }}
472
- className="w-full rounded-md border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
466
+ className="w-full rounded-md border border-gray-200 px-3 py-2 text-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
473
467
  autoFocus
474
468
  />
475
469
  <button
@@ -503,7 +497,7 @@ export function FilterBuilder({
503
497
  applyFilter(undefined, textInput.trim(), null);
504
498
  }
505
499
  }}
506
- className="w-full rounded-md border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
500
+ className="w-full rounded-md border border-gray-200 px-3 py-2 text-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
507
501
  autoFocus
508
502
  />
509
503
  <button
@@ -525,12 +519,11 @@ export function FilterBuilder({
525
519
  selectedOperator !== 'date_preset' &&
526
520
  selectedOperator !== 'between' && (
527
521
  <div className="p-4">
528
- <input
529
- type="date"
530
- value={dateInputStart}
531
- onChange={(e) => setDateInputStart(e.target.value)}
532
- className="w-full rounded-md border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
533
- autoFocus
522
+ <DatePicker
523
+ embedded
524
+ startDate={dateInputStart}
525
+ onDateChange={(start) => setDateInputStart(start)}
526
+ label="Choisir une date"
534
527
  />
535
528
  <button
536
529
  onClick={() => {
@@ -549,25 +542,17 @@ export function FilterBuilder({
549
542
  {/* Date between */}
550
543
  {selectedField.type === 'date' && selectedOperator === 'between' && (
551
544
  <div className="space-y-3 p-4">
552
- <div>
553
- <label className="mb-1 block text-xs font-medium text-gray-500">Du</label>
554
- <input
555
- type="date"
556
- value={dateInputStart}
557
- onChange={(e) => setDateInputStart(e.target.value)}
558
- className="w-full rounded-md border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
559
- autoFocus
560
- />
561
- </div>
562
- <div>
563
- <label className="mb-1 block text-xs font-medium text-gray-500">Au</label>
564
- <input
565
- type="date"
566
- value={dateInputEnd}
567
- onChange={(e) => setDateInputEnd(e.target.value)}
568
- className="w-full rounded-md border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
569
- />
570
- </div>
545
+ <DatePicker
546
+ embedded
547
+ isPeriod
548
+ startDate={dateInputStart}
549
+ endDate={dateInputEnd}
550
+ label="Période"
551
+ onDateChange={(start, end) => {
552
+ setDateInputStart(start);
553
+ if (end) setDateInputEnd(end);
554
+ }}
555
+ />
571
556
  <button
572
557
  onClick={() => {
573
558
  if (dateInputStart && dateInputEnd) {
@@ -91,7 +91,7 @@ export function SaveViewDialog({
91
91
  value={name}
92
92
  onChange={(e) => setName(e.target.value)}
93
93
  placeholder="Ex: Nouveaux contacts cette semaine"
94
- className="w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
94
+ className="w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
95
95
  />
96
96
  </div>
97
97
 
@@ -44,6 +44,8 @@ interface ViewsTabBarProps {
44
44
  hasUnsavedChanges?: boolean;
45
45
  activeFilters?: ViewFilter[];
46
46
  entityType?: 'contacts' | 'companies';
47
+ /** Quand true, pas de bordure ni padding (intégré sur une ligne avec le titre) */
48
+ inline?: boolean;
47
49
  }
48
50
 
49
51
  export type { ViewPermissions };
@@ -65,6 +67,7 @@ export function ViewsTabBar({
65
67
  hasUnsavedChanges,
66
68
  activeFilters,
67
69
  entityType = 'contacts',
70
+ inline = false,
68
71
  }: ViewsTabBarProps) {
69
72
  const [showAllViews, setShowAllViews] = useState(false);
70
73
  const [contextMenu, setContextMenu] = useState<string | null>(null);
@@ -120,12 +123,18 @@ export function ViewsTabBar({
120
123
  canEdit(view) || canDelete(view) || permissions.canCreate;
121
124
 
122
125
  return (
123
- <div className="flex items-center gap-1 border-b border-gray-200 bg-white px-4 sm:px-6 lg:px-8">
126
+ <div
127
+ className={cn(
128
+ 'flex min-w-0 items-center gap-1',
129
+ !inline && 'border-b border-gray-200 bg-white px-4 sm:px-6 lg:px-8',
130
+ inline && 'flex-1 overflow-x-clip',
131
+ )}
132
+ >
124
133
  {/* Vue "Tous les contacts" (défaut) */}
125
134
  <button
126
135
  onClick={() => onSelectView(null)}
127
136
  className={cn(
128
- 'relative flex shrink-0 cursor-pointer items-center gap-1.5 border-b-2 px-3 py-2.5 text-sm font-medium whitespace-nowrap transition-colors',
137
+ 'ui-tab-indicator relative flex shrink-0 cursor-pointer items-center gap-1.5 border-b-2 px-3 py-2.5 text-sm font-medium whitespace-nowrap',
129
138
  activeViewId === null
130
139
  ? 'border-blue-600 text-blue-600'
131
140
  : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700',
@@ -150,7 +159,7 @@ export function ViewsTabBar({
150
159
  <button
151
160
  onClick={() => onSelectView(view.id)}
152
161
  className={cn(
153
- 'relative flex cursor-pointer items-center gap-1.5 border-b-2 px-3 py-2.5 text-sm font-medium whitespace-nowrap transition-colors',
162
+ 'ui-tab-indicator relative flex cursor-pointer items-center gap-1.5 border-b-2 px-3 py-2.5 text-sm font-medium whitespace-nowrap',
154
163
  activeViewId === view.id
155
164
  ? 'border-blue-600 text-blue-600'
156
165
  : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700',
@@ -188,7 +197,7 @@ export function ViewsTabBar({
188
197
  {contextMenu === view.id && (
189
198
  <div
190
199
  ref={contextMenuRef}
191
- className="absolute top-full right-0 z-50 mt-1 w-48 rounded-lg border border-gray-200 bg-white py-1 shadow-lg"
200
+ className="ui-dropdown-enter absolute top-full right-0 z-50 mt-1 w-48 rounded-lg border border-gray-200 bg-white py-1 shadow-lg"
192
201
  >
193
202
  {canEdit(view) && (
194
203
  <button
@@ -289,7 +298,7 @@ export function ViewsTabBar({
289
298
  </button>
290
299
 
291
300
  {showAllViews && (
292
- <div className="absolute top-full left-0 z-50 mt-1 w-72 rounded-lg border border-gray-200 bg-white py-1 shadow-lg">
301
+ <div className="ui-dropdown-enter absolute top-full left-0 z-50 mt-1 w-72 rounded-lg border border-gray-200 bg-white py-1 shadow-lg">
293
302
  <div className="px-3 py-2 text-xs font-semibold tracking-wide text-gray-400 uppercase">
294
303
  Vues enregistrées
295
304
  </div>
@@ -338,7 +347,7 @@ export function ViewsTabBar({
338
347
  {contextMenu === view.id && (
339
348
  <div
340
349
  ref={contextMenuRef}
341
- className="absolute top-full right-0 z-50 mt-1 w-48 rounded-lg border border-gray-200 bg-white py-1 shadow-lg"
350
+ className="ui-dropdown-enter absolute top-full right-0 z-50 mt-1 w-48 rounded-lg border border-gray-200 bg-white py-1 shadow-lg"
342
351
  >
343
352
  {canEdit(view) && (
344
353
  <button
@@ -1,56 +1,69 @@
1
1
  'use client';
2
2
 
3
- import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis, Legend } from 'recharts';
3
+ import {
4
+ Bar,
5
+ BarChart,
6
+ ResponsiveContainer,
7
+ Tooltip,
8
+ XAxis,
9
+ YAxis,
10
+ CartesianGrid,
11
+ Legend,
12
+ } from 'recharts';
13
+ import { useDashboardTheme } from '@/contexts/dashboard-theme-context';
4
14
 
5
15
  interface ActivityChartProps {
6
- data: Array<{ date: string; interactions: number; tasks: number }>;
16
+ readonly data: Array<{ date: string; interactions: number; tasks: number }>;
7
17
  }
8
18
 
9
- export function ActivityChart({ data }: ActivityChartProps) {
19
+ export function ActivityChart({ data }: Readonly<ActivityChartProps>) {
20
+ const { theme } = useDashboardTheme();
21
+
10
22
  return (
11
- <div className="rounded-xl border border-gray-200/50 bg-white p-6 shadow-lg transition-shadow duration-300 hover:shadow-xl">
23
+ <div className="flex h-full flex-col rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
12
24
  <div className="mb-4">
13
- <h3 className="text-lg font-bold text-gray-900">Activité des 7 Derniers Jours</h3>
14
- <p className="mt-1 text-sm text-gray-500">Interactions et tâches créées</p>
25
+ <h3 className="text-base font-semibold text-gray-900">Activité des 7 Derniers Jours</h3>
26
+ <p className="mt-0.5 text-xs text-gray-400">Interactions et tâches créées</p>
15
27
  </div>
16
- <div className="h-[300px]">
28
+ <div className="min-h-0 flex-1">
17
29
  <ResponsiveContainer width="100%" height="100%">
18
- <BarChart data={data} barCategoryGap="20%">
19
- <defs>
20
- <linearGradient id="interactionsGradient" x1="0" y1="0" x2="0" y2="1">
21
- <stop offset="0%" stopColor="#6366f1" stopOpacity={1} />
22
- <stop offset="100%" stopColor="#4f46e5" stopOpacity={0.8} />
23
- </linearGradient>
24
- <linearGradient id="tasksGradient" x1="0" y1="0" x2="0" y2="1">
25
- <stop offset="0%" stopColor="#8b5cf6" stopOpacity={1} />
26
- <stop offset="100%" stopColor="#7c3aed" stopOpacity={0.8} />
27
- </linearGradient>
28
- </defs>
30
+ <BarChart
31
+ data={data}
32
+ barCategoryGap="25%"
33
+ margin={{ top: 5, right: 5, left: -20, bottom: 0 }}
34
+ >
35
+ <CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" vertical={false} />
29
36
  <XAxis
30
37
  dataKey="date"
31
- stroke="#9ca3af"
32
- fontSize={12}
38
+ stroke="#d1d5db"
39
+ fontSize={11}
33
40
  tickLine={false}
34
41
  axisLine={false}
42
+ dy={8}
35
43
  />
36
- <YAxis stroke="#9ca3af" fontSize={12} tickLine={false} axisLine={false} />
44
+ <YAxis stroke="#d1d5db" fontSize={11} tickLine={false} axisLine={false} width={40} />
37
45
  <Tooltip
38
46
  contentStyle={{
39
- backgroundColor: 'white',
40
- border: '1px solid #e5e7eb',
47
+ backgroundColor: '#fff',
48
+ border: '1px solid #f3f4f6',
41
49
  borderRadius: '12px',
42
- boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
50
+ boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
51
+ fontSize: '13px',
43
52
  }}
44
53
  labelStyle={{ color: '#374151', fontWeight: 600 }}
45
54
  />
46
- <Legend wrapperStyle={{ paddingTop: '20px' }} iconType="circle" />
55
+ <Legend
56
+ wrapperStyle={{ paddingTop: '12px', fontSize: '12px' }}
57
+ iconType="circle"
58
+ iconSize={8}
59
+ />
47
60
  <Bar
48
61
  dataKey="interactions"
49
- fill="url(#interactionsGradient)"
50
- radius={[8, 8, 0, 0]}
62
+ fill={theme.hex[500]}
63
+ radius={[6, 6, 0, 0]}
51
64
  name="Interactions"
52
65
  />
53
- <Bar dataKey="tasks" fill="url(#tasksGradient)" radius={[8, 8, 0, 0]} name="Tâches" />
66
+ <Bar dataKey="tasks" fill={theme.hex[300]} radius={[6, 6, 0, 0]} name="Tâches" />
54
67
  </BarChart>
55
68
  </ResponsiveContainer>
56
69
  </div>
@@ -0,0 +1,157 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { X, Plus, BarChart3, ListTodo, TrendingUp } from 'lucide-react';
5
+ import { cn } from '@/lib/utils';
6
+ import { WIDGET_REGISTRY, type WidgetDefinition } from '@/lib/widget-registry';
7
+
8
+ interface AddWidgetDialogProps {
9
+ readonly isOpen: boolean;
10
+ readonly onClose: () => void;
11
+ readonly onAdd: (type: string, w: number, h: number) => void;
12
+ readonly existingTypes: string[];
13
+ }
14
+
15
+ const CATEGORY_LABELS: Record<string, { label: string; icon: typeof BarChart3 }> = {
16
+ stat: { label: 'Statistiques', icon: TrendingUp },
17
+ chart: { label: 'Graphiques', icon: BarChart3 },
18
+ list: { label: 'Listes', icon: ListTodo },
19
+ };
20
+
21
+ export function AddWidgetDialog({ isOpen, onClose, onAdd, existingTypes }: AddWidgetDialogProps) {
22
+ const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
23
+
24
+ if (!isOpen) return null;
25
+
26
+ const categories = ['stat', 'chart', 'list'] as const;
27
+
28
+ const filteredWidgets = selectedCategory
29
+ ? WIDGET_REGISTRY.filter((w) => w.category === selectedCategory)
30
+ : WIDGET_REGISTRY;
31
+
32
+ const handleAdd = (widget: WidgetDefinition) => {
33
+ onAdd(widget.type, widget.defaultW, widget.defaultH);
34
+ };
35
+
36
+ return (
37
+ <div className="ui-fade-in fixed inset-0 z-50 flex items-center justify-center">
38
+ <button
39
+ type="button"
40
+ className="absolute inset-0 cursor-pointer bg-black/40 backdrop-blur-sm"
41
+ onClick={onClose}
42
+ aria-label="Fermer la fenêtre"
43
+ />
44
+
45
+ <div className="ui-scale-in relative z-10 w-full max-w-2xl rounded-2xl bg-white shadow-2xl">
46
+ <div className="flex items-center justify-between border-b border-gray-100 px-6 py-4">
47
+ <div>
48
+ <h2 className="text-lg font-semibold text-gray-900">Ajouter un Widget</h2>
49
+ <p className="mt-0.5 text-sm text-gray-500">
50
+ Choisissez un widget à ajouter au tableau de bord
51
+ </p>
52
+ </div>
53
+ <button
54
+ type="button"
55
+ onClick={onClose}
56
+ aria-label="Fermer"
57
+ className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
58
+ >
59
+ <X className="h-4 w-4" />
60
+ </button>
61
+ </div>
62
+
63
+ <div className="flex gap-2 border-b border-gray-100 px-6 py-3">
64
+ <button
65
+ type="button"
66
+ onClick={() => setSelectedCategory(null)}
67
+ className={cn(
68
+ 'cursor-pointer rounded-full px-3.5 py-1.5 text-xs font-medium transition-colors',
69
+ selectedCategory === null
70
+ ? 'dash-pill-active'
71
+ : 'bg-gray-100 text-gray-600 hover:bg-gray-200',
72
+ )}
73
+ >
74
+ Tous
75
+ </button>
76
+ {categories.map((cat) => {
77
+ const { label, icon: Icon } = CATEGORY_LABELS[cat];
78
+ return (
79
+ <button
80
+ type="button"
81
+ key={cat}
82
+ onClick={() => setSelectedCategory(cat)}
83
+ className={cn(
84
+ 'flex cursor-pointer items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs font-medium transition-colors',
85
+ selectedCategory === cat
86
+ ? 'dash-pill-active'
87
+ : 'bg-gray-100 text-gray-600 hover:bg-gray-200',
88
+ )}
89
+ >
90
+ <Icon className="h-3 w-3" />
91
+ {label}
92
+ </button>
93
+ );
94
+ })}
95
+ </div>
96
+
97
+ <div className="max-h-[400px] overflow-y-auto p-6">
98
+ <div className="grid grid-cols-2 gap-3">
99
+ {filteredWidgets.map((widget) => {
100
+ const isAlreadyAdded = existingTypes.includes(widget.type);
101
+ const Icon = widget.icon;
102
+
103
+ return (
104
+ <button
105
+ type="button"
106
+ key={widget.type}
107
+ onClick={() => !isAlreadyAdded && handleAdd(widget)}
108
+ disabled={isAlreadyAdded}
109
+ className={cn(
110
+ 'group flex items-start gap-3 rounded-xl border p-4 text-left transition-[color,background-color,border-color,box-shadow] duration-150',
111
+ isAlreadyAdded
112
+ ? 'cursor-not-allowed border-gray-100 bg-gray-50 opacity-50'
113
+ : 'dash-hover-border dash-hover-bg border-gray-100 bg-white hover:shadow-sm',
114
+ )}
115
+ >
116
+ <div
117
+ className={cn(
118
+ 'flex h-9 w-9 shrink-0 items-center justify-center rounded-lg',
119
+ isAlreadyAdded ? 'bg-gray-100' : 'dash-icon-box dash-icon-box-gh',
120
+ )}
121
+ >
122
+ <Icon
123
+ className={cn(
124
+ 'h-4 w-4',
125
+ isAlreadyAdded ? 'text-gray-400' : 'dash-icon-color',
126
+ )}
127
+ />
128
+ </div>
129
+ <div className="min-w-0 flex-1">
130
+ <div className="flex items-center justify-between">
131
+ <p className="text-sm font-medium text-gray-900">{widget.label}</p>
132
+ {isAlreadyAdded ? (
133
+ <span className="text-[10px] font-medium text-gray-400">Déjà ajouté</span>
134
+ ) : (
135
+ <Plus className="dash-hover-text-gh h-4 w-4 text-gray-300 transition-colors" />
136
+ )}
137
+ </div>
138
+ <p className="mt-0.5 text-xs text-gray-500">{widget.description}</p>
139
+ <p className="mt-1 text-[10px] text-gray-400">
140
+ Taille : {widget.defaultW}x{widget.defaultH}
141
+ </p>
142
+ </div>
143
+ </button>
144
+ );
145
+ })}
146
+ </div>
147
+ </div>
148
+
149
+ <div className="border-t border-gray-100 px-6 py-3">
150
+ <p className="text-center text-xs text-gray-400">
151
+ Les widgets peuvent être déplacés et redimensionnés après ajout
152
+ </p>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ );
157
+ }
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef, useEffect } from 'react';
4
+ import { Palette, Check } from 'lucide-react';
5
+ import { useDashboardTheme } from '@/contexts/dashboard-theme-context';
6
+
7
+ export function DashboardColorPicker() {
8
+ const { theme, setThemeKey, themes } = useDashboardTheme();
9
+ const [isOpen, setIsOpen] = useState(false);
10
+ const ref = useRef<HTMLDivElement>(null);
11
+
12
+ useEffect(() => {
13
+ function handleClickOutside(event: MouseEvent) {
14
+ if (ref.current && !ref.current.contains(event.target as Node)) {
15
+ setIsOpen(false);
16
+ }
17
+ }
18
+ if (isOpen) {
19
+ document.addEventListener('mousedown', handleClickOutside);
20
+ return () => document.removeEventListener('mousedown', handleClickOutside);
21
+ }
22
+ }, [isOpen]);
23
+
24
+ return (
25
+ <div ref={ref} className="relative">
26
+ <button
27
+ onClick={() => setIsOpen(!isOpen)}
28
+ className="inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-xl border border-gray-200 bg-white shadow-sm transition-[background-color,box-shadow,transform] duration-150 hover:bg-gray-50 hover:shadow-md active:scale-[0.98]"
29
+ title="Changer la couleur du thème"
30
+ aria-label="Changer la couleur du thème"
31
+ >
32
+ <Palette aria-hidden="true" className="h-4 w-4 text-gray-600" />
33
+ </button>
34
+
35
+ {isOpen && (
36
+ <div className="absolute right-0 z-50 mt-2 w-56 rounded-xl border border-gray-100 bg-white p-3 shadow-xl">
37
+ <p className="mb-2.5 text-[11px] font-medium tracking-wider text-gray-400 uppercase">
38
+ Couleur d&apos;accent
39
+ </p>
40
+ <div className="grid grid-cols-4 gap-2">
41
+ {themes.map((t) => (
42
+ <button
43
+ key={t.key}
44
+ onClick={() => {
45
+ setThemeKey(t.key);
46
+ setIsOpen(false);
47
+ }}
48
+ className="group relative flex h-10 w-10 cursor-pointer items-center justify-center rounded-xl transition-transform duration-150 hover:scale-110"
49
+ style={{ backgroundColor: t.hex[500] }}
50
+ title={t.label}
51
+ aria-label={t.label}
52
+ >
53
+ {theme.key === t.key && <Check aria-hidden="true" className="h-4 w-4 text-white drop-shadow-sm" />}
54
+ <span className="absolute -bottom-5 left-1/2 -translate-x-1/2 text-[10px] whitespace-nowrap text-gray-500 opacity-0 transition-opacity group-hover:opacity-100">
55
+ {t.label}
56
+ </span>
57
+ </button>
58
+ ))}
59
+ </div>
60
+ </div>
61
+ )}
62
+ </div>
63
+ );
64
+ }