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,434 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { ArrowLeft, Shield, Users as UsersIcon, Key, Plus, Edit, Trash2, X } from 'lucide-react';
|
|
6
|
+
import { PERMISSIONS, PERMISSIONS_BY_CATEGORY } from '@/lib/permissions';
|
|
7
|
+
import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
|
|
8
|
+
|
|
9
|
+
interface Role {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description: string | null;
|
|
13
|
+
permissions: string[];
|
|
14
|
+
isSystem: boolean;
|
|
15
|
+
usersCount: number;
|
|
16
|
+
createdAt: string | null;
|
|
17
|
+
updatedAt: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface RoleModalProps {
|
|
21
|
+
isOpen: boolean;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
onSave: () => void;
|
|
24
|
+
role?: Role;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
|
|
28
|
+
const [formData, setFormData] = useState({
|
|
29
|
+
name: role?.name || '',
|
|
30
|
+
description: role?.description || '',
|
|
31
|
+
permissions: role?.permissions || [],
|
|
32
|
+
});
|
|
33
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
34
|
+
const [error, setError] = useState('');
|
|
35
|
+
|
|
36
|
+
// Réinitialiser le formulaire quand le rôle change
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (role) {
|
|
39
|
+
setFormData({
|
|
40
|
+
name: role.name,
|
|
41
|
+
description: role.description || '',
|
|
42
|
+
permissions: role.permissions,
|
|
43
|
+
});
|
|
44
|
+
} else {
|
|
45
|
+
setFormData({
|
|
46
|
+
name: '',
|
|
47
|
+
description: '',
|
|
48
|
+
permissions: [],
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
setError('');
|
|
52
|
+
}, [role, isOpen]);
|
|
53
|
+
|
|
54
|
+
const togglePermission = (permissionCode: string) => {
|
|
55
|
+
setFormData((prev) => ({
|
|
56
|
+
...prev,
|
|
57
|
+
permissions: prev.permissions.includes(permissionCode)
|
|
58
|
+
? prev.permissions.filter((p) => p !== permissionCode)
|
|
59
|
+
: [...prev.permissions, permissionCode],
|
|
60
|
+
}));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
setError('');
|
|
66
|
+
setIsSubmitting(true);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const url = role ? `/api/roles/${role.id}` : '/api/roles';
|
|
70
|
+
const method = role ? 'PUT' : 'POST';
|
|
71
|
+
|
|
72
|
+
const response = await fetch(url, {
|
|
73
|
+
method,
|
|
74
|
+
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
body: JSON.stringify(formData),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const data = await response.json();
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error(data.error || "Erreur lors de l'enregistrement");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
onSave();
|
|
85
|
+
onClose();
|
|
86
|
+
} catch (err: any) {
|
|
87
|
+
setError(err.message);
|
|
88
|
+
} finally {
|
|
89
|
+
setIsSubmitting(false);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (!isOpen) return null;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-gray-500/20 p-4 backdrop-blur-sm">
|
|
97
|
+
<div className="my-8 w-full max-w-2xl rounded-lg bg-white shadow-xl">
|
|
98
|
+
<form onSubmit={handleSubmit}>
|
|
99
|
+
{/* Header */}
|
|
100
|
+
<div className="flex items-center justify-between border-b border-gray-200 p-6">
|
|
101
|
+
<h2 className="text-xl font-semibold text-gray-900">
|
|
102
|
+
{role ? 'Modifier le profil' : 'Nouveau profil'}
|
|
103
|
+
</h2>
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
onClick={onClose}
|
|
107
|
+
disabled={isSubmitting}
|
|
108
|
+
className="cursor-pointer rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600 disabled:cursor-not-allowed disabled:opacity-50"
|
|
109
|
+
>
|
|
110
|
+
<X className="h-5 w-5" />
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{/* Content */}
|
|
115
|
+
<div className="max-h-[70vh] overflow-y-auto p-6">
|
|
116
|
+
{error && (
|
|
117
|
+
<div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>
|
|
118
|
+
)}
|
|
119
|
+
<div className="space-y-6">
|
|
120
|
+
{/* Nom et description */}
|
|
121
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
122
|
+
<div>
|
|
123
|
+
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
|
124
|
+
Nom du profil *
|
|
125
|
+
</label>
|
|
126
|
+
<input
|
|
127
|
+
type="text"
|
|
128
|
+
id="name"
|
|
129
|
+
value={formData.name}
|
|
130
|
+
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
|
|
131
|
+
className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 focus:outline-none"
|
|
132
|
+
required
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
<div className="sm:col-span-2">
|
|
136
|
+
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
|
|
137
|
+
Description
|
|
138
|
+
</label>
|
|
139
|
+
<textarea
|
|
140
|
+
id="description"
|
|
141
|
+
value={formData.description}
|
|
142
|
+
onChange={(e) =>
|
|
143
|
+
setFormData((prev) => ({
|
|
144
|
+
...prev,
|
|
145
|
+
description: e.target.value,
|
|
146
|
+
}))
|
|
147
|
+
}
|
|
148
|
+
rows={2}
|
|
149
|
+
className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 focus:outline-none"
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{/* Permissions */}
|
|
155
|
+
<div>
|
|
156
|
+
<h3 className="mb-4 text-base font-semibold text-gray-900">Permissions</h3>
|
|
157
|
+
|
|
158
|
+
<div className="space-y-6">
|
|
159
|
+
{Object.entries(PERMISSIONS_BY_CATEGORY).map(([category, permissions]) => (
|
|
160
|
+
<div key={category}>
|
|
161
|
+
<h4 className="mb-3 text-sm font-medium tracking-wide text-gray-500 uppercase">
|
|
162
|
+
{category}
|
|
163
|
+
</h4>
|
|
164
|
+
<div className="space-y-2">
|
|
165
|
+
{permissions.map((permission) => (
|
|
166
|
+
<label
|
|
167
|
+
key={permission.code}
|
|
168
|
+
className="flex cursor-pointer items-start gap-3 rounded-lg border border-gray-200 p-3 transition-colors hover:bg-gray-50"
|
|
169
|
+
>
|
|
170
|
+
<input
|
|
171
|
+
type="checkbox"
|
|
172
|
+
checked={formData.permissions.includes(permission.code)}
|
|
173
|
+
onChange={() => togglePermission(permission.code)}
|
|
174
|
+
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
175
|
+
/>
|
|
176
|
+
<div className="flex-1">
|
|
177
|
+
<div className="font-medium text-gray-900">{permission.name}</div>
|
|
178
|
+
<div className="text-sm text-gray-500">{permission.description}</div>
|
|
179
|
+
</div>
|
|
180
|
+
</label>
|
|
181
|
+
))}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
))}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{/* Footer */}
|
|
191
|
+
<div className="flex items-center justify-end gap-3 border-t border-gray-200 p-6">
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
onClick={onClose}
|
|
195
|
+
disabled={isSubmitting}
|
|
196
|
+
className="cursor-pointer rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
|
|
197
|
+
>
|
|
198
|
+
Annuler
|
|
199
|
+
</button>
|
|
200
|
+
<button
|
|
201
|
+
type="submit"
|
|
202
|
+
disabled={isSubmitting}
|
|
203
|
+
className="cursor-pointer rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
204
|
+
>
|
|
205
|
+
{isSubmitting ? 'Enregistrement...' : 'Enregistrer'}
|
|
206
|
+
</button>
|
|
207
|
+
</div>
|
|
208
|
+
</form>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export default function RolesPage() {
|
|
215
|
+
const { toggle: toggleMobileMenu, isOpen: isMobileMenuOpen } = useMobileMenuContext();
|
|
216
|
+
const [showModal, setShowModal] = useState(false);
|
|
217
|
+
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
|
|
218
|
+
const [roles, setRoles] = useState<Role[]>([]);
|
|
219
|
+
const [loading, setLoading] = useState(true);
|
|
220
|
+
const [error, setError] = useState('');
|
|
221
|
+
|
|
222
|
+
const fetchRoles = async () => {
|
|
223
|
+
try {
|
|
224
|
+
setLoading(true);
|
|
225
|
+
const response = await fetch('/api/roles');
|
|
226
|
+
if (!response.ok) {
|
|
227
|
+
throw new Error('Erreur lors du chargement des profils');
|
|
228
|
+
}
|
|
229
|
+
const data = await response.json();
|
|
230
|
+
setRoles(data);
|
|
231
|
+
} catch (err: any) {
|
|
232
|
+
setError(err.message);
|
|
233
|
+
} finally {
|
|
234
|
+
setLoading(false);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
fetchRoles();
|
|
240
|
+
}, []);
|
|
241
|
+
|
|
242
|
+
const handleEditRole = (roleId: string) => {
|
|
243
|
+
const role = roles.find((r) => r.id === roleId);
|
|
244
|
+
if (role) {
|
|
245
|
+
setSelectedRole(role);
|
|
246
|
+
setShowModal(true);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const handleDeleteRole = async (roleId: string) => {
|
|
251
|
+
const role = roles.find((r) => r.id === roleId);
|
|
252
|
+
if (!role) return;
|
|
253
|
+
|
|
254
|
+
const confirmMessage = role.isSystem
|
|
255
|
+
? `⚠️ Attention : "${role.name}" est un profil système.\n\nÊtes-vous sûr de vouloir le supprimer ?`
|
|
256
|
+
: `Êtes-vous sûr de vouloir supprimer le profil "${role.name}" ?`;
|
|
257
|
+
|
|
258
|
+
if (!confirm(confirmMessage)) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const response = await fetch(`/api/roles/${roleId}`, {
|
|
264
|
+
method: 'DELETE',
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const data = await response.json();
|
|
268
|
+
|
|
269
|
+
if (!response.ok) {
|
|
270
|
+
throw new Error(data.error || 'Erreur lors de la suppression');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
await fetchRoles();
|
|
274
|
+
} catch (err: any) {
|
|
275
|
+
setError(err.message);
|
|
276
|
+
setTimeout(() => setError(''), 5000);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const handleCloseModal = () => {
|
|
281
|
+
setShowModal(false);
|
|
282
|
+
setSelectedRole(null);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const handleSaveRole = async () => {
|
|
286
|
+
await fetchRoles();
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<div className="h-full">
|
|
291
|
+
<div className="border-b border-gray-200 bg-white">
|
|
292
|
+
<div className="p-4 sm:p-6">
|
|
293
|
+
<div className="mb-4">
|
|
294
|
+
<Link
|
|
295
|
+
href="/users"
|
|
296
|
+
className="inline-flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900"
|
|
297
|
+
>
|
|
298
|
+
<ArrowLeft className="h-4 w-4" />
|
|
299
|
+
Retour
|
|
300
|
+
</Link>
|
|
301
|
+
</div>
|
|
302
|
+
<div className="flex items-center justify-between">
|
|
303
|
+
<div>
|
|
304
|
+
<h1 className="text-2xl font-bold text-gray-900">Gestion des profils</h1>
|
|
305
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
306
|
+
Créer et configurer les profils avec leurs permissions
|
|
307
|
+
</p>
|
|
308
|
+
</div>
|
|
309
|
+
<button
|
|
310
|
+
onClick={() => {
|
|
311
|
+
setSelectedRole(null);
|
|
312
|
+
setShowModal(true);
|
|
313
|
+
}}
|
|
314
|
+
className="flex cursor-pointer items-center gap-2 rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700"
|
|
315
|
+
>
|
|
316
|
+
<Plus className="h-4 w-4" />
|
|
317
|
+
Nouveau profil
|
|
318
|
+
</button>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<div className="p-4 sm:p-6">
|
|
324
|
+
{error && <div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>}
|
|
325
|
+
|
|
326
|
+
{loading ? (
|
|
327
|
+
<div className="grid gap-6 lg:grid-cols-2">
|
|
328
|
+
{[1, 2, 3, 4].map((i) => (
|
|
329
|
+
<div key={i} className="h-64 animate-pulse rounded-lg bg-gray-200" />
|
|
330
|
+
))}
|
|
331
|
+
</div>
|
|
332
|
+
) : (
|
|
333
|
+
<div className="grid gap-6 lg:grid-cols-2">
|
|
334
|
+
{roles
|
|
335
|
+
.sort((a, b) => b.permissions.length - a.permissions.length) // Trier par nombre de permissions (DESC)
|
|
336
|
+
.map((role) => {
|
|
337
|
+
const visiblePermissions = role.permissions.slice(0, 4);
|
|
338
|
+
const remainingCount = role.permissions.length - 4;
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<div
|
|
342
|
+
key={role.id}
|
|
343
|
+
className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm"
|
|
344
|
+
>
|
|
345
|
+
<div className="flex items-start justify-between">
|
|
346
|
+
<div className="flex items-start gap-3">
|
|
347
|
+
<div className="rounded-lg bg-green-100 p-2">
|
|
348
|
+
<Shield className="h-5 w-5 text-green-600" />
|
|
349
|
+
</div>
|
|
350
|
+
<div>
|
|
351
|
+
<h3 className="font-semibold text-gray-900">
|
|
352
|
+
{role.name}
|
|
353
|
+
{role.isSystem && (
|
|
354
|
+
<span className="ml-2 inline-block rounded bg-indigo-100 px-2 py-0.5 text-xs text-indigo-600">
|
|
355
|
+
Système
|
|
356
|
+
</span>
|
|
357
|
+
)}
|
|
358
|
+
</h3>
|
|
359
|
+
<p className="mt-1 text-sm text-gray-600">{role.description}</p>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
<div className="flex items-center gap-2">
|
|
363
|
+
<button
|
|
364
|
+
onClick={() => handleEditRole(role.id)}
|
|
365
|
+
className="cursor-pointer rounded-lg p-2 text-orange-600 hover:bg-orange-50"
|
|
366
|
+
title="Modifier"
|
|
367
|
+
>
|
|
368
|
+
<Edit className="h-4 w-4" />
|
|
369
|
+
</button>
|
|
370
|
+
<button
|
|
371
|
+
onClick={() => handleDeleteRole(role.id)}
|
|
372
|
+
className="cursor-pointer rounded-lg p-2 text-red-600 hover:bg-red-50"
|
|
373
|
+
title="Supprimer"
|
|
374
|
+
>
|
|
375
|
+
<Trash2 className="h-4 w-4" />
|
|
376
|
+
</button>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<div className="mt-4 flex items-center gap-4 text-sm text-gray-600">
|
|
381
|
+
<div className="flex items-center gap-1">
|
|
382
|
+
<UsersIcon className="h-4 w-4" />
|
|
383
|
+
<span>
|
|
384
|
+
{role.usersCount} utilisateur{role.usersCount > 1 ? 's' : ''}
|
|
385
|
+
</span>
|
|
386
|
+
</div>
|
|
387
|
+
<div className="flex items-center gap-1">
|
|
388
|
+
<Key className="h-4 w-4" />
|
|
389
|
+
<span>
|
|
390
|
+
{role.permissions.length} permission
|
|
391
|
+
{role.permissions.length > 1 ? 's' : ''}
|
|
392
|
+
</span>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
<div className="mt-4">
|
|
397
|
+
<h4 className="mb-2 text-xs font-medium tracking-wide text-gray-500 uppercase">
|
|
398
|
+
Permissions
|
|
399
|
+
</h4>
|
|
400
|
+
<div className="flex flex-wrap gap-2">
|
|
401
|
+
{visiblePermissions.map((permCode) => {
|
|
402
|
+
const perm = PERMISSIONS.find((p) => p.code === permCode);
|
|
403
|
+
return (
|
|
404
|
+
<span
|
|
405
|
+
key={permCode}
|
|
406
|
+
className="rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-700"
|
|
407
|
+
>
|
|
408
|
+
{perm?.name || permCode}
|
|
409
|
+
</span>
|
|
410
|
+
);
|
|
411
|
+
})}
|
|
412
|
+
{remainingCount > 0 && (
|
|
413
|
+
<span className="rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700">
|
|
414
|
+
+{remainingCount} autres
|
|
415
|
+
</span>
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
);
|
|
421
|
+
})}
|
|
422
|
+
</div>
|
|
423
|
+
)}
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<RoleModal
|
|
427
|
+
isOpen={showModal}
|
|
428
|
+
onClose={handleCloseModal}
|
|
429
|
+
onSave={handleSaveRole}
|
|
430
|
+
role={selectedRole || undefined}
|
|
431
|
+
/>
|
|
432
|
+
</div>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
5
|
+
|
|
6
|
+
// GET /api/audit-logs - Dernières actions système (utilisateurs / rôles)
|
|
7
|
+
export async function GET(request: NextRequest) {
|
|
8
|
+
try {
|
|
9
|
+
const session = await auth.api.getSession({
|
|
10
|
+
headers: request.headers,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (!session) {
|
|
14
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// On réutilise la permission de vue utilisateurs pour l’instant
|
|
18
|
+
const hasPermission = await checkPermission('users.view');
|
|
19
|
+
if (!hasPermission) {
|
|
20
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { searchParams } = new URL(request.url);
|
|
24
|
+
const limit = parseInt(searchParams.get('limit') || '10', 10);
|
|
25
|
+
|
|
26
|
+
const logs = await prisma.auditLog.findMany({
|
|
27
|
+
orderBy: { createdAt: 'desc' },
|
|
28
|
+
take: Math.min(Math.max(limit, 1), 50),
|
|
29
|
+
include: {
|
|
30
|
+
actor: {
|
|
31
|
+
select: { id: true, name: true, email: true },
|
|
32
|
+
},
|
|
33
|
+
targetUser: {
|
|
34
|
+
select: { id: true, name: true, email: true },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return NextResponse.json(
|
|
40
|
+
logs.map((log) => ({
|
|
41
|
+
id: log.id,
|
|
42
|
+
action: log.action,
|
|
43
|
+
entityType: log.entityType,
|
|
44
|
+
entityId: log.entityId,
|
|
45
|
+
metadata: log.metadata,
|
|
46
|
+
createdAt: log.createdAt,
|
|
47
|
+
actor: log.actor,
|
|
48
|
+
targetUser: log.targetUser,
|
|
49
|
+
})),
|
|
50
|
+
);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Erreur lors de la récupération des logs d’audit:', error);
|
|
53
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { prisma } from '@/lib/prisma';
|
|
4
|
+
|
|
5
|
+
// GET /api/auth/check-active - Vérifie si l'utilisateur courant est actif
|
|
6
|
+
export async function GET(request: NextRequest) {
|
|
7
|
+
try {
|
|
8
|
+
const session = await auth.api.getSession({
|
|
9
|
+
headers: request.headers,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
if (!session || !session.user?.id) {
|
|
13
|
+
return NextResponse.json({ active: false }, { status: 200 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const user = await prisma.user.findUnique({
|
|
17
|
+
where: { id: session.user.id },
|
|
18
|
+
select: { active: true },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const isActive = user?.active ?? true;
|
|
22
|
+
|
|
23
|
+
return NextResponse.json({ active: isActive }, { status: 200 });
|
|
24
|
+
} catch (error: any) {
|
|
25
|
+
console.error('Erreur lors de la vérification du statut utilisateur:', error);
|
|
26
|
+
return NextResponse.json(
|
|
27
|
+
{ active: false, error: error.message || 'Erreur serveur' },
|
|
28
|
+
{ status: 200 },
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { exchangeGoogleCodeForTokens } from '@/lib/google-calendar';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GET /api/auth/google/callback
|
|
8
|
+
* Gère le callback OAuth de Google et sauvegarde les tokens
|
|
9
|
+
*/
|
|
10
|
+
export async function GET(request: NextRequest) {
|
|
11
|
+
try {
|
|
12
|
+
const session = await auth.api.getSession({
|
|
13
|
+
headers: request.headers,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
if (!session) {
|
|
17
|
+
return NextResponse.redirect(new URL('/signin', request.url));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { searchParams } = new URL(request.url);
|
|
21
|
+
const code = searchParams.get('code');
|
|
22
|
+
const error = searchParams.get('error');
|
|
23
|
+
|
|
24
|
+
if (error) {
|
|
25
|
+
return NextResponse.redirect(
|
|
26
|
+
new URL(
|
|
27
|
+
`/settings?error=${encodeURIComponent('Erreur lors de la connexion Google')}`,
|
|
28
|
+
request.url,
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!code) {
|
|
34
|
+
return NextResponse.redirect(
|
|
35
|
+
new URL(
|
|
36
|
+
`/settings?error=${encodeURIComponent("Code d'autorisation manquant")}`,
|
|
37
|
+
request.url,
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const redirectUri =
|
|
43
|
+
process.env.GOOGLE_REDIRECT_URI || 'http://localhost:3000/api/auth/google/callback';
|
|
44
|
+
const tokens = await exchangeGoogleCodeForTokens(code, redirectUri);
|
|
45
|
+
|
|
46
|
+
// Calculer la date d'expiration du token
|
|
47
|
+
const tokenExpiresAt = new Date();
|
|
48
|
+
tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + (tokens.expires_in || 3600));
|
|
49
|
+
|
|
50
|
+
// Récupérer l'email du compte Google (optionnel, via l'API userinfo)
|
|
51
|
+
let googleEmail: string | null = null;
|
|
52
|
+
try {
|
|
53
|
+
const userInfoResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
|
54
|
+
headers: {
|
|
55
|
+
Authorization: `Bearer ${tokens.access_token}`,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
if (userInfoResponse.ok) {
|
|
59
|
+
const userInfo = await userInfoResponse.json();
|
|
60
|
+
googleEmail = userInfo.email || null;
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error("Erreur lors de la récupération de l'email Google:", err);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Sauvegarder ou mettre à jour les tokens
|
|
67
|
+
await prisma.userGoogleAccount.upsert({
|
|
68
|
+
where: { userId: session.user.id },
|
|
69
|
+
create: {
|
|
70
|
+
userId: session.user.id,
|
|
71
|
+
accessToken: tokens.access_token,
|
|
72
|
+
refreshToken: tokens.refresh_token || '',
|
|
73
|
+
tokenExpiresAt,
|
|
74
|
+
email: googleEmail,
|
|
75
|
+
},
|
|
76
|
+
update: {
|
|
77
|
+
accessToken: tokens.access_token,
|
|
78
|
+
refreshToken: tokens.refresh_token || undefined,
|
|
79
|
+
tokenExpiresAt,
|
|
80
|
+
email: googleEmail,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return NextResponse.redirect(new URL('/settings?success=google_connected', request.url));
|
|
85
|
+
} catch (error: any) {
|
|
86
|
+
console.error('Erreur lors du callback Google:', error);
|
|
87
|
+
return NextResponse.redirect(
|
|
88
|
+
new URL(
|
|
89
|
+
`/settings?error=${encodeURIComponent(error.message || 'Erreur lors de la connexion Google')}`,
|
|
90
|
+
request.url,
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { prisma } from '@/lib/prisma';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* POST /api/auth/google/disconnect
|
|
7
|
+
* Déconnecte le compte Google de l'utilisateur
|
|
8
|
+
*/
|
|
9
|
+
export async function POST(request: NextRequest) {
|
|
10
|
+
try {
|
|
11
|
+
const session = await auth.api.getSession({
|
|
12
|
+
headers: request.headers,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (!session) {
|
|
16
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Supprimer le compte Google
|
|
20
|
+
await prisma.userGoogleAccount.deleteMany({
|
|
21
|
+
where: { userId: session.user.id },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return NextResponse.json({ success: true });
|
|
25
|
+
} catch (error: any) {
|
|
26
|
+
console.error('Erreur lors de la déconnexion Google:', error);
|
|
27
|
+
return NextResponse.json(
|
|
28
|
+
{ error: error.message || 'Erreur lors de la déconnexion' },
|
|
29
|
+
{ status: 500 },
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GET /api/auth/google
|
|
5
|
+
* Redirige vers l'URL d'autorisation Google OAuth
|
|
6
|
+
*/
|
|
7
|
+
export async function GET(request: NextRequest) {
|
|
8
|
+
const clientId = process.env.GOOGLE_CLIENT_ID;
|
|
9
|
+
const redirectUri =
|
|
10
|
+
process.env.GOOGLE_REDIRECT_URI || 'http://localhost:3000/api/auth/google/callback';
|
|
11
|
+
|
|
12
|
+
if (!clientId) {
|
|
13
|
+
return NextResponse.json({ error: 'GOOGLE_CLIENT_ID non configuré' }, { status: 500 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const scopes = [
|
|
17
|
+
'https://www.googleapis.com/auth/calendar.events',
|
|
18
|
+
'https://www.googleapis.com/auth/spreadsheets.readonly',
|
|
19
|
+
'https://www.googleapis.com/auth/drive.file', // Accès aux fichiers créés par l'application
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const params = new URLSearchParams({
|
|
23
|
+
client_id: clientId,
|
|
24
|
+
redirect_uri: redirectUri,
|
|
25
|
+
response_type: 'code',
|
|
26
|
+
scope: scopes.join(' '),
|
|
27
|
+
access_type: 'offline',
|
|
28
|
+
prompt: 'consent', // Force le consentement pour obtenir le refresh_token
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
|
|
32
|
+
|
|
33
|
+
return NextResponse.redirect(authUrl);
|
|
34
|
+
}
|