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
@@ -0,0 +1,113 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { Users } from 'lucide-react';
5
+
6
+ interface Contact {
7
+ id: string;
8
+ name: string | null;
9
+ phone: string | null;
10
+ email: string | null;
11
+ status: string;
12
+ interactionsCount: number;
13
+ assignedCommercial?: string;
14
+ assignedTelepro?: string;
15
+ }
16
+
17
+ interface TopContactsListProps {
18
+ readonly contacts: Contact[];
19
+ }
20
+
21
+ export function TopContactsList({ contacts }: TopContactsListProps) {
22
+ if (contacts.length === 0) {
23
+ return (
24
+ <div className="flex h-full flex-col rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
25
+ <div className="flex items-center justify-between">
26
+ <div>
27
+ <h3 className="text-base font-semibold text-gray-900">Derniers Prospects</h3>
28
+ <p className="mt-0.5 text-xs text-gray-400">Contacts récemment ajoutés</p>
29
+ </div>
30
+ <Link href="/contacts" className="dash-link cursor-pointer text-xs font-medium">
31
+ Voir tout →
32
+ </Link>
33
+ </div>
34
+ <div className="flex flex-1 items-center justify-center">
35
+ <div className="text-center">
36
+ <Users className="mx-auto h-10 w-10 text-gray-200" />
37
+ <p className="mt-2 text-sm text-gray-400">Aucun contact</p>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ );
42
+ }
43
+
44
+ return (
45
+ <div className="flex h-full flex-col rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
46
+ <div className="mb-4 flex items-center justify-between">
47
+ <div>
48
+ <h3 className="text-base font-semibold text-gray-900">Derniers Prospects</h3>
49
+ <p className="mt-0.5 text-xs text-gray-400">Contacts récemment ajoutés</p>
50
+ </div>
51
+ <Link href="/contacts" className="dash-link cursor-pointer text-xs font-medium">
52
+ Voir tout →
53
+ </Link>
54
+ </div>
55
+ <div className="flex-1 overflow-auto">
56
+ <table className="w-full">
57
+ <thead>
58
+ <tr className="border-b border-gray-100">
59
+ <th className="pb-2.5 text-left text-[11px] font-medium tracking-wider text-gray-400 uppercase">
60
+ Prospect
61
+ </th>
62
+ <th className="pb-2.5 text-left text-[11px] font-medium tracking-wider text-gray-400 uppercase">
63
+ Email
64
+ </th>
65
+ <th className="pb-2.5 text-left text-[11px] font-medium tracking-wider text-gray-400 uppercase">
66
+ Télépro
67
+ </th>
68
+ <th className="pb-2.5 text-left text-[11px] font-medium tracking-wider text-gray-400 uppercase">
69
+ Statut
70
+ </th>
71
+ </tr>
72
+ </thead>
73
+ <tbody>
74
+ {contacts.map((contact) => {
75
+ const nameParts = (contact.name ?? '').split(' ');
76
+ const firstName = nameParts[0] || '';
77
+ const lastName = nameParts.slice(1).join(' ') || '';
78
+ const initials = `${firstName[0] || ''}${lastName[0] || ''}`.toUpperCase();
79
+
80
+ return (
81
+ <tr
82
+ key={contact.id}
83
+ className="dash-hover-bg border-b border-gray-50 transition-colors"
84
+ >
85
+ <td className="py-3">
86
+ <div className="flex items-center gap-2.5">
87
+ <div className="dash-avatar flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold">
88
+ {initials || '?'}
89
+ </div>
90
+ <Link
91
+ href={`/contacts/${contact.id}`}
92
+ className="dash-hover-text cursor-pointer text-sm font-medium text-gray-900 transition-colors"
93
+ >
94
+ {contact.name || 'Sans nom'}
95
+ </Link>
96
+ </div>
97
+ </td>
98
+ <td className="py-3 text-xs text-gray-500">{contact.email || '-'}</td>
99
+ <td className="py-3 text-xs text-gray-500">{contact.assignedTelepro || '-'}</td>
100
+ <td className="py-3">
101
+ <span className="inline-flex rounded-full bg-gray-100 px-2 py-0.5 text-[10px] font-medium text-gray-700">
102
+ {contact.status}
103
+ </span>
104
+ </td>
105
+ </tr>
106
+ );
107
+ })}
108
+ </tbody>
109
+ </table>
110
+ </div>
111
+ </div>
112
+ );
113
+ }
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import Link from 'next/link';
4
- import { Calendar, Phone, Video, Mail, CheckCircle2, Clock } from 'lucide-react';
4
+ import { Calendar, Phone, Video, Mail, CheckCircle2, Clock, ListTodo } from 'lucide-react';
5
+ import { cn } from '@/lib/utils';
5
6
 
