create-crm-tmp 1.0.2 → 1.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 (73) hide show
  1. package/bin/create-crm-tmp.js +7 -3
  2. package/package.json +1 -1
  3. package/template/README.md +70 -5
  4. package/template/WORKFLOWS_CRON.md +49 -27
  5. package/template/package.json +18 -16
  6. package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +20 -0
  7. package/template/prisma/schema.prisma +17 -0
  8. package/template/src/app/(dashboard)/agenda/page.tsx +279 -225
  9. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +1 -5
  10. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +20 -47
  11. package/template/src/app/(dashboard)/automatisation/new/page.tsx +0 -2
  12. package/template/src/app/(dashboard)/closing/page.tsx +5 -57
  13. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +60 -44
  14. package/template/src/app/(dashboard)/contacts/page.tsx +156 -210
  15. package/template/src/app/(dashboard)/dashboard/page.tsx +438 -91
  16. package/template/src/app/(dashboard)/settings/page.tsx +179 -77
  17. package/template/src/app/(dashboard)/users/layout.tsx +30 -0
  18. package/template/src/app/(dashboard)/users/list/page.tsx +213 -159
  19. package/template/src/app/(dashboard)/users/page.tsx +13 -46
  20. package/template/src/app/(dashboard)/users/permissions/page.tsx +0 -2
  21. package/template/src/app/(dashboard)/users/roles/page.tsx +0 -2
  22. package/template/src/app/api/audit-logs/route.ts +0 -2
  23. package/template/src/app/api/auth/google/status/route.ts +46 -7
  24. package/template/src/app/api/closing-reasons/route.ts +0 -2
  25. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +2 -1
  26. package/template/src/app/api/contacts/[id]/files/route.ts +25 -20
  27. package/template/src/app/api/contacts/[id]/route.ts +2 -3
  28. package/template/src/app/api/contacts/export/route.ts +14 -11
  29. package/template/src/app/api/contacts/import/route.ts +2 -6
  30. package/template/src/app/api/contacts/route.ts +1 -1
  31. package/template/src/app/api/dashboard/stats/route.ts +7 -0
  32. package/template/src/app/api/dashboard/widgets/[id]/route.ts +47 -0
  33. package/template/src/app/api/dashboard/widgets/route.ts +181 -0
  34. package/template/src/app/api/integrations/google-sheet/sync/route.ts +58 -28
  35. package/template/src/app/api/reminders/route.ts +4 -2
  36. package/template/src/app/api/roles/route.ts +1 -1
  37. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +1 -6
  38. package/template/src/app/api/settings/closing-reasons/route.ts +0 -2
  39. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +10 -5
  40. package/template/src/app/api/settings/google-sheet/route.ts +3 -3
  41. package/template/src/app/api/tasks/[id]/route.ts +4 -4
  42. package/template/src/app/api/tasks/meet/route.ts +1 -2
  43. package/template/src/app/api/tasks/route.ts +16 -18
  44. package/template/src/app/api/users/for-agenda/route.ts +1 -2
  45. package/template/src/app/api/workflows/[id]/route.ts +2 -9
  46. package/template/src/app/api/workflows/route.ts +0 -1
  47. package/template/src/app/globals.css +96 -0
  48. package/template/src/components/dashboard/activity-chart.tsx +37 -37
  49. package/template/src/components/dashboard/add-widget-dialog.tsx +161 -0
  50. package/template/src/components/dashboard/color-picker.tsx +65 -0
  51. package/template/src/components/dashboard/contacts-chart.tsx +36 -30
  52. package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
  53. package/template/src/components/dashboard/recent-activity.tsx +79 -86
  54. package/template/src/components/dashboard/sales-analytics-chart.tsx +4 -8
  55. package/template/src/components/dashboard/stat-card.tsx +42 -40
  56. package/template/src/components/dashboard/status-distribution-chart.tsx +64 -27
  57. package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
  58. package/template/src/components/dashboard/top-contacts-list.tsx +41 -51
  59. package/template/src/components/dashboard/upcoming-tasks-list.tsx +71 -78
  60. package/template/src/components/dashboard/widget-wrapper.tsx +39 -0
  61. package/template/src/components/header.tsx +21 -12
  62. package/template/src/components/page-header.tsx +14 -47
  63. package/template/src/components/sidebar.tsx +3 -4
  64. package/template/src/contexts/dashboard-theme-context.tsx +58 -0
  65. package/template/src/lib/audit-log.ts +0 -2
  66. package/template/src/lib/dashboard-themes.ts +140 -0
  67. package/template/src/lib/default-widgets.ts +14 -0
  68. package/template/src/lib/google-drive.ts +38 -30
  69. package/template/src/lib/permissions.ts +56 -1
  70. package/template/src/lib/prisma.ts +0 -1
  71. package/template/src/lib/widget-registry.ts +177 -0
  72. package/template/src/lib/workflow-executor.ts +7 -13
  73. package/README.md +0 -89
