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.
- package/bin/create-crm-tmp.js +56 -35
- package/package.json +1 -1
- package/template/README.md +230 -115
- package/template/eslint.config.mjs +13 -0
- package/template/next.config.ts +14 -0
- package/template/package.json +15 -2
- package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +132 -637
- package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
- package/template/src/app/(auth)/layout.tsx +1 -1
- package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
- package/template/src/app/(auth)/reset-password/page.tsx +4 -4
- package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
- package/template/src/app/(auth)/signin/page.tsx +14 -6
- package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
- package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
- package/template/src/app/(dashboard)/closing/page.tsx +78 -62
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
- package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
- package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
- package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
- package/template/src/app/(dashboard)/layout.tsx +6 -2
- package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
- package/template/src/app/(dashboard)/templates/page.tsx +55 -54
- package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
- package/template/src/app/(dashboard)/users/page.tsx +1 -1
- package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
- package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
- package/template/src/app/api/agenda/google-events/route.ts +92 -0
- package/template/src/app/api/auth/check-active/route.ts +3 -2
- package/template/src/app/api/auth/google/route.ts +2 -1
- package/template/src/app/api/auth/google/status/route.ts +7 -31
- package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
- package/template/src/app/api/companies/[id]/route.ts +1 -2
- package/template/src/app/api/companies/route.ts +42 -12
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
- package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
- package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
- package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
- package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
- package/template/src/app/api/contacts/[id]/route.ts +106 -34
- package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
- package/template/src/app/api/contacts/export/route.ts +9 -13
- package/template/src/app/api/contacts/import/route.ts +55 -25
- package/template/src/app/api/contacts/import-preview/route.ts +1 -1
- package/template/src/app/api/contacts/origins/route.ts +63 -0
- package/template/src/app/api/contacts/route.ts +153 -41
- package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
- package/template/src/app/api/dashboard/widgets/route.ts +181 -0
- package/template/src/app/api/dev/reminders/test/route.ts +114 -0
- package/template/src/app/api/editor/upload-image/route.ts +61 -0
- package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
- package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
- package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
- package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
- package/template/src/app/api/reminders/clear/route.ts +120 -0
- package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
- package/template/src/app/api/reminders/route.ts +164 -39
- package/template/src/app/api/reminders/state/route.ts +164 -0
- package/template/src/app/api/reset-password/request/route.ts +1 -1
- package/template/src/app/api/reset-password/verify/route.ts +1 -1
- package/template/src/app/api/send/route.ts +16 -4
- package/template/src/app/api/settings/google-ads/route.ts +14 -0
- package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
- package/template/src/app/api/settings/google-calendar/route.ts +124 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
- package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
- package/template/src/app/api/settings/google-sheet/route.ts +14 -0
- package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
- package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
- package/template/src/app/api/settings/meta-leads/route.ts +14 -2
- package/template/src/app/api/settings/smtp/route.ts +53 -6
- package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
- package/template/src/app/api/tasks/[id]/route.ts +234 -58
- package/template/src/app/api/tasks/meet/route.ts +27 -19
- package/template/src/app/api/tasks/route.ts +62 -17
- package/template/src/app/api/users/[id]/route.ts +20 -14
- package/template/src/app/api/users/list/route.ts +57 -19
- package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
- package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
- package/template/src/app/api/workflows/[id]/route.ts +0 -4
- package/template/src/app/api/workflows/process/route.ts +22 -51
- package/template/src/app/api/workflows/route.ts +0 -4
- package/template/src/app/globals.css +342 -4
- package/template/src/app/layout.tsx +11 -3
- package/template/src/app/page.tsx +1 -1
- package/template/src/components/address-autocomplete.tsx +7 -6
- package/template/src/components/config-error-alert.tsx +46 -0
- package/template/src/components/contacts/filter-bar.tsx +12 -3
- package/template/src/components/contacts/filter-builder.tsx +28 -43
- package/template/src/components/contacts/save-view-dialog.tsx +1 -1
- package/template/src/components/contacts/views-tab-bar.tsx +15 -6
- package/template/src/components/dashboard/activity-chart.tsx +41 -28
- package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
- package/template/src/components/dashboard/color-picker.tsx +64 -0
- package/template/src/components/dashboard/contacts-chart.tsx +69 -0
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
- package/template/src/components/dashboard/recent-activity.tsx +154 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -40
- package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
- package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
- package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
- package/template/src/components/date-picker.tsx +9 -6
- package/template/src/components/editor/upload-editor-image.ts +42 -0
- package/template/src/components/editor.tsx +161 -22
- package/template/src/components/email-template.tsx +2 -2
- package/template/src/components/global-search.tsx +30 -28
- package/template/src/components/header.tsx +178 -80
- package/template/src/components/inactive-account-guard.tsx +58 -0
- package/template/src/components/integration-notifications-listener.tsx +12 -0
- package/template/src/components/invitation-email-template.tsx +2 -2
- package/template/src/components/meet-cancellation-email-template.tsx +3 -3
- package/template/src/components/meet-confirmation-email-template.tsx +3 -3
- package/template/src/components/meet-update-email-template.tsx +3 -3
- package/template/src/components/page-header.tsx +5 -5
- package/template/src/components/protected-page.tsx +1 -1
- package/template/src/components/reset-password-email-template.tsx +2 -2
- package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
- package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
- package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
- package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
- package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
- package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
- package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
- package/template/src/components/sidebar.tsx +45 -26
- package/template/src/components/skeleton.tsx +40 -43
- package/template/src/components/ui/accordion.tsx +2 -2
- package/template/src/components/ui/alert-dialog.tsx +1 -1
- package/template/src/components/ui/button.tsx +20 -9
- package/template/src/components/ui/components.tsx +1 -1
- package/template/src/components/ui/date-picker.tsx +422 -0
- package/template/src/components/ui/datetime-picker.tsx +338 -0
- package/template/src/components/ui/status-select.tsx +271 -0
- package/template/src/components/ui/tooltip.tsx +37 -0
- package/template/src/components/view-as-modal.tsx +13 -7
- package/template/src/contexts/app-toast-context.tsx +245 -57
- package/template/src/contexts/dashboard-theme-context.tsx +53 -0
- package/template/src/contexts/sidebar-context.tsx +22 -17
- package/template/src/contexts/task-reminder-context.tsx +134 -160
- package/template/src/contexts/view-as-context.tsx +33 -6
- package/template/src/hooks/use-focus-trap.ts +2 -2
- package/template/src/hooks/useIntegrationNotifications.ts +49 -0
- package/template/src/lib/auth.ts +8 -1
- package/template/src/lib/config-links.ts +14 -0
- package/template/src/lib/contact-duplicate.ts +79 -61
- package/template/src/lib/contact-interactions.ts +21 -21
- package/template/src/lib/contact-view-filters.ts +24 -64
- package/template/src/lib/contacts-list-url.ts +190 -0
- package/template/src/lib/dashboard-stats.ts +65 -7
- package/template/src/lib/dashboard-themes.ts +135 -0
- package/template/src/lib/date-utils.ts +127 -0
- package/template/src/lib/default-widgets.ts +12 -0
- package/template/src/lib/editor-html-image-dimensions.ts +172 -0
- package/template/src/lib/editor-image-limits.ts +19 -0
- package/template/src/lib/email-html-sanitize.ts +19 -0
- package/template/src/lib/encryption.ts +9 -6
- package/template/src/lib/fr-geography.ts +192 -0
- package/template/src/lib/google-calendar-agenda.ts +201 -0
- package/template/src/lib/google-calendar.ts +255 -5
- package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
- package/template/src/lib/google-sheet-sync-runner.ts +514 -0
- package/template/src/lib/integration-import-log.ts +21 -0
- package/template/src/lib/permissions.ts +40 -10
- package/template/src/lib/prisma.ts +4 -1
- package/template/src/lib/qstash.ts +65 -0
- package/template/src/lib/reminder-state-server.ts +80 -0
- package/template/src/lib/reminder-state.ts +29 -0
- package/template/src/lib/supabase-storage.ts +113 -0
- package/template/src/lib/template-variables.ts +164 -23
- package/template/src/lib/utils.ts +45 -0
- package/template/src/lib/widget-registry.ts +173 -0
- package/template/src/lib/workflow-executor.ts +16 -70
- package/template/src/proxy.ts +1 -0
- package/template/vercel.json +3 -10
- package/template/skills-lock.json +0 -25
- package/template/src/components/dashboard/dashboard-content.tsx +0 -79
- package/template/src/lib/google-drive.ts +0 -1101
- package/template/src/types/yousign.ts +0 -52
|
@@ -1,51 +1,547 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState, useCallback, useRef, useMemo } from 'react';
|
|
4
|
+
import dynamic from 'next/dynamic';
|
|
5
|
+
import { useContainerWidth, type Layout, type LayoutItem } from 'react-grid-layout';
|
|
6
|
+
import { Plus, LayoutDashboard, RotateCcw, ShieldAlert } from 'lucide-react';
|
|
6
7
|
import { PageHeader } from '@/components/page-header';
|
|
7
|
-
import {
|
|
8
|
+
import { WidgetWrapper } from '@/components/dashboard/widget-wrapper';
|
|
9
|
+
import { AddWidgetDialog } from '@/components/dashboard/add-widget-dialog';
|
|
10
|
+
import { DashboardColorPicker } from '@/components/dashboard/color-picker';
|
|
11
|
+
import { useUserRole } from '@/hooks/use-user-role';
|
|
12
|
+
import { DashboardThemeProvider, useDashboardTheme } from '@/contexts/dashboard-theme-context';
|
|
13
|
+
import { getWidgetDefinition } from '@/lib/widget-registry';
|
|
14
|
+
import { useAppToast } from '@/contexts/app-toast-context';
|
|
15
|
+
import { devToast } from '@/lib/utils';
|
|
16
|
+
|
|
17
|
+
const GridLayout = dynamic(() => import('react-grid-layout').then((mod) => mod.GridLayout), {
|
|
18
|
+
ssr: false,
|
|
19
|
+
});
|
|
20
|
+
const StatCard = dynamic(
|
|
21
|
+
() => import('@/components/dashboard/stat-card').then((mod) => mod.StatCard),
|
|
22
|
+
{ ssr: false },
|
|
23
|
+
);
|
|
24
|
+
const ContactsChart = dynamic(
|
|
25
|
+
() => import('@/components/dashboard/contacts-chart').then((mod) => mod.ContactsChart),
|
|
26
|
+
{ ssr: false },
|
|
27
|
+
);
|
|
28
|
+
const ActivityChart = dynamic(
|
|
29
|
+
() => import('@/components/dashboard/activity-chart').then((mod) => mod.ActivityChart),
|
|
30
|
+
{ ssr: false },
|
|
31
|
+
);
|
|
32
|
+
const StatusDistributionChart = dynamic(
|
|
33
|
+
() =>
|
|
34
|
+
import('@/components/dashboard/status-distribution-chart').then(
|
|
35
|
+
(mod) => mod.StatusDistributionChart,
|
|
36
|
+
),
|
|
37
|
+
{ ssr: false },
|
|
38
|
+
);
|
|
39
|
+
const TasksPieChart = dynamic(
|
|
40
|
+
() => import('@/components/dashboard/tasks-pie-chart').then((mod) => mod.TasksPieChart),
|
|
41
|
+
{ ssr: false },
|
|
42
|
+
);
|
|
43
|
+
const UpcomingTasksList = dynamic(
|
|
44
|
+
() => import('@/components/dashboard/upcoming-tasks-list').then((mod) => mod.UpcomingTasksList),
|
|
45
|
+
{ ssr: false },
|
|
46
|
+
);
|
|
47
|
+
const RecentActivity = dynamic(
|
|
48
|
+
() => import('@/components/dashboard/recent-activity').then((mod) => mod.RecentActivity),
|
|
49
|
+
{ ssr: false },
|
|
50
|
+
);
|
|
51
|
+
const TopContactsList = dynamic(
|
|
52
|
+
() => import('@/components/dashboard/top-contacts-list').then((mod) => mod.TopContactsList),
|
|
53
|
+
{ ssr: false },
|
|
54
|
+
);
|
|
55
|
+
const InteractionsByTypeChart = dynamic(
|
|
56
|
+
() =>
|
|
57
|
+
import('@/components/dashboard/interactions-by-type-chart').then(
|
|
58
|
+
(mod) => mod.InteractionsByTypeChart,
|
|
59
|
+
),
|
|
60
|
+
{ ssr: false },
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
interface DashboardWidget {
|
|
64
|
+
id: string;
|
|
65
|
+
type: string;
|
|
66
|
+
x: number;
|
|
67
|
+
y: number;
|
|
68
|
+
w: number;
|
|
69
|
+
h: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface DashboardStats {
|
|
73
|
+
overview: {
|
|
74
|
+
totalContacts: number;
|
|
75
|
+
contactsThisMonth: number;
|
|
76
|
+
contactsGrowth: number;
|
|
77
|
+
monthsData: Array<{ month: string; count: number }>;
|
|
78
|
+
};
|
|
79
|
+
statusDistribution: Array<{ name: string; value: number }>;
|
|
80
|
+
tasks: {
|
|
81
|
+
total: number;
|
|
82
|
+
completed: number;
|
|
83
|
+
pending: number;
|
|
84
|
+
upcoming: Array<{
|
|
85
|
+
id: string;
|
|
86
|
+
title: string;
|
|
87
|
+
type: string;
|
|
88
|
+
scheduledAt: string;
|
|
89
|
+
contact: { id: string; name: string } | null;
|
|
90
|
+
priority: string;
|
|
91
|
+
}>;
|
|
92
|
+
byType: Array<{ type: string; count: number }>;
|
|
93
|
+
};
|
|
94
|
+
interactions: {
|
|
95
|
+
recent: Array<{
|
|
96
|
+
id: string;
|
|
97
|
+
type: string;
|
|
98
|
+
title: string | null;
|
|
99
|
+
content: string;
|
|
100
|
+
date: string;
|
|
101
|
+
contact: {
|
|
102
|
+
id: string;
|
|
103
|
+
name: string;
|
|
104
|
+
};
|
|
105
|
+
}>;
|
|
106
|
+
byType: Array<{ type: string; count: number }>;
|
|
107
|
+
};
|
|
108
|
+
activity: {
|
|
109
|
+
last7Days: Array<{ date: string; interactions: number; tasks: number }>;
|
|
110
|
+
};
|
|
111
|
+
topContacts: Array<{
|
|
112
|
+
id: string;
|
|
113
|
+
name: string;
|
|
114
|
+
phone: string;
|
|
115
|
+
email: string | null;
|
|
116
|
+
status: string;
|
|
117
|
+
interactionsCount: number;
|
|
118
|
+
assignedCommercial?: string;
|
|
119
|
+
assignedTelepro?: string;
|
|
120
|
+
}>;
|
|
121
|
+
}
|
|
8
122
|
|
|
9
|
-
function
|
|
123
|
+
export default function DashboardPage() {
|
|
10
124
|
return (
|
|
11
|
-
<
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
|
15
|
-
{Array.from({ length: 4 }).map((_, i) => (
|
|
16
|
-
<Skeleton key={i} className="h-32 rounded-lg" />
|
|
17
|
-
))}
|
|
18
|
-
</div>
|
|
19
|
-
<div className="mt-6 grid gap-6 lg:grid-cols-2">
|
|
20
|
-
{Array.from({ length: 4 }).map((_, i) => (
|
|
21
|
-
<Skeleton key={i} className="h-96 rounded-lg" />
|
|
22
|
-
))}
|
|
23
|
-
</div>
|
|
24
|
-
</div>
|
|
25
|
-
</div>
|
|
125
|
+
<DashboardThemeProvider>
|
|
126
|
+
<DashboardContent />
|
|
127
|
+
</DashboardThemeProvider>
|
|
26
128
|
);
|
|
27
129
|
}
|
|
28
130
|
|
|
29
|
-
|
|
30
|
-
const
|
|
131
|
+
function DashboardContent() {
|
|
132
|
+
const toast = useAppToast();
|
|
133
|
+
const [widgets, setWidgets] = useState<DashboardWidget[]>([]);
|
|
134
|
+
const [stats, setStats] = useState<DashboardStats | null>(null);
|
|
135
|
+
const [loading, setLoading] = useState(true);
|
|
136
|
+
const [error, setError] = useState<string | null>(null);
|
|
137
|
+
const [showAddDialog, setShowAddDialog] = useState(false);
|
|
138
|
+
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
139
|
+
const { hasPermission, isLoading: permissionsLoading } = useUserRole();
|
|
140
|
+
const { theme } = useDashboardTheme();
|
|
31
141
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
142
|
+
const canViewDashboard = hasPermission('dashboard.view');
|
|
143
|
+
const canManageWidgets = hasPermission('dashboard.widgets.manage');
|
|
144
|
+
const canResetDashboard = hasPermission('dashboard.widgets.reset');
|
|
145
|
+
const {
|
|
146
|
+
width: containerWidth,
|
|
147
|
+
containerRef,
|
|
148
|
+
mounted,
|
|
149
|
+
} = useContainerWidth({ initialWidth: 1200 });
|
|
35
150
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
151
|
+
const cols = useMemo(() => {
|
|
152
|
+
if (containerWidth < 768) return 2;
|
|
153
|
+
return 12;
|
|
154
|
+
}, [containerWidth]);
|
|
39
155
|
|
|
40
|
-
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
async function fetchData() {
|
|
158
|
+
try {
|
|
159
|
+
const [widgetsRes, statsRes] = await Promise.all([
|
|
160
|
+
fetch('/api/dashboard/widgets'),
|
|
161
|
+
fetch('/api/dashboard/stats'),
|
|
162
|
+
]);
|
|
41
163
|
|
|
42
|
-
|
|
43
|
-
|
|
164
|
+
if (widgetsRes.ok) {
|
|
165
|
+
setWidgets(await widgetsRes.json());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (statsRes.ok) {
|
|
169
|
+
setStats(await statsRes.json());
|
|
170
|
+
}
|
|
171
|
+
} catch (err) {
|
|
172
|
+
const message = err instanceof Error ? err.message : 'Une erreur est survenue';
|
|
173
|
+
setError(message);
|
|
174
|
+
toast.error(devToast('Une erreur est survenue lors du chargement du tableau de bord', err));
|
|
175
|
+
} finally {
|
|
176
|
+
setLoading(false);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fetchData();
|
|
181
|
+
}, []);
|
|
182
|
+
|
|
183
|
+
const saveLayout = useCallback((updatedWidgets: DashboardWidget[]) => {
|
|
184
|
+
if (saveTimeoutRef.current) {
|
|
185
|
+
clearTimeout(saveTimeoutRef.current);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
saveTimeoutRef.current = setTimeout(async () => {
|
|
189
|
+
try {
|
|
190
|
+
await fetch('/api/dashboard/widgets', {
|
|
191
|
+
method: 'PUT',
|
|
192
|
+
headers: { 'Content-Type': 'application/json' },
|
|
193
|
+
body: JSON.stringify({
|
|
194
|
+
widgets: updatedWidgets.map((w) => ({
|
|
195
|
+
id: w.id,
|
|
196
|
+
x: w.x,
|
|
197
|
+
y: w.y,
|
|
198
|
+
w: w.w,
|
|
199
|
+
h: w.h,
|
|
200
|
+
})),
|
|
201
|
+
}),
|
|
202
|
+
});
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.error('Erreur sauvegarde layout:', err);
|
|
205
|
+
toast.error(devToast('Impossible de sauvegarder la disposition du tableau de bord. Veuillez réessayer.', err));
|
|
206
|
+
}
|
|
207
|
+
}, 500);
|
|
208
|
+
}, []);
|
|
209
|
+
|
|
210
|
+
const handleLayoutChange = useCallback(
|
|
211
|
+
(layout: Layout) => {
|
|
212
|
+
if (cols !== 12) return;
|
|
213
|
+
|
|
214
|
+
const updatedWidgets = widgets.map((widget) => {
|
|
215
|
+
const layoutItem = layout.find((l: LayoutItem) => l.i === widget.id);
|
|
216
|
+
if (layoutItem) {
|
|
217
|
+
return {
|
|
218
|
+
...widget,
|
|
219
|
+
x: layoutItem.x,
|
|
220
|
+
y: layoutItem.y,
|
|
221
|
+
w: layoutItem.w,
|
|
222
|
+
h: layoutItem.h,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return widget;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
setWidgets(updatedWidgets);
|
|
229
|
+
saveLayout(updatedWidgets);
|
|
230
|
+
},
|
|
231
|
+
[widgets, saveLayout, cols],
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const handleAddWidget = useCallback(async (type: string, w: number, h: number) => {
|
|
235
|
+
try {
|
|
236
|
+
const res = await fetch('/api/dashboard/widgets', {
|
|
237
|
+
method: 'POST',
|
|
238
|
+
headers: { 'Content-Type': 'application/json' },
|
|
239
|
+
body: JSON.stringify({ type, w, h }),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (res.ok) {
|
|
243
|
+
const newWidget = await res.json();
|
|
244
|
+
setWidgets((prev) => [...prev, newWidget]);
|
|
245
|
+
}
|
|
246
|
+
} catch (err) {
|
|
247
|
+
console.error('Erreur ajout widget:', err);
|
|
248
|
+
toast.error(devToast('Impossible d\'ajouter le widget. Veuillez réessayer.', err));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
setShowAddDialog(false);
|
|
252
|
+
}, []);
|
|
253
|
+
|
|
254
|
+
const handleRemoveWidget = useCallback(async (widgetId: string) => {
|
|
255
|
+
try {
|
|
256
|
+
const res = await fetch(`/api/dashboard/widgets/${widgetId}`, {
|
|
257
|
+
method: 'DELETE',
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (res.ok) {
|
|
261
|
+
setWidgets((prev) => prev.filter((w) => w.id !== widgetId));
|
|
262
|
+
}
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.error('Erreur suppression widget:', err);
|
|
265
|
+
toast.error(devToast('Impossible de retirer le widget. Veuillez réessayer.', err));
|
|
266
|
+
}
|
|
267
|
+
}, []);
|
|
268
|
+
|
|
269
|
+
const handleResetLayout = useCallback(async () => {
|
|
270
|
+
try {
|
|
271
|
+
const res = await fetch('/api/dashboard/widgets', {
|
|
272
|
+
method: 'POST',
|
|
273
|
+
headers: { 'Content-Type': 'application/json' },
|
|
274
|
+
body: JSON.stringify({ initDefault: true }),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (res.ok) {
|
|
278
|
+
setWidgets(await res.json());
|
|
279
|
+
}
|
|
280
|
+
} catch (err) {
|
|
281
|
+
console.error('Erreur réinitialisation layout:', err);
|
|
282
|
+
toast.error(devToast('Impossible de réinitialiser le tableau de bord. Veuillez réessayer.', err));
|
|
283
|
+
}
|
|
284
|
+
}, []);
|
|
285
|
+
|
|
286
|
+
const gridLayout = useMemo((): Layout => {
|
|
287
|
+
return widgets.map((widget) => {
|
|
288
|
+
const def = getWidgetDefinition(widget.type);
|
|
289
|
+
|
|
290
|
+
if (cols === 2) {
|
|
291
|
+
const isSmallWidget = widget.w <= 4;
|
|
292
|
+
const mobileW = isSmallWidget ? 1 : 2;
|
|
293
|
+
return {
|
|
294
|
+
i: widget.id,
|
|
295
|
+
x: isSmallWidget ? widget.x % 2 : 0,
|
|
296
|
+
y: widget.y,
|
|
297
|
+
w: mobileW,
|
|
298
|
+
h: widget.h,
|
|
299
|
+
minW: 1,
|
|
300
|
+
minH: def?.minH ?? 2,
|
|
301
|
+
maxH: def?.maxH,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
i: widget.id,
|
|
307
|
+
x: widget.x,
|
|
308
|
+
y: widget.y,
|
|
309
|
+
w: widget.w,
|
|
310
|
+
h: widget.h,
|
|
311
|
+
minW: def?.minW ?? 2,
|
|
312
|
+
minH: def?.minH ?? 2,
|
|
313
|
+
maxH: def?.maxH,
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
}, [widgets, cols]);
|
|
317
|
+
|
|
318
|
+
const renderWidgetContent = useCallback(
|
|
319
|
+
(widget: DashboardWidget) => {
|
|
320
|
+
if (!stats) return null;
|
|
321
|
+
|
|
322
|
+
switch (widget.type) {
|
|
323
|
+
case 'stat_total_contacts':
|
|
324
|
+
return (
|
|
325
|
+
<StatCard
|
|
326
|
+
title="Total Contacts"
|
|
327
|
+
value={stats.overview.totalContacts.toLocaleString('fr-FR')}
|
|
328
|
+
trend={{
|
|
329
|
+
value: stats.overview.contactsGrowth,
|
|
330
|
+
label: 'vs mois dernier',
|
|
331
|
+
}}
|
|
332
|
+
accentColor="dash-accent-bar"
|
|
333
|
+
/>
|
|
334
|
+
);
|
|
335
|
+
case 'stat_new_contacts':
|
|
336
|
+
return (
|
|
337
|
+
<StatCard
|
|
338
|
+
title="Nouveaux ce Mois"
|
|
339
|
+
value={stats.overview.contactsThisMonth.toLocaleString('fr-FR')}
|
|
340
|
+
subtitle="contacts créés"
|
|
341
|
+
accentColor="bg-emerald-500"
|
|
342
|
+
/>
|
|
343
|
+
);
|
|
344
|
+
case 'stat_completed_tasks':
|
|
345
|
+
return (
|
|
346
|
+
<StatCard
|
|
347
|
+
title="Tâches Complétées"
|
|
348
|
+
value={stats.tasks.completed.toLocaleString('fr-FR')}
|
|
349
|
+
subtitle={`sur ${stats.tasks.total} au total`}
|
|
350
|
+
accentColor="bg-blue-500"
|
|
351
|
+
/>
|
|
352
|
+
);
|
|
353
|
+
case 'stat_pending_tasks':
|
|
354
|
+
return (
|
|
355
|
+
<StatCard
|
|
356
|
+
title="Tâches en Attente"
|
|
357
|
+
value={stats.tasks.pending.toLocaleString('fr-FR')}
|
|
358
|
+
subtitle="à traiter"
|
|
359
|
+
accentColor="bg-amber-500"
|
|
360
|
+
/>
|
|
361
|
+
);
|
|
362
|
+
case 'contacts_chart':
|
|
363
|
+
return <ContactsChart data={stats.overview.monthsData} />;
|
|
364
|
+
case 'activity_chart':
|
|
365
|
+
return <ActivityChart data={stats.activity.last7Days} />;
|
|
366
|
+
case 'status_distribution':
|
|
367
|
+
return <StatusDistributionChart data={stats.statusDistribution} />;
|
|
368
|
+
case 'tasks_pie':
|
|
369
|
+
return <TasksPieChart completed={stats.tasks.completed} pending={stats.tasks.pending} />;
|
|
370
|
+
case 'upcoming_tasks':
|
|
371
|
+
return <UpcomingTasksList tasks={stats.tasks.upcoming} />;
|
|
372
|
+
case 'recent_activity':
|
|
373
|
+
return <RecentActivity interactions={stats.interactions.recent} />;
|
|
374
|
+
case 'top_contacts':
|
|
375
|
+
return <TopContactsList contacts={stats.topContacts} />;
|
|
376
|
+
case 'interactions_by_type':
|
|
377
|
+
return <InteractionsByTypeChart data={stats.interactions.byType} />;
|
|
378
|
+
default:
|
|
379
|
+
return (
|
|
380
|
+
<div className="flex h-full items-center justify-center rounded-2xl border border-gray-100 bg-white p-5 text-sm text-gray-400">
|
|
381
|
+
Widget inconnu : {widget.type}
|
|
382
|
+
</div>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
[stats],
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const renderContent = () => {
|
|
390
|
+
if (loading || permissionsLoading) {
|
|
391
|
+
return (
|
|
392
|
+
<>
|
|
393
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
394
|
+
{[1, 2, 3, 4].map((i) => (
|
|
395
|
+
<div key={i} className="h-[100px] animate-pulse rounded-2xl bg-gray-100" />
|
|
396
|
+
))}
|
|
397
|
+
</div>
|
|
398
|
+
<div className="mt-4 grid gap-4 lg:grid-cols-2">
|
|
399
|
+
{[1, 2, 3, 4].map((i) => (
|
|
400
|
+
<div key={i} className="h-[300px] animate-pulse rounded-2xl bg-gray-100" />
|
|
401
|
+
))}
|
|
402
|
+
</div>
|
|
403
|
+
</>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (!canViewDashboard) {
|
|
408
|
+
return (
|
|
409
|
+
<div className="ui-fade-in flex h-96 flex-col items-center justify-center rounded-2xl border-2 border-dashed border-gray-200 bg-gray-50/50">
|
|
410
|
+
<ShieldAlert className="h-12 w-12 text-gray-300" />
|
|
411
|
+
<h3 className="mt-4 text-base font-medium text-gray-600">Accès restreint</h3>
|
|
412
|
+
<p className="mt-1 text-center text-sm text-gray-400">
|
|
413
|
+
Vous n'avez pas la permission d'accéder au tableau de bord.
|
|
414
|
+
<br />
|
|
415
|
+
Contactez votre administrateur pour obtenir les droits nécessaires.
|
|
416
|
+
</p>
|
|
417
|
+
</div>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (error) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return null;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const themeVars = {
|
|
429
|
+
'--dash-50': theme.hex[50],
|
|
430
|
+
'--dash-100': theme.hex[100],
|
|
431
|
+
'--dash-200': theme.hex[200],
|
|
432
|
+
'--dash-300': theme.hex[300],
|
|
433
|
+
'--dash-400': theme.hex[400],
|
|
434
|
+
'--dash-500': theme.hex[500],
|
|
435
|
+
'--dash-600': theme.hex[600],
|
|
436
|
+
'--dash-700': theme.hex[700],
|
|
437
|
+
} as React.CSSProperties;
|
|
44
438
|
|
|
45
|
-
export default function DashboardPage() {
|
|
46
439
|
return (
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
|
|
440
|
+
<div className="h-full w-full min-w-0" style={themeVars}>
|
|
441
|
+
<PageHeader
|
|
442
|
+
title="Tableau de Bord"
|
|
443
|
+
description="Vue d'ensemble de votre activité"
|
|
444
|
+
action={
|
|
445
|
+
!loading && !permissionsLoading && !error && canViewDashboard ? (
|
|
446
|
+
<div className="flex items-center gap-2">
|
|
447
|
+
<DashboardColorPicker />
|
|
448
|
+
{widgets.length > 0 && canResetDashboard && (
|
|
449
|
+
<button
|
|
450
|
+
onClick={handleResetLayout}
|
|
451
|
+
className="inline-flex cursor-pointer items-center gap-2 rounded-xl border border-gray-200 bg-white px-4 py-2.5 text-sm font-medium text-gray-600 shadow-sm transition-[background-color,color,box-shadow,transform] duration-150 hover:bg-gray-50 hover:text-gray-900 hover:shadow-md active:scale-[0.98]"
|
|
452
|
+
>
|
|
453
|
+
<RotateCcw className="h-4 w-4" />
|
|
454
|
+
Réinitialiser
|
|
455
|
+
</button>
|
|
456
|
+
)}
|
|
457
|
+
{canManageWidgets && (
|
|
458
|
+
<button
|
|
459
|
+
onClick={() => setShowAddDialog(true)}
|
|
460
|
+
className="dash-btn inline-flex cursor-pointer items-center gap-2 rounded-xl px-4 py-2.5 text-sm font-medium shadow-sm transition-[box-shadow,transform] duration-150 hover:shadow-md active:scale-[0.98]"
|
|
461
|
+
>
|
|
462
|
+
<Plus className="h-4 w-4" />
|
|
463
|
+
Ajouter un Widget
|
|
464
|
+
</button>
|
|
465
|
+
)}
|
|
466
|
+
</div>
|
|
467
|
+
) : undefined
|
|
468
|
+
}
|
|
469
|
+
/>
|
|
470
|
+
|
|
471
|
+
<div ref={containerRef} className="p-4 sm:p-6">
|
|
472
|
+
{renderContent()}
|
|
473
|
+
{!loading && !permissionsLoading && !error && canViewDashboard && widgets.length === 0 && (
|
|
474
|
+
<div className="ui-fade-in flex h-96 flex-col items-center justify-center rounded-2xl border-2 border-dashed border-gray-200 bg-gray-50/50">
|
|
475
|
+
<LayoutDashboard className="h-12 w-12 text-gray-300" />
|
|
476
|
+
<h3 className="mt-4 text-base font-medium text-gray-600">Aucun widget configuré</h3>
|
|
477
|
+
<p className="mt-1 text-sm text-gray-400">
|
|
478
|
+
{canManageWidgets
|
|
479
|
+
? 'Ajoutez des widgets pour personnaliser votre tableau de bord'
|
|
480
|
+
: "Aucun widget n'est configuré. Contactez votre administrateur."}
|
|
481
|
+
</p>
|
|
482
|
+
{canManageWidgets && (
|
|
483
|
+
<button
|
|
484
|
+
onClick={() => setShowAddDialog(true)}
|
|
485
|
+
className="dash-btn mt-4 inline-flex cursor-pointer items-center gap-2 rounded-xl px-4 py-2.5 text-sm font-medium transition-colors"
|
|
486
|
+
>
|
|
487
|
+
<Plus className="h-4 w-4" />
|
|
488
|
+
Ajouter un Widget
|
|
489
|
+
</button>
|
|
490
|
+
)}
|
|
491
|
+
</div>
|
|
492
|
+
)}
|
|
493
|
+
|
|
494
|
+
{!loading &&
|
|
495
|
+
!permissionsLoading &&
|
|
496
|
+
!error &&
|
|
497
|
+
canViewDashboard &&
|
|
498
|
+
widgets.length > 0 &&
|
|
499
|
+
mounted && (
|
|
500
|
+
<GridLayout
|
|
501
|
+
className="layout ui-fade-in"
|
|
502
|
+
width={containerWidth}
|
|
503
|
+
layout={gridLayout}
|
|
504
|
+
gridConfig={{
|
|
505
|
+
cols,
|
|
506
|
+
rowHeight: 60,
|
|
507
|
+
margin: [16, 16] as const,
|
|
508
|
+
containerPadding: [0, 0] as const,
|
|
509
|
+
maxRows: Infinity,
|
|
510
|
+
}}
|
|
511
|
+
onLayoutChange={handleLayoutChange}
|
|
512
|
+
dragConfig={{
|
|
513
|
+
handle: '.drag-handle',
|
|
514
|
+
enabled: canManageWidgets,
|
|
515
|
+
threshold: 3,
|
|
516
|
+
bounded: false,
|
|
517
|
+
}}
|
|
518
|
+
resizeConfig={{ enabled: canManageWidgets, handles: ['se'] }}
|
|
519
|
+
>
|
|
520
|
+
{widgets.map((widget) => (
|
|
521
|
+
<div key={widget.id}>
|
|
522
|
+
<WidgetWrapper
|
|
523
|
+
onRemove={
|
|
524
|
+
canManageWidgets
|
|
525
|
+
? () => {
|
|
526
|
+
handleRemoveWidget(widget.id);
|
|
527
|
+
}
|
|
528
|
+
: undefined
|
|
529
|
+
}
|
|
530
|
+
>
|
|
531
|
+
{renderWidgetContent(widget)}
|
|
532
|
+
</WidgetWrapper>
|
|
533
|
+
</div>
|
|
534
|
+
))}
|
|
535
|
+
</GridLayout>
|
|
536
|
+
)}
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
<AddWidgetDialog
|
|
540
|
+
isOpen={showAddDialog}
|
|
541
|
+
onClose={() => setShowAddDialog(false)}
|
|
542
|
+
onAdd={handleAddWidget}
|
|
543
|
+
existingTypes={widgets.map((w) => w.type)}
|
|
544
|
+
/>
|
|
545
|
+
</div>
|
|
50
546
|
);
|
|
51
547
|
}
|