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,124 @@
1
+ 'use client';
2
+
3
+ import { X } from 'lucide-react';
4
+ import { cn } from '@/lib/utils';
5
+
6
+ export interface ImportResultItem {
7
+ configId: string;
8
+ configName: string;
9
+ imported: number;
10
+ updated?: number;
11
+ skipped?: number;
12
+ duplicates?: number;
13
+ error?: string;
14
+ }
15
+
16
+ interface ImportResultDialogProps {
17
+ open: boolean;
18
+ onClose: () => void;
19
+ configName?: string;
20
+ results: ImportResultItem[];
21
+ totalImported?: number;
22
+ totalUpdated?: number;
23
+ totalSkipped?: number;
24
+ }
25
+
26
+ export function ImportResultDialog({
27
+ open,
28
+ onClose,
29
+ configName,
30
+ results,
31
+ totalImported = 0,
32
+ totalUpdated = 0,
33
+ totalSkipped = 0,
34
+ }: Readonly<ImportResultDialogProps>) {
35
+ if (!open) return null;
36
+
37
+ const hasSingleResult = results.length === 1;
38
+ const single = hasSingleResult ? results[0] : null;
39
+ const totalDuplicates = results.reduce((acc, r) => acc + (r.duplicates ?? 0), 0);
40
+ const totalErrors = results.reduce((acc, r) => acc + (r.error ? 1 : 0), 0);
41
+
42
+ const imported = hasSingleResult && single ? single.imported : totalImported;
43
+ const duplicates = hasSingleResult && single ? (single.duplicates ?? 0) : totalDuplicates;
44
+ const updated = hasSingleResult && single ? (single.updated ?? 0) : totalUpdated;
45
+ let ignoredOrErrors: number;
46
+ if (hasSingleResult && single) {
47
+ ignoredOrErrors = (single.skipped ?? 0) + (single.error ? 1 : 0);
48
+ } else {
49
+ ignoredOrErrors = totalSkipped + totalErrors;
50
+ }
51
+
52
+ return (
53
+ <div className="fixed inset-0 z-50 flex min-h-dvh items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm">
54
+ <div className="w-full max-w-md overflow-hidden rounded-lg bg-white shadow-xl overscroll-contain">
55
+ <div className="flex items-center justify-between border-b border-gray-100 px-5 py-4">
56
+ <h2 className="text-lg font-bold text-gray-900">Résultat de l&apos;import</h2>
57
+ <button
58
+ type="button"
59
+ onClick={onClose}
60
+ className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
61
+ aria-label="Fermer"
62
+ >
63
+ <X className="h-5 w-5" />
64
+ </button>
65
+ </div>
66
+ <div className="px-5 py-4">
67
+ {configName && (
68
+ <p className="mb-4 text-sm text-gray-600">
69
+ Configuration : <span className="font-medium text-gray-900">{configName}</span>
70
+ </p>
71
+ )}
72
+ <div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-gray-700">
73
+ <span><strong className="text-gray-900">{imported}</strong> importés</span>
74
+ <span><strong className="text-gray-900">{duplicates}</strong> doublons</span>
75
+ <span><strong className="text-gray-900">{updated}</strong> mis à jour</span>
76
+ <span><strong className="text-gray-900">{ignoredOrErrors}</strong> ignorés / erreurs</span>
77
+ </div>
78
+ {hasSingleResult && single?.error && (
79
+ <div className="mt-4 rounded-lg border border-rose-200 bg-rose-50 px-3 py-2 text-sm text-rose-800">
80
+ {single.error}
81
+ </div>
82
+ )}
83
+ {!hasSingleResult && results.length > 0 && totalErrors > 0 && (
84
+ <p className="mt-4 text-sm text-red-600">
85
+ {totalErrors} configuration(s) en erreur
86
+ </p>
87
+ )}
88
+ {!hasSingleResult && results.length > 1 && (
89
+ <ul className="mt-4 space-y-1.5 border-t border-gray-100 pt-3 text-sm text-gray-600">
90
+ {results.map((r) => (
91
+ <li
92
+ key={r.configId}
93
+ className={cn(
94
+ 'flex justify-between gap-2',
95
+ r.error && 'text-red-600',
96
+ )}
97
+ >
98
+ <span className="font-medium text-gray-900">{r.configName}</span>
99
+ <span>
100
+ {r.imported} importés
101
+ {(r.duplicates ?? 0) > 0 && ` · ${r.duplicates} doublons`}
102
+ {r.error && ` · ${r.error}`}
103
+ </span>
104
+ </li>
105
+ ))}
106
+ </ul>
107
+ )}
108
+ {results.length === 0 && (
109
+ <p className="text-sm text-gray-500">Aucun résultat à afficher.</p>
110
+ )}
111
+ </div>
112
+ <div className="flex justify-end border-t border-gray-100 px-5 py-3">
113
+ <button
114
+ type="button"
115
+ onClick={onClose}
116
+ className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
117
+ >
118
+ Fermer
119
+ </button>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ );
124
+ }
@@ -0,0 +1,57 @@
1
+ 'use client';
2
+
3
+ import { X, FileText } from 'lucide-react';
4
+ import { IntegrationLogsTable, type IntegrationType } from './IntegrationLogsTable';
5
+
6
+ interface IntegrationLogPanelProps {
7
+ open: boolean;
8
+ onClose: () => void;
9
+ integrationType: IntegrationType;
10
+ configId?: string;
11
+ configName?: string;
12
+ }
13
+
14
+ const TYPE_LABELS: Record<string, string> = {
15
+ google_sheet: 'Google Sheets',
16
+ meta_lead: 'Meta Lead Ads',
17
+ google_ads: 'Google Ads',
18
+ };
19
+
20
+ export function IntegrationLogPanel({
21
+ open,
22
+ onClose,
23
+ integrationType,
24
+ configId,
25
+ configName,
26
+ }: IntegrationLogPanelProps) {
27
+ if (!open) return null;
28
+
29
+ return (
30
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 backdrop-blur-sm">
31
+ <div className="flex h-[85vh] w-full max-w-4xl flex-col rounded-lg border border-gray-200 bg-white shadow-xl overscroll-contain">
32
+ <div className="flex items-center justify-between border-b border-gray-100 px-5 py-4">
33
+ <div className="flex items-center gap-2">
34
+ <FileText className="h-5 w-5 text-gray-500" />
35
+ <h3 className="text-lg font-semibold text-gray-900">
36
+ Logs {TYPE_LABELS[integrationType] ?? integrationType}
37
+ {configName && (
38
+ <span className="ml-2 font-normal text-gray-600">— {configName}</span>
39
+ )}
40
+ </h3>
41
+ </div>
42
+ <button
43
+ type="button"
44
+ onClick={onClose}
45
+ className="rounded p-1.5 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
46
+ aria-label="Fermer"
47
+ >
48
+ <X className="h-5 w-5" />
49
+ </button>
50
+ </div>
51
+ <div className="flex-1 overflow-auto px-5 py-4">
52
+ <IntegrationLogsTable integrationType={integrationType} configId={configId} />
53
+ </div>
54
+ </div>
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,186 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useMemo, useState } from 'react';
4
+ import { RefreshCw } from 'lucide-react';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ export type IntegrationType = 'google_sheet' | 'meta_lead' | 'google_ads';
8
+
9
+ export interface IntegrationLogEntry {
10
+ id: string;
11
+ integrationType: string;
12
+ configId: string;
13
+ configName: string;
14
+ action: string;
15
+ actorId: string | null;
16
+ actor: { id: string; name: string; email: string } | null;
17
+ totalImported: number;
18
+ totalDuplicates: number;
19
+ totalUpdated: number;
20
+ totalErrors: number;
21
+ errorDetails: unknown;
22
+ createdAt: string;
23
+ }
24
+
25
+ const ACTION_LABELS: Record<string, string> = {
26
+ created: 'Création',
27
+ updated: 'Modification',
28
+ synced: 'Synchronisation',
29
+ synced_manual: 'Synchronisation manuelle',
30
+ synced_auto: 'Synchronisation automatique',
31
+ deleted: 'Suppression',
32
+ };
33
+
34
+ interface IntegrationLogsTableProps {
35
+ integrationType: IntegrationType;
36
+ configId?: string;
37
+ pageSize?: number;
38
+ enabled?: boolean;
39
+ className?: string;
40
+ }
41
+
42
+ export function IntegrationLogsTable({
43
+ integrationType,
44
+ configId,
45
+ pageSize = 20,
46
+ enabled = true,
47
+ className,
48
+ }: Readonly<IntegrationLogsTableProps>) {
49
+ const [logs, setLogs] = useState<IntegrationLogEntry[]>([]);
50
+ const [total, setTotal] = useState(0);
51
+ const [page, setPage] = useState(1);
52
+ const [loading, setLoading] = useState(false);
53
+
54
+ useEffect(() => {
55
+ if (!enabled) return;
56
+ setPage(1);
57
+ }, [enabled, integrationType, configId]);
58
+
59
+ useEffect(() => {
60
+ if (!enabled) return;
61
+ let cancelled = false;
62
+ setLoading(true);
63
+
64
+ const params = new URLSearchParams({
65
+ integrationType,
66
+ page: String(page),
67
+ pageSize: String(pageSize),
68
+ });
69
+ if (configId) params.set('configId', configId);
70
+
71
+ fetch(`/api/settings/integrations/logs?${params}`)
72
+ .then((res) => res.json())
73
+ .then((data) => {
74
+ if (cancelled) return;
75
+ setLogs(data.logs ?? []);
76
+ setTotal(data.total ?? 0);
77
+ })
78
+ .catch(() => {
79
+ if (!cancelled) setLogs([]);
80
+ })
81
+ .finally(() => {
82
+ if (!cancelled) setLoading(false);
83
+ });
84
+
85
+ return () => {
86
+ cancelled = true;
87
+ };
88
+ }, [enabled, integrationType, configId, page, pageSize]);
89
+
90
+ const totalPages = useMemo(() => Math.ceil(total / pageSize), [total, pageSize]);
91
+
92
+ if (!enabled) return null;
93
+
94
+ return (
95
+ <div className={cn('flex h-full flex-col', className)}>
96
+ <div className="flex-1 overflow-auto">
97
+ {loading ? (
98
+ <div className="flex items-center justify-center py-12">
99
+ <RefreshCw className="h-8 w-8 animate-spin text-gray-400" role="status" aria-label="Chargement" />
100
+ </div>
101
+ ) : logs.length === 0 ? (
102
+ <p className="py-12 text-center text-sm text-gray-500">Aucun log pour le moment.</p>
103
+ ) : (
104
+ <div className="overflow-x-auto rounded-xl border border-slate-200 bg-white/80">
105
+ <table className="w-full text-sm">
106
+ <thead>
107
+ <tr className="border-b border-slate-200 text-left text-[11px] font-semibold uppercase tracking-wide text-slate-500">
108
+ <th className="pb-2 pr-4 pl-4">Date / Heure</th>
109
+ <th className="pb-2 pr-4">Action</th>
110
+ <th className="pb-2 pr-4 text-right">Importés</th>
111
+ <th className="pb-2 pr-4 text-right">Doublons</th>
112
+ <th className="pb-2 pr-4 text-right">Mis à jour</th>
113
+ <th className="pb-2 pr-4 text-right">Échoués</th>
114
+ </tr>
115
+ </thead>
116
+ <tbody>
117
+ {logs.map((log) => (
118
+ <tr key={log.id} className="border-b border-slate-100 hover:bg-slate-50/70">
119
+ <td className="whitespace-nowrap py-2.5 pr-4 pl-4 text-slate-700">
120
+ {new Date(log.createdAt).toLocaleString('fr-FR', {
121
+ day: '2-digit',
122
+ month: '2-digit',
123
+ year: 'numeric',
124
+ hour: '2-digit',
125
+ minute: '2-digit',
126
+ })}
127
+ </td>
128
+ <td className="py-2.5 pr-4">
129
+ <span className="rounded-md border border-slate-200 bg-slate-100 px-2 py-0.5 text-slate-700">
130
+ {ACTION_LABELS[log.action] ?? log.action}
131
+ </span>
132
+ </td>
133
+ <td className="py-2.5 pr-4 text-right font-semibold text-slate-900">
134
+ {log.totalImported}
135
+ </td>
136
+ <td className="py-2.5 pr-4 text-right font-medium text-amber-600">
137
+ {log.totalDuplicates}
138
+ </td>
139
+ <td className="py-2.5 pr-4 text-right text-slate-700">{log.totalUpdated}</td>
140
+ <td
141
+ className={cn(
142
+ 'py-2.5 pr-4 text-right',
143
+ log.totalErrors > 0 ? 'font-medium text-rose-600' : 'text-slate-700',
144
+ )}
145
+ >
146
+ {log.totalErrors}
147
+ </td>
148
+ </tr>
149
+ ))}
150
+ </tbody>
151
+ </table>
152
+ </div>
153
+ )}
154
+ </div>
155
+
156
+ {totalPages > 1 && (
157
+ <div className="mt-3 flex items-center justify-between border-t border-slate-100 pt-3">
158
+ <p className="text-sm text-slate-500">{total} entrée(s)</p>
159
+ <div className="flex items-center gap-2">
160
+ <button
161
+ type="button"
162
+ aria-label="Page precedente"
163
+ onClick={() => setPage((p) => Math.max(1, p - 1))}
164
+ disabled={page <= 1}
165
+ className="rounded-lg border border-slate-200 px-3 py-1.5 text-sm text-slate-700 disabled:opacity-50"
166
+ >
167
+ Précédent
168
+ </button>
169
+ <span className="px-1 text-sm text-slate-600">
170
+ {page} / {totalPages}
171
+ </span>
172
+ <button
173
+ type="button"
174
+ aria-label="Page suivante"
175
+ onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
176
+ disabled={page >= totalPages}
177
+ className="rounded-lg border border-slate-200 px-3 py-1.5 text-sm text-slate-700 disabled:opacity-50"
178
+ >
179
+ Suivant
180
+ </button>
181
+ </div>
182
+ </div>
183
+ )}
184
+ </div>
185
+ );
186
+ }