6
7
  interface Task {
7
8
  id: string;
@@ -13,25 +14,26 @@ interface Task {
13
14
  }
14
15
 
15
16
  interface UpcomingTasksListProps {
16
- tasks: Task[];
17
+ readonly tasks: Task[];
17
18
  }
18
19
 
19
- const taskIcons = {
20
+ const taskIcons: Record<string, typeof Phone> = {
20
21
  CALL: Phone,
21
22
  MEETING: Calendar,
22
23
  EMAIL: Mail,
23
24
  VIDEO_CONFERENCE: Video,
25
+ TASK: ListTodo,
24
26
  OTHER: CheckCircle2,
25
27
  };
26
28
 
27
- const priorityColors = {
28
- LOW: 'text-gray-500 bg-gray-100',
29
- MEDIUM: 'text-blue-600 bg-blue-100',
30
- HIGH: 'text-orange-600 bg-orange-100',
31
- URGENT: 'text-red-600 bg-red-100',
29
+ const priorityStyles: Record<string, string> = {
30
+ LOW: 'bg-gray-100 text-gray-600',
31
+ MEDIUM: 'bg-blue-50 text-blue-600',
32
+ HIGH: 'bg-orange-50 text-orange-600',
33
+ URGENT: 'bg-red-50 text-red-600',
32
34
  };
33
35
 
34
- const priorityLabels = {
36
+ const priorityLabels: Record<string, string> = {
35
37
  LOW: 'Basse',
36
38
  MEDIUM: 'Moyenne',
37
39
  HIGH: 'Haute',
@@ -39,87 +41,76 @@ const priorityLabels = {
39
41
  };
40
42
 
41
43
  export function UpcomingTasksList({ tasks }: UpcomingTasksListProps) {
42
- if (tasks.length === 0) {
43
- return (
44
- <div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
45
- <div className="flex items-center justify-between">
46
- <h3 className="text-lg font-semibold text-gray-900">Tâches à Venir</h3>
47
- <Link
48
- href="/agenda"
49
- className="text-sm font-medium text-blue-600 hover:text-blue-700"
50
- >
51
- Voir tout
52
- </Link>
53
- </div>
54
- <div className="mt-6 text-center text-sm text-gray-500">
55
- <Clock className="mx-auto h-12 w-12 text-gray-400" />
56
- <p className="mt-2">Aucune tâche à venir</p>
57
- </div>
58
- </div>
59
- );
60
- }
61
-
62
44
  return (
63
- <div className="rounded-xl border border-gray-200/50 bg-white p-6 shadow-lg transition-shadow duration-300 hover:shadow-xl">
45
+ <div className="flex h-full flex-col rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
64
46
  <div className="mb-4 flex items-center justify-between">
65
- <h3 className="text-lg font-bold text-gray-900">Liste des Tâches</h3>
66
- <Link
67
- href="/agenda"
68
- className="text-sm font-semibold text-blue-600 transition-colors hover:text-blue-700"
69
- >
47
+ <div>
48
+ <h3 className="text-base font-semibold text-gray-900">Tâches à Venir</h3>
49
+ <p className="mt-0.5 text-xs text-gray-400">Prochains rendez-vous et tâches</p>
50
+ </div>
51
+ <Link href="/agenda" className="dash-link cursor-pointer text-xs font-medium">
70
52
  Voir tout →
71
53
  </Link>
72
54
  </div>
73
- <div className="space-y-3">
74
- {tasks.map((task) => {
75
- const Icon = taskIcons[task.type as keyof typeof taskIcons] || CheckCircle2;
76
- const scheduledDate = new Date(task.scheduledAt);
77
55
 
78
- return (
79
- <div
80
- key={task.id}
81
- className="group flex items-start gap-3 rounded-lg border border-gray-100 bg-gray-50/50 p-3 transition-all duration-200 hover:border-blue-200 hover:bg-blue-50/30"
82
- >
83
- <input
84
- type="checkbox"
85
- className="mt-1 h-4 w-4 cursor-pointer rounded border-gray-300 text-blue-600 focus:ring-2 focus:ring-gray-400/30"
86
- />
87
- <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-linear-to-br from-blue-100 to-purple-100">
88
- <Icon className="h-4 w-4 text-blue-600" />
89
- </div>
90
- <div className="flex-1">
91
- <div className="flex items-start justify-between">
92
- <div>
93
- <p className="font-semibold text-gray-900">{task.title}</p>
94
- {task.contact && (
95
- <Link
96
- href={`/contacts/${task.contact.id}`}
97
- className="text-sm text-gray-600 transition-colors hover:text-blue-600"
98
- >
99
- {task.contact.name}
100
- </Link>
101
- )}
56
+ {tasks.length === 0 ? (
57
+ <div className="flex flex-1 items-center justify-center">
58
+ <div className="text-center">
59
+ <Clock className="mx-auto h-10 w-10 text-gray-200" />
60
+ <p className="mt-2 text-sm text-gray-400">Aucune tâche à venir</p>
61
+ </div>
62
+ </div>
63
+ ) : (
64
+ <div className="flex-1 space-y-2 overflow-auto">
65
+ {tasks.map((task) => {
66
+ const Icon = taskIcons[task.type] || CheckCircle2;
67
+ const scheduledDate = new Date(task.scheduledAt);
68
+
69
+ return (
70
+ <div
71
+ key={task.id}
72
+ className="group dash-hover-border-light dash-hover-bg flex items-start gap-3 rounded-xl border border-gray-50 bg-gray-50/50 p-3 transition-colors duration-150"
73
+ >
74
+ <div className="dash-icon-box flex h-8 w-8 shrink-0 items-center justify-center rounded-lg">
75
+ <Icon className="dash-icon-color h-3.5 w-3.5" />
76
+ </div>
77
+ <div className="min-w-0 flex-1">
78
+ <div className="flex items-start justify-between gap-2">
79
+ <p className="truncate text-sm font-medium text-gray-900">{task.title}</p>
80
+ <span
81
+ className={cn(
82
+ 'shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium',
83
+ priorityStyles[task.priority] || 'bg-gray-100 text-gray-600',
84
+ )}
85
+ >
86
+ {priorityLabels[task.priority] || task.priority}
87
+ </span>
102
88
  </div>
103
- <button className="cursor-pointer opacity-0 transition-opacity group-hover:opacity-100 hover:opacity-100">
104
- <span className="text-gray-400">⋯</span>
105
- </button>
89
+ {task.contact && (
90
+ <Link
91
+ href={`/contacts/${task.contact.id}`}
92
+ className="dash-hover-text cursor-pointer text-xs text-gray-500 transition-colors"
93
+ >
94
+ {task.contact.name}
95
+ </Link>
96
+ )}
97
+ <p className="mt-1 text-[11px] text-gray-400">
98
+ {scheduledDate.toLocaleDateString('fr-FR', {
99
+ day: 'numeric',
100
+ month: 'long',
101
+ })}{' '}
102
+ à{' '}
103
+ {scheduledDate.toLocaleTimeString('fr-FR', {
104
+ hour: '2-digit',
105
+ minute: '2-digit',
106
+ })}
107
+ </p>
106
108
  </div>
107
- <p className="mt-1 text-xs text-gray-500">
108
- {scheduledDate.toLocaleDateString('fr-FR', {
109
- day: 'numeric',
110
- month: 'long',
111
- })}{' '}
112
- à{' '}
113
- {scheduledDate.toLocaleTimeString('fr-FR', {
114
- hour: '2-digit',
115
- minute: '2-digit',
116
- })}
117
- </p>
118
109
  </div>
119
- </div>
120
- );
121
- })}
122
- </div>
110
+ );
111
+ })}
112
+ </div>
113
+ )}
123
114
  </div>
124
115
  );
125
116
  }
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+
3
+ import { X, GripVertical } from 'lucide-react';
4
+ import { cn } from '@/lib/utils';
5
+
6
+ interface WidgetWrapperProps {
7
+ readonly children: React.ReactNode;
8
+ readonly onRemove?: () => void;
9
+ readonly className?: string;
10
+ }
11
+
12
+ export function WidgetWrapper({ children, onRemove, className }: WidgetWrapperProps) {
13
+ return (
14
+ <div className={cn('group/widget relative h-full w-full', className)}>
15
+ {onRemove && (
16
+ <div className="absolute top-0 right-0 z-10 flex items-center gap-1 opacity-0 transition-[opacity,transform] duration-(--duration-normal) ease-(--ease-standard) group-hover/widget:opacity-100">
17
+ <div className="drag-handle flex h-7 w-7 cursor-grab items-center justify-center rounded-lg bg-white/90 shadow-sm ring-1 ring-gray-200/60 backdrop-blur-sm transition-colors hover:bg-gray-50 active:cursor-grabbing">
18
+ <GripVertical className="h-3.5 w-3.5 text-gray-400" />
19
+ </div>
20
+ <button
21
+ type="button"
22
+ onClick={(e) => {
23
+ e.stopPropagation();
24
+ onRemove();
25
+ }}
26
+ aria-label="Supprimer le widget"
27
+ className="flex h-7 w-7 cursor-pointer items-center justify-center rounded-lg bg-white/90 shadow-sm ring-1 ring-gray-200/60 backdrop-blur-sm transition-colors hover:bg-red-50 hover:text-red-600"
28
+ >
29
+ <X className="h-3.5 w-3.5" />
30
+ </button>
31
+ </div>
32
+ )}
33
+ {children}
34
+ </div>
35
+ );
36
+ }
@@ -235,7 +235,7 @@ export const DatePicker: React.FC<DatePickerProps> = ({
235
235
  <div
236
236
  ref={dropdownRef}
237
237
  className={cn(
238
- 'fixed z-[9999] transition-all duration-200',
238
+ 'fixed z-[9999] transition-opacity duration-200',
239
239
  'left-1/2 -translate-x-1/2',
240
240
  isDesktopView && 'top-1/2 -translate-y-1/2',
241
241
  )}
@@ -272,9 +272,10 @@ export const DatePicker: React.FC<DatePickerProps> = ({
272
272
  </div>
273
273
  <button
274
274
  onClick={handleClose}
275
+ aria-label="Fermer"
275
276
  className="ml-2 shrink-0 cursor-pointer rounded-lg p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
276
277
  >
277
- <X className="h-4 w-4" />
278
+ <X aria-hidden="true" className="h-4 w-4" />
278
279
  </button>
279
280
  </div>
280
281
 
@@ -283,24 +284,26 @@ export const DatePicker: React.FC<DatePickerProps> = ({
283
284
  <div className="mb-2 flex items-center justify-between">
284
285
  <button
285
286
  type="button"
287
+ aria-label="Mois precedent"
286
288
  onClick={() =>
287
289
  setCurrentMonth(new Date(currentMonth.setMonth(currentMonth.getMonth() - 1)))
288
290
  }
289
291
  className="shrink-0 cursor-pointer rounded-lg p-1.5 text-gray-600 transition-colors hover:bg-gray-100"
290
292
  >
291
- <ChevronLeft className="h-4 w-4" />
293
+ <ChevronLeft aria-hidden="true" className="h-4 w-4" />
292
294
  </button>
293
295
  <h3 className="min-w-0 flex-1 truncate text-center text-xs font-semibold text-gray-900 sm:text-sm">
294
296
  {currentMonth.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })}
295
297
  </h3>
296
298
  <button
297
299
  type="button"
300
+ aria-label="Mois suivant"
298
301
  onClick={() =>
299
302
  setCurrentMonth(new Date(currentMonth.setMonth(currentMonth.getMonth() + 1)))
300
303
  }
301
304
  className="shrink-0 cursor-pointer rounded-lg p-1.5 text-gray-600 transition-colors hover:bg-gray-100"
302
305
  >
303
- <ChevronRight className="h-4 w-4" />
306
+ <ChevronRight aria-hidden="true" className="h-4 w-4" />
304
307
  </button>
305
308
  </div>
306
309
 
@@ -329,7 +332,7 @@ export const DatePicker: React.FC<DatePickerProps> = ({
329
332
  type="button"
330
333
  onClick={() => handleDateClick(date)}
331
334
  className={cn(
332
- 'aspect-square cursor-pointer rounded-md text-[10px] font-medium transition-all sm:text-xs',
335
+ 'aspect-square cursor-pointer rounded-md text-[10px] font-medium transition-colors sm:text-xs',
333
336
  selected &&
334
337
  'z-10 bg-blue-600 text-white shadow-md shadow-blue-500/30 hover:bg-blue-700',
335
338
  inRange && !selected && 'bg-blue-50 text-blue-700 hover:bg-blue-100',
@@ -383,7 +386,7 @@ export const DatePicker: React.FC<DatePickerProps> = ({
383
386
  ref={buttonRef}
384
387
  type="button"
385
388
  onClick={() => setIsOpen(!isOpen)}
386
- className="flex w-full cursor-pointer items-center justify-between rounded-xl border border-gray-300 bg-white px-4 py-2.5 text-left text-sm shadow-sm transition-colors hover:border-blue-400 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
389
+ className="flex w-full cursor-pointer items-center justify-between rounded-xl border border-gray-300 bg-white px-4 py-2.5 text-left text-sm shadow-sm transition-colors hover:border-blue-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
387
390
  >
388
391
  <div className="flex items-center gap-2">
389
392
  <Calendar className="h-4 w-4 text-gray-400" />
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Upload une image vers Supabase Storage via l'API /api/editor/upload-image.
3
+ * Retourne l'URL publique persistante de l'image.
4
+ */
5
+ export async function uploadEditorImage(file: File | Blob, fileName?: string): Promise<string> {
6
+ const name = fileName || (file instanceof File ? file.name : 'image.png');
7
+ const mimeType = file.type || 'image/png';
8
+
9
+ // 1. Obtenir une URL d'upload signée
10
+ const res = await fetch('/api/editor/upload-image', {
11
+ method: 'POST',
12
+ headers: { 'Content-Type': 'application/json' },
13
+ body: JSON.stringify({
14
+ fileName: name,
15
+ fileSize: file.size,
16
+ mimeType,
17
+ }),
18
+ });
19
+
20
+ if (!res.ok) {
21
+ const err = await res.json().catch(() => ({ error: 'Erreur inconnue' }));
22
+ throw new Error(err.error || "Erreur lors de la préparation de l'upload");
23
+ }
24
+
25
+ const { signedUrl, token, publicUrl } = await res.json();
26
+
27
+ // 2. Upload direct vers Supabase Storage
28
+ const uploadRes = await fetch(signedUrl, {
29
+ method: 'PUT',
30
+ headers: {
31
+ 'Content-Type': mimeType,
32
+ Authorization: `Bearer ${token}`,
33
+ },
34
+ body: file,
35
+ });
36
+
37
+ if (!uploadRes.ok) {
38
+ throw new Error("Erreur lors de l'upload de l'image");
39
+ }
40
+
41
+ return publicUrl;
42
+ }