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
|
@@ -4,8 +4,11 @@ import { useState } from 'react';
|
|
|
4
4
|
import Link from 'next/link';
|
|
5
5
|
import { ArrowLeft, Key, Search } from 'lucide-react';
|
|
6
6
|
import { PERMISSIONS_BY_CATEGORY, PERMISSION_CATEGORIES } from '@/lib/permissions';
|
|
7
|
+
import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
|
|
8
|
+
import { ProtectedPage } from '@/components/protected-page';
|
|
7
9
|
|
|
8
10
|
export default function PermissionsPage() {
|
|
11
|
+
const { toggle: toggleMobileMenu, isOpen: isMobileMenuOpen } = useMobileMenuContext();
|
|
9
12
|
const [searchTerm, setSearchTerm] = useState('');
|
|
10
13
|
const [selectedCategory, setSelectedCategory] = useState<string>('all');
|
|
11
14
|
|
|
@@ -54,8 +57,8 @@ export default function PermissionsPage() {
|
|
|
54
57
|
text: 'text-orange-700',
|
|
55
58
|
},
|
|
56
59
|
[PERMISSION_CATEGORIES.SETTINGS]: {
|
|
57
|
-
bg: 'bg-
|
|
58
|
-
text: 'text-
|
|
60
|
+
bg: 'bg-blue-50',
|
|
61
|
+
text: 'text-blue-700',
|
|
59
62
|
},
|
|
60
63
|
[PERMISSION_CATEGORIES.GENERAL]: {
|
|
61
64
|
bg: 'bg-gray-50',
|
|
@@ -64,116 +67,118 @@ export default function PermissionsPage() {
|
|
|
64
67
|
};
|
|
65
68
|
|
|
66
69
|
return (
|
|
67
|
-
<
|
|
68
|
-
<div className="
|
|
69
|
-
<div className="
|
|
70
|
-
<div className="
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
70
|
+
<ProtectedPage requiredPermission="users.manage_roles">
|
|
71
|
+
<div className="h-full">
|
|
72
|
+
<div className="border-b border-gray-200 bg-white">
|
|
73
|
+
<div className="p-4 sm:p-6">
|
|
74
|
+
<div className="mb-4">
|
|
75
|
+
<Link
|
|
76
|
+
href="/users"
|
|
77
|
+
className="inline-flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900"
|
|
78
|
+
>
|
|
79
|
+
<ArrowLeft className="h-4 w-4" />
|
|
80
|
+
Retour
|
|
81
|
+
</Link>
|
|
82
|
+
</div>
|
|
83
|
+
<div>
|
|
84
|
+
<h1 className="text-2xl font-bold text-gray-900">Gestion des permissions</h1>
|
|
85
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
86
|
+
{totalPermissions} permissions disponibles dans le système
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
84
89
|
</div>
|
|
85
90
|
</div>
|
|
86
|
-
</div>
|
|
87
91
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
92
|
+
<div className="p-4 sm:p-6">
|
|
93
|
+
{/* Barre de recherche et filtres */}
|
|
94
|
+
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
95
|
+
<div className="relative flex-1 sm:max-w-md">
|
|
96
|
+
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
|
97
|
+
<input
|
|
98
|
+
type="text"
|
|
99
|
+
placeholder="Rechercher une permission..."
|
|
100
|
+
value={searchTerm}
|
|
101
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
102
|
+
className="w-full rounded-lg border border-gray-300 py-2 pr-4 pl-10 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
101
105
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
106
|
+
<select
|
|
107
|
+
value={selectedCategory}
|
|
108
|
+
onChange={(e) => setSelectedCategory(e.target.value)}
|
|
109
|
+
className="rounded-lg border border-gray-300 px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
110
|
+
>
|
|
111
|
+
<option value="all">Toutes les catégories</option>
|
|
112
|
+
{Object.values(PERMISSION_CATEGORIES).map((category) => (
|
|
113
|
+
<option key={category} value={category}>
|
|
114
|
+
{category}
|
|
115
|
+
</option>
|
|
116
|
+
))}
|
|
117
|
+
</select>
|
|
118
|
+
</div>
|
|
115
119
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
{/* Liste des permissions par catégorie */}
|
|
121
|
+
<div className="space-y-8">
|
|
122
|
+
{filteredPermissions.map(([category, permissions]) => {
|
|
123
|
+
if (permissions.length === 0) return null;
|
|
120
124
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
const colors = categoryColors[category] || {
|
|
126
|
+
bg: 'bg-gray-50',
|
|
127
|
+
text: 'text-gray-700',
|
|
128
|
+
};
|
|
125
129
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
return (
|
|
131
|
+
<div key={category}>
|
|
132
|
+
<div className="mb-4 flex items-center gap-3">
|
|
133
|
+
<div className={`rounded-lg p-2 ${colors.bg}`}>
|
|
134
|
+
<Key className={`h-5 w-5 ${colors.text}`} />
|
|
135
|
+
</div>
|
|
136
|
+
<div>
|
|
137
|
+
<h2 className="text-lg font-semibold text-gray-900">{category}</h2>
|
|
138
|
+
<p className="text-sm text-gray-500">
|
|
139
|
+
{permissions.length} permission
|
|
140
|
+
{permissions.length > 1 ? 's' : ''}
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
|
138
143
|
</div>
|
|
139
|
-
</div>
|
|
140
144
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
145
|
+
<div className="space-y-3">
|
|
146
|
+
{permissions.map((permission) => (
|
|
147
|
+
<div
|
|
148
|
+
key={permission.code}
|
|
149
|
+
className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"
|
|
150
|
+
>
|
|
151
|
+
<div className="flex items-start gap-3">
|
|
152
|
+
<div className={`mt-0.5 rounded-lg p-2 ${colors.bg}`}>
|
|
153
|
+
<Key className={`h-4 w-4 ${colors.text}`} />
|
|
154
|
+
</div>
|
|
155
|
+
<div className="flex-1">
|
|
156
|
+
<h3 className="font-semibold text-gray-900">{permission.name}</h3>
|
|
157
|
+
<p className="mt-1 text-sm text-gray-600">{permission.description}</p>
|
|
158
|
+
<code className="mt-2 inline-block rounded bg-gray-100 px-2 py-1 font-mono text-xs text-gray-600">
|
|
159
|
+
{permission.code}
|
|
160
|
+
</code>
|
|
161
|
+
</div>
|
|
157
162
|
</div>
|
|
158
163
|
</div>
|
|
159
|
-
|
|
160
|
-
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
161
166
|
</div>
|
|
162
|
-
|
|
163
|
-
)
|
|
164
|
-
})}
|
|
165
|
-
</div>
|
|
166
|
-
|
|
167
|
-
{filteredPermissions.every(([, perms]) => perms.length === 0) && (
|
|
168
|
-
<div className="py-12 text-center">
|
|
169
|
-
<Key className="mx-auto h-12 w-12 text-gray-400" />
|
|
170
|
-
<h3 className="mt-4 text-sm font-medium text-gray-900">Aucune permission trouvée</h3>
|
|
171
|
-
<p className="mt-1 text-sm text-gray-500">
|
|
172
|
-
Essayez de modifier vos critères de recherche
|
|
173
|
-
</p>
|
|
167
|
+
);
|
|
168
|
+
})}
|
|
174
169
|
</div>
|
|
175
|
-
|
|
170
|
+
|
|
171
|
+
{filteredPermissions.every(([, perms]) => perms.length === 0) && (
|
|
172
|
+
<div className="py-12 text-center">
|
|
173
|
+
<Key className="mx-auto h-12 w-12 text-gray-400" />
|
|
174
|
+
<h3 className="mt-4 text-sm font-medium text-gray-900">Aucune permission trouvée</h3>
|
|
175
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
176
|
+
Essayez de modifier vos critères de recherche
|
|
177
|
+
</p>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
176
181
|
</div>
|
|
177
|
-
</
|
|
182
|
+
</ProtectedPage>
|
|
178
183
|
);
|
|
179
184
|
}
|
|
@@ -4,6 +4,10 @@ import { useState, useEffect } from 'react';
|
|
|
4
4
|
import Link from 'next/link';
|
|
5
5
|
import { ArrowLeft, Shield, Users as UsersIcon, Key, Plus, Edit, Trash2, X } from 'lucide-react';
|
|
6
6
|
import { PERMISSIONS, PERMISSIONS_BY_CATEGORY } from '@/lib/permissions';
|
|
7
|
+
import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
|
|
8
|
+
import { useConfirm } from '@/hooks/use-confirm';
|
|
9
|
+
import { ProtectedPage } from '@/components/protected-page';
|
|
10
|
+
import { useAppToast } from '@/contexts/app-toast-context';
|
|
7
11
|
|
|
8
12
|
interface Role {
|
|
9
13
|
id: string;
|
|
@@ -24,6 +28,7 @@ interface RoleModalProps {
|
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
|
|
31
|
+
const toast = useAppToast();
|
|
27
32
|
const [formData, setFormData] = useState({
|
|
28
33
|
name: role?.name || '',
|
|
29
34
|
description: role?.description || '',
|
|
@@ -50,6 +55,12 @@ function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
|
|
|
50
55
|
setError('');
|
|
51
56
|
}, [role, isOpen]);
|
|
52
57
|
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!error) return;
|
|
60
|
+
toast.error(error);
|
|
61
|
+
setError('');
|
|
62
|
+
}, [error, toast]);
|
|
63
|
+
|
|
53
64
|
const togglePermission = (permissionCode: string) => {
|
|
54
65
|
setFormData((prev) => ({
|
|
55
66
|
...prev,
|
|
@@ -112,9 +123,6 @@ function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
|
|
|
112
123
|
|
|
113
124
|
{/* Content */}
|
|
114
125
|
<div className="max-h-[70vh] overflow-y-auto p-6">
|
|
115
|
-
{error && (
|
|
116
|
-
<div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>
|
|
117
|
-
)}
|
|
118
126
|
<div className="space-y-6">
|
|
119
127
|
{/* Nom et description */}
|
|
120
128
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
@@ -127,7 +135,7 @@ function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
|
|
|
127
135
|
id="name"
|
|
128
136
|
value={formData.name}
|
|
129
137
|
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
|
|
130
|
-
className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:
|
|
138
|
+
className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
131
139
|
required
|
|
132
140
|
/>
|
|
133
141
|
</div>
|
|
@@ -145,7 +153,7 @@ function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
|
|
|
145
153
|
}))
|
|
146
154
|
}
|
|
147
155
|
rows={2}
|
|
148
|
-
className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:
|
|
156
|
+
className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
149
157
|
/>
|
|
150
158
|
</div>
|
|
151
159
|
</div>
|
|
@@ -170,7 +178,7 @@ function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
|
|
|
170
178
|
type="checkbox"
|
|
171
179
|
checked={formData.permissions.includes(permission.code)}
|
|
172
180
|
onChange={() => togglePermission(permission.code)}
|
|
173
|
-
className="mt-1 h-4 w-4 rounded border-gray-300 text-
|
|
181
|
+
className="mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-gray-400/30"
|
|
174
182
|
/>
|
|
175
183
|
<div className="flex-1">
|
|
176
184
|
<div className="font-medium text-gray-900">{permission.name}</div>
|
|
@@ -211,6 +219,9 @@ function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
|
|
|
211
219
|
}
|
|
212
220
|
|
|
213
221
|
export default function RolesPage() {
|
|
222
|
+
const { toggle: toggleMobileMenu, isOpen: isMobileMenuOpen } = useMobileMenuContext();
|
|
223
|
+
const { confirm, ConfirmDialog } = useConfirm();
|
|
224
|
+
const toast = useAppToast();
|
|
214
225
|
const [showModal, setShowModal] = useState(false);
|
|
215
226
|
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
|
|
216
227
|
const [roles, setRoles] = useState<Role[]>([]);
|
|
@@ -237,6 +248,12 @@ export default function RolesPage() {
|
|
|
237
248
|
fetchRoles();
|
|
238
249
|
}, []);
|
|
239
250
|
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
if (!error) return;
|
|
253
|
+
toast.error(error);
|
|
254
|
+
setError('');
|
|
255
|
+
}, [error, toast]);
|
|
256
|
+
|
|
240
257
|
const handleEditRole = (roleId: string) => {
|
|
241
258
|
const role = roles.find((r) => r.id === roleId);
|
|
242
259
|
if (role) {
|
|
@@ -249,11 +266,20 @@ export default function RolesPage() {
|
|
|
249
266
|
const role = roles.find((r) => r.id === roleId);
|
|
250
267
|
if (!role) return;
|
|
251
268
|
|
|
269
|
+
const confirmTitle = role.isSystem ? '⚠️ Supprimer un profil système' : 'Supprimer le profil';
|
|
252
270
|
const confirmMessage = role.isSystem
|
|
253
|
-
?
|
|
271
|
+
? `Attention : "${role.name}" est un profil système. Êtes-vous sûr de vouloir le supprimer ?`
|
|
254
272
|
: `Êtes-vous sûr de vouloir supprimer le profil "${role.name}" ?`;
|
|
255
273
|
|
|
256
|
-
|
|
274
|
+
const confirmed = await confirm({
|
|
275
|
+
title: confirmTitle,
|
|
276
|
+
description: confirmMessage,
|
|
277
|
+
confirmText: 'Supprimer',
|
|
278
|
+
cancelText: 'Annuler',
|
|
279
|
+
variant: 'destructive',
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
if (!confirmed) {
|
|
257
283
|
return;
|
|
258
284
|
}
|
|
259
285
|
|
|
@@ -285,148 +311,149 @@ export default function RolesPage() {
|
|
|
285
311
|
};
|
|
286
312
|
|
|
287
313
|
return (
|
|
288
|
-
<
|
|
289
|
-
<div className="
|
|
290
|
-
<div className="
|
|
291
|
-
<div className="
|
|
292
|
-
<
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
<div>
|
|
302
|
-
<
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
314
|
+
<ProtectedPage requiredPermission="users.manage_roles">
|
|
315
|
+
<div className="h-full">
|
|
316
|
+
<div className="border-b border-gray-200 bg-white">
|
|
317
|
+
<div className="p-4 sm:p-6">
|
|
318
|
+
<div className="mb-4">
|
|
319
|
+
<Link
|
|
320
|
+
href="/users"
|
|
321
|
+
className="inline-flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900"
|
|
322
|
+
>
|
|
323
|
+
<ArrowLeft className="h-4 w-4" />
|
|
324
|
+
Retour
|
|
325
|
+
</Link>
|
|
326
|
+
</div>
|
|
327
|
+
<div className="flex items-center justify-between">
|
|
328
|
+
<div>
|
|
329
|
+
<h1 className="text-2xl font-bold text-gray-900">Gestion des profils</h1>
|
|
330
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
331
|
+
Créer et configurer les profils avec leurs permissions
|
|
332
|
+
</p>
|
|
333
|
+
</div>
|
|
334
|
+
<button
|
|
335
|
+
onClick={() => {
|
|
336
|
+
setSelectedRole(null);
|
|
337
|
+
setShowModal(true);
|
|
338
|
+
}}
|
|
339
|
+
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"
|
|
340
|
+
>
|
|
341
|
+
<Plus className="h-4 w-4" />
|
|
342
|
+
Nouveau profil
|
|
343
|
+
</button>
|
|
306
344
|
</div>
|
|
307
|
-
<button
|
|
308
|
-
onClick={() => {
|
|
309
|
-
setSelectedRole(null);
|
|
310
|
-
setShowModal(true);
|
|
311
|
-
}}
|
|
312
|
-
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"
|
|
313
|
-
>
|
|
314
|
-
<Plus className="h-4 w-4" />
|
|
315
|
-
Nouveau profil
|
|
316
|
-
</button>
|
|
317
345
|
</div>
|
|
318
346
|
</div>
|
|
319
|
-
</div>
|
|
320
|
-
|
|
321
|
-
<div className="p-4 sm:p-6">
|
|
322
|
-
{error && <div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>}
|
|
323
347
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
<div className="flex items-start
|
|
345
|
-
<div className="
|
|
346
|
-
<
|
|
348
|
+
<div className="p-4 sm:p-6">
|
|
349
|
+
{loading ? (
|
|
350
|
+
<div className="grid gap-6 lg:grid-cols-2">
|
|
351
|
+
{[1, 2, 3, 4].map((i) => (
|
|
352
|
+
<div key={i} className="h-64 animate-pulse rounded-lg bg-gray-200" />
|
|
353
|
+
))}
|
|
354
|
+
</div>
|
|
355
|
+
) : (
|
|
356
|
+
<div className="grid gap-6 lg:grid-cols-2">
|
|
357
|
+
{roles
|
|
358
|
+
.sort((a, b) => b.permissions.length - a.permissions.length) // Trier par nombre de permissions (DESC)
|
|
359
|
+
.map((role) => {
|
|
360
|
+
const visiblePermissions = role.permissions.slice(0, 4);
|
|
361
|
+
const remainingCount = role.permissions.length - 4;
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<div
|
|
365
|
+
key={role.id}
|
|
366
|
+
className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm"
|
|
367
|
+
>
|
|
368
|
+
<div className="flex items-start justify-between">
|
|
369
|
+
<div className="flex items-start gap-3">
|
|
370
|
+
<div className="rounded-lg bg-green-100 p-2">
|
|
371
|
+
<Shield className="h-5 w-5 text-green-600" />
|
|
372
|
+
</div>
|
|
373
|
+
<div>
|
|
374
|
+
<h3 className="font-semibold text-gray-900">
|
|
375
|
+
{role.name}
|
|
376
|
+
{role.isSystem && (
|
|
377
|
+
<span className="ml-2 inline-block rounded bg-blue-100 px-2 py-0.5 text-xs text-blue-800">
|
|
378
|
+
Système
|
|
379
|
+
</span>
|
|
380
|
+
)}
|
|
381
|
+
</h3>
|
|
382
|
+
<p className="mt-1 text-sm text-gray-600">{role.description}</p>
|
|
383
|
+
</div>
|
|
347
384
|
</div>
|
|
348
|
-
<div>
|
|
349
|
-
<
|
|
350
|
-
{role.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
385
|
+
<div className="flex items-center gap-2">
|
|
386
|
+
<button
|
|
387
|
+
onClick={() => handleEditRole(role.id)}
|
|
388
|
+
className="cursor-pointer rounded-lg p-2 text-orange-600 hover:bg-orange-50"
|
|
389
|
+
title="Modifier"
|
|
390
|
+
>
|
|
391
|
+
<Edit className="h-4 w-4" />
|
|
392
|
+
</button>
|
|
393
|
+
<button
|
|
394
|
+
onClick={() => handleDeleteRole(role.id)}
|
|
395
|
+
className="cursor-pointer rounded-lg p-2 text-red-600 hover:bg-red-50"
|
|
396
|
+
title="Supprimer"
|
|
397
|
+
>
|
|
398
|
+
<Trash2 className="h-4 w-4" />
|
|
399
|
+
</button>
|
|
358
400
|
</div>
|
|
359
401
|
</div>
|
|
360
|
-
<div className="flex items-center gap-2">
|
|
361
|
-
<button
|
|
362
|
-
onClick={() => handleEditRole(role.id)}
|
|
363
|
-
className="cursor-pointer rounded-lg p-2 text-orange-600 hover:bg-orange-50"
|
|
364
|
-
title="Modifier"
|
|
365
|
-
>
|
|
366
|
-
<Edit className="h-4 w-4" />
|
|
367
|
-
</button>
|
|
368
|
-
<button
|
|
369
|
-
onClick={() => handleDeleteRole(role.id)}
|
|
370
|
-
className="cursor-pointer rounded-lg p-2 text-red-600 hover:bg-red-50"
|
|
371
|
-
title="Supprimer"
|
|
372
|
-
>
|
|
373
|
-
<Trash2 className="h-4 w-4" />
|
|
374
|
-
</button>
|
|
375
|
-
</div>
|
|
376
|
-
</div>
|
|
377
402
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
403
|
+
<div className="mt-4 flex items-center gap-4 text-sm text-gray-600">
|
|
404
|
+
<div className="flex items-center gap-1">
|
|
405
|
+
<UsersIcon className="h-4 w-4" />
|
|
406
|
+
<span>
|
|
407
|
+
{role.usersCount} utilisateur{role.usersCount > 1 ? 's' : ''}
|
|
408
|
+
</span>
|
|
409
|
+
</div>
|
|
410
|
+
<div className="flex items-center gap-1">
|
|
411
|
+
<Key className="h-4 w-4" />
|
|
412
|
+
<span>
|
|
413
|
+
{role.permissions.length} permission
|
|
414
|
+
{role.permissions.length > 1 ? 's' : ''}
|
|
415
|
+
</span>
|
|
416
|
+
</div>
|
|
391
417
|
</div>
|
|
392
|
-
</div>
|
|
393
418
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
419
|
+
<div className="mt-4">
|
|
420
|
+
<h4 className="mb-2 text-xs font-medium tracking-wide text-gray-500 uppercase">
|
|
421
|
+
Permissions
|
|
422
|
+
</h4>
|
|
423
|
+
<div className="flex flex-wrap gap-2">
|
|
424
|
+
{visiblePermissions.map((permCode) => {
|
|
425
|
+
const perm = PERMISSIONS.find((p) => p.code === permCode);
|
|
426
|
+
return (
|
|
427
|
+
<span
|
|
428
|
+
key={permCode}
|
|
429
|
+
className="rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-700"
|
|
430
|
+
>
|
|
431
|
+
{perm?.name || permCode}
|
|
432
|
+
</span>
|
|
433
|
+
);
|
|
434
|
+
})}
|
|
435
|
+
{remainingCount > 0 && (
|
|
436
|
+
<span className="rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700">
|
|
437
|
+
+{remainingCount} autres
|
|
407
438
|
</span>
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
{remainingCount > 0 && (
|
|
411
|
-
<span className="rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700">
|
|
412
|
-
+{remainingCount} autres
|
|
413
|
-
</span>
|
|
414
|
-
)}
|
|
439
|
+
)}
|
|
440
|
+
</div>
|
|
415
441
|
</div>
|
|
416
442
|
</div>
|
|
417
|
-
|
|
418
|
-
)
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
</div>
|
|
443
|
+
);
|
|
444
|
+
})}
|
|
445
|
+
</div>
|
|
446
|
+
)}
|
|
447
|
+
</div>
|
|
423
448
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
449
|
+
<RoleModal
|
|
450
|
+
isOpen={showModal}
|
|
451
|
+
onClose={handleCloseModal}
|
|
452
|
+
onSave={handleSaveRole}
|
|
453
|
+
role={selectedRole || undefined}
|
|
454
|
+
/>
|
|
455
|
+
<ConfirmDialog />
|
|
456
|
+
</div>
|
|
457
|
+
</ProtectedPage>
|
|
431
458
|
);
|
|
432
459
|
}
|
|
@@ -15,7 +15,7 @@ export async function GET(request: NextRequest) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// On réutilise la permission de vue utilisateurs pour l’instant
|
|
18
|
-
const hasPermission = await checkPermission('
|
|
18
|
+
const hasPermission = await checkPermission('audit.view_all');
|
|
19
19
|
if (!hasPermission) {
|
|
20
20
|
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
21
21
|
}
|