create-crm-tmp 1.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/bin/create-crm-tmp.js +93 -0
- package/package.json +25 -0
- package/template/.prettierignore +33 -0
- package/template/.prettierrc.json +25 -0
- package/template/README.md +173 -0
- package/template/eslint.config.mjs +18 -0
- package/template/exemple-contacts.csv +11 -0
- package/template/next.config.ts +8 -0
- package/template/package.json +64 -0
- package/template/postcss.config.mjs +7 -0
- package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
- package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +582 -0
- package/template/prisma.config.ts +14 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
- package/template/src/app/(auth)/layout.tsx +3 -0
- package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
- package/template/src/app/(auth)/reset-password/page.tsx +146 -0
- package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
- package/template/src/app/(auth)/signin/page.tsx +166 -0
- package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
- package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
- package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
- package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
- package/template/src/app/(dashboard)/layout.tsx +30 -0
- package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
- package/template/src/app/(dashboard)/templates/page.tsx +567 -0
- package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
- package/template/src/app/(dashboard)/users/page.tsx +457 -0
- package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
- package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
- package/template/src/app/api/audit-logs/route.ts +57 -0
- package/template/src/app/api/auth/[...all]/route.ts +4 -0
- package/template/src/app/api/auth/check-active/route.ts +31 -0
- package/template/src/app/api/auth/google/callback/route.ts +94 -0
- package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
- package/template/src/app/api/auth/google/route.ts +34 -0
- package/template/src/app/api/auth/google/status/route.ts +32 -0
- package/template/src/app/api/closing-reasons/route.ts +27 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
- package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
- package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
- package/template/src/app/api/contacts/[id]/route.ts +322 -0
- package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
- package/template/src/app/api/contacts/export/route.ts +270 -0
- package/template/src/app/api/contacts/import/route.ts +381 -0
- package/template/src/app/api/contacts/route.ts +283 -0
- package/template/src/app/api/dashboard/stats/route.ts +299 -0
- package/template/src/app/api/email/track/[id]/route.ts +68 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
- package/template/src/app/api/invite/complete/route.ts +88 -0
- package/template/src/app/api/invite/validate/route.ts +55 -0
- package/template/src/app/api/reminders/route.ts +95 -0
- package/template/src/app/api/reset-password/complete/route.ts +73 -0
- package/template/src/app/api/reset-password/request/route.ts +84 -0
- package/template/src/app/api/reset-password/validate/route.ts +49 -0
- package/template/src/app/api/reset-password/verify/route.ts +74 -0
- package/template/src/app/api/roles/[id]/route.ts +183 -0
- package/template/src/app/api/roles/route.ts +140 -0
- package/template/src/app/api/send/route.ts +282 -0
- package/template/src/app/api/settings/change-password/route.ts +95 -0
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
- package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
- package/template/src/app/api/settings/company/route.ts +121 -0
- package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
- package/template/src/app/api/settings/google-ads/route.ts +122 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
- package/template/src/app/api/settings/google-sheet/route.ts +254 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
- package/template/src/app/api/settings/meta-leads/route.ts +132 -0
- package/template/src/app/api/settings/profile/route.ts +42 -0
- package/template/src/app/api/settings/smtp/route.ts +130 -0
- package/template/src/app/api/settings/smtp/test/route.ts +121 -0
- package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
- package/template/src/app/api/settings/statuses/route.ts +83 -0
- package/template/src/app/api/statuses/route.ts +25 -0
- package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
- package/template/src/app/api/tasks/[id]/route.ts +728 -0
- package/template/src/app/api/tasks/meet/route.ts +240 -0
- package/template/src/app/api/tasks/route.ts +417 -0
- package/template/src/app/api/templates/[id]/route.ts +140 -0
- package/template/src/app/api/templates/route.ts +91 -0
- package/template/src/app/api/users/[id]/route.ts +168 -0
- package/template/src/app/api/users/list/route.ts +45 -0
- package/template/src/app/api/users/me/route.ts +48 -0
- package/template/src/app/api/users/route.ts +250 -0
- package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
- package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
- package/template/src/app/api/workflows/[id]/route.ts +192 -0
- package/template/src/app/api/workflows/process/route.ts +293 -0
- package/template/src/app/api/workflows/route.ts +124 -0
- package/template/src/app/favicon.ico +0 -0
- package/template/src/app/globals.css +1416 -0
- package/template/src/app/layout.tsx +31 -0
- package/template/src/app/page.tsx +32 -0
- package/template/src/components/dashboard/activity-chart.tsx +67 -0
- package/template/src/components/dashboard/contacts-chart.tsx +63 -0
- package/template/src/components/dashboard/recent-activity.tsx +164 -0
- package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
- package/template/src/components/dashboard/stat-card.tsx +61 -0
- package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
- package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
- package/template/src/components/editor.tsx +856 -0
- package/template/src/components/email-template.tsx +35 -0
- package/template/src/components/header.tsx +320 -0
- package/template/src/components/invitation-email-template.tsx +79 -0
- package/template/src/components/meet-cancellation-email-template.tsx +120 -0
- package/template/src/components/meet-confirmation-email-template.tsx +156 -0
- package/template/src/components/meet-update-email-template.tsx +209 -0
- package/template/src/components/page-header.tsx +61 -0
- package/template/src/components/reset-password-email-template.tsx +79 -0
- package/template/src/components/sidebar.tsx +294 -0
- package/template/src/components/skeleton.tsx +380 -0
- package/template/src/components/ui/commands.tsx +396 -0
- package/template/src/components/ui/components.tsx +150 -0
- package/template/src/components/ui/theme.tsx +5 -0
- package/template/src/components/view-as-banner.tsx +45 -0
- package/template/src/components/view-as-modal.tsx +186 -0
- package/template/src/contexts/mobile-menu-context.tsx +31 -0
- package/template/src/contexts/sidebar-context.tsx +107 -0
- package/template/src/contexts/task-reminder-context.tsx +239 -0
- package/template/src/contexts/view-as-context.tsx +84 -0
- package/template/src/hooks/use-user-role.ts +82 -0
- package/template/src/lib/audit-log.ts +45 -0
- package/template/src/lib/auth-client.ts +16 -0
- package/template/src/lib/auth.ts +35 -0
- package/template/src/lib/check-permission.ts +193 -0
- package/template/src/lib/contact-duplicate.ts +112 -0
- package/template/src/lib/contact-interactions.ts +371 -0
- package/template/src/lib/encryption.ts +99 -0
- package/template/src/lib/google-calendar.ts +300 -0
- package/template/src/lib/google-drive.ts +372 -0
- package/template/src/lib/permissions.ts +412 -0
- package/template/src/lib/prisma.ts +32 -0
- package/template/src/lib/roles.ts +120 -0
- package/template/src/lib/template-variables.ts +76 -0
- package/template/src/lib/utils.ts +46 -0
- package/template/src/lib/workflow-executor.ts +482 -0
- package/template/src/proxy.ts +91 -0
- package/template/tsconfig.json +34 -0
- package/template/vercel.json +8 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import {
|
|
6
|
+
Users,
|
|
7
|
+
Shield,
|
|
8
|
+
Key,
|
|
9
|
+
ArrowLeft,
|
|
10
|
+
UserPlus,
|
|
11
|
+
UserCog,
|
|
12
|
+
ShieldPlus,
|
|
13
|
+
ShieldCheck,
|
|
14
|
+
ShieldX,
|
|
15
|
+
} from 'lucide-react';
|
|
16
|
+
import { PERMISSIONS } from '@/lib/permissions';
|
|
17
|
+
import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
|
|
18
|
+
|
|
19
|
+
interface Stats {
|
|
20
|
+
activeUsers: number;
|
|
21
|
+
totalUsers: number;
|
|
22
|
+
rolesCount: number;
|
|
23
|
+
permissionsCount: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface AuditLog {
|
|
27
|
+
id: string;
|
|
28
|
+
action: string;
|
|
29
|
+
entityType: string;
|
|
30
|
+
entityId?: string | null;
|
|
31
|
+
metadata?: any;
|
|
32
|
+
createdAt: string;
|
|
33
|
+
actor?: {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string | null;
|
|
36
|
+
email: string;
|
|
37
|
+
} | null;
|
|
38
|
+
targetUser?: {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string | null;
|
|
41
|
+
email: string;
|
|
42
|
+
} | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default function AccessControlPage() {
|
|
46
|
+
const { toggle: toggleMobileMenu, isOpen: isMobileMenuOpen } = useMobileMenuContext();
|
|
47
|
+
const [stats, setStats] = useState<Stats>({
|
|
48
|
+
activeUsers: 0,
|
|
49
|
+
totalUsers: 0,
|
|
50
|
+
rolesCount: 0,
|
|
51
|
+
permissionsCount: PERMISSIONS.length,
|
|
52
|
+
});
|
|
53
|
+
const [loading, setLoading] = useState(true);
|
|
54
|
+
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
|
|
55
|
+
const [auditLoading, setAuditLoading] = useState(true);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
async function fetchStats() {
|
|
59
|
+
try {
|
|
60
|
+
const [usersResponse, rolesResponse] = await Promise.all([
|
|
61
|
+
fetch('/api/users'),
|
|
62
|
+
fetch('/api/roles'),
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
if (usersResponse.ok) {
|
|
66
|
+
const users = await usersResponse.json();
|
|
67
|
+
setStats((prev) => ({
|
|
68
|
+
...prev,
|
|
69
|
+
activeUsers: users.filter((u: { active: boolean }) => u.active).length,
|
|
70
|
+
totalUsers: users.length,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (rolesResponse.ok) {
|
|
75
|
+
const roles = await rolesResponse.json();
|
|
76
|
+
setStats((prev) => ({
|
|
77
|
+
...prev,
|
|
78
|
+
rolesCount: roles.length,
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('Erreur lors du chargement des stats:', error);
|
|
83
|
+
} finally {
|
|
84
|
+
setLoading(false);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fetchStats();
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
async function fetchAuditLogs() {
|
|
93
|
+
try {
|
|
94
|
+
setAuditLoading(true);
|
|
95
|
+
const response = await fetch('/api/audit-logs?limit=10');
|
|
96
|
+
if (response.ok) {
|
|
97
|
+
const data = await response.json();
|
|
98
|
+
setAuditLogs(
|
|
99
|
+
data.map((log: any) => ({
|
|
100
|
+
...log,
|
|
101
|
+
createdAt: log.createdAt,
|
|
102
|
+
})),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Erreur lors du chargement des logs d’audit:', error);
|
|
107
|
+
} finally {
|
|
108
|
+
setAuditLoading(false);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fetchAuditLogs();
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
const cards = [
|
|
116
|
+
{
|
|
117
|
+
title: 'Utilisateurs',
|
|
118
|
+
subtitle: 'Gérer les comptes utilisateurs',
|
|
119
|
+
description:
|
|
120
|
+
'Créer, modifier et administrer les utilisateurs du système. Activer ou désactiver les comptes selon les besoins.',
|
|
121
|
+
icon: Users,
|
|
122
|
+
iconBg: 'bg-orange-100',
|
|
123
|
+
iconColor: 'text-orange-600',
|
|
124
|
+
href: '/users/list',
|
|
125
|
+
stat: loading ? '...' : stats.activeUsers,
|
|
126
|
+
statLabel: 'Utilisateurs actifs',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
title: 'Profils',
|
|
130
|
+
subtitle: 'Définir les rôles',
|
|
131
|
+
description:
|
|
132
|
+
'Créer et configurer les profils utilisateur (Admin, Manager, Compta, etc.) avec leurs descriptions et attributions.',
|
|
133
|
+
icon: Shield,
|
|
134
|
+
iconBg: 'bg-green-100',
|
|
135
|
+
iconColor: 'text-green-600',
|
|
136
|
+
href: '/users/roles',
|
|
137
|
+
stat: stats.rolesCount,
|
|
138
|
+
statLabel: 'Profils configurés',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
title: 'Permissions',
|
|
142
|
+
subtitle: 'Configurer les droits',
|
|
143
|
+
description:
|
|
144
|
+
"Assigner les permissions aux profils et contrôler l'accès aux différentes fonctionnalités du système.",
|
|
145
|
+
icon: Key,
|
|
146
|
+
iconBg: 'bg-purple-100',
|
|
147
|
+
iconColor: 'text-purple-600',
|
|
148
|
+
href: '/users/permissions',
|
|
149
|
+
stat: stats.permissionsCount,
|
|
150
|
+
statLabel: 'Permissions disponibles',
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<div className="h-full">
|
|
156
|
+
<div className="border-b border-gray-200 bg-white">
|
|
157
|
+
<div className="p-4 sm:p-6 flex gap-3">
|
|
158
|
+
<div className="mb-4 flex items-center gap-3">
|
|
159
|
+
{/* Mobile menu button */}
|
|
160
|
+
<button
|
|
161
|
+
onClick={toggleMobileMenu}
|
|
162
|
+
className="shrink-0 cursor-pointer rounded-lg p-2 text-gray-700 transition-colors hover:bg-gray-100 lg:hidden"
|
|
163
|
+
aria-label="Toggle menu"
|
|
164
|
+
>
|
|
165
|
+
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
166
|
+
{isMobileMenuOpen ? (
|
|
167
|
+
<path
|
|
168
|
+
strokeLinecap="round"
|
|
169
|
+
strokeLinejoin="round"
|
|
170
|
+
strokeWidth={2}
|
|
171
|
+
d="M6 18L18 6M6 6l12 12"
|
|
172
|
+
/>
|
|
173
|
+
) : (
|
|
174
|
+
<path
|
|
175
|
+
strokeLinecap="round"
|
|
176
|
+
strokeLinejoin="round"
|
|
177
|
+
strokeWidth={2}
|
|
178
|
+
d="M4 6h16M4 12h16M4 18h16"
|
|
179
|
+
/>
|
|
180
|
+
)}
|
|
181
|
+
</svg>
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
<div>
|
|
185
|
+
<h1 className="text-2xl font-bold text-gray-900">Droits d'accès</h1>
|
|
186
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
187
|
+
Gérer les utilisateurs, profils et permissions de votre système
|
|
188
|
+
</p>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div className="p-4 sm:p-6 space-y-8">
|
|
194
|
+
<div className="grid gap-6 lg:grid-cols-3">
|
|
195
|
+
{cards.map((card) => {
|
|
196
|
+
const Icon = card.icon;
|
|
197
|
+
return (
|
|
198
|
+
<Link
|
|
199
|
+
key={card.title}
|
|
200
|
+
href={card.href}
|
|
201
|
+
className="group cursor-pointer rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-all hover:border-indigo-300 hover:shadow-md"
|
|
202
|
+
>
|
|
203
|
+
<div className="flex items-start gap-4">
|
|
204
|
+
<div className={`rounded-lg p-3 ${card.iconBg}`}>
|
|
205
|
+
<Icon className={`h-6 w-6 ${card.iconColor}`} />
|
|
206
|
+
</div>
|
|
207
|
+
<div className="flex-1">
|
|
208
|
+
<h3 className="text-lg font-semibold text-gray-900 group-hover:text-indigo-600">
|
|
209
|
+
{card.title}
|
|
210
|
+
</h3>
|
|
211
|
+
<p className="text-sm font-medium text-gray-600">{card.subtitle}</p>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<p className="mt-4 text-sm text-gray-600">{card.description}</p>
|
|
216
|
+
|
|
217
|
+
<div className="mt-6 flex items-baseline gap-2">
|
|
218
|
+
<span className="text-2xl font-bold text-gray-900">{card.stat}</span>
|
|
219
|
+
<span className="text-sm text-gray-500">{card.statLabel}</span>
|
|
220
|
+
</div>
|
|
221
|
+
</Link>
|
|
222
|
+
);
|
|
223
|
+
})}
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
{/* Historique des modifications récentes */}
|
|
227
|
+
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
|
|
228
|
+
<div className="mb-4 flex items-center justify-between">
|
|
229
|
+
<div>
|
|
230
|
+
<h2 className="text-base font-semibold text-gray-900">
|
|
231
|
+
Historique des modifications récentes
|
|
232
|
+
</h2>
|
|
233
|
+
<p className="text-sm text-gray-500">
|
|
234
|
+
Les 10 dernières actions effectuées sur les utilisateurs et profils
|
|
235
|
+
</p>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
{auditLoading ? (
|
|
240
|
+
<p className="text-sm text-gray-500">Chargement de l’historique...</p>
|
|
241
|
+
) : auditLogs.length === 0 ? (
|
|
242
|
+
<p className="text-sm text-gray-500">Aucune action récente.</p>
|
|
243
|
+
) : (
|
|
244
|
+
<ul className="divide-y divide-gray-100">
|
|
245
|
+
{auditLogs.map((log) => {
|
|
246
|
+
const actorLabel = log.actor?.name || log.actor?.email || 'Système';
|
|
247
|
+
const dateLabel = new Date(log.createdAt).toLocaleString('fr-FR', {
|
|
248
|
+
day: '2-digit',
|
|
249
|
+
month: 'short',
|
|
250
|
+
year: 'numeric',
|
|
251
|
+
hour: '2-digit',
|
|
252
|
+
minute: '2-digit',
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Traduction des actions et sélection de l'icône
|
|
256
|
+
let title = '';
|
|
257
|
+
let subtitle = '';
|
|
258
|
+
let details: React.ReactElement[] = [];
|
|
259
|
+
let IconComponent: React.ComponentType<{ className?: string }> = Shield;
|
|
260
|
+
|
|
261
|
+
if (log.action === 'USER_CREATED') {
|
|
262
|
+
title = `${actorLabel} • Ajout Utilisateur`;
|
|
263
|
+
IconComponent = UserPlus;
|
|
264
|
+
const metadata = log.metadata as {
|
|
265
|
+
name?: string;
|
|
266
|
+
email?: string;
|
|
267
|
+
customRoleId?: string;
|
|
268
|
+
customRoleName?: string;
|
|
269
|
+
wasExisting?: boolean;
|
|
270
|
+
};
|
|
271
|
+
if (metadata?.name) {
|
|
272
|
+
subtitle = metadata.name;
|
|
273
|
+
}
|
|
274
|
+
details = [
|
|
275
|
+
metadata?.email && (
|
|
276
|
+
<li key="email" className="text-xs text-gray-600">
|
|
277
|
+
<span className="font-medium">Email :</span> {metadata.email}
|
|
278
|
+
</li>
|
|
279
|
+
),
|
|
280
|
+
metadata?.name && (
|
|
281
|
+
<li key="name" className="text-xs text-gray-600">
|
|
282
|
+
<span className="font-medium">Nom :</span> {metadata.name}
|
|
283
|
+
</li>
|
|
284
|
+
),
|
|
285
|
+
metadata?.customRoleName && (
|
|
286
|
+
<li key="profil" className="text-xs text-gray-600">
|
|
287
|
+
<span className="font-medium">Profil :</span> {metadata.customRoleName}
|
|
288
|
+
</li>
|
|
289
|
+
),
|
|
290
|
+
].filter(Boolean) as React.ReactElement[];
|
|
291
|
+
} else if (log.action === 'USER_UPDATED') {
|
|
292
|
+
title = `${actorLabel} • Modification Utilisateur`;
|
|
293
|
+
IconComponent = UserCog;
|
|
294
|
+
if (log.targetUser?.name) {
|
|
295
|
+
subtitle = log.targetUser.name;
|
|
296
|
+
}
|
|
297
|
+
const changes = log.metadata?.changes as
|
|
298
|
+
| Record<string, { old: any; new: any }>
|
|
299
|
+
| undefined;
|
|
300
|
+
if (changes) {
|
|
301
|
+
const fieldLabels: Record<string, string> = {
|
|
302
|
+
name: 'Nom',
|
|
303
|
+
email: 'Email',
|
|
304
|
+
customRoleId: 'Profil',
|
|
305
|
+
profil: 'Profil',
|
|
306
|
+
active: 'Statut',
|
|
307
|
+
phone: 'Téléphone',
|
|
308
|
+
companyId: 'Société',
|
|
309
|
+
folderView: 'Vue des dossiers',
|
|
310
|
+
viewUnassignedContacts: 'Voir contacts non attribués',
|
|
311
|
+
};
|
|
312
|
+
details = Object.entries(changes).map(([field, value]) => {
|
|
313
|
+
const label = fieldLabels[field] || field;
|
|
314
|
+
const oldValue =
|
|
315
|
+
value.old === null || value.old === undefined || value.old === ''
|
|
316
|
+
? 'vide'
|
|
317
|
+
: String(value.old);
|
|
318
|
+
const newValue =
|
|
319
|
+
value.new === null || value.new === undefined || value.new === ''
|
|
320
|
+
? 'vide'
|
|
321
|
+
: String(value.new);
|
|
322
|
+
return (
|
|
323
|
+
<li key={field} className="text-xs text-gray-600">
|
|
324
|
+
<span className="font-medium">{label} :</span>{' '}
|
|
325
|
+
<span className="line-through text-red-500">{oldValue}</span>{' '}
|
|
326
|
+
<span className="text-green-600">→ {newValue}</span>
|
|
327
|
+
</li>
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
} else if (log.action === 'ROLE_CREATED') {
|
|
332
|
+
title = `${actorLabel} • Ajout Profil`;
|
|
333
|
+
IconComponent = ShieldPlus;
|
|
334
|
+
const metadata = log.metadata as {
|
|
335
|
+
name?: string;
|
|
336
|
+
description?: string;
|
|
337
|
+
permissions?: string[];
|
|
338
|
+
};
|
|
339
|
+
if (metadata?.name) {
|
|
340
|
+
subtitle = metadata.name;
|
|
341
|
+
}
|
|
342
|
+
details = [
|
|
343
|
+
metadata?.description && (
|
|
344
|
+
<li key="description" className="text-xs text-gray-600">
|
|
345
|
+
<span className="font-medium">Description :</span> {metadata.description}
|
|
346
|
+
</li>
|
|
347
|
+
),
|
|
348
|
+
metadata?.permissions && (
|
|
349
|
+
<li key="permissions" className="text-xs text-gray-600">
|
|
350
|
+
<span className="font-medium">Permissions :</span>{' '}
|
|
351
|
+
{metadata.permissions.length} permission
|
|
352
|
+
{metadata.permissions.length > 1 ? 's' : ''}
|
|
353
|
+
</li>
|
|
354
|
+
),
|
|
355
|
+
].filter(Boolean) as React.ReactElement[];
|
|
356
|
+
} else if (log.action === 'ROLE_UPDATED') {
|
|
357
|
+
title = `${actorLabel} • Modification Profil`;
|
|
358
|
+
IconComponent = ShieldCheck;
|
|
359
|
+
const metadata = log.metadata as {
|
|
360
|
+
before?: { name?: string; description?: string; permissions?: string[] };
|
|
361
|
+
after?: { name?: string; description?: string; permissions?: string[] };
|
|
362
|
+
};
|
|
363
|
+
if (metadata?.after?.name) {
|
|
364
|
+
subtitle = metadata.after.name;
|
|
365
|
+
}
|
|
366
|
+
if (metadata?.before && metadata?.after) {
|
|
367
|
+
const before = metadata.before;
|
|
368
|
+
const after = metadata.after;
|
|
369
|
+
if (before.name !== after.name) {
|
|
370
|
+
details.push(
|
|
371
|
+
<li key="name" className="text-xs text-gray-600">
|
|
372
|
+
<span className="font-medium">Nom :</span>{' '}
|
|
373
|
+
<span className="line-through text-red-500">{before.name || 'vide'}</span>{' '}
|
|
374
|
+
<span className="text-green-600">→ {after.name || 'vide'}</span>
|
|
375
|
+
</li>,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
if (before.description !== after.description) {
|
|
379
|
+
details.push(
|
|
380
|
+
<li key="description" className="text-xs text-gray-600">
|
|
381
|
+
<span className="font-medium">Description :</span>{' '}
|
|
382
|
+
<span className="line-through text-red-500">
|
|
383
|
+
{before.description || 'vide'}
|
|
384
|
+
</span>{' '}
|
|
385
|
+
<span className="text-green-600">→ {after.description || 'vide'}</span>
|
|
386
|
+
</li>,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
const beforePerms = (before.permissions || []).length;
|
|
390
|
+
const afterPerms = (after.permissions || []).length;
|
|
391
|
+
if (beforePerms !== afterPerms) {
|
|
392
|
+
details.push(
|
|
393
|
+
<li key="permissions" className="text-xs text-gray-600">
|
|
394
|
+
<span className="font-medium">Permissions :</span>{' '}
|
|
395
|
+
<span className="line-through text-red-500">{beforePerms}</span>{' '}
|
|
396
|
+
<span className="text-green-600">→ {afterPerms}</span>
|
|
397
|
+
</li>,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} else if (log.action === 'ROLE_DELETED') {
|
|
402
|
+
title = `${actorLabel} • Suppression Profil`;
|
|
403
|
+
IconComponent = ShieldX;
|
|
404
|
+
const metadata = log.metadata as { name?: string; description?: string };
|
|
405
|
+
if (metadata?.name) {
|
|
406
|
+
subtitle = metadata.name;
|
|
407
|
+
}
|
|
408
|
+
if (metadata?.description) {
|
|
409
|
+
details.push(
|
|
410
|
+
<li key="description" className="text-xs text-gray-600">
|
|
411
|
+
<span className="font-medium">Description :</span> {metadata.description}
|
|
412
|
+
</li>,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
} else {
|
|
416
|
+
title = log.action;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Couleur de l'icône selon le type d'action
|
|
420
|
+
const iconColorClass =
|
|
421
|
+
log.action === 'USER_CREATED' || log.action === 'ROLE_CREATED'
|
|
422
|
+
? 'text-green-600'
|
|
423
|
+
: log.action === 'ROLE_DELETED'
|
|
424
|
+
? 'text-red-600'
|
|
425
|
+
: 'text-indigo-600';
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<li key={log.id} className="py-5">
|
|
429
|
+
<div className="flex items-start justify-between gap-6">
|
|
430
|
+
<div className="flex items-start gap-3 flex-1">
|
|
431
|
+
<div
|
|
432
|
+
className={`mt-0.5 shrink-0 rounded-lg p-2 bg-gray-50 ${iconColorClass}`}
|
|
433
|
+
>
|
|
434
|
+
<IconComponent className="h-5 w-5" />
|
|
435
|
+
</div>
|
|
436
|
+
<div className="flex-1">
|
|
437
|
+
<div className="mb-1 text-sm font-semibold text-gray-900">{title}</div>
|
|
438
|
+
{subtitle && (
|
|
439
|
+
<div className="mb-1 text-sm font-medium text-gray-700">{subtitle}</div>
|
|
440
|
+
)}
|
|
441
|
+
{details.length > 0 && (
|
|
442
|
+
<ul className="mt-3 space-y-1.5">{details}</ul>
|
|
443
|
+
)}
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
<div className="shrink-0 text-xs font-medium text-gray-400">{dateLabel}</div>
|
|
447
|
+
</div>
|
|
448
|
+
</li>
|
|
449
|
+
);
|
|
450
|
+
})}
|
|
451
|
+
</ul>
|
|
452
|
+
)}
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
);
|
|
457
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { ArrowLeft, Key, Search } from 'lucide-react';
|
|
6
|
+
import { PERMISSIONS_BY_CATEGORY, PERMISSION_CATEGORIES } from '@/lib/permissions';
|
|
7
|
+
import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
|
|
8
|
+
|
|
9
|
+
export default function PermissionsPage() {
|
|
10
|
+
const { toggle: toggleMobileMenu, isOpen: isMobileMenuOpen } = useMobileMenuContext();
|
|
11
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
12
|
+
const [selectedCategory, setSelectedCategory] = useState<string>('all');
|
|
13
|
+
|
|
14
|
+
// Filtrer les permissions par catégorie et terme de recherche
|
|
15
|
+
const filteredCategories = Object.entries(PERMISSIONS_BY_CATEGORY).filter(
|
|
16
|
+
([category]) => selectedCategory === 'all' || category === selectedCategory,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const filteredPermissions = filteredCategories.map(([category, permissions]) => {
|
|
20
|
+
const filtered = permissions.filter(
|
|
21
|
+
(p) =>
|
|
22
|
+
p.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
23
|
+
p.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
24
|
+
p.code.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
25
|
+
);
|
|
26
|
+
return [category, filtered] as const;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const totalPermissions = PERMISSIONS_BY_CATEGORY
|
|
30
|
+
? Object.values(PERMISSIONS_BY_CATEGORY).reduce((sum, perms) => sum + perms.length, 0)
|
|
31
|
+
: 0;
|
|
32
|
+
|
|
33
|
+
const categoryColors: Record<string, { bg: string; text: string }> = {
|
|
34
|
+
[PERMISSION_CATEGORIES.ANALYTICS]: {
|
|
35
|
+
bg: 'bg-yellow-50',
|
|
36
|
+
text: 'text-yellow-700',
|
|
37
|
+
},
|
|
38
|
+
[PERMISSION_CATEGORIES.CONTACTS]: {
|
|
39
|
+
bg: 'bg-blue-50',
|
|
40
|
+
text: 'text-blue-700',
|
|
41
|
+
},
|
|
42
|
+
[PERMISSION_CATEGORIES.TASKS]: {
|
|
43
|
+
bg: 'bg-green-50',
|
|
44
|
+
text: 'text-green-700',
|
|
45
|
+
},
|
|
46
|
+
[PERMISSION_CATEGORIES.TEMPLATES]: {
|
|
47
|
+
bg: 'bg-purple-50',
|
|
48
|
+
text: 'text-purple-700',
|
|
49
|
+
},
|
|
50
|
+
[PERMISSION_CATEGORIES.INTEGRATIONS]: {
|
|
51
|
+
bg: 'bg-pink-50',
|
|
52
|
+
text: 'text-pink-700',
|
|
53
|
+
},
|
|
54
|
+
[PERMISSION_CATEGORIES.USERS]: {
|
|
55
|
+
bg: 'bg-orange-50',
|
|
56
|
+
text: 'text-orange-700',
|
|
57
|
+
},
|
|
58
|
+
[PERMISSION_CATEGORIES.SETTINGS]: {
|
|
59
|
+
bg: 'bg-indigo-50',
|
|
60
|
+
text: 'text-indigo-700',
|
|
61
|
+
},
|
|
62
|
+
[PERMISSION_CATEGORIES.GENERAL]: {
|
|
63
|
+
bg: 'bg-gray-50',
|
|
64
|
+
text: 'text-gray-700',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="h-full">
|
|
70
|
+
<div className="border-b border-gray-200 bg-white">
|
|
71
|
+
<div className="p-4 sm:p-6">
|
|
72
|
+
<div className="mb-4">
|
|
73
|
+
<Link
|
|
74
|
+
href="/users"
|
|
75
|
+
className="inline-flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900"
|
|
76
|
+
>
|
|
77
|
+
<ArrowLeft className="h-4 w-4" />
|
|
78
|
+
Retour
|
|
79
|
+
</Link>
|
|
80
|
+
</div>
|
|
81
|
+
<div>
|
|
82
|
+
<h1 className="text-2xl font-bold text-gray-900">Gestion des permissions</h1>
|
|
83
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
84
|
+
{totalPermissions} permissions disponibles dans le système
|
|
85
|
+
</p>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div className="p-4 sm:p-6">
|
|
91
|
+
{/* Barre de recherche et filtres */}
|
|
92
|
+
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
93
|
+
<div className="relative flex-1 sm:max-w-md">
|
|
94
|
+
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
|
95
|
+
<input
|
|
96
|
+
type="text"
|
|
97
|
+
placeholder="Rechercher une permission..."
|
|
98
|
+
value={searchTerm}
|
|
99
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
100
|
+
className="w-full rounded-lg border border-gray-300 py-2 pr-4 pl-10 text-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 focus:outline-none"
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<select
|
|
105
|
+
value={selectedCategory}
|
|
106
|
+
onChange={(e) => setSelectedCategory(e.target.value)}
|
|
107
|
+
className="rounded-lg border border-gray-300 px-4 py-2 text-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 focus:outline-none"
|
|
108
|
+
>
|
|
109
|
+
<option value="all">Toutes les catégories</option>
|
|
110
|
+
{Object.values(PERMISSION_CATEGORIES).map((category) => (
|
|
111
|
+
<option key={category} value={category}>
|
|
112
|
+
{category}
|
|
113
|
+
</option>
|
|
114
|
+
))}
|
|
115
|
+
</select>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Liste des permissions par catégorie */}
|
|
119
|
+
<div className="space-y-8">
|
|
120
|
+
{filteredPermissions.map(([category, permissions]) => {
|
|
121
|
+
if (permissions.length === 0) return null;
|
|
122
|
+
|
|
123
|
+
const colors = categoryColors[category] || {
|
|
124
|
+
bg: 'bg-gray-50',
|
|
125
|
+
text: 'text-gray-700',
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div key={category}>
|
|
130
|
+
<div className="mb-4 flex items-center gap-3">
|
|
131
|
+
<div className={`rounded-lg p-2 ${colors.bg}`}>
|
|
132
|
+
<Key className={`h-5 w-5 ${colors.text}`} />
|
|
133
|
+
</div>
|
|
134
|
+
<div>
|
|
135
|
+
<h2 className="text-lg font-semibold text-gray-900">{category}</h2>
|
|
136
|
+
<p className="text-sm text-gray-500">
|
|
137
|
+
{permissions.length} permission
|
|
138
|
+
{permissions.length > 1 ? 's' : ''}
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div className="space-y-3">
|
|
144
|
+
{permissions.map((permission) => (
|
|
145
|
+
<div
|
|
146
|
+
key={permission.code}
|
|
147
|
+
className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"
|
|
148
|
+
>
|
|
149
|
+
<div className="flex items-start gap-3">
|
|
150
|
+
<div className={`mt-0.5 rounded-lg p-2 ${colors.bg}`}>
|
|
151
|
+
<Key className={`h-4 w-4 ${colors.text}`} />
|
|
152
|
+
</div>
|
|
153
|
+
<div className="flex-1">
|
|
154
|
+
<h3 className="font-semibold text-gray-900">{permission.name}</h3>
|
|
155
|
+
<p className="mt-1 text-sm text-gray-600">{permission.description}</p>
|
|
156
|
+
<code className="mt-2 inline-block rounded bg-gray-100 px-2 py-1 font-mono text-xs text-gray-600">
|
|
157
|
+
{permission.code}
|
|
158
|
+
</code>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
})}
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{filteredPermissions.every(([, perms]) => perms.length === 0) && (
|
|
170
|
+
<div className="py-12 text-center">
|
|
171
|
+
<Key className="mx-auto h-12 w-12 text-gray-400" />
|
|
172
|
+
<h3 className="mt-4 text-sm font-medium text-gray-900">Aucune permission trouvée</h3>
|
|
173
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
174
|
+
Essayez de modifier vos critères de recherche
|
|
175
|
+
</p>
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|