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.
Files changed (220) hide show
  1. package/package.json +1 -1
  2. package/template/.prettierignore +2 -0
  3. package/template/README.md +53 -67
  4. package/template/components.json +22 -0
  5. package/template/exemple-contacts.csv +54 -0
  6. package/template/next.config.ts +27 -1
  7. package/template/package.json +64 -27
  8. package/template/prisma/schema.prisma +821 -72
  9. package/template/skills-lock.json +25 -0
  10. package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
  11. package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
  12. package/template/src/app/(auth)/reset-password/page.tsx +12 -8
  13. package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
  14. package/template/src/app/(auth)/signin/page.tsx +20 -17
  15. package/template/src/app/(dashboard)/agenda/page.tsx +2231 -2188
  16. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
  17. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
  18. package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
  19. package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
  20. package/template/src/app/(dashboard)/closing/page.tsx +500 -468
  21. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5035 -4126
  22. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
  23. package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
  24. package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
  25. package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
  26. package/template/src/app/(dashboard)/error.tsx +37 -0
  27. package/template/src/app/(dashboard)/layout.tsx +1 -1
  28. package/template/src/app/(dashboard)/loading.tsx +5 -0
  29. package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
  30. package/template/src/app/(dashboard)/settings/page.tsx +2685 -2489
  31. package/template/src/app/(dashboard)/templates/page.tsx +500 -300
  32. package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
  33. package/template/src/app/(dashboard)/users/page.tsx +279 -310
  34. package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
  35. package/template/src/app/(dashboard)/users/roles/page.tsx +164 -137
  36. package/template/src/app/api/audit-logs/route.ts +1 -1
  37. package/template/src/app/api/auth/google/callback/route.ts +8 -5
  38. package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
  39. package/template/src/app/api/companies/[id]/activities/route.ts +131 -0
  40. package/template/src/app/api/companies/[id]/route.ts +195 -0
  41. package/template/src/app/api/companies/export/route.ts +206 -0
  42. package/template/src/app/api/companies/route.ts +166 -0
  43. package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
  44. package/template/src/app/api/contact-views/[id]/route.ts +197 -0
  45. package/template/src/app/api/contact-views/route.ts +146 -0
  46. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +77 -0
  47. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
  48. package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
  49. package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
  50. package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
  51. package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
  52. package/template/src/app/api/contacts/[id]/route.ts +111 -20
  53. package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
  54. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
  55. package/template/src/app/api/contacts/export/route.ts +12 -17
  56. package/template/src/app/api/contacts/import/route.ts +22 -19
  57. package/template/src/app/api/contacts/import-preview/route.ts +139 -0
  58. package/template/src/app/api/contacts/route.ts +202 -49
  59. package/template/src/app/api/dashboard/stats/route.ts +9 -292
  60. package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
  61. package/template/src/app/api/invite/complete/route.ts +20 -23
  62. package/template/src/app/api/reminders/route.ts +1 -0
  63. package/template/src/app/api/reset-password/complete/route.ts +11 -13
  64. package/template/src/app/api/send/route.ts +9 -85
  65. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
  66. package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
  67. package/template/src/app/api/settings/company/route.ts +19 -26
  68. package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
  69. package/template/src/app/api/settings/google-ads/route.ts +20 -23
  70. package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
  71. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
  72. package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
  73. package/template/src/app/api/settings/google-sheet/route.ts +20 -23
  74. package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
  75. package/template/src/app/api/settings/meta-leads/route.ts +20 -23
  76. package/template/src/app/api/settings/statuses/[id]/route.ts +33 -23
  77. package/template/src/app/api/settings/statuses/route.ts +24 -22
  78. package/template/src/app/api/statuses/route.ts +2 -5
  79. package/template/src/app/api/tasks/[id]/attendees/route.ts +14 -7
  80. package/template/src/app/api/tasks/[id]/route.ts +161 -137
  81. package/template/src/app/api/tasks/meet/route.ts +11 -8
  82. package/template/src/app/api/tasks/route.ts +155 -95
  83. package/template/src/app/api/templates/[id]/route.ts +22 -13
  84. package/template/src/app/api/templates/route.ts +22 -5
  85. package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
  86. package/template/src/app/api/users/[id]/route.ts +16 -1
  87. package/template/src/app/api/users/commercials/route.ts +38 -0
  88. package/template/src/app/api/users/for-agenda/route.ts +1 -2
  89. package/template/src/app/api/users/route.ts +94 -55
  90. package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
  91. package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
  92. package/template/src/app/api/workflows/[id]/route.ts +33 -6
  93. package/template/src/app/api/workflows/process/route.ts +509 -146
  94. package/template/src/app/api/workflows/route.ts +46 -4
  95. package/template/src/app/globals.css +210 -101
  96. package/template/src/app/layout.tsx +19 -8
  97. package/template/src/app/page.tsx +37 -7
  98. package/template/src/components/address-autocomplete.tsx +232 -0
  99. package/template/src/components/contacts/filter-bar.tsx +181 -0
  100. package/template/src/components/contacts/filter-builder.tsx +589 -0
  101. package/template/src/components/contacts/save-view-dialog.tsx +160 -0
  102. package/template/src/components/contacts/views-tab-bar.tsx +440 -0
  103. package/template/src/components/dashboard/activity-chart.tsx +31 -39
  104. package/template/src/components/dashboard/dashboard-content.tsx +79 -0
  105. package/template/src/components/dashboard/stat-card.tsx +40 -42
  106. package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
  107. package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
  108. package/template/src/components/date-picker.tsx +396 -0
  109. package/template/src/components/editor.tsx +27 -13
  110. package/template/src/components/email-template.tsx +4 -2
  111. package/template/src/components/global-search.tsx +358 -0
  112. package/template/src/components/header.tsx +57 -62
  113. package/template/src/components/invitation-email-template.tsx +4 -2
  114. package/template/src/components/lazy-editor.tsx +11 -0
  115. package/template/src/components/meet-cancellation-email-template.tsx +11 -3
  116. package/template/src/components/meet-confirmation-email-template.tsx +10 -3
  117. package/template/src/components/meet-update-email-template.tsx +10 -3
  118. package/template/src/components/page-header.tsx +19 -15
  119. package/template/src/components/protected-page.tsx +94 -0
  120. package/template/src/components/reset-password-email-template.tsx +4 -2
  121. package/template/src/components/sidebar.tsx +92 -94
  122. package/template/src/components/skeleton.tsx +128 -42
  123. package/template/src/components/ui/accordion.tsx +64 -0
  124. package/template/src/components/ui/alert-dialog.tsx +139 -0
  125. package/template/src/components/ui/button.tsx +60 -0
  126. package/template/src/components/view-as-banner.tsx +1 -1
  127. package/template/src/components/view-as-modal.tsx +21 -16
  128. package/template/src/config/nav-pages.ts +108 -0
  129. package/template/src/contexts/app-toast-context.tsx +174 -0
  130. package/template/src/contexts/sidebar-context.tsx +16 -47
  131. package/template/src/contexts/task-reminder-context.tsx +6 -6
  132. package/template/src/contexts/view-as-context.tsx +11 -16
  133. package/template/src/hooks/use-alert.tsx +65 -0
  134. package/template/src/hooks/use-confirm.tsx +87 -0
  135. package/template/src/hooks/use-contact-views.ts +140 -0
  136. package/template/src/hooks/use-contacts.ts +69 -0
  137. package/template/src/hooks/use-fetch.ts +17 -0
  138. package/template/src/hooks/use-focus-trap.ts +73 -0
  139. package/template/src/hooks/use-statuses.ts +22 -0
  140. package/template/src/lib/address-api.ts +155 -0
  141. package/template/src/lib/cache.ts +73 -0
  142. package/template/src/lib/check-permission.ts +12 -177
  143. package/template/src/lib/contact-interactions.ts +3 -1
  144. package/template/src/lib/contact-view-filters.ts +341 -0
  145. package/template/src/lib/dashboard-stats.ts +224 -0
  146. package/template/src/lib/date-utils.ts +49 -0
  147. package/template/src/lib/get-auth-user.ts +25 -0
  148. package/template/src/lib/google-calendar.ts +54 -12
  149. package/template/src/lib/google-drive.ts +796 -75
  150. package/template/src/lib/google-fetch.ts +63 -0
  151. package/template/src/lib/local-storage.ts +34 -0
  152. package/template/src/lib/permissions.ts +245 -47
  153. package/template/src/lib/prisma.ts +11 -11
  154. package/template/src/lib/roles.ts +14 -39
  155. package/template/src/lib/template-variables.ts +67 -33
  156. package/template/src/lib/utils.ts +26 -2
  157. package/template/src/lib/workflow-executor.ts +445 -229
  158. package/template/src/proxy.ts +34 -73
  159. package/template/src/types/contact-views.ts +351 -0
  160. package/template/src/types/yousign.ts +52 -0
  161. package/template/vercel.json +12 -0
  162. package/template/WORKFLOWS_CRON.md +0 -185
  163. package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
  164. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
  165. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
  166. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
  167. package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
  168. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
  169. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
  170. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
  171. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
  172. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
  173. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
  174. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
  175. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
  176. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
  177. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
  178. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
  179. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
  180. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
  181. package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
  182. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
  183. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
  184. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
  185. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
  186. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
  187. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
  188. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
  189. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
  190. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
  191. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
  192. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
  193. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
  194. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
  195. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
  196. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
  197. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
  198. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
  199. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
  200. package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
  201. package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
  202. package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
  203. package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
  204. package/template/prisma/migrations/migration_lock.toml +0 -3
  205. package/template/src/app/(dashboard)/users/layout.tsx +0 -30
  206. package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
  207. package/template/src/app/api/dashboard/widgets/route.ts +0 -181
  208. package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
  209. package/template/src/components/dashboard/color-picker.tsx +0 -65
  210. package/template/src/components/dashboard/contacts-chart.tsx +0 -69
  211. package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
  212. package/template/src/components/dashboard/recent-activity.tsx +0 -157
  213. package/template/src/components/dashboard/sales-analytics-chart.tsx +0 -77
  214. package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
  215. package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
  216. package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
  217. package/template/src/contexts/dashboard-theme-context.tsx +0 -58
  218. package/template/src/lib/dashboard-themes.ts +0 -140
  219. package/template/src/lib/default-widgets.ts +0 -14
  220. 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-indigo-50',
