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
|
@@ -1,193 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { headers } from 'next/headers';
|
|
3
|
-
import { prisma } from '@/lib/prisma';
|
|
1
|
+
import { getAuthUser } from '@/lib/get-auth-user';
|
|
4
2
|
|
|
5
|
-
/**
|
|
6
|
-
* Vérifie si l'utilisateur actuel a une permission spécifique
|
|
7
|
-
* @param requiredPermission - Code de la permission à vérifier
|
|
8
|
-
* @returns true si l'utilisateur a la permission, false sinon
|
|
9
|
-
*/
|
|
10
3
|
export async function checkPermission(requiredPermission: string): Promise<boolean> {
|
|
11
4
|
try {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (!session) {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const userId = session.user.id;
|
|
21
|
-
|
|
22
|
-
// Récupérer l'utilisateur avec son profil personnalisé si applicable
|
|
23
|
-
const user = await prisma.user.findUnique({
|
|
24
|
-
where: { id: userId },
|
|
25
|
-
include: {
|
|
26
|
-
customRole: true,
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
if (!user) {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Les permissions viennent uniquement du profil assigné
|
|
35
|
-
if (!user.customRole) {
|
|
36
|
-
// Aucun profil assigné = aucune permission
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const userPermissions = user.customRole.permissions as string[];
|
|
41
|
-
|
|
42
|
-
// Vérifier si la permission est dans la liste
|
|
43
|
-
return userPermissions.includes(requiredPermission);
|
|
5
|
+
const authUser = await getAuthUser();
|
|
6
|
+
if (!authUser) return false;
|
|
7
|
+
return authUser.permissions.includes(requiredPermission);
|
|
44
8
|
} catch (error) {
|
|
45
9
|
console.error('Erreur lors de la vérification des permissions:', error);
|
|
46
10
|
return false;
|
|
47
11
|
}
|
|
48
12
|
}
|
|
49
13
|
|
|
50
|
-
/**
|
|
51
|
-
* Vérifie si l'utilisateur actuel a plusieurs permissions
|
|
52
|
-
* @param requiredPermissions - Tableau des codes de permissions à vérifier
|
|
53
|
-
* @param requireAll - Si true, toutes les permissions sont requises. Si false, au moins une est requise
|
|
54
|
-
* @returns true si l'utilisateur a les permissions, false sinon
|
|
55
|
-
*/
|
|
56
14
|
export async function checkPermissions(
|
|
57
|
-
requiredPermissions: string[]
|
|
58
|
-
|
|
59
|
-
): Promise<boolean> {
|
|
15
|
+
...requiredPermissions: string[]
|
|
16
|
+
): Promise<Record<string, boolean>> {
|
|
60
17
|
try {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (!session) {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const userId = session.user.id;
|
|
70
|
-
|
|
71
|
-
// Récupérer l'utilisateur avec son profil personnalisé si applicable
|
|
72
|
-
const user = await prisma.user.findUnique({
|
|
73
|
-
where: { id: userId },
|
|
74
|
-
include: {
|
|
75
|
-
customRole: true,
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
if (!user) {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Les permissions viennent uniquement du profil assigné
|
|
84
|
-
if (!user.customRole) {
|
|
85
|
-
// Aucun profil assigné = aucune permission
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const userPermissions = user.customRole.permissions as string[];
|
|
90
|
-
|
|
91
|
-
// Vérifier les permissions
|
|
92
|
-
if (requireAll) {
|
|
93
|
-
return requiredPermissions.every((perm) => userPermissions.includes(perm));
|
|
94
|
-
} else {
|
|
95
|
-
return requiredPermissions.some((perm) => userPermissions.includes(perm));
|
|
18
|
+
const authUser = await getAuthUser();
|
|
19
|
+
if (!authUser) {
|
|
20
|
+
return Object.fromEntries(requiredPermissions.map((p) => [p, false]));
|
|
96
21
|
}
|
|
22
|
+
const permSet = new Set(authUser.permissions);
|
|
23
|
+
return Object.fromEntries(requiredPermissions.map((p) => [p, permSet.has(p)]));
|
|
97
24
|
} catch (error) {
|
|
98
25
|
console.error('Erreur lors de la vérification des permissions:', error);
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Récupère toutes les permissions de l'utilisateur actuel
|
|
105
|
-
* @returns Tableau des codes de permissions de l'utilisateur
|
|
106
|
-
*/
|
|
107
|
-
export async function getUserPermissions(): Promise<string[]> {
|
|
108
|
-
try {
|
|
109
|
-
const session = await auth.api.getSession({
|
|
110
|
-
headers: await headers(),
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
if (!session) {
|
|
114
|
-
return [];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const userId = session.user.id;
|
|
118
|
-
|
|
119
|
-
// Récupérer l'utilisateur avec son profil personnalisé si applicable
|
|
120
|
-
const user = await prisma.user.findUnique({
|
|
121
|
-
where: { id: userId },
|
|
122
|
-
include: {
|
|
123
|
-
customRole: true,
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
if (!user) {
|
|
128
|
-
return [];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Les permissions viennent uniquement du profil assigné
|
|
132
|
-
if (!user.customRole) {
|
|
133
|
-
// Aucun profil assigné = aucune permission
|
|
134
|
-
return [];
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return user.customRole.permissions as string[];
|
|
138
|
-
} catch (error) {
|
|
139
|
-
console.error('Erreur lors de la récupération des permissions:', error);
|
|
140
|
-
return [];
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Middleware pour protéger une route API avec des permissions
|
|
146
|
-
* Exemple d'utilisation :
|
|
147
|
-
*
|
|
148
|
-
* export async function GET(req: NextRequest) {
|
|
149
|
-
* const hasPermission = await requirePermission('contacts.view_all');
|
|
150
|
-
* if (!hasPermission) {
|
|
151
|
-
* return NextResponse.json({ error: 'Non autorisé' }, { status: 403 });
|
|
152
|
-
* }
|
|
153
|
-
* // ... reste du code
|
|
154
|
-
* }
|
|
155
|
-
*/
|
|
156
|
-
export async function requirePermission(requiredPermission: string): Promise<boolean> {
|
|
157
|
-
return checkPermission(requiredPermission);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Helper pour vérifier si un utilisateur est admin
|
|
162
|
-
* Un admin est un utilisateur avec un profil ayant toutes les permissions
|
|
163
|
-
*/
|
|
164
|
-
export async function isAdmin(): Promise<boolean> {
|
|
165
|
-
try {
|
|
166
|
-
const session = await auth.api.getSession({
|
|
167
|
-
headers: await headers(),
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
if (!session) {
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const userId = session.user.id;
|
|
175
|
-
|
|
176
|
-
const user = await prisma.user.findUnique({
|
|
177
|
-
where: { id: userId },
|
|
178
|
-
include: {
|
|
179
|
-
customRole: true,
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
if (!user || !user.customRole) {
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Vérifier si le profil a la permission de gestion des utilisateurs
|
|
188
|
-
const permissions = user.customRole.permissions as string[];
|
|
189
|
-
return permissions.includes('users.manage_roles');
|
|
190
|
-
} catch (error) {
|
|
191
|
-
return false;
|
|
26
|
+
return Object.fromEntries(requiredPermissions.map((p) => [p, false]));
|
|
192
27
|
}
|
|
193
28
|
}
|
|
@@ -81,7 +81,7 @@ export async function logContactUpdate(
|
|
|
81
81
|
postalCode: 'Code postal',
|
|
82
82
|
civility: 'Civilité',
|
|
83
83
|
origin: 'Origine',
|
|
84
|
-
|
|
84
|
+
company: 'Entreprise',
|
|
85
85
|
closingReason: 'Motif de fermeture',
|
|
86
86
|
};
|
|
87
87
|
|
|
@@ -103,6 +103,8 @@ export async function logContactUpdate(
|
|
|
103
103
|
|
|
104
104
|
const formatValue = (value: any) => {
|
|
105
105
|
if (value === null || value === undefined) return 'Aucun';
|
|
106
|
+
if (typeof value === 'object' && value !== null && 'name' in value)
|
|
107
|
+
return value.name ?? 'Aucun';
|
|
106
108
|
return String(value);
|
|
107
109
|
};
|
|
108
110
|
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import type { ViewFilter, DatePreset } from '@/types/contact-views';
|
|
2
|
+
import { FRENCH_DEPARTMENTS, type Department } from '@/lib/french-regions';
|
|
3
|
+
|
|
4
|
+
function startOfDay(date: Date): Date {
|
|
5
|
+
const d = new Date(date);
|
|
6
|
+
d.setHours(0, 0, 0, 0);
|
|
7
|
+
return d;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function endOfDay(date: Date): Date {
|
|
11
|
+
const d = new Date(date);
|
|
12
|
+
d.setHours(23, 59, 59, 999);
|
|
13
|
+
return d;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getMonday(date: Date): Date {
|
|
17
|
+
const d = new Date(date);
|
|
18
|
+
const day = d.getDay();
|
|
19
|
+
const diff = d.getDate() - day + (day === 0 ? -6 : 1);
|
|
20
|
+
d.setDate(diff);
|
|
21
|
+
return startOfDay(d);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getQuarterStart(date: Date): Date {
|
|
25
|
+
const month = date.getMonth();
|
|
26
|
+
const quarterStartMonth = month - (month % 3);
|
|
27
|
+
return startOfDay(new Date(date.getFullYear(), quarterStartMonth, 1));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getQuarterEnd(date: Date): Date {
|
|
31
|
+
const month = date.getMonth();
|
|
32
|
+
const quarterEndMonth = month - (month % 3) + 3;
|
|
33
|
+
return endOfDay(new Date(date.getFullYear(), quarterEndMonth, 0));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resolveDatePreset(preset: DatePreset): { gte?: Date; lte?: Date } {
|
|
37
|
+
const now = new Date();
|
|
38
|
+
const today = startOfDay(now);
|
|
39
|
+
|
|
40
|
+
switch (preset) {
|
|
41
|
+
case 'today':
|
|
42
|
+
return { gte: startOfDay(today), lte: endOfDay(today) };
|
|
43
|
+
case 'yesterday': {
|
|
44
|
+
const d = new Date(today);
|
|
45
|
+
d.setDate(d.getDate() - 1);
|
|
46
|
+
return { gte: startOfDay(d), lte: endOfDay(d) };
|
|
47
|
+
}
|
|
48
|
+
case 'tomorrow': {
|
|
49
|
+
const d = new Date(today);
|
|
50
|
+
d.setDate(d.getDate() + 1);
|
|
51
|
+
return { gte: startOfDay(d), lte: endOfDay(d) };
|
|
52
|
+
}
|
|
53
|
+
case 'this_week': {
|
|
54
|
+
const monday = getMonday(today);
|
|
55
|
+
const sunday = new Date(monday);
|
|
56
|
+
sunday.setDate(sunday.getDate() + 6);
|
|
57
|
+
return { gte: monday, lte: endOfDay(sunday) };
|
|
58
|
+
}
|
|
59
|
+
case 'this_week_so_far': {
|
|
60
|
+
const monday = getMonday(today);
|
|
61
|
+
return { gte: monday, lte: endOfDay(today) };
|
|
62
|
+
}
|
|
63
|
+
case 'last_week': {
|
|
64
|
+
const monday = getMonday(today);
|
|
65
|
+
monday.setDate(monday.getDate() - 7);
|
|
66
|
+
const sunday = new Date(monday);
|
|
67
|
+
sunday.setDate(sunday.getDate() + 6);
|
|
68
|
+
return { gte: monday, lte: endOfDay(sunday) };
|
|
69
|
+
}
|
|
70
|
+
case 'next_week': {
|
|
71
|
+
const monday = getMonday(today);
|
|
72
|
+
monday.setDate(monday.getDate() + 7);
|
|
73
|
+
const sunday = new Date(monday);
|
|
74
|
+
sunday.setDate(sunday.getDate() + 6);
|
|
75
|
+
return { gte: monday, lte: endOfDay(sunday) };
|
|
76
|
+
}
|
|
77
|
+
case 'this_month':
|
|
78
|
+
return {
|
|
79
|
+
gte: startOfDay(new Date(now.getFullYear(), now.getMonth(), 1)),
|
|
80
|
+
lte: endOfDay(new Date(now.getFullYear(), now.getMonth() + 1, 0)),
|
|
81
|
+
};
|
|
82
|
+
case 'this_month_so_far':
|
|
83
|
+
return {
|
|
84
|
+
gte: startOfDay(new Date(now.getFullYear(), now.getMonth(), 1)),
|
|
85
|
+
lte: endOfDay(today),
|
|
86
|
+
};
|
|
87
|
+
case 'last_month':
|
|
88
|
+
return {
|
|
89
|
+
gte: startOfDay(new Date(now.getFullYear(), now.getMonth() - 1, 1)),
|
|
90
|
+
lte: endOfDay(new Date(now.getFullYear(), now.getMonth(), 0)),
|
|
91
|
+
};
|
|
92
|
+
case 'next_month':
|
|
93
|
+
return {
|
|
94
|
+
gte: startOfDay(new Date(now.getFullYear(), now.getMonth() + 1, 1)),
|
|
95
|
+
lte: endOfDay(new Date(now.getFullYear(), now.getMonth() + 2, 0)),
|
|
96
|
+
};
|
|
97
|
+
case 'this_quarter':
|
|
98
|
+
return { gte: getQuarterStart(now), lte: getQuarterEnd(now) };
|
|
99
|
+
case 'this_quarter_so_far':
|
|
100
|
+
return { gte: getQuarterStart(now), lte: endOfDay(today) };
|
|
101
|
+
case 'last_quarter': {
|
|
102
|
+
const prevQuarter = new Date(now);
|
|
103
|
+
prevQuarter.setMonth(prevQuarter.getMonth() - 3);
|
|
104
|
+
return {
|
|
105
|
+
gte: getQuarterStart(prevQuarter),
|
|
106
|
+
lte: getQuarterEnd(prevQuarter),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
case 'this_year':
|
|
110
|
+
return {
|
|
111
|
+
gte: startOfDay(new Date(now.getFullYear(), 0, 1)),
|
|
112
|
+
lte: endOfDay(new Date(now.getFullYear(), 11, 31)),
|
|
113
|
+
};
|
|
114
|
+
case 'this_year_so_far':
|
|
115
|
+
return {
|
|
116
|
+
gte: startOfDay(new Date(now.getFullYear(), 0, 1)),
|
|
117
|
+
lte: endOfDay(today),
|
|
118
|
+
};
|
|
119
|
+
case 'last_year':
|
|
120
|
+
return {
|
|
121
|
+
gte: startOfDay(new Date(now.getFullYear() - 1, 0, 1)),
|
|
122
|
+
lte: endOfDay(new Date(now.getFullYear() - 1, 11, 31)),
|
|
123
|
+
};
|
|
124
|
+
case 'last_7_days':
|
|
125
|
+
case 'last_14_days':
|
|
126
|
+
case 'last_30_days':
|
|
127
|
+
case 'last_60_days':
|
|
128
|
+
case 'last_90_days':
|
|
129
|
+
case 'last_180_days':
|
|
130
|
+
case 'last_365_days': {
|
|
131
|
+
const daysMap: Record<string, number> = {
|
|
132
|
+
last_7_days: 7,
|
|
133
|
+
last_14_days: 14,
|
|
134
|
+
last_30_days: 30,
|
|
135
|
+
last_60_days: 60,
|
|
136
|
+
last_90_days: 90,
|
|
137
|
+
last_180_days: 180,
|
|
138
|
+
last_365_days: 365,
|
|
139
|
+
};
|
|
140
|
+
const d = new Date(today);
|
|
141
|
+
d.setDate(d.getDate() - daysMap[preset]);
|
|
142
|
+
return { gte: startOfDay(d), lte: endOfDay(today) };
|
|
143
|
+
}
|
|
144
|
+
default:
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const departmentsByRegion = new Map<string, Department[]>();
|
|
150
|
+
for (const dept of FRENCH_DEPARTMENTS) {
|
|
151
|
+
const list = departmentsByRegion.get(dept.regionCode) ?? [];
|
|
152
|
+
list.push(dept);
|
|
153
|
+
departmentsByRegion.set(dept.regionCode, list);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildPostalCodeConditions(deptCodes: string[], negate: boolean): any | null {
|
|
157
|
+
if (deptCodes.length === 0) return null;
|
|
158
|
+
|
|
159
|
+
const startsWithConditions: any[] = [];
|
|
160
|
+
const corsicaCodes: string[] = [];
|
|
161
|
+
|
|
162
|
+
for (const code of deptCodes) {
|
|
163
|
+
if (code === '2A' || code === '2B') {
|
|
164
|
+
corsicaCodes.push(code);
|
|
165
|
+
} else {
|
|
166
|
+
startsWithConditions.push({ postalCode: { startsWith: code } });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (corsicaCodes.length > 0) {
|
|
171
|
+
if (corsicaCodes.includes('2A') && corsicaCodes.includes('2B')) {
|
|
172
|
+
startsWithConditions.push({ postalCode: { startsWith: '20' } });
|
|
173
|
+
} else if (corsicaCodes.includes('2A')) {
|
|
174
|
+
startsWithConditions.push({
|
|
175
|
+
AND: [{ postalCode: { startsWith: '20' } }, { postalCode: { lt: '20200' } }],
|
|
176
|
+
});
|
|
177
|
+
} else {
|
|
178
|
+
startsWithConditions.push({
|
|
179
|
+
AND: [{ postalCode: { startsWith: '20' } }, { postalCode: { gte: '20200' } }],
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (startsWithConditions.length === 0) return null;
|
|
185
|
+
|
|
186
|
+
const orCondition =
|
|
187
|
+
startsWithConditions.length === 1 ? startsWithConditions[0] : { OR: startsWithConditions };
|
|
188
|
+
|
|
189
|
+
return negate ? { NOT: orCondition } : orCondition;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function buildGeoCondition(filter: ViewFilter): any | null {
|
|
193
|
+
const { field, operator, value } = filter;
|
|
194
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
195
|
+
|
|
196
|
+
if (field === 'department') {
|
|
197
|
+
return buildPostalCodeConditions(value, operator === 'is_none_of');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (field === 'region') {
|
|
201
|
+
const deptCodes: string[] = [];
|
|
202
|
+
for (const regionCode of value) {
|
|
203
|
+
const depts = departmentsByRegion.get(regionCode);
|
|
204
|
+
if (depts) {
|
|
205
|
+
for (const d of depts) deptCodes.push(d.code);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return buildPostalCodeConditions(deptCodes, operator === 'is_none_of');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function buildFieldCondition(filter: ViewFilter): any | null {
|
|
215
|
+
const { field, operator, value, preset } = filter;
|
|
216
|
+
|
|
217
|
+
if (field === 'region' || field === 'department') {
|
|
218
|
+
return buildGeoCondition(filter);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
switch (operator) {
|
|
222
|
+
case 'equals': {
|
|
223
|
+
if (field === 'createdAt' || field === 'updatedAt') {
|
|
224
|
+
if (!value || typeof value !== 'string') return null;
|
|
225
|
+
const d = new Date(value);
|
|
226
|
+
if (isNaN(d.getTime())) return null;
|
|
227
|
+
return { [field]: { gte: startOfDay(d), lte: endOfDay(d) } };
|
|
228
|
+
}
|
|
229
|
+
return { [field]: value };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
case 'not_equals':
|
|
233
|
+
return { NOT: { [field]: value } };
|
|
234
|
+
|
|
235
|
+
case 'contains':
|
|
236
|
+
return { [field]: { contains: value as string, mode: 'insensitive' } };
|
|
237
|
+
|
|
238
|
+
case 'not_contains':
|
|
239
|
+
return {
|
|
240
|
+
NOT: { [field]: { contains: value as string, mode: 'insensitive' } },
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
case 'starts_with':
|
|
244
|
+
return {
|
|
245
|
+
[field]: { startsWith: value as string, mode: 'insensitive' },
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
case 'ends_with':
|
|
249
|
+
return { [field]: { endsWith: value as string, mode: 'insensitive' } };
|
|
250
|
+
|
|
251
|
+
case 'is_any_of': {
|
|
252
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
253
|
+
const hasUnassigned = value.includes('UNASSIGNED');
|
|
254
|
+
const realValues = value.filter((v) => v !== 'UNASSIGNED');
|
|
255
|
+
|
|
256
|
+
if (hasUnassigned && realValues.length > 0) {
|
|
257
|
+
return { OR: [{ [field]: null }, { [field]: { in: realValues } }] };
|
|
258
|
+
} else if (hasUnassigned) {
|
|
259
|
+
return { [field]: null };
|
|
260
|
+
}
|
|
261
|
+
return { [field]: { in: realValues } };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
case 'is_none_of': {
|
|
265
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
266
|
+
const hasUnassigned = value.includes('UNASSIGNED');
|
|
267
|
+
const realValues = value.filter((v) => v !== 'UNASSIGNED');
|
|
268
|
+
|
|
269
|
+
if (hasUnassigned && realValues.length > 0) {
|
|
270
|
+
return {
|
|
271
|
+
AND: [{ [field]: { not: null } }, { [field]: { notIn: realValues } }],
|
|
272
|
+
};
|
|
273
|
+
} else if (hasUnassigned) {
|
|
274
|
+
return { [field]: { not: null } };
|
|
275
|
+
}
|
|
276
|
+
return { [field]: { notIn: realValues } };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
case 'gt': {
|
|
280
|
+
const d = new Date(value as string);
|
|
281
|
+
if (isNaN(d.getTime())) return null;
|
|
282
|
+
return { [field]: { gt: endOfDay(d) } };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
case 'gte': {
|
|
286
|
+
const d = new Date(value as string);
|
|
287
|
+
if (isNaN(d.getTime())) return null;
|
|
288
|
+
return { [field]: { gte: startOfDay(d) } };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
case 'lt': {
|
|
292
|
+
const d = new Date(value as string);
|
|
293
|
+
if (isNaN(d.getTime())) return null;
|
|
294
|
+
return { [field]: { lt: startOfDay(d) } };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
case 'lte': {
|
|
298
|
+
const d = new Date(value as string);
|
|
299
|
+
if (isNaN(d.getTime())) return null;
|
|
300
|
+
return { [field]: { lte: endOfDay(d) } };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
case 'between': {
|
|
304
|
+
if (!Array.isArray(value) || value.length !== 2) return null;
|
|
305
|
+
const start = new Date(value[0]);
|
|
306
|
+
const end = new Date(value[1]);
|
|
307
|
+
if (isNaN(start.getTime()) || isNaN(end.getTime())) return null;
|
|
308
|
+
return { [field]: { gte: startOfDay(start), lte: endOfDay(end) } };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
case 'is_known':
|
|
312
|
+
return { [field]: { not: null } };
|
|
313
|
+
|
|
314
|
+
case 'is_unknown':
|
|
315
|
+
return { [field]: null };
|
|
316
|
+
|
|
317
|
+
case 'date_preset': {
|
|
318
|
+
if (!preset) return null;
|
|
319
|
+
const range = resolveDatePreset(preset);
|
|
320
|
+
return { [field]: range };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
default:
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function buildPrismaWhereFromFilters(filters: ViewFilter[]): Record<string, any> {
|
|
329
|
+
const conditions: any[] = [];
|
|
330
|
+
|
|
331
|
+
for (const filter of filters) {
|
|
332
|
+
const condition = buildFieldCondition(filter);
|
|
333
|
+
if (condition) {
|
|
334
|
+
conditions.push(condition);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (conditions.length === 0) return {};
|
|
339
|
+
if (conditions.length === 1) return conditions[0];
|
|
340
|
+
return { AND: conditions };
|
|
341
|
+
}
|