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.
- package/bin/create-crm-tmp.js +7 -3
- package/package.json +1 -1
- package/template/README.md +70 -5
- package/template/WORKFLOWS_CRON.md +49 -27
- package/template/package.json +18 -16
- package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +20 -0
- package/template/prisma/schema.prisma +17 -0
- package/template/src/app/(dashboard)/agenda/page.tsx +279 -225
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +1 -5
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +20 -47
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +0 -2
- package/template/src/app/(dashboard)/closing/page.tsx +5 -57
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +60 -44
- package/template/src/app/(dashboard)/contacts/page.tsx +156 -210
- package/template/src/app/(dashboard)/dashboard/page.tsx +438 -91
- package/template/src/app/(dashboard)/settings/page.tsx +179 -77
- package/template/src/app/(dashboard)/users/layout.tsx +30 -0
- package/template/src/app/(dashboard)/users/list/page.tsx +213 -159
- package/template/src/app/(dashboard)/users/page.tsx +13 -46
- package/template/src/app/(dashboard)/users/permissions/page.tsx +0 -2
- package/template/src/app/(dashboard)/users/roles/page.tsx +0 -2
- package/template/src/app/api/audit-logs/route.ts +0 -2
- package/template/src/app/api/auth/google/status/route.ts +46 -7
- package/template/src/app/api/closing-reasons/route.ts +0 -2
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +2 -1
- package/template/src/app/api/contacts/[id]/files/route.ts +25 -20
- package/template/src/app/api/contacts/[id]/route.ts +2 -3
- package/template/src/app/api/contacts/export/route.ts +14 -11
- package/template/src/app/api/contacts/import/route.ts +2 -6
- package/template/src/app/api/contacts/route.ts +1 -1
- package/template/src/app/api/dashboard/stats/route.ts +7 -0
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +47 -0
- package/template/src/app/api/dashboard/widgets/route.ts +181 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +58 -28
- package/template/src/app/api/reminders/route.ts +4 -2
- package/template/src/app/api/roles/route.ts +1 -1
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +1 -6
- package/template/src/app/api/settings/closing-reasons/route.ts +0 -2
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +10 -5
- package/template/src/app/api/settings/google-sheet/route.ts +3 -3
- package/template/src/app/api/tasks/[id]/route.ts +4 -4
- package/template/src/app/api/tasks/meet/route.ts +1 -2
- package/template/src/app/api/tasks/route.ts +16 -18
- package/template/src/app/api/users/for-agenda/route.ts +1 -2
- package/template/src/app/api/workflows/[id]/route.ts +2 -9
- package/template/src/app/api/workflows/route.ts +0 -1
- package/template/src/app/globals.css +96 -0
- package/template/src/components/dashboard/activity-chart.tsx +37 -37
- package/template/src/components/dashboard/add-widget-dialog.tsx +161 -0
- package/template/src/components/dashboard/color-picker.tsx +65 -0
- package/template/src/components/dashboard/contacts-chart.tsx +36 -30
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
- package/template/src/components/dashboard/recent-activity.tsx +79 -86
- package/template/src/components/dashboard/sales-analytics-chart.tsx +4 -8
- package/template/src/components/dashboard/stat-card.tsx +42 -40
- package/template/src/components/dashboard/status-distribution-chart.tsx +64 -27
- package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
- package/template/src/components/dashboard/top-contacts-list.tsx +41 -51
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +71 -78
- package/template/src/components/dashboard/widget-wrapper.tsx +39 -0
- package/template/src/components/header.tsx +21 -12
- package/template/src/components/page-header.tsx +14 -47
- package/template/src/components/sidebar.tsx +3 -4
- package/template/src/contexts/dashboard-theme-context.tsx +58 -0
- package/template/src/lib/audit-log.ts +0 -2
- package/template/src/lib/dashboard-themes.ts +140 -0
- package/template/src/lib/default-widgets.ts +14 -0
- package/template/src/lib/google-drive.ts +38 -30
- package/template/src/lib/permissions.ts +56 -1
- package/template/src/lib/prisma.ts +0 -1
- package/template/src/lib/widget-registry.ts +177 -0
- package/template/src/lib/workflow-executor.ts +7 -13
- 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'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 {
|
|
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-
|
|
12
|
-
<div className="mb-4
|
|
13
|
-
<
|
|
14
|
-
|
|
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-
|
|
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="
|
|
23
|
-
<stop offset="
|
|
24
|
-
<stop offset="
|
|
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="#
|
|
31
|
-
fontSize={
|
|
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
|
-
|
|
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: '
|
|
45
|
-
border: '1px solid #
|
|
48
|
+
backgroundColor: '#fff',
|
|
49
|
+
border: '1px solid #f3f4f6',
|
|
46
50
|
borderRadius: '12px',
|
|
47
|
-
boxShadow: '0 4px
|
|
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=
|
|
55
|
-
strokeWidth={
|
|
56
|
-
fill="url(#
|
|
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-
|
|
51
|
-
SMS: 'bg-
|
|
52
|
-
EMAIL: 'bg-
|
|
53
|
-
MEETING: 'bg-
|
|
54
|
-
NOTE: 'bg-gray-
|
|
55
|
-
STATUS_CHANGE: 'bg-amber-
|
|
56
|
-
CONTACT_UPDATE: 'bg-cyan-
|
|
57
|
-
APPOINTMENT_CREATED: 'bg-emerald-
|
|
58
|
-
APPOINTMENT_DELETED: 'bg-red-
|
|
59
|
-
APPOINTMENT_CHANGED: 'bg-orange-
|
|
60
|
-
ASSIGNMENT_CHANGE: 'bg-pink-
|
|
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-
|
|
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
|
-
<
|
|
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="
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
24
|
-
|
|
20
|
+
subtitle,
|
|
21
|
+
accentColor = 'dash-accent-bar',
|
|
25
22
|
}: StatCardProps) {
|
|
26
23
|
return (
|
|
27
|
-
<div className="group relative overflow-hidden rounded-
|
|
28
|
-
|
|
29
|
-
<div className=
|
|
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-
|
|
32
|
-
<p className="mt-2
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
}
|