58
- text: 'text-indigo-700',
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
- <div className="h-full">
68
- <div className="border-b border-gray-200 bg-white">
69
- <div className="p-4 sm:p-6">
70
- <div className="mb-4">
71
- <Link
72
- href="/users"
73
- className="inline-flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900"
74
- >
75
- <ArrowLeft className="h-4 w-4" />
76
- Retour
77
- </Link>
78
- </div>
79
- <div>
80
- <h1 className="text-2xl font-bold text-gray-900">Gestion des permissions</h1>
81
- <p className="mt-1 text-sm text-gray-500">
82
- {totalPermissions} permissions disponibles dans le système
83
- </p>
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
- <div className="p-4 sm:p-6">
89
- {/* Barre de recherche et filtres */}
90
- <div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
91
- <div className="relative flex-1 sm:max-w-md">
92
- <Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
93
- <input
94
- type="text"
95
- placeholder="Rechercher une permission..."
96
- value={searchTerm}
97
- onChange={(e) => setSearchTerm(e.target.value)}
98
- 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"
99
- />
100
- </div>
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
- <select
103
- value={selectedCategory}
104
- onChange={(e) => setSelectedCategory(e.target.value)}
105
- 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"
106
- >
107
- <option value="all">Toutes les catégories</option>
108
- {Object.values(PERMISSION_CATEGORIES).map((category) => (
109
- <option key={category} value={category}>
110
- {category}
111
- </option>
112
- ))}
113
- </select>
114
- </div>
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
- {/* Liste des permissions par catégorie */}
117
- <div className="space-y-8">
118
- {filteredPermissions.map(([category, permissions]) => {
119
- if (permissions.length === 0) return null;
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
- const colors = categoryColors[category] || {
122
- bg: 'bg-gray-50',
123
- text: 'text-gray-700',
124
- };
125
+ const colors = categoryColors[category] || {
126
+ bg: 'bg-gray-50',
127
+ text: 'text-gray-700',
128
+ };
125
129
 
126
- return (
127
- <div key={category}>
128
- <div className="mb-4 flex items-center gap-3">
129
- <div className={`rounded-lg p-2 ${colors.bg}`}>
130
- <Key className={`h-5 w-5 ${colors.text}`} />
131
- </div>
132
- <div>
133
- <h2 className="text-lg font-semibold text-gray-900">{category}</h2>
134
- <p className="text-sm text-gray-500">
135
- {permissions.length} permission
136
- {permissions.length > 1 ? 's' : ''}
137
- </p>
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
- <div className="space-y-3">
142
- {permissions.map((permission) => (
143
- <div
144
- key={permission.code}
145
- className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"
146
- >
147
- <div className="flex items-start gap-3">
148
- <div className={`mt-0.5 rounded-lg p-2 ${colors.bg}`}>
149
- <Key className={`h-4 w-4 ${colors.text}`} />
150
- </div>
151
- <div className="flex-1">
152
- <h3 className="font-semibold text-gray-900">{permission.name}</h3>
153
- <p className="mt-1 text-sm text-gray-600">{permission.description}</p>
154
- <code className="mt-2 inline-block rounded bg-gray-100 px-2 py-1 font-mono text-xs text-gray-600">
155
- {permission.code}
156
- </code>
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
- </div>
160
- ))}
164
+ ))}
165
+ </div>
161
166
  </div>
162
- </div>
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
- </div>
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:border-indigo-500 focus:ring-1 focus:ring-indigo-500 focus:outline-none"
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:border-indigo-500 focus:ring-1 focus:ring-indigo-500 focus:outline-none"
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-indigo-600 focus:ring-indigo-500"
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
- ? `⚠️ Attention : "${role.name}" est un profil système.\n\nÊtes-vous sûr de vouloir le supprimer ?`
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
- if (!confirm(confirmMessage)) {
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
- <div className="h-full">
289
- <div className="border-b border-gray-200 bg-white">
290
- <div className="p-4 sm:p-6">
291
- <div className="mb-4">
292
- <Link
293
- href="/users"
294
- className="inline-flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900"
295
- >
296
- <ArrowLeft className="h-4 w-4" />
297
- Retour
298
- </Link>
299
- </div>
300
- <div className="flex items-center justify-between">
301
- <div>
302
- <h1 className="text-2xl font-bold text-gray-900">Gestion des profils</h1>
303
- <p className="mt-1 text-sm text-gray-500">
304
- Créer et configurer les profils avec leurs permissions
305
- </p>
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
- {loading ? (
325
- <div className="grid gap-6 lg:grid-cols-2">
326
- {[1, 2, 3, 4].map((i) => (
327
- <div key={i} className="h-64 animate-pulse rounded-lg bg-gray-200" />
328
- ))}
329
- </div>
330
- ) : (
331
- <div className="grid gap-6 lg:grid-cols-2">
332
- {roles
333
- .sort((a, b) => b.permissions.length - a.permissions.length) // Trier par nombre de permissions (DESC)
334
- .map((role) => {
335
- const visiblePermissions = role.permissions.slice(0, 4);
336
- const remainingCount = role.permissions.length - 4;
337
-
338
- return (
339
- <div
340
- key={role.id}
341
- className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm"
342
- >
343
- <div className="flex items-start justify-between">
344
- <div className="flex items-start gap-3">
345
- <div className="rounded-lg bg-green-100 p-2">
346
- <Shield className="h-5 w-5 text-green-600" />
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
- <h3 className="font-semibold text-gray-900">
350
- {role.name}
351
- {role.isSystem && (
352
- <span className="ml-2 inline-block rounded bg-indigo-100 px-2 py-0.5 text-xs text-indigo-600">
353
- Système
354
- </span>
355
- )}
356
- </h3>
357
- <p className="mt-1 text-sm text-gray-600">{role.description}</p>
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
- <div className="mt-4 flex items-center gap-4 text-sm text-gray-600">
379
- <div className="flex items-center gap-1">
380
- <UsersIcon className="h-4 w-4" />
381
- <span>
382
- {role.usersCount} utilisateur{role.usersCount > 1 ? 's' : ''}
383
- </span>
384
- </div>
385
- <div className="flex items-center gap-1">
386
- <Key className="h-4 w-4" />
387
- <span>
388
- {role.permissions.length} permission
389
- {role.permissions.length > 1 ? 's' : ''}
390
- </span>
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
- <div className="mt-4">
395
- <h4 className="mb-2 text-xs font-medium tracking-wide text-gray-500 uppercase">
396
- Permissions
397
- </h4>
398
- <div className="flex flex-wrap gap-2">
399
- {visiblePermissions.map((permCode) => {
400
- const perm = PERMISSIONS.find((p) => p.code === permCode);
401
- return (
402
- <span
403
- key={permCode}
404
- className="rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-700"
405
- >
406
- {perm?.name || permCode}
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
- </div>
418
- );
419
- })}
420
- </div>
421
- )}
422
- </div>
443
+ );
444
+ })}
445
+ </div>
446
+ )}
447
+ </div>
423
448
 
424
- <RoleModal
425
- isOpen={showModal}
426
- onClose={handleCloseModal}
427
- onSave={handleSaveRole}
428
- role={selectedRole || undefined}
429
- />
430
- </div>
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('users.view');
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
  }