@@ -0,0 +1,65 @@
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
+ // Fermer le dropdown au clic extérieur
13
+ useEffect(() => {
14
+ function handleClickOutside(event: MouseEvent) {
15
+ if (ref.current && !ref.current.contains(event.target as Node)) {
16
+ setIsOpen(false);
17
+ }
18
+ }
19
+ if (isOpen) {
20
+ document.addEventListener('mousedown', handleClickOutside);
21
+ return () => document.removeEventListener('mousedown', handleClickOutside);
22
+ }
23
+ }, [isOpen]);
24
+
25
+ return (
26
+ <div ref={ref} className="relative">
27
+ <button
28
+ onClick={() => setIsOpen(!isOpen)}
29
+ className="inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-xl border border-gray-200 bg-white shadow-sm transition-all duration-150 hover:bg-gray-50 hover:shadow-md active:scale-[0.98]"
30
+ title="Changer la couleur du thème"
31
+ >
32
+ <Palette 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-all duration-150 hover:scale-110"
49
+ style={{ backgroundColor: t.hex[500] }}
50
+ title={t.label}
51
+ >
52
+ {theme.key === t.key && (
53
+ <Check className="h-4 w-4 text-white drop-shadow-sm" />
54
+ )}
55
+ <span className="absolute -bottom-5 left-1/2 -translate-x-1/2 whitespace-nowrap text-[10px] text-gray-500 opacity-0 transition-opacity group-hover:opacity-100">
56
+ {t.label}
57
+ </span>
58
+ </button>
59
+ ))}
60
+ </div>
61
+ </div>
62
+ )}
63
+ </div>
64
+ );
65
+ }
@@ -1,59 +1,65 @@
1
1
  'use client';
2
2
 
