create-crm-tmp 1.0.2 → 1.1.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 (73) hide show
  1. package/bin/create-crm-tmp.js +7 -3
  2. package/package.json +1 -1
  3. package/template/README.md +70 -5
  4. package/template/WORKFLOWS_CRON.md +49 -27
  5. package/template/package.json +18 -16
  6. package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +20 -0
  7. package/template/prisma/schema.prisma +17 -0
  8. package/template/src/app/(dashboard)/agenda/page.tsx +279 -225
  9. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +1 -5
  10. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +20 -47
  11. package/template/src/app/(dashboard)/automatisation/new/page.tsx +0 -2
  12. package/template/src/app/(dashboard)/closing/page.tsx +5 -57
  13. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +60 -44
  14. package/template/src/app/(dashboard)/contacts/page.tsx +156 -210
  15. package/template/src/app/(dashboard)/dashboard/page.tsx +438 -91
  16. package/template/src/app/(dashboard)/settings/page.tsx +179 -77
  17. package/template/src/app/(dashboard)/users/layout.tsx +30 -0
  18. package/template/src/app/(dashboard)/users/list/page.tsx +213 -159
  19. package/template/src/app/(dashboard)/users/page.tsx +13 -46
  20. package/template/src/app/(dashboard)/users/permissions/page.tsx +0 -2
  21. package/template/src/app/(dashboard)/users/roles/page.tsx +0 -2
  22. package/template/src/app/api/audit-logs/route.ts +0 -2
  23. package/template/src/app/api/auth/google/status/route.ts +46 -7
  24. package/template/src/app/api/closing-reasons/route.ts +0 -2
  25. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +2 -1
  26. package/template/src/app/api/contacts/[id]/files/route.ts +25 -20
  27. package/template/src/app/api/contacts/[id]/route.ts +2 -3
  28. package/template/src/app/api/contacts/export/route.ts +14 -11
  29. package/template/src/app/api/contacts/import/route.ts +2 -6
  30. package/template/src/app/api/contacts/route.ts +1 -1
  31. package/template/src/app/api/dashboard/stats/route.ts +7 -0
  32. package/template/src/app/api/dashboard/widgets/[id]/route.ts +47 -0
  33. package/template/src/app/api/dashboard/widgets/route.ts +181 -0
  34. package/template/src/app/api/integrations/google-sheet/sync/route.ts +58 -28
  35. package/template/src/app/api/reminders/route.ts +4 -2
  36. package/template/src/app/api/roles/route.ts +1 -1
  37. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +1 -6
  38. package/template/src/app/api/settings/closing-reasons/route.ts +0 -2
  39. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +10 -5
  40. package/template/src/app/api/settings/google-sheet/route.ts +3 -3
  41. package/template/src/app/api/tasks/[id]/route.ts +4 -4
  42. package/template/src/app/api/tasks/meet/route.ts +1 -2
  43. package/template/src/app/api/tasks/route.ts +16 -18
  44. package/template/src/app/api/users/for-agenda/route.ts +1 -2
  45. package/template/src/app/api/workflows/[id]/route.ts +2 -9
  46. package/template/src/app/api/workflows/route.ts +0 -1
  47. package/template/src/app/globals.css +96 -0
  48. package/template/src/components/dashboard/activity-chart.tsx +37 -37
  49. package/template/src/components/dashboard/add-widget-dialog.tsx +161 -0
  50. package/template/src/components/dashboard/color-picker.tsx +65 -0
  51. package/template/src/components/dashboard/contacts-chart.tsx +36 -30
  52. package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
  53. package/template/src/components/dashboard/recent-activity.tsx +79 -86
  54. package/template/src/components/dashboard/sales-analytics-chart.tsx +4 -8
  55. package/template/src/components/dashboard/stat-card.tsx +42 -40
  56. package/template/src/components/dashboard/status-distribution-chart.tsx +64 -27
  57. package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
  58. package/template/src/components/dashboard/top-contacts-list.tsx +41 -51
  59. package/template/src/components/dashboard/upcoming-tasks-list.tsx +71 -78
  60. package/template/src/components/dashboard/widget-wrapper.tsx +39 -0
  61. package/template/src/components/header.tsx +21 -12
  62. package/template/src/components/page-header.tsx +14 -47
  63. package/template/src/components/sidebar.tsx +3 -4
  64. package/template/src/contexts/dashboard-theme-context.tsx +58 -0
  65. package/template/src/lib/audit-log.ts +0 -2
  66. package/template/src/lib/dashboard-themes.ts +140 -0
  67. package/template/src/lib/default-widgets.ts +14 -0
  68. package/template/src/lib/google-drive.ts +38 -30
  69. package/template/src/lib/permissions.ts +56 -1
  70. package/template/src/lib/prisma.ts +0 -1
  71. package/template/src/lib/widget-registry.ts +177 -0
  72. package/template/src/lib/workflow-executor.ts +7 -13
  73. package/README.md +0 -89
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useState, useEffect, useRef, useMemo } from 'react';
4
4
  import { useUserRole } from '@/hooks/use-user-role';
