create-crm-tmp 1.1.2 → 2.0.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/package.json +1 -1
- package/template/.prettierignore +2 -0
- package/template/README.md +53 -67
- package/template/components.json +22 -0
- package/template/exemple-contacts.csv +54 -0
- package/template/next.config.ts +27 -1
- package/template/package.json +64 -27
- package/template/prisma/schema.prisma +821 -72
- package/template/skills-lock.json +25 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
- package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
- package/template/src/app/(auth)/reset-password/page.tsx +12 -8
- package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
- package/template/src/app/(auth)/signin/page.tsx +20 -17
- package/template/src/app/(dashboard)/agenda/page.tsx +2231 -2188
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
- package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
- package/template/src/app/(dashboard)/closing/page.tsx +500 -468
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5035 -4126
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
- package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
- package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
- package/template/src/app/(dashboard)/error.tsx +37 -0
- package/template/src/app/(dashboard)/layout.tsx +1 -1
- package/template/src/app/(dashboard)/loading.tsx +5 -0
- package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
- package/template/src/app/(dashboard)/settings/page.tsx +2685 -2489
- package/template/src/app/(dashboard)/templates/page.tsx +500 -300
- package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
- package/template/src/app/(dashboard)/users/page.tsx +279 -310
- package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
- package/template/src/app/(dashboard)/users/roles/page.tsx +164 -137
- package/template/src/app/api/audit-logs/route.ts +1 -1
- package/template/src/app/api/auth/google/callback/route.ts +8 -5
- package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
- package/template/src/app/api/companies/[id]/activities/route.ts +131 -0
- package/template/src/app/api/companies/[id]/route.ts +195 -0
- package/template/src/app/api/companies/export/route.ts +206 -0
- package/template/src/app/api/companies/route.ts +166 -0
- package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
- package/template/src/app/api/contact-views/[id]/route.ts +197 -0
- package/template/src/app/api/contact-views/route.ts +146 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +77 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
- package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
- package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
- package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
- package/template/src/app/api/contacts/[id]/route.ts +111 -20
- package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
- package/template/src/app/api/contacts/export/route.ts +12 -17
- package/template/src/app/api/contacts/import/route.ts +22 -19
- package/template/src/app/api/contacts/import-preview/route.ts +139 -0
- package/template/src/app/api/contacts/route.ts +202 -49
- package/template/src/app/api/dashboard/stats/route.ts +9 -292
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
- package/template/src/app/api/invite/complete/route.ts +20 -23
- package/template/src/app/api/reminders/route.ts +1 -0
- package/template/src/app/api/reset-password/complete/route.ts +11 -13
- package/template/src/app/api/send/route.ts +9 -85
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
- package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
- package/template/src/app/api/settings/company/route.ts +19 -26
- package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-ads/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
- package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
- package/template/src/app/api/settings/google-sheet/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/route.ts +20 -23
- package/template/src/app/api/settings/statuses/[id]/route.ts +33 -23
- package/template/src/app/api/settings/statuses/route.ts +24 -22
- package/template/src/app/api/statuses/route.ts +2 -5
- package/template/src/app/api/tasks/[id]/attendees/route.ts +14 -7
- package/template/src/app/api/tasks/[id]/route.ts +161 -137
- package/template/src/app/api/tasks/meet/route.ts +11 -8
- package/template/src/app/api/tasks/route.ts +155 -95
- package/template/src/app/api/templates/[id]/route.ts +22 -13
- package/template/src/app/api/templates/route.ts +22 -5
- package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
- package/template/src/app/api/users/[id]/route.ts +16 -1
- package/template/src/app/api/users/commercials/route.ts +38 -0
- package/template/src/app/api/users/for-agenda/route.ts +1 -2
- package/template/src/app/api/users/route.ts +94 -55
- package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
- package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
- package/template/src/app/api/workflows/[id]/route.ts +33 -6
- package/template/src/app/api/workflows/process/route.ts +509 -146
- package/template/src/app/api/workflows/route.ts +46 -4
- package/template/src/app/globals.css +210 -101
- package/template/src/app/layout.tsx +19 -8
- package/template/src/app/page.tsx +37 -7
- package/template/src/components/address-autocomplete.tsx +232 -0
- package/template/src/components/contacts/filter-bar.tsx +181 -0
- package/template/src/components/contacts/filter-builder.tsx +589 -0
- package/template/src/components/contacts/save-view-dialog.tsx +160 -0
- package/template/src/components/contacts/views-tab-bar.tsx +440 -0
- package/template/src/components/dashboard/activity-chart.tsx +31 -39
- package/template/src/components/dashboard/dashboard-content.tsx +79 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -42
- package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
- package/template/src/components/date-picker.tsx +396 -0
- package/template/src/components/editor.tsx +27 -13
- package/template/src/components/email-template.tsx +4 -2
- package/template/src/components/global-search.tsx +358 -0
- package/template/src/components/header.tsx +57 -62
- package/template/src/components/invitation-email-template.tsx +4 -2
- package/template/src/components/lazy-editor.tsx +11 -0
- package/template/src/components/meet-cancellation-email-template.tsx +11 -3
- package/template/src/components/meet-confirmation-email-template.tsx +10 -3
- package/template/src/components/meet-update-email-template.tsx +10 -3
- package/template/src/components/page-header.tsx +19 -15
- package/template/src/components/protected-page.tsx +94 -0
- package/template/src/components/reset-password-email-template.tsx +4 -2
- package/template/src/components/sidebar.tsx +92 -94
- package/template/src/components/skeleton.tsx +128 -42
- package/template/src/components/ui/accordion.tsx +64 -0
- package/template/src/components/ui/alert-dialog.tsx +139 -0
- package/template/src/components/ui/button.tsx +60 -0
- package/template/src/components/view-as-banner.tsx +1 -1
- package/template/src/components/view-as-modal.tsx +21 -16
- package/template/src/config/nav-pages.ts +108 -0
- package/template/src/contexts/app-toast-context.tsx +174 -0
- package/template/src/contexts/sidebar-context.tsx +16 -47
- package/template/src/contexts/task-reminder-context.tsx +6 -6
- package/template/src/contexts/view-as-context.tsx +11 -16
- package/template/src/hooks/use-alert.tsx +65 -0
- package/template/src/hooks/use-confirm.tsx +87 -0
- package/template/src/hooks/use-contact-views.ts +140 -0
- package/template/src/hooks/use-contacts.ts +69 -0
- package/template/src/hooks/use-fetch.ts +17 -0
- package/template/src/hooks/use-focus-trap.ts +73 -0
- package/template/src/hooks/use-statuses.ts +22 -0
- package/template/src/lib/address-api.ts +155 -0
- package/template/src/lib/cache.ts +73 -0
- package/template/src/lib/check-permission.ts +12 -177
- package/template/src/lib/contact-interactions.ts +3 -1
- package/template/src/lib/contact-view-filters.ts +341 -0
- package/template/src/lib/dashboard-stats.ts +224 -0
- package/template/src/lib/date-utils.ts +49 -0
- package/template/src/lib/get-auth-user.ts +25 -0
- package/template/src/lib/google-calendar.ts +54 -12
- package/template/src/lib/google-drive.ts +796 -75
- package/template/src/lib/google-fetch.ts +63 -0
- package/template/src/lib/local-storage.ts +34 -0
- package/template/src/lib/permissions.ts +245 -47
- package/template/src/lib/prisma.ts +11 -11
- package/template/src/lib/roles.ts +14 -39
- package/template/src/lib/template-variables.ts +67 -33
- package/template/src/lib/utils.ts +26 -2
- package/template/src/lib/workflow-executor.ts +445 -229
- package/template/src/proxy.ts +34 -73
- package/template/src/types/contact-views.ts +351 -0
- package/template/src/types/yousign.ts +52 -0
- package/template/vercel.json +12 -0
- package/template/WORKFLOWS_CRON.md +0 -185
- package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
- package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
- package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
- package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
- package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
- package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
- package/template/prisma/migrations/migration_lock.toml +0 -3
- package/template/src/app/(dashboard)/users/layout.tsx +0 -30
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
- package/template/src/app/api/dashboard/widgets/route.ts +0 -181
- package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
- package/template/src/components/dashboard/color-picker.tsx +0 -65
- package/template/src/components/dashboard/contacts-chart.tsx +0 -69
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
- package/template/src/components/dashboard/recent-activity.tsx +0 -157
- package/template/src/components/dashboard/sales-analytics-chart.tsx +0 -77
- package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
- package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
- package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
- package/template/src/contexts/dashboard-theme-context.tsx +0 -58
- package/template/src/lib/dashboard-themes.ts +0 -140
- package/template/src/lib/default-widgets.ts +0 -14
- package/template/src/lib/widget-registry.ts +0 -177
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { prisma } from '@/lib/prisma';
|
|
2
|
+
import { getCachedStatuses } from '@/lib/cache';
|
|
3
|
+
|
|
4
|
+
export interface DashboardStats {
|
|
5
|
+
overview: {
|
|
6
|
+
totalContacts: number;
|
|
7
|
+
contactsThisMonth: number;
|
|
8
|
+
contactsGrowth: number;
|
|
9
|
+
statusData: Array<{ name: string; value: number }>;
|
|
10
|
+
monthlyContacts: Array<{ month: string; count: number }>;
|
|
11
|
+
};
|
|
12
|
+
tasks: {
|
|
13
|
+
total: number;
|
|
14
|
+
completed: number;
|
|
15
|
+
pending: number;
|
|
16
|
+
upcoming: Array<{
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
type: string;
|
|
20
|
+
scheduledAt: string;
|
|
21
|
+
contact: { id: string; name: string } | null;
|
|
22
|
+
priority: string;
|
|
23
|
+
}>;
|
|
24
|
+
byType: Array<{ type: string; count: number }>;
|
|
25
|
+
};
|
|
26
|
+
interactions: {
|
|
27
|
+
recent: any[];
|
|
28
|
+
byType: Array<{ type: string; count: number }>;
|
|
29
|
+
};
|
|
30
|
+
activity: {
|
|
31
|
+
last7Days: Array<{ date: string; interactions: number; tasks: number }>;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function getDashboardStats(
|
|
36
|
+
userId: string,
|
|
37
|
+
permissions: string[],
|
|
38
|
+
): Promise<DashboardStats> {
|
|
39
|
+
const canViewAllContacts = permissions.includes('contacts.view_all');
|
|
40
|
+
const canViewOwnContacts = permissions.includes('contacts.view_own');
|
|
41
|
+
const canViewUnassigned = permissions.includes('contacts.view_unassigned');
|
|
42
|
+
|
|
43
|
+
let contactFilter: any = {};
|
|
44
|
+
if (!canViewAllContacts && canViewOwnContacts) {
|
|
45
|
+
const conditions: any[] = [
|
|
46
|
+
{ assignedCommercialId: userId },
|
|
47
|
+
{ assignedTeleproId: userId },
|
|
48
|
+
{ createdById: userId },
|
|
49
|
+
];
|
|
50
|
+
if (canViewUnassigned) {
|
|
51
|
+
conditions.push({ AND: [{ assignedCommercialId: null }, { assignedTeleproId: null }] });
|
|
52
|
+
}
|
|
53
|
+
contactFilter = { OR: conditions };
|
|
54
|
+
} else if (!canViewAllContacts && !canViewOwnContacts) {
|
|
55
|
+
contactFilter = { id: '__none__' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const now = new Date();
|
|
59
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
60
|
+
const lastMonthStart = new Date(startOfMonth);
|
|
61
|
+
lastMonthStart.setMonth(lastMonthStart.getMonth() - 1);
|
|
62
|
+
|
|
63
|
+
const twelveMonthsAgo = new Date(now.getFullYear(), now.getMonth() - 11, 1);
|
|
64
|
+
const sevenDaysAgo = new Date(now);
|
|
65
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 6);
|
|
66
|
+
sevenDaysAgo.setHours(0, 0, 0, 0);
|
|
67
|
+
|
|
68
|
+
const [
|
|
69
|
+
totalContacts,
|
|
70
|
+
contactsThisMonth,
|
|
71
|
+
contactsLastMonth,
|
|
72
|
+
statusDistribution,
|
|
73
|
+
statuses,
|
|
74
|
+
upcomingTasks,
|
|
75
|
+
recentInteractions,
|
|
76
|
+
totalTasks,
|
|
77
|
+
completedTasks,
|
|
78
|
+
tasksThisMonthByType,
|
|
79
|
+
interactionsThisMonth,
|
|
80
|
+
contactsLast12Months,
|
|
81
|
+
interactionsLast7Days,
|
|
82
|
+
tasksLast7Days,
|
|
83
|
+
] = await Promise.all([
|
|
84
|
+
prisma.contact.count({ where: contactFilter }),
|
|
85
|
+
prisma.contact.count({
|
|
86
|
+
where: { ...contactFilter, createdAt: { gte: startOfMonth } },
|
|
87
|
+
}),
|
|
88
|
+
prisma.contact.count({
|
|
89
|
+
where: { ...contactFilter, createdAt: { gte: lastMonthStart, lt: startOfMonth } },
|
|
90
|
+
}),
|
|
91
|
+
prisma.contact.groupBy({ by: ['statusId'], where: contactFilter, _count: true }),
|
|
92
|
+
getCachedStatuses(),
|
|
93
|
+
prisma.task.findMany({
|
|
94
|
+
where: { assignedUserId: userId, completed: false, scheduledAt: { gte: now } },
|
|
95
|
+
select: {
|
|
96
|
+
id: true,
|
|
97
|
+
title: true,
|
|
98
|
+
type: true,
|
|
99
|
+
scheduledAt: true,
|
|
100
|
+
priority: true,
|
|
101
|
+
contact: { select: { id: true, firstName: true, lastName: true, phone: true } },
|
|
102
|
+
},
|
|
103
|
+
orderBy: { scheduledAt: 'asc' },
|
|
104
|
+
take: 6,
|
|
105
|
+
}),
|
|
106
|
+
prisma.interaction.findMany({
|
|
107
|
+
where: { userId },
|
|
108
|
+
select: {
|
|
109
|
+
id: true,
|
|
110
|
+
type: true,
|
|
111
|
+
content: true,
|
|
112
|
+
createdAt: true,
|
|
113
|
+
contact: { select: { id: true, firstName: true, lastName: true, phone: true } },
|
|
114
|
+
user: { select: { id: true, name: true } },
|
|
115
|
+
},
|
|
116
|
+
orderBy: { createdAt: 'desc' },
|
|
117
|
+
take: 5,
|
|
118
|
+
}),
|
|
119
|
+
prisma.task.count({ where: { assignedUserId: userId } }),
|
|
120
|
+
prisma.task.count({ where: { assignedUserId: userId, completed: true } }),
|
|
121
|
+
prisma.task.groupBy({
|
|
122
|
+
by: ['type'],
|
|
123
|
+
where: { assignedUserId: userId, createdAt: { gte: startOfMonth } },
|
|
124
|
+
_count: true,
|
|
125
|
+
}),
|
|
126
|
+
prisma.interaction.groupBy({
|
|
127
|
+
by: ['type'],
|
|
128
|
+
where: { userId, createdAt: { gte: startOfMonth } },
|
|
129
|
+
_count: true,
|
|
130
|
+
}),
|
|
131
|
+
prisma.contact.findMany({
|
|
132
|
+
where: { ...contactFilter, createdAt: { gte: twelveMonthsAgo } },
|
|
133
|
+
select: { createdAt: true },
|
|
134
|
+
}),
|
|
135
|
+
prisma.interaction.findMany({
|
|
136
|
+
where: { userId, createdAt: { gte: sevenDaysAgo } },
|
|
137
|
+
select: { createdAt: true },
|
|
138
|
+
}),
|
|
139
|
+
prisma.task.findMany({
|
|
140
|
+
where: { assignedUserId: userId, createdAt: { gte: sevenDaysAgo } },
|
|
141
|
+
select: { createdAt: true },
|
|
142
|
+
}),
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
const contactsGrowth =
|
|
146
|
+
contactsLastMonth > 0 ? ((contactsThisMonth - contactsLastMonth) / contactsLastMonth) * 100 : 0;
|
|
147
|
+
|
|
148
|
+
const monthsData = [];
|
|
149
|
+
for (let i = 11; i >= 0; i--) {
|
|
150
|
+
const monthStart = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
|
151
|
+
const monthEnd = new Date(monthStart.getFullYear(), monthStart.getMonth() + 1, 1);
|
|
152
|
+
const count = contactsLast12Months.filter(
|
|
153
|
+
(c) => c.createdAt >= monthStart && c.createdAt < monthEnd,
|
|
154
|
+
).length;
|
|
155
|
+
monthsData.push({
|
|
156
|
+
month: monthStart.toLocaleString('fr-FR', { month: 'short' }),
|
|
157
|
+
count,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const statusData = statuses.map((status) => ({
|
|
162
|
+
name: status.name,
|
|
163
|
+
value: statusDistribution.find((s) => s.statusId === status.id)?._count || 0,
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
const pendingTasks = totalTasks - completedTasks;
|
|
167
|
+
const tasksByType = tasksThisMonthByType.map((t) => ({ type: t.type, count: t._count }));
|
|
168
|
+
const interactionsByType = interactionsThisMonth.map((i) => ({ type: i.type, count: i._count }));
|
|
169
|
+
|
|
170
|
+
const last7Days = [];
|
|
171
|
+
for (let i = 6; i >= 0; i--) {
|
|
172
|
+
const dayStart = new Date(now);
|
|
173
|
+
dayStart.setDate(dayStart.getDate() - i);
|
|
174
|
+
dayStart.setHours(0, 0, 0, 0);
|
|
175
|
+
const dayEnd = new Date(dayStart);
|
|
176
|
+
dayEnd.setDate(dayEnd.getDate() + 1);
|
|
177
|
+
|
|
178
|
+
last7Days.push({
|
|
179
|
+
date: dayStart.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' }),
|
|
180
|
+
interactions: interactionsLast7Days.filter(
|
|
181
|
+
(x) => x.createdAt >= dayStart && x.createdAt < dayEnd,
|
|
182
|
+
).length,
|
|
183
|
+
tasks: tasksLast7Days.filter((x) => x.createdAt >= dayStart && x.createdAt < dayEnd).length,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
overview: {
|
|
189
|
+
totalContacts,
|
|
190
|
+
contactsThisMonth,
|
|
191
|
+
contactsGrowth: Math.round(contactsGrowth * 10) / 10,
|
|
192
|
+
statusData,
|
|
193
|
+
monthlyContacts: monthsData,
|
|
194
|
+
},
|
|
195
|
+
tasks: {
|
|
196
|
+
total: totalTasks,
|
|
197
|
+
completed: completedTasks,
|
|
198
|
+
pending: pendingTasks,
|
|
199
|
+
upcoming: upcomingTasks.map((task) => ({
|
|
200
|
+
id: task.id,
|
|
201
|
+
title: task.title || 'Sans titre',
|
|
202
|
+
type: task.type,
|
|
203
|
+
scheduledAt: task.scheduledAt?.toISOString() ?? new Date().toISOString(),
|
|
204
|
+
contact: task.contact
|
|
205
|
+
? {
|
|
206
|
+
id: task.contact.id,
|
|
207
|
+
name:
|
|
208
|
+
`${task.contact.firstName || ''} ${task.contact.lastName || ''}`.trim() ||
|
|
209
|
+
task.contact.phone,
|
|
210
|
+
}
|
|
211
|
+
: null,
|
|
212
|
+
priority: task.priority || 'MEDIUM',
|
|
213
|
+
})),
|
|
214
|
+
byType: tasksByType,
|
|
215
|
+
},
|
|
216
|
+
interactions: {
|
|
217
|
+
recent: recentInteractions,
|
|
218
|
+
byType: interactionsByType,
|
|
219
|
+
},
|
|
220
|
+
activity: {
|
|
221
|
+
last7Days,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calcule la semaine ISO 8601 pour une date donnée
|
|
3
|
+
* @param date - La date pour laquelle calculer la semaine ISO
|
|
4
|
+
* @returns Le numéro de la semaine ISO (1-53)
|
|
5
|
+
*/
|
|
6
|
+
export function getISOWeek(date: Date): number {
|
|
7
|
+
const target = new Date(date.valueOf());
|
|
8
|
+
const dayNr = (date.getDay() + 6) % 7;
|
|
9
|
+
target.setDate(target.getDate() - dayNr + 3);
|
|
10
|
+
const firstThursday = target.valueOf();
|
|
11
|
+
target.setMonth(0, 1);
|
|
12
|
+
if (target.getDay() !== 4) {
|
|
13
|
+
target.setMonth(0, 1 + ((4 - target.getDay() + 7) % 7));
|
|
14
|
+
}
|
|
15
|
+
return 1 + Math.ceil((firstThursday - target.valueOf()) / 604800000);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Formate une durée en heures et minutes
|
|
20
|
+
* @param hours - Nombre d'heures (peut être décimal)
|
|
21
|
+
* @returns Une chaîne formatée (ex: "2h 30min")
|
|
22
|
+
*/
|
|
23
|
+
export function formatDuration(hours: number): string {
|
|
24
|
+
const h = Math.floor(hours);
|
|
25
|
+
const m = Math.round((hours - h) * 60);
|
|
26
|
+
if (m === 0) return `${h}h`;
|
|
27
|
+
return `${h}h ${m}min`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Calcule le mercredi de N semaines avant la semaine ISO d'une date donnée
|
|
32
|
+
* @param date - La date de référence
|
|
33
|
+
* @param weeksBefore - Nombre de semaines avant (par défaut 4)
|
|
34
|
+
* @returns Le mercredi de la semaine cible
|
|
35
|
+
*/
|
|
36
|
+
export function getWednesdayBeforeISOWeek(date: Date, weeksBefore: number = 4): Date {
|
|
37
|
+
// Obtenir le lundi de la semaine ISO de la date donnée
|
|
38
|
+
const target = new Date(date.valueOf());
|
|
39
|
+
const dayNr = (date.getDay() + 6) % 7; // 0 = lundi, 6 = dimanche
|
|
40
|
+
target.setDate(target.getDate() - dayNr); // Aller au lundi de cette semaine
|
|
41
|
+
|
|
42
|
+
// Reculer de N semaines
|
|
43
|
+
target.setDate(target.getDate() - weeksBefore * 7);
|
|
44
|
+
|
|
45
|
+
// Aller au mercredi de cette semaine (lundi + 2 jours)
|
|
46
|
+
target.setDate(target.getDate() + 2);
|
|
47
|
+
|
|
48
|
+
return target;
|
|
49
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cache } from 'react';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { headers } from 'next/headers';
|
|
4
|
+
import { prisma } from '@/lib/prisma';
|
|
5
|
+
|
|
6
|
+
export const getAuthUser = cache(async () => {
|
|
7
|
+
const session = await auth.api.getSession({
|
|
8
|
+
headers: await headers(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
if (!session) return null;
|
|
12
|
+
|
|
13
|
+
const user = await prisma.user.findUnique({
|
|
14
|
+
where: { id: session.user.id },
|
|
15
|
+
include: { customRole: true },
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (!user) return null;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
session,
|
|
22
|
+
user,
|
|
23
|
+
permissions: (user.customRole?.permissions as string[]) ?? [],
|
|
24
|
+
};
|
|
25
|
+
});
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* Utilitaires pour gérer l'authentification et les appels à Google Calendar API
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { prisma } from './prisma';
|
|
6
|
+
import { decrypt } from './encryption';
|
|
7
|
+
import { googleFetch } from './google-fetch';
|
|
8
|
+
|
|
5
9
|
interface GoogleTokenResponse {
|
|
6
10
|
access_token: string;
|
|
7
11
|
refresh_token?: string;
|
|
@@ -13,12 +17,15 @@ interface GoogleCalendarEvent {
|
|
|
13
17
|
id?: string;
|
|
14
18
|
summary: string;
|
|
15
19
|
description?: string;
|
|
20
|
+
location?: string;
|
|
16
21
|
start: {
|
|
17
|
-
dateTime
|
|
22
|
+
dateTime?: string;
|
|
23
|
+
date?: string;
|
|
18
24
|
timeZone?: string;
|
|
19
25
|
};
|
|
20
26
|
end: {
|
|
21
|
-
dateTime
|
|
27
|
+
dateTime?: string;
|
|
28
|
+
date?: string;
|
|
22
29
|
timeZone?: string;
|
|
23
30
|
};
|
|
24
31
|
attendees?: Array<{ email: string }>;
|
|
@@ -37,12 +44,15 @@ interface GoogleCalendarEventResponse {
|
|
|
37
44
|
id: string;
|
|
38
45
|
summary: string;
|
|
39
46
|
description?: string;
|
|
47
|
+
location?: string;
|
|
40
48
|
start: {
|
|
41
|
-
dateTime
|
|
49
|
+
dateTime?: string;
|
|
50
|
+
date?: string;
|
|
42
51
|
timeZone?: string;
|
|
43
52
|
};
|
|
44
53
|
end: {
|
|
45
|
-
dateTime
|
|
54
|
+
dateTime?: string;
|
|
55
|
+
date?: string;
|
|
46
56
|
timeZone?: string;
|
|
47
57
|
};
|
|
48
58
|
attendees?: Array<{ email: string }>;
|
|
@@ -74,7 +84,7 @@ export async function exchangeGoogleCodeForTokens(
|
|
|
74
84
|
throw new Error('GOOGLE_CLIENT_ID et GOOGLE_CLIENT_SECRET doivent être configurés');
|
|
75
85
|
}
|
|
76
86
|
|
|
77
|
-
const response = await
|
|
87
|
+
const response = await googleFetch('https://oauth2.googleapis.com/token', {
|
|
78
88
|
method: 'POST',
|
|
79
89
|
headers: {
|
|
80
90
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -96,6 +106,28 @@ export async function exchangeGoogleCodeForTokens(
|
|
|
96
106
|
return response.json();
|
|
97
107
|
}
|
|
98
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Récupère le compte Google de l'utilisateur courant
|
|
111
|
+
* Chaque utilisateur utilise son propre compte Google pour Google Calendar
|
|
112
|
+
*/
|
|
113
|
+
export async function getUserGoogleAccount(userId: string) {
|
|
114
|
+
const googleAccount = await prisma.userGoogleAccount.findUnique({
|
|
115
|
+
where: { userId },
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!googleAccount) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
'Aucun compte Google connecté. Veuillez connecter votre compte Google dans les paramètres pour utiliser Google Calendar.',
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
...googleAccount,
|
|
126
|
+
accessToken: decrypt(googleAccount.accessToken),
|
|
127
|
+
refreshToken: decrypt(googleAccount.refreshToken),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
99
131
|
/**
|
|
100
132
|
* Erreur personnalisée pour les tokens Google invalides
|
|
101
133
|
*/
|
|
@@ -121,7 +153,7 @@ export async function refreshGoogleToken(refreshToken: string): Promise<GoogleTo
|
|
|
121
153
|
throw new Error('GOOGLE_CLIENT_ID et GOOGLE_CLIENT_SECRET doivent être configurés');
|
|
122
154
|
}
|
|
123
155
|
|
|
124
|
-
const response = await
|
|
156
|
+
const response = await googleFetch('https://oauth2.googleapis.com/token', {
|
|
125
157
|
method: 'POST',
|
|
126
158
|
headers: {
|
|
127
159
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -175,8 +207,18 @@ export async function getValidAccessToken(
|
|
|
175
207
|
const fiveMinutesFromNow = new Date(now.getTime() + 5 * 60 * 1000);
|
|
176
208
|
|
|
177
209
|
if (expiresAt <= fiveMinutesFromNow) {
|
|
178
|
-
|
|
179
|
-
|
|
210
|
+
try {
|
|
211
|
+
const newTokens = await refreshGoogleToken(refreshToken);
|
|
212
|
+
return newTokens.access_token;
|
|
213
|
+
} catch (error: any) {
|
|
214
|
+
// Si le token ne peut pas être rafraîchi (révoqué, expiré, etc.)
|
|
215
|
+
if (error instanceof GoogleTokenError && error.isRevoked) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
'La connexion Google a expiré ou a été révoquée. Veuillez reconnecter votre compte Google dans les paramètres.',
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
180
222
|
}
|
|
181
223
|
|
|
182
224
|
return accessToken;
|
|
@@ -189,7 +231,7 @@ export async function createGoogleCalendarEvent(
|
|
|
189
231
|
accessToken: string,
|
|
190
232
|
event: GoogleCalendarEvent,
|
|
191
233
|
): Promise<GoogleCalendarEventResponse> {
|
|
192
|
-
const response = await
|
|
234
|
+
const response = await googleFetch(
|
|
193
235
|
'https://www.googleapis.com/calendar/v3/calendars/primary/events?conferenceDataVersion=1',
|
|
194
236
|
{
|
|
195
237
|
method: 'POST',
|
|
@@ -217,7 +259,7 @@ export async function updateGoogleCalendarEvent(
|
|
|
217
259
|
eventId: string,
|
|
218
260
|
event: Partial<GoogleCalendarEvent>,
|
|
219
261
|
): Promise<GoogleCalendarEventResponse> {
|
|
220
|
-
const response = await
|
|
262
|
+
const response = await googleFetch(
|
|
221
263
|
`https://www.googleapis.com/calendar/v3/calendars/primary/events/${eventId}?conferenceDataVersion=1`,
|
|
222
264
|
{
|
|
223
265
|
method: 'PATCH',
|
|
@@ -244,7 +286,7 @@ export async function deleteGoogleCalendarEvent(
|
|
|
244
286
|
accessToken: string,
|
|
245
287
|
eventId: string,
|
|
246
288
|
): Promise<void> {
|
|
247
|
-
const response = await
|
|
289
|
+
const response = await googleFetch(
|
|
248
290
|
`https://www.googleapis.com/calendar/v3/calendars/primary/events/${eventId}`,
|
|
249
291
|
{
|
|
250
292
|
method: 'DELETE',
|
|
@@ -280,7 +322,7 @@ export async function getGoogleCalendarEvent(
|
|
|
280
322
|
accessToken: string,
|
|
281
323
|
eventId: string,
|
|
282
324
|
): Promise<GoogleCalendarEventResponse> {
|
|
283
|
-
const response = await
|
|
325
|
+
const response = await googleFetch(
|
|
284
326
|
`https://www.googleapis.com/calendar/v3/calendars/primary/events/${eventId}`,
|
|
285
327
|
{
|
|
286
328
|
method: 'GET',
|