3
- import { Area, AreaChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
3
+ import {
4
+ Area,
5
+ AreaChart,
6
+ ResponsiveContainer,
7
+ Tooltip,
8
+ XAxis,
9
+ YAxis,
10
+ CartesianGrid,
11
+ } from 'recharts';
12
+ import { useDashboardTheme } from '@/contexts/dashboard-theme-context';
4
13
 
5
14
  interface ContactsChartProps {
6
- data: Array<{ month: string; count: number }>;
15
+ readonly data: Array<{ month: string; count: number }>;
7
16
  }
8
17
 
9
- export function ContactsChart({ data }: ContactsChartProps) {
18
+ export function ContactsChart({ data }: Readonly<ContactsChartProps>) {
19
+ const { theme } = useDashboardTheme();
20
+
10
21
  return (
11
- <div className="rounded-xl border border-gray-200/50 bg-white p-6 shadow-lg transition-shadow duration-300 hover:shadow-xl">
12
- <div className="mb-4 flex items-center justify-between">
13
- <div>
14
- <h3 className="text-lg font-bold text-gray-900">Évolution des Contacts</h3>
15
- <p className="mt-1 text-sm text-gray-500">Nombre de contacts créés par mois</p>
16
- </div>
22
+ <div className="flex h-full flex-col rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
23
+ <div className="mb-4">
24
+ <h3 className="text-base font-semibold text-gray-900">Évolution des Contacts</h3>
25
+ <p className="mt-0.5 text-xs text-gray-400">Contacts créés par mois</p>
17
26
  </div>
18
- <div className="h-[300px]">
27
+ <div className="min-h-0 flex-1">
19
28
  <ResponsiveContainer width="100%" height="100%">
20
- <AreaChart data={data}>
29
+ <AreaChart data={data} margin={{ top: 5, right: 5, left: -20, bottom: 0 }}>
21
30
  <defs>
22
- <linearGradient id="colorCount" x1="0" y1="0" x2="0" y2="1">
23
- <stop offset="5%" stopColor="#8b5cf6" stopOpacity={0.4} />
24
- <stop offset="50%" stopColor="#6366f1" stopOpacity={0.3} />
25
- <stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
31
+ <linearGradient id="colorContactsAccent" x1="0" y1="0" x2="0" y2="1">
32
+ <stop offset="0%" stopColor={theme.hex[500]} stopOpacity={0.3} />
33
+ <stop offset="95%" stopColor={theme.hex[500]} stopOpacity={0.02} />
26
34
  </linearGradient>
27
35
  </defs>
36
+ <CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" vertical={false} />
28
37
  <XAxis
29
38
  dataKey="month"
30
- stroke="#9ca3af"
31
- fontSize={12}
32
- tickLine={false}
33
- axisLine={false}
34
- />
35
- <YAxis
36
- stroke="#9ca3af"
37
- fontSize={12}
39
+ stroke="#d1d5db"
40
+ fontSize={11}
38
41
  tickLine={false}
39
42
  axisLine={false}
40
- tickFormatter={(value) => `${value}`}
43
+ dy={8}
41
44
  />
45
+ <YAxis stroke="#d1d5db" fontSize={11} tickLine={false} axisLine={false} width={40} />
42
46
  <Tooltip
43
47
  contentStyle={{
44
- backgroundColor: 'white',
45
- border: '1px solid #e5e7eb',
48
+ backgroundColor: '#fff',
49
+ border: '1px solid #f3f4f6',
46
50
  borderRadius: '12px',
47
- boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
51
+ boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
52
+ fontSize: '13px',
48
53
  }}
49
54
  labelStyle={{ color: '#374151', fontWeight: 600 }}
50
55
  />
51
56
  <Area
52
57
  type="monotone"
53
58
  dataKey="count"
54
- stroke="#8b5cf6"
55
- strokeWidth={3}
56
- fill="url(#colorCount)"
59
+ stroke={theme.hex[500]}
60
+ strokeWidth={2.5}
61
+ fill="url(#colorContactsAccent)"
62
+ name="Contacts"
57
63
  />
58
64
  </AreaChart>
59
65
  </ResponsiveContainer>
@@ -0,0 +1,121 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Bar,
5
+ BarChart,
6
+ ResponsiveContainer,
7
+ Tooltip,
8
+ XAxis,
9
+ YAxis,
10
+ CartesianGrid,
11
+ Cell,
12
+ } from 'recharts';
13
+
14
+ interface InteractionsByTypeChartProps {
15
+ readonly data: Array<{ type: string; count: number }>;
16
+ }
17
+
18
+ const TYPE_LABELS: Record<string, string> = {
19
+ CALL: 'Appels',
20
+ SMS: 'SMS',
21
+ EMAIL: 'Emails',
22
+ MEETING: 'Réunions',
23
+ NOTE: 'Notes',
24
+ TASK: 'Tâches',
25
+ STATUS_CHANGE: 'Changements',
26
+ APPOINTMENT_CREATED: 'RDV créés',
27
+ APPOINTMENT_DELETED: 'RDV supprimés',
28
+ APPOINTMENT_CHANGED: 'RDV modifiés',
29
+ ASSIGNMENT_CHANGE: 'Assignations',
30
+ CONTACT_UPDATE: 'Mises à jour',
31
+ FILE_UPLOADED: 'Fichiers',
32
+ FILE_REPLACED: 'Fichiers remplacés',
33
+ FILE_DELETED: 'Fichiers supprimés',
34
+ };
35
+
36
+ const TYPE_COLORS: Record<string, string> = {
37
+ CALL: '#3b82f6',
38
+ SMS: '#10b981',
39
+ EMAIL: '#f97316',
40
+ MEETING: '#8b5cf6',
41
+ NOTE: '#6b7280',
42
+ TASK: '#f59e0b',
43
+ STATUS_CHANGE: '#ef4444',
44
+ APPOINTMENT_CREATED: '#14b8a6',
45
+ APPOINTMENT_DELETED: '#ef4444',
46
+ APPOINTMENT_CHANGED: '#f97316',
47
+ ASSIGNMENT_CHANGE: '#ec4899',
48
+ CONTACT_UPDATE: '#06b6d4',
49
+ FILE_UPLOADED: '#84cc16',
50
+ FILE_REPLACED: '#a855f7',
51
+ FILE_DELETED: '#dc2626',
52
+ };
53
+
54
+ export function InteractionsByTypeChart({ data }: Readonly<InteractionsByTypeChartProps>) {
55
+ const chartData = data
56
+ .filter((d) => d.count > 0)
57
+ .map((d) => ({
58
+ ...d,
59
+ label: TYPE_LABELS[d.type] || d.type,
60
+ fill: TYPE_COLORS[d.type] || '#f97316',
61
+ }))
62
+ .sort((a, b) => b.count - a.count);
63
+
64
+ return (
65
+ <div className="flex h-full flex-col rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
66
+ <div className="mb-4">
67
+ <h3 className="text-base font-semibold text-gray-900">Interactions par Type</h3>
68
+ <p className="mt-0.5 text-xs text-gray-400">Répartition des interactions ce mois</p>
69
+ </div>
70
+
71
+ {chartData.length === 0 ? (
72
+ <div className="flex flex-1 items-center justify-center">
73
+ <p className="text-sm text-gray-400">Aucune interaction ce mois</p>
74
+ </div>
75
+ ) : (
76
+ <div className="min-h-0 flex-1">
77
+ <ResponsiveContainer width="100%" height="100%">
78
+ <BarChart
79
+ data={chartData}
80
+ layout="vertical"
81
+ margin={{ top: 0, right: 20, left: 0, bottom: 0 }}
82
+ >
83
+ <CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" horizontal={false} />
84
+ <XAxis
85
+ type="number"
86
+ stroke="#d1d5db"
87
+ fontSize={11}
88
+ tickLine={false}
89
+ axisLine={false}
90
+ />
91
+ <YAxis
92
+ dataKey="label"
93
+ type="category"
94
+ stroke="#d1d5db"
95
+ fontSize={11}
96
+ tickLine={false}
97
+ axisLine={false}
98
+ width={80}
99
+ />
100
+ <Tooltip
101
+ contentStyle={{
102
+ backgroundColor: '#fff',
103
+ border: '1px solid #f3f4f6',
104
+ borderRadius: '12px',
105
+ boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
106
+ fontSize: '13px',
107
+ }}
108
+ formatter={(value: number) => [value, 'Interactions']}
109
+ />
110
+ <Bar dataKey="count" radius={[0, 6, 6, 0]} barSize={16}>
111
+ {chartData.map((entry) => (
112
+ <Cell key={`type-${entry.label}`} fill={entry.fill} />
113
+ ))}
114
+ </Bar>
115
+ </BarChart>
116
+ </ResponsiveContainer>
117
+ </div>
118
+ )}
119
+ </div>
120
+ );
121
+ }
@@ -29,10 +29,10 @@ interface Interaction {
29
29
  }
30
30
 
31
31
  interface RecentActivityProps {
32
- interactions: Interaction[];
32
+ readonly interactions: Interaction[];
33
33
  }
34
34
 
35
- const interactionIcons = {
35
+ const interactionIcons: Record<string, typeof Phone> = {
36
36
  CALL: Phone,
37
37
  SMS: MessageSquare,
38
38
  EMAIL: Mail,
@@ -46,21 +46,21 @@ const interactionIcons = {
46
46
  ASSIGNMENT_CHANGE: UserCheck,
47
47
  };
48
48
 
49
- const interactionColors = {
50
- CALL: 'bg-blue-100 text-blue-600',
51
- SMS: 'bg-green-100 text-green-600',
52
- EMAIL: 'bg-purple-100 text-purple-600',
53
- MEETING: 'bg-indigo-100 text-indigo-600',
54
- NOTE: 'bg-gray-100 text-gray-600',
55
- STATUS_CHANGE: 'bg-amber-100 text-amber-600',
56
- CONTACT_UPDATE: 'bg-cyan-100 text-cyan-600',
57
- APPOINTMENT_CREATED: 'bg-emerald-100 text-emerald-600',
58
- APPOINTMENT_DELETED: 'bg-red-100 text-red-600',
59
- APPOINTMENT_CHANGED: 'bg-orange-100 text-orange-600',
60
- ASSIGNMENT_CHANGE: 'bg-pink-100 text-pink-600',
49
+ const interactionColors: Record<string, string> = {
50
+ CALL: 'bg-blue-50 text-blue-500',
51
+ SMS: 'bg-emerald-50 text-emerald-500',
52
+ EMAIL: 'bg-orange-50 text-orange-500',
53
+ MEETING: 'bg-purple-50 text-purple-500',
54
+ NOTE: 'bg-gray-50 text-gray-500',
55
+ STATUS_CHANGE: 'bg-amber-50 text-amber-500',
56
+ CONTACT_UPDATE: 'bg-cyan-50 text-cyan-500',
57
+ APPOINTMENT_CREATED: 'bg-emerald-50 text-emerald-500',
58
+ APPOINTMENT_DELETED: 'bg-red-50 text-red-500',
59
+ APPOINTMENT_CHANGED: 'bg-orange-50 text-orange-500',
60
+ ASSIGNMENT_CHANGE: 'bg-pink-50 text-pink-500',
61
61
  };
62
62
 
63
- const interactionLabels = {
63
+ const interactionLabels: Record<string, string> = {
64
64
  CALL: 'Appel',
65
65
  SMS: 'SMS',
66
66
  EMAIL: 'Email',
@@ -75,90 +75,83 @@ const interactionLabels = {
75
75
  };
76
76
 
77
77
  export function RecentActivity({ interactions }: RecentActivityProps) {
78
- if (interactions.length === 0) {
79
- return (
80
- <div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
81
- <div className="flex items-center justify-between">
82
- <h3 className="text-lg font-semibold text-gray-900">Activité Récente</h3>
83
- </div>
84
- <div className="mt-6 text-center text-sm text-gray-500">
85
- <FileText className="mx-auto h-12 w-12 text-gray-400" />
86
- <p className="mt-2">Aucune activité récente</p>
87
- </div>
88
- </div>
89
- );
90
- }
91
-
92
78
  return (
93
- <div className="rounded-xl border border-gray-200/50 bg-white p-6 shadow-lg transition-shadow duration-300 hover:shadow-xl">
79
+ <div className="flex h-full flex-col rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
94
80
  <div className="mb-4 flex items-center justify-between">
95
- <h3 className="text-lg font-bold text-gray-900">Activité Récente</h3>
81
+ <div>
82
+ <h3 className="text-base font-semibold text-gray-900">Activité Récente</h3>
83
+ <p className="mt-0.5 text-xs text-gray-400">Dernières interactions</p>
84
+ </div>
96
85
  <Link
97
86
  href="/contacts"
98
- className="text-sm font-semibold text-indigo-600 transition-colors hover:text-indigo-700"
87
+ className="dash-link cursor-pointer text-xs font-medium"
99
88
  >
100
89
  Voir tout →
101
90
  </Link>
102
91
  </div>
103
- <div className="space-y-3">
104
- {interactions.map((interaction) => {
105
- const Icon =
106
- interactionIcons[interaction.type as keyof typeof interactionIcons] || FileText;
107
- const color =
108
- interactionColors[interaction.type as keyof typeof interactionColors] ||
109
- 'bg-gray-100 text-gray-600';
110
- const label =
111
- interactionLabels[interaction.type as keyof typeof interactionLabels] ||
112
- interaction.type;
113
- const date = new Date(interaction.date);
114
- const now = new Date();
115
- const diffMs = now.getTime() - date.getTime();
116
- const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
117
- const diffMinutes = Math.floor(diffMs / (1000 * 60));
118
92
 
119
- let timeAgo = '';
120
- if (diffMinutes < 1) {
121
- timeAgo = "À l'instant";
122
- } else if (diffMinutes < 60) {
123
- timeAgo = `Il y a ${diffMinutes} min`;
124
- } else if (diffHours < 24) {
125
- timeAgo = `Il y a ${diffHours}h`;
126
- } else {
127
- timeAgo = date.toLocaleDateString('fr-FR', {
128
- day: 'numeric',
129
- month: 'short',
130
- });
131
- }
93
+ {interactions.length === 0 ? (
94
+ <div className="flex flex-1 items-center justify-center">
95
+ <div className="text-center">
96
+ <FileText className="mx-auto h-10 w-10 text-gray-200" />
97
+ <p className="mt-2 text-sm text-gray-400">Aucune activité récente</p>
98
+ </div>
99
+ </div>
100
+ ) : (
101
+ <div className="flex-1 space-y-1 overflow-auto">
102
+ {interactions.map((interaction) => {
103
+ const Icon = interactionIcons[interaction.type] || FileText;
104
+ const color = interactionColors[interaction.type] || 'bg-gray-50 text-gray-500';
105
+ const label = interactionLabels[interaction.type] || interaction.type;
106
+ const date = new Date(interaction.date);
107
+ const now = new Date();
108
+ const diffMs = now.getTime() - date.getTime();
109
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
110
+ const diffMinutes = Math.floor(diffMs / (1000 * 60));
132
111
 
133
- return (
134
- <div
135
- key={interaction.id}
136
- className="group flex items-start gap-3 rounded-lg border-l-4 border-transparent bg-gray-50/50 p-3 transition-all duration-200 hover:border-indigo-400 hover:bg-indigo-50/30"
137
- >
138
- <div className={cn('rounded-lg p-2 shadow-sm', color)}>
139
- <Icon className="h-4 w-4" />
140
- </div>
141
- <div className="flex-1">
142
- <div className="flex items-start justify-between">
143
- <div>
144
- <p className="text-sm font-semibold text-gray-900">{label}</p>
145
- <Link
146
- href={`/contacts/${interaction.contact.id}`}
147
- className="text-sm text-gray-600 transition-colors hover:text-indigo-600"
148
- >
149
- {interaction.contact.name}
150
- </Link>
151
- {interaction.title && (
152
- <p className="mt-1 text-xs text-gray-500">{interaction.title}</p>
153
- )}
112
+ let timeAgo = '';
113
+ if (diffMinutes < 1) {
114
+ timeAgo = "À l'instant";
115
+ } else if (diffMinutes < 60) {
116
+ timeAgo = `Il y a ${diffMinutes} min`;
117
+ } else if (diffHours < 24) {
118
+ timeAgo = `Il y a ${diffHours}h`;
119
+ } else {
120
+ timeAgo = date.toLocaleDateString('fr-FR', {
121
+ day: 'numeric',
122
+ month: 'short',
123
+ });
124
+ }
125
+
126
+ return (
127
+ <div
128
+ key={interaction.id}
129
+ className="group flex items-start gap-3 rounded-xl border-l-2 border-transparent px-3 py-2.5 transition-all duration-150 dash-hover-border-left"
130
+ >
131
+ <div className={cn('rounded-lg p-1.5', color)}>
132
+ <Icon className="h-3.5 w-3.5" />
133
+ </div>
134
+ <div className="min-w-0 flex-1">
135
+ <div className="flex items-start justify-between gap-2">
136
+ <div className="min-w-0">
137
+ <p className="text-sm font-medium text-gray-900">{label}</p>
138
+ <Link
139
+ href={`/contacts/${interaction.contact.id}`}
140
+ className="dash-hover-text cursor-pointer text-xs text-gray-500 transition-colors"
141
+ >
142
+ {interaction.contact.name}
143
+ </Link>
144
+ </div>
145
+ <span className="shrink-0 text-[10px] font-medium text-gray-400">
146
+ {timeAgo}
147
+ </span>
154
148
  </div>
155
- <span className="text-xs font-medium text-gray-500">{timeAgo}</span>
156
149
  </div>
157
150
  </div>
158
- </div>
159
- );
160
- })}
161
- </div>
151
+ );
152
+ })}
153
+ </div>
154
+ )}
162
155
  </div>
163
156
  );
164
157
  }
@@ -9,7 +9,8 @@ interface SalesAnalyticsChartProps {
9
9
  export function SalesAnalyticsChart({ data }: SalesAnalyticsChartProps) {
10
10
  // Calculer le total et la croissance
11
11
  const total = data.reduce((sum, item) => sum + item.count, 0);
12
- const previousTotal = data.length > 1 ? data.slice(0, -1).reduce((sum, item) => sum + item.count, 0) : 0;
12
+ const previousTotal =
13
+ data.length > 1 ? data.slice(0, -1).reduce((sum, item) => sum + item.count, 0) : 0;
13
14
  const growth = previousTotal > 0 ? ((total - previousTotal) / previousTotal) * 100 : 0;
14
15
 
15
16
  return (
@@ -18,7 +19,7 @@ export function SalesAnalyticsChart({ data }: SalesAnalyticsChartProps) {
18
19
  <div>
19
20
  <h3 className="text-lg font-bold text-gray-900">Analytiques des Ventes</h3>
20
21
  </div>
21
- <select className="rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500">
22
+ <select className="rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none">
22
23
  <option>Mensuel</option>
23
24
  <option>Hebdomadaire</option>
24
25
  <option>Annuel</option>
@@ -67,15 +68,10 @@ export function SalesAnalyticsChart({ data }: SalesAnalyticsChartProps) {
67
68
  labelStyle={{ color: '#374151', fontWeight: 600 }}
68
69
  formatter={(value: number) => [`${value.toLocaleString('fr-FR')}`, 'Contacts']}
69
70
  />
70
- <Bar
71
- dataKey="count"
72
- fill="url(#barGradient)"
73
- radius={[8, 8, 0, 0]}
74
- />
71
+ <Bar dataKey="count" fill="url(#barGradient)" radius={[8, 8, 0, 0]} />
75
72
  </BarChart>
76
73
  </ResponsiveContainer>
77
74
  </div>
78
75
  </div>
79
76
  );
80
77
  }
81
-
@@ -1,61 +1,63 @@
1
1
  'use client';
2
2
 
3
- import { LucideIcon } from 'lucide-react';
4
3
  import { cn } from '@/lib/utils';
5
4
 
6
5
  interface StatCardProps {
7
- title: string;
8
- value: string | number;
9
- icon: LucideIcon;
10
- trend?: {
11
- value: number;
12
- label: string;
6
+ readonly title: string;
7
+ readonly value: string | number;
8
+ readonly trend?: {
9
+ readonly value: number;
10
+ readonly label: string;
13
11
  };
14
- iconColor?: string;
15
- iconBgColor?: string;
12
+ readonly subtitle?: string;
13
+ readonly accentColor?: string;
16
14
  }
17
15
 
18
16
  export function StatCard({
19
17
  title,
20
18
  value,
21
- icon: Icon,
22
19
  trend,
23
- iconColor = 'text-indigo-600',
24
- iconBgColor = 'bg-indigo-100',
20
+ subtitle,
21
+ accentColor = 'dash-accent-bar',
25
22
  }: StatCardProps) {
26
23
  return (
27
- <div className="group relative overflow-hidden rounded-xl border border-gray-200/50 bg-white p-6 shadow-lg transition-all duration-300 hover:shadow-xl hover:-translate-y-1">
28
- <div className="absolute inset-0 bg-gradient-to-br from-white via-purple-50/30 to-indigo-50/30 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
29
- <div className="relative flex items-center justify-between">
24
+ <div className="group relative flex h-full flex-col justify-between overflow-hidden rounded-2xl border border-gray-100 bg-white p-5 shadow-sm transition-all duration-200 hover:shadow-md">
25
+ {/* Barre d'accent en haut */}
26
+ <div className={cn('absolute top-0 left-0 h-1 w-full', accentColor)} />
27
+
28
+ <div className="flex items-start justify-between">
30
29
  <div className="flex-1">
31
- <p className="text-sm font-medium text-gray-600">{title}</p>
32
- <p className="mt-2 bg-gradient-to-r from-gray-900 to-gray-700 bg-clip-text text-3xl font-bold text-transparent">
33
- {value}
34
- </p>
35
- {trend && (
36
- <p className="mt-2 flex items-center gap-1 text-sm">
37
- <span
38
- className={cn(
39
- 'font-semibold',
40
- trend.value >= 0 ? 'text-emerald-600' : 'text-red-600',
41
- )}
42
- >
43
- {trend.value >= 0 ? '+' : ''}
44
- {trend.value}%
45
- </span>
46
- <span className="text-gray-500">{trend.label}</span>
47
- </p>
48
- )}
30
+ <p className="text-sm font-medium text-gray-500">{title}</p>
31
+ <p className="mt-2 text-3xl font-bold tracking-tight text-gray-900">{value}</p>
49
32
  </div>
50
- <div
51
- className={cn(
52
- 'rounded-xl bg-gradient-to-br p-4 shadow-md transition-transform duration-300 group-hover:scale-110 group-hover:shadow-lg',
53
- iconBgColor,
54
- )}
55
- >
56
- <Icon className={cn('h-6 w-6', iconColor)} />
33
+
34
+ {/* Mini bar chart décoratif */}
35
+ <div className="flex items-end gap-[3px] opacity-60">
36
+ {[40, 65, 45, 80, 55, 70, 90].map((barH) => (
37
+ <div
38
+ key={`bar-${barH}`}
39
+ className={cn('w-[4px] rounded-full', accentColor)}
40
+ style={{ height: `${barH * 0.3}px`, opacity: 0.4 + (barH / 90) * 0.6 }}
41
+ />
42
+ ))}
57
43
  </div>
58
44
  </div>
45
+
46
+ <div className="mt-3 flex items-center gap-2">
47
+ {trend && (
48
+ <span
49
+ className={cn(
50
+ 'inline-flex items-center gap-0.5 rounded-full px-2 py-0.5 text-xs font-semibold',
51
+ trend.value >= 0 ? 'bg-emerald-50 text-emerald-600' : 'bg-red-50 text-red-600',
52
+ )}
53
+ >
54
+ {trend.value >= 0 ? '↑' : '↓'} {Math.abs(trend.value)}%
55
+ </span>
56
+ )}
57
+ {(subtitle || trend?.label) && (
58
+ <span className="text-xs text-gray-400">{subtitle || trend?.label}</span>
59
+ )}
60
+ </div>
59
61
  </div>
60
62
  );
61
63
  }