5
+ import { useViewAs } from '@/contexts/view-as-context';
5
6
  import { cn } from '@/lib/utils';
6
7
  import {
7
8
  Calendar,
@@ -95,6 +96,7 @@ interface User {
95
96
 
96
97
  export default function AgendaPage() {
97
98
  const { isAdmin, hasPermission } = useUserRole();
99
+ const { viewAsUser, isViewingAsOther } = useViewAs();
98
100
  const [tasks, setTasks] = useState<Task[]>([]);
99
101
  const [contacts, setContacts] = useState<Contact[]>([]);
100
102
  const [users, setUsers] = useState<User[]>([]);
@@ -201,7 +203,7 @@ export default function AgendaPage() {
201
203
  const response = await fetch('/api/auth/google/status');
202
204
  if (response.ok) {
203
205
  const data = await response.json();
204
- setGoogleConnected(!!data.connected);
206
+ setGoogleConnected(!!data.calendar?.connected);
205
207
  } else {
206
208
  setGoogleConnected(false);
207
209
  }
@@ -440,7 +442,7 @@ export default function AgendaPage() {
440
442
 
441
443
  useEffect(() => {
442
444
  fetchTasks();
443
- }, [currentDate, view]);
445
+ }, [currentDate, view, isViewingAsOther, viewAsUser?.id]);
444
446
 
445
447
  const fetchTasks = async () => {
446
448
  try {
@@ -449,8 +451,12 @@ export default function AgendaPage() {
449
451
  const url = new URL('/api/tasks', globalThis.location.origin);
450
452
  url.searchParams.set('startDate', start.toISOString());
451
453
  url.searchParams.set('endDate', end.toISOString());
452
- // Toujours envoyer showOtherUsers si le toggle est activé, l'API vérifiera la permission
453
- if (showOtherUsersEvents) {
454
+
455
+ // En mode "vue en tant que", afficher les tâches de l'utilisateur simulé
456
+ if (isViewingAsOther && viewAsUser?.id) {
457
+ url.searchParams.set('assignedTo', viewAsUser.id);
458
+ } else if (showOtherUsersEvents) {
459
+ // Toujours envoyer showOtherUsers si le toggle est activé, l'API vérifiera la permission
454
460
  url.searchParams.set('showOtherUsers', 'true');
455
461
  }
456
462
 
@@ -1047,232 +1053,255 @@ export default function AgendaPage() {
1047
1053
  return (
1048
1054
  <div className="bg-crms-bg flex h-full flex-col">
1049
1055
  {/* Header avec titre et navigation */}
1050
- <div className="border-b border-gray-200 bg-white px-4 py-4 sm:px-6 lg:px-8">
1056
+ <div className="border-b border-gray-200 bg-white px-4 py-3 sm:px-6 sm:py-4 lg:px-8">
1051
1057
  {/* Titre et informations de date */}
1052
- <div className="mb-4 flex items-start justify-between gap-4">
1053
- <div className="flex items-start gap-4">
1054
- {/* Date actuelle en grand */}
1055
- <div className="flex flex-col">
1056
- <div className="text-3xl font-bold text-gray-900">{formatCurrentDay().day}</div>
1057
- <div className="text-xs font-medium text-gray-500 uppercase">
1058
- {formatCurrentDay().month}
1058
+ <div className="mb-3 sm:mb-4">
1059
+ <div className="flex items-center justify-between gap-3">
1060
+ <div className="flex items-center gap-3 sm:gap-4">
1061
+ {/* Date actuelle en grand */}
1062
+ <div className="flex flex-col items-center">
1063
+ <div className="text-2xl font-bold text-gray-900 sm:text-3xl">
1064
+ {formatCurrentDay().day}
1065
+ </div>
1066
+ <div className="text-[10px] font-medium text-gray-500 uppercase sm:text-xs">
1067
+ {formatCurrentDay().month}
1068
+ </div>
1069
+ </div>
1070
+ <div className="flex flex-col">
1071
+ <h1 className="text-xl font-bold text-gray-900 sm:text-2xl">Agenda</h1>
1072
+ <p className="text-xs text-gray-500 sm:text-sm">{formatCurrentMonthYear()}</p>
1073
+ <p className="hidden text-xs text-gray-400 sm:block">{formatHeaderDateRange()}</p>
1059
1074
  </div>
1060
1075
  </div>
1061
- <div className="flex flex-col">
1062
- <h1 className="text-2xl font-bold text-gray-900">Agenda</h1>
1063
- <p className="mt-1 text-sm text-gray-500">{formatCurrentMonthYear()}</p>
1064
- <p className="text-xs text-gray-400">{formatHeaderDateRange()}</p>
1076
+
1077
+ {/* Actions visibles en ligne sur desktop */}
1078
+ <div className="hidden items-center gap-2 sm:flex">
1079
+ <button
1080
+ type="button"
1081
+ onClick={() => setShowCreateMeetModal(true)}
1082
+ className="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-green-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-green-700"
1083
+ >
1084
+ <Video className="h-4 w-4" />
1085
+ Google Meet
1086
+ </button>
1087
+ <button
1088
+ type="button"
1089
+ onClick={() => setShowCreateMeetingModal(true)}
1090
+ className="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-blue-700"
1091
+ >
1092
+ <Calendar className="h-4 w-4" />
1093
+ Rendez-vous
1094
+ </button>
1095
+ <button
1096
+ type="button"
1097
+ onClick={() => setShowCreateTaskModal(true)}
1098
+ className="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-indigo-700"
1099
+ >
1100
+ <Bookmark className="h-4 w-4" />
1101
+ Tâche
1102
+ </button>
1065
1103
  </div>
1066
1104
  </div>
1067
1105
 
1068
- {/* Actions à droite */}
1069
- <div className="flex items-center gap-2">
1106
+ {/* Actions ligne dédiée sur mobile */}
1107
+ <div className="mt-3 flex items-center gap-2 sm:hidden">
1070
1108
  <button
1071
1109
  type="button"
1072
1110
  onClick={() => setShowCreateMeetModal(true)}
1073
- className="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-green-600 px-3 py-2 text-xs font-semibold text-white shadow-sm transition-colors hover:bg-green-700 sm:px-4 sm:text-sm"
1111
+ className="inline-flex flex-1 cursor-pointer items-center justify-center gap-1.5 rounded-lg bg-green-600 px-2.5 py-2 text-xs font-semibold text-white shadow-sm transition-colors hover:bg-green-700"
1074
1112
  >
1075
- <Video className="h-4 w-4" />
1076
- <span className="hidden sm:inline">Google Meet</span>
1113
+ <Video className="h-3.5 w-3.5" />
1114
+ Meet
1077
1115
  </button>
1078
1116
  <button
1079
1117
  type="button"
1080
1118
  onClick={() => setShowCreateMeetingModal(true)}
1081
- className="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-blue-600 px-3 py-2 text-xs font-semibold text-white shadow-sm transition-colors hover:bg-blue-700 sm:px-4 sm:text-sm"
1119
+ className="inline-flex flex-1 cursor-pointer items-center justify-center gap-1.5 rounded-lg bg-blue-600 px-2.5 py-2 text-xs font-semibold text-white shadow-sm transition-colors hover:bg-blue-700"
1082
1120
  >
1083
- <Calendar className="h-4 w-4" />
1084
- <span className="hidden sm:inline">Rendez-vous</span>
1121
+ <Calendar className="h-3.5 w-3.5" />
1122
+ RDV
1085
1123
  </button>
1086
1124
  <button
1087
1125
  type="button"
1088
1126
  onClick={() => setShowCreateTaskModal(true)}
1089
- className="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-indigo-600 px-3 py-2 text-xs font-semibold text-white shadow-sm transition-colors hover:bg-indigo-700 sm:px-4 sm:text-sm"
1127
+ className="inline-flex flex-1 cursor-pointer items-center justify-center gap-1.5 rounded-lg bg-indigo-600 px-2.5 py-2 text-xs font-semibold text-white shadow-sm transition-colors hover:bg-indigo-700"
1090
1128
  >
1091
- <Bookmark className="h-4 w-4" />
1092
- <span className="hidden sm:inline">Tâche</span>
1093
- <span className="sm:hidden">Ajouter</span>
1129
+ <Bookmark className="h-3.5 w-3.5" />
1130
+ Tâche
1094
1131
  </button>
1095
1132
  </div>
1096
1133
  </div>
1097
1134
 
1098
1135
  {/* Filtres et navigation */}
1099
- <div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
1100
- {/* Filtres d'événements */}
1101
- <div className="flex items-center gap-2">
1102
- <button
1103
- type="button"
1104
- onClick={() =>
1105
- setFilters({
1106
- tasks: true,
1107
- meetings: true,
1108
- googleMeets: true,
1109
- })
1110
- }
1111
- className={cn(
1112
- 'cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors',
1113
- filters.tasks && filters.meetings && filters.googleMeets
1114
- ? 'bg-gray-100 text-gray-900'
1115
- : 'bg-white text-gray-600 hover:bg-gray-50',
1116
- )}
1117
- >
1118
- Tous les événements
1119
- </button>
1120
- <button
1121
- type="button"
1122
- onClick={() =>
1123
- setFilters({
1124
- tasks: true,
1125
- meetings: false,
1126
- googleMeets: false,
1127
- })
1128
- }
1129
- className={cn(
1130
- 'cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors',
1131
- filters.tasks && !filters.meetings && !filters.googleMeets
1132
- ? 'bg-gray-100 text-gray-900'
1133
- : 'bg-white text-gray-600 hover:bg-gray-50',
1134
- )}
1135
- >
1136
- Tâches
1137
- </button>
1138
- <button
1139
- type="button"
1140
- onClick={() =>
1141
- setFilters({
1142
- tasks: false,
1143
- meetings: true,
1144
- googleMeets: false,
1145
- })
1146
- }
1147
- className={cn(
1148
- 'cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors',
1149
- !filters.tasks && filters.meetings && !filters.googleMeets
1150
- ? 'bg-gray-100 text-gray-900'
1151
- : 'bg-white text-gray-600 hover:bg-gray-50',
1152
- )}
1153
- >
1154
- Rendez-vous
1155
- </button>
1156
- <button
1157
- type="button"
1158
- onClick={() =>
1159
- setFilters({
1160
- tasks: false,
1161
- meetings: false,
1162
- googleMeets: true,
1163
- })
1164
- }
1165
- className={cn(
1166
- 'cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors',
1167
- !filters.tasks && !filters.meetings && filters.googleMeets
1168
- ? 'bg-gray-100 text-gray-900'
1169
- : 'bg-white text-gray-600 hover:bg-gray-50',
1170
- )}
1171
- >
1172
- Google Meet
1173
- </button>
1174
- {hasPermission('tasks.view_other_users_events') && (
1175
- <>
1176
- {/* Filtre par utilisateur (affiché quand "Voir les autres utilisateurs" est actif) */}
1177
- <div className="mx-2 h-6 w-px bg-gray-300" />
1178
- <button
1179
- type="button"
1180
- onClick={() => {
1181
- setShowOtherUsersEvents(!showOtherUsersEvents);
1182
- // Si on active et que les utilisateurs ne sont pas encore chargés, les charger
1183
- if (!showOtherUsersEvents && users.length === 0) {
1184
- fetchUsers();
1185
- }
1186
- // Réinitialiser le filtre utilisateur
1187
- if (users.length > 0) {
1188
- setSelectedUserIds(new Set(users.map((u) => u.id)));
1189
- }
1190
- }}
1191
- className={cn(
1192
- 'cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors',
1193
- showOtherUsersEvents
1194
- ? 'bg-gray-100 text-gray-900'
1195
- : 'bg-white text-gray-600 hover:bg-gray-50',
1196
- )}
1197
- >
1198
- Voir les autres utilisateurs
1199
- </button>
1200
- {showOtherUsersEvents && (
1201
- <>
1202
- <div className="mx-2 h-6 w-px bg-gray-300" />
1203
- {users.length > 0 ? (
1204
- <div className="flex flex-wrap items-center gap-2">
1205
- {users.map((user) => {
1206
- const isSelected = selectedUserIds.has(user.id);
1207
- const userColor = user.eventColor || '#6366F1';
1208
- // Convertir la couleur hex en rgba pour l'opacité
1209
- const hexToRgba = (hex: string, alpha: number) => {
1210
- const r = Number.parseInt(hex.slice(1, 3), 16);
1211
- const g = Number.parseInt(hex.slice(3, 5), 16);
1212
- const b = Number.parseInt(hex.slice(5, 7), 16);
1213
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
1214
- };
1215
- return (
1216
- <button
1217
- key={user.id}
1218
- type="button"
1219
- onClick={() => {
1220
- const newSelected = new Set(selectedUserIds);
1221
- if (isSelected) {
1222
- newSelected.delete(user.id);
1223
- } else {
1224
- newSelected.add(user.id);
1225
- }
1226
- setSelectedUserIds(newSelected);
1227
- }}
1228
- className={cn(
1229
- 'cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-all',
1230
- isSelected ? 'text-white shadow-sm' : 'text-gray-700',
1231
- )}
1232
- style={{
1233
- backgroundColor: isSelected
1234
- ? userColor
1235
- : hexToRgba(userColor, 0.15),
1236
- border: isSelected
1237
- ? `1px solid ${userColor}`
1238
- : `1px solid ${hexToRgba(userColor, 0.3)}`,
1239
- }}
1240
- title={user.name}
1241
- onMouseEnter={(e) => {
1242
- if (!isSelected) {
1243
- e.currentTarget.style.backgroundColor = hexToRgba(
1244
- userColor,
1245
- 0.25,
1246
- );
1247
- }
1248
- }}
1249
- onMouseLeave={(e) => {
1250
- if (!isSelected) {
1251
- e.currentTarget.style.backgroundColor = hexToRgba(
1252
- userColor,
1253
- 0.15,
1254
- );
1255
- }
1256
- }}
1257
- >
1258
- <span className="max-w-[100px] truncate">{user.name}</span>
1259
- </button>
1260
- );
1261
- })}
1262
- </div>
1263
- ) : (
1264
- <div className="text-xs text-gray-500">Chargement des utilisateurs...</div>
1136
+ <div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
1137
+ {/* Filtres + badges utilisateurs */}
1138
+ <div className="flex gap-2 max-sm:flex-col">
1139
+ {/* Filtres d'événements */}
1140
+ <div className="flex items-center gap-2 overflow-x-auto pb-1 sm:pb-0">
1141
+ <button
1142
+ type="button"
1143
+ onClick={() =>
1144
+ setFilters({
1145
+ tasks: true,
1146
+ meetings: true,
1147
+ googleMeets: true,
1148
+ })
1149
+ }
1150
+ className={cn(
1151
+ 'shrink-0 cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium whitespace-nowrap transition-colors',
1152
+ filters.tasks && filters.meetings && filters.googleMeets
1153
+ ? 'bg-gray-100 text-gray-900'
1154
+ : 'bg-white text-gray-600 hover:bg-gray-50',
1155
+ )}
1156
+ >
1157
+ Tous
1158
+ </button>
1159
+ <button
1160
+ type="button"
1161
+ onClick={() =>
1162
+ setFilters({
1163
+ tasks: true,
1164
+ meetings: false,
1165
+ googleMeets: false,
1166
+ })
1167
+ }
1168
+ className={cn(
1169
+ 'shrink-0 cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium whitespace-nowrap transition-colors',
1170
+ filters.tasks && !filters.meetings && !filters.googleMeets
1171
+ ? 'bg-gray-100 text-gray-900'
1172
+ : 'bg-white text-gray-600 hover:bg-gray-50',
1173
+ )}
1174
+ >
1175
+ Tâches
1176
+ </button>
1177
+ <button
1178
+ type="button"
1179
+ onClick={() =>
1180
+ setFilters({
1181
+ tasks: false,
1182
+ meetings: true,
1183
+ googleMeets: false,
1184
+ })
1185
+ }
1186
+ className={cn(
1187
+ 'shrink-0 cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium whitespace-nowrap transition-colors',
1188
+ !filters.tasks && filters.meetings && !filters.googleMeets
1189
+ ? 'bg-gray-100 text-gray-900'
1190
+ : 'bg-white text-gray-600 hover:bg-gray-50',
1191
+ )}
1192
+ >
1193
+ RDV
1194
+ </button>
1195
+ <button
1196
+ type="button"
1197
+ onClick={() =>
1198
+ setFilters({
1199
+ tasks: false,
1200
+ meetings: false,
1201
+ googleMeets: true,
1202
+ })
1203
+ }
1204
+ className={cn(
1205
+ 'shrink-0 cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium whitespace-nowrap transition-colors',
1206
+ !filters.tasks && !filters.meetings && filters.googleMeets
1207
+ ? 'bg-gray-100 text-gray-900'
1208
+ : 'bg-white text-gray-600 hover:bg-gray-50',
1209
+ )}
1210
+ >
1211
+ Meet
1212
+ </button>
1213
+ {hasPermission('tasks.view_other_users_events') && (
1214
+ <>
1215
+ <div className="mx-2 h-6 w-px shrink-0 bg-gray-300" />
1216
+ <button
1217
+ type="button"
1218
+ onClick={() => {
1219
+ setShowOtherUsersEvents(!showOtherUsersEvents);
1220
+ if (!showOtherUsersEvents && users.length === 0) {
1221
+ fetchUsers();
1222
+ }
1223
+ if (users.length > 0) {
1224
+ setSelectedUserIds(new Set(users.map((u) => u.id)));
1225
+ }
1226
+ }}
1227
+ className={cn(
1228
+ 'shrink-0 cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium whitespace-nowrap transition-colors',
1229
+ showOtherUsersEvents
1230
+ ? 'bg-gray-100 text-gray-900'
1231
+ : 'bg-white text-gray-600 hover:bg-gray-50',
1265
1232
  )}
1266
- </>
1233
+ >
1234
+ <Eye className="mr-1 inline h-3 w-3 sm:hidden" />
1235
+ <span className="hidden sm:inline">Voir les autres utilisateurs</span>
1236
+ <span className="sm:hidden">Autres</span>
1237
+ </button>
1238
+ </>
1239
+ )}
1240
+ </div>
1241
+
1242
+ {/* Badges utilisateurs — ligne dédiée en dessous des filtres */}
1243
+ {hasPermission('tasks.view_other_users_events') && showOtherUsersEvents && (
1244
+ <div className="flex flex-wrap items-center gap-2">
1245
+ {users.length > 0 ? (
1246
+ users.map((user) => {
1247
+ const isSelected = selectedUserIds.has(user.id);
1248
+ const userColor = user.eventColor || '#6366F1';
1249
+ const hexToRgba = (hex: string, alpha: number) => {
1250
+ const r = Number.parseInt(hex.slice(1, 3), 16);
1251
+ const g = Number.parseInt(hex.slice(3, 5), 16);
1252
+ const b = Number.parseInt(hex.slice(5, 7), 16);
1253
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
1254
+ };
1255
+ return (
1256
+ <button
1257
+ key={user.id}
1258
+ type="button"
1259
+ onClick={() => {
1260
+ const newSelected = new Set(selectedUserIds);
1261
+ if (isSelected) {
1262
+ newSelected.delete(user.id);
1263
+ } else {
1264
+ newSelected.add(user.id);
1265
+ }
1266
+ setSelectedUserIds(newSelected);
1267
+ }}
1268
+ className={cn(
1269
+ 'cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-all',
1270
+ isSelected ? 'text-white shadow-sm' : 'text-gray-700',
1271
+ )}
1272
+ style={{
1273
+ backgroundColor: isSelected ? userColor : hexToRgba(userColor, 0.15),
1274
+ border: isSelected
1275
+ ? `1px solid ${userColor}`
1276
+ : `1px solid ${hexToRgba(userColor, 0.3)}`,
1277
+ }}
1278
+ title={user.name}
1279
+ onMouseEnter={(e) => {
1280
+ if (!isSelected) {
1281
+ e.currentTarget.style.backgroundColor = hexToRgba(userColor, 0.25);
1282
+ }
1283
+ }}
1284
+ onMouseLeave={(e) => {
1285
+ if (!isSelected) {
1286
+ e.currentTarget.style.backgroundColor = hexToRgba(userColor, 0.15);
1287
+ }
1288
+ }}
1289
+ >
1290
+ <span className="max-w-[100px] truncate">{user.name}</span>
1291
+ </button>
1292
+ );
1293
+ })
1294
+ ) : (
1295
+ <div className="text-xs text-gray-500">Chargement des utilisateurs...</div>
1267
1296
  )}
1268
- </>
1297
+ </div>
1269
1298
  )}
1270
1299
  </div>
1271
1300
 
1272
1301
  {/* Navigation et sélecteur de vue */}
1273
- <div className="flex items-center gap-2">
1302
+ <div className="flex items-center justify-between gap-2 sm:justify-end">
1274
1303
  {/* Navigation de date */}
1275
- <div className="flex items-center gap-1 rounded-lg border border-gray-200 bg-white p-1">
1304
+ <div className="flex items-center gap-0.5 rounded-lg border border-gray-200 bg-white p-0.5 sm:gap-1 sm:p-1">
1276
1305
  <button
1277
1306
  onClick={() => navigateDate('prev')}
1278
1307
  className="cursor-pointer rounded-md p-1.5 text-gray-600 transition-colors hover:bg-gray-100"
@@ -1282,9 +1311,9 @@ export default function AgendaPage() {
1282
1311
  </button>
1283
1312
  <button
1284
1313
  onClick={goToToday}
1285
- className="cursor-pointer rounded-md px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-100"
1314
+ className="cursor-pointer rounded-md px-2 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-100 sm:px-3"
1286
1315
  >
1287
- Aujourd'hui
1316
+ Auj.
1288
1317
  </button>
1289
1318
  <button
1290
1319
  onClick={() => navigateDate('next')}
@@ -1299,11 +1328,11 @@ export default function AgendaPage() {
1299
1328
  <select
1300
1329
  value={view}
1301
1330
  onChange={(e) => setView(e.target.value as 'month' | 'week' | 'day')}
1302
- className="cursor-pointer rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
1331
+ className="cursor-pointer rounded-lg border border-gray-200 bg-white px-2 py-1.5 text-xs font-medium text-gray-700 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none sm:px-3"
1303
1332
  >
1304
- <option value="month">Vue mois</option>
1305
- <option value="week">Vue semaine</option>
1306
- <option value="day">Vue jour</option>
1333
+ <option value="month">Mois</option>
1334
+ <option value="week">Semaine</option>
1335
+ <option value="day">Jour</option>
1307
1336
  </select>
1308
1337
  </div>
1309
1338
  </div>
@@ -1320,9 +1349,10 @@ export default function AgendaPage() {
1320
1349
  {['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'].map((day) => (
1321
1350
  <div
1322
1351
  key={day}
1323
- className="border-r border-gray-200 px-4 py-3 text-center text-xs font-medium tracking-wide text-gray-500 uppercase last:border-r-0"
1352
+ className="border-r border-gray-200 px-1 py-2 text-center text-[10px] font-medium tracking-wide text-gray-500 uppercase last:border-r-0 sm:px-4 sm:py-3 sm:text-xs"
1324
1353
  >
1325
- {day}
1354
+ <span className="sm:hidden">{day[0]}</span>
1355
+ <span className="hidden sm:inline">{day}</span>
1326
1356
  </div>
1327
1357
  ))}
1328
1358
  </div>
@@ -1339,7 +1369,7 @@ export default function AgendaPage() {
1339
1369
  setView('day');
1340
1370
  }}
1341
1371
  className={cn(
1342
- 'min-h-[120px] cursor-pointer p-2 transition-colors hover:bg-gray-50/50',
1372
+ 'min-h-[60px] cursor-pointer p-1 transition-colors hover:bg-gray-50/50 sm:min-h-[120px] sm:p-2',
1343
1373
  !day.isCurrentMonth && 'bg-gray-50/30',
1344
1374
  isToday && 'bg-indigo-50/40',
1345
1375
  )}
@@ -1347,7 +1377,7 @@ export default function AgendaPage() {
1347
1377
  <div className="relative">
1348
1378
  <div
1349
1379
  className={cn(
1350
- 'mb-2 text-sm font-semibold',
1380
+ 'mb-1 text-[10px] font-semibold sm:mb-2 sm:text-sm',
1351
1381
  isToday
1352
1382
  ? 'text-indigo-600'
1353
1383
  : day.isCurrentMonth
@@ -1362,7 +1392,26 @@ export default function AgendaPage() {
1362
1392
  <div className="absolute top-0 right-0 h-1.5 w-1.5 rounded-full bg-blue-500" />
1363
1393
  )}
1364
1394
  </div>
1365
- <div className="space-y-1" onClick={(e) => e.stopPropagation()}>
1395
+
1396
+ {/* Mobile : indicateur compacte (points colorés) */}
1397
+ <div
1398
+ className="flex flex-wrap gap-0.5 sm:hidden"
1399
+ onClick={(e) => e.stopPropagation()}
1400
+ >
1401
+ {dayTasks.slice(0, 4).map((task) => (
1402
+ <div
1403
+ key={task.id}
1404
+ className="h-1.5 w-1.5 rounded-full"
1405
+ style={{ backgroundColor: getEventColor(task) }}
1406
+ />
1407
+ ))}
1408
+ </div>
1409
+
1410
+ {/* Desktop : événements détaillés */}
1411
+ <div
1412
+ className="hidden space-y-1 sm:block"
1413
+ onClick={(e) => e.stopPropagation()}
1414
+ >
1366
1415
  {dayTasks.slice(0, 3).map((task) => {
1367
1416
  const eventColor = getEventColor(task);
1368
1417
  return (
@@ -1431,25 +1480,27 @@ export default function AgendaPage() {
1431
1480
  <div className="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
1432
1481
  <div className="overflow-auto">
1433
1482
  <div
1434
- className="grid border-b border-gray-200 bg-gray-50/60"
1435
- style={{ gridTemplateColumns: '59px repeat(7, 1fr)' }}
1483
+ className="grid min-w-[600px] border-b border-gray-200 bg-gray-50/60"
1484
+ style={{ gridTemplateColumns: '40px repeat(7, 1fr)' }}
1436
1485
  >
1437
- <div className="px-2 py-3 text-right text-xs font-medium text-gray-500"></div>
1486
+ <div className="px-1 py-2 text-right text-xs font-medium text-gray-500 sm:px-2 sm:py-3"></div>
1438
1487
  {getWeekDays().map((day) => {
1439
1488
  const isToday = day.toDateString() === new Date().toDateString();
1440
1489
  return (
1441
1490
  <div
1442
1491
  key={day.toISOString()}
1443
- className={cn('border-l border-gray-200 px-3 py-3 text-center')}
1492
+ className={cn(
1493
+ 'border-l border-gray-200 px-1 py-2 text-center sm:px-3 sm:py-3',
1494
+ )}
1444
1495
  >
1445
- <div className="text-xs font-medium text-gray-500 uppercase">
1496
+ <div className="text-[10px] font-medium text-gray-500 uppercase sm:text-xs">
1446
1497
  {day
1447
1498
  .toLocaleDateString('fr-FR', { weekday: 'short' })
1448
1499
  .replaceAll('.', '')}
1449
1500
  </div>
1450
1501
  <div
1451
1502
  className={cn(
1452
- 'mx-auto mt-2 flex h-8 w-8 items-center justify-center rounded-full text-sm font-semibold',
1503
+ 'mx-auto mt-1 flex h-6 w-6 items-center justify-center rounded-full text-xs font-semibold sm:mt-2 sm:h-8 sm:w-8 sm:text-sm',
1453
1504
  isToday ? 'bg-gray-900 text-white' : 'text-gray-900',
1454
1505
  )}
1455
1506
  >
@@ -1460,11 +1511,14 @@ export default function AgendaPage() {
1460
1511
  })}
1461
1512
  </div>
1462
1513
  <div
1463
- className="relative grid"
1464
- style={{ gridTemplateColumns: '60px repeat(7, 1fr)' }}
1514
+ className="relative grid min-w-[600px]"
1515
+ style={{ gridTemplateColumns: '40px repeat(7, 1fr)' }}
1465
1516
  >
1466
1517
  {/* Colonne des heures */}
1467
- <div className="relative border-r border-gray-200 bg-gray-50/40">
1518
+ <div
1519
+ className="relative border-r border-gray-200 bg-gray-50/40"
1520
+ style={{ minWidth: '40px' }}
1521
+ >
1468
1522
  {HOURS.map((hour) => {
1469
1523
  // Vérifier si l'heure actuelle est dans cette plage horaire
1470
1524
  const weekDays = getWeekDays();
@@ -1481,7 +1535,7 @@ export default function AgendaPage() {
1481
1535
  return (
1482
1536
  <div
1483
1537
  key={hour}
1484
- className="relative flex h-16 items-start justify-end border-b border-gray-100 pt-1 pr-2 text-xs text-gray-500"
1538
+ className="relative flex h-16 items-start justify-end border-b border-gray-100 pt-1 pr-1 text-[10px] text-gray-500 sm:pr-2 sm:text-xs"
1485
1539
  >
1486
1540
  {!shouldHideHour && formatHourLabel(hour)}
1487
1541
  </div>
@@ -2102,9 +2156,9 @@ export default function AgendaPage() {
2102
2156
  editMeetData.scheduledAt && editMeetData.scheduledAt.includes('T')
2103
2157
  ? editMeetData.scheduledAt.split('T')[1]
2104
2158
  : new Date(editingMeetTask.scheduledAt)
2105
- .toISOString()
2106
- .split('T')[1]
2107
- .slice(0, 5);
2159
+ .toISOString()
2160
+ .split('T')[1]
2161
+ .slice(0, 5);
2108
2162
  setEditMeetData({
2109
2163
  ...editMeetData,
2110
2164
  scheduledAt: `${e.target.value}T${time || '09:00'}`,
@@ -2119,9 +2173,9 @@ export default function AgendaPage() {
2119
2173
  editMeetData.scheduledAt && editMeetData.scheduledAt.includes('T')
2120
2174
  ? editMeetData.scheduledAt.split('T')[1].slice(0, 5)
2121
2175
  : new Date(editingMeetTask.scheduledAt)
2122
- .toISOString()
2123
- .split('T')[1]
2124
- .slice(0, 5)
2176
+ .toISOString()
2177
+ .split('T')[1]
2178
+ .slice(0, 5)
2125
2179
  }
2126
2180
  onChange={(e) => {
2127
2181
  const datePart =