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.
- package/bin/create-crm-tmp.js +7 -3
- package/package.json +1 -1
- package/template/README.md +70 -5
- package/template/WORKFLOWS_CRON.md +49 -27
- package/template/package.json +18 -16
- package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +20 -0
- package/template/prisma/schema.prisma +17 -0
- package/template/src/app/(dashboard)/agenda/page.tsx +279 -225
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +1 -5
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +20 -47
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +0 -2
- package/template/src/app/(dashboard)/closing/page.tsx +5 -57
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +60 -44
- package/template/src/app/(dashboard)/contacts/page.tsx +156 -210
- package/template/src/app/(dashboard)/dashboard/page.tsx +438 -91
- package/template/src/app/(dashboard)/settings/page.tsx +179 -77
- package/template/src/app/(dashboard)/users/layout.tsx +30 -0
- package/template/src/app/(dashboard)/users/list/page.tsx +213 -159
- package/template/src/app/(dashboard)/users/page.tsx +13 -46
- package/template/src/app/(dashboard)/users/permissions/page.tsx +0 -2
- package/template/src/app/(dashboard)/users/roles/page.tsx +0 -2
- package/template/src/app/api/audit-logs/route.ts +0 -2
- package/template/src/app/api/auth/google/status/route.ts +46 -7
- package/template/src/app/api/closing-reasons/route.ts +0 -2
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +2 -1
- package/template/src/app/api/contacts/[id]/files/route.ts +25 -20
- package/template/src/app/api/contacts/[id]/route.ts +2 -3
- package/template/src/app/api/contacts/export/route.ts +14 -11
- package/template/src/app/api/contacts/import/route.ts +2 -6
- package/template/src/app/api/contacts/route.ts +1 -1
- package/template/src/app/api/dashboard/stats/route.ts +7 -0
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +47 -0
- package/template/src/app/api/dashboard/widgets/route.ts +181 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +58 -28
- package/template/src/app/api/reminders/route.ts +4 -2
- package/template/src/app/api/roles/route.ts +1 -1
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +1 -6
- package/template/src/app/api/settings/closing-reasons/route.ts +0 -2
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +10 -5
- package/template/src/app/api/settings/google-sheet/route.ts +3 -3
- package/template/src/app/api/tasks/[id]/route.ts +4 -4
- package/template/src/app/api/tasks/meet/route.ts +1 -2
- package/template/src/app/api/tasks/route.ts +16 -18
- package/template/src/app/api/users/for-agenda/route.ts +1 -2
- package/template/src/app/api/workflows/[id]/route.ts +2 -9
- package/template/src/app/api/workflows/route.ts +0 -1
- package/template/src/app/globals.css +96 -0
- package/template/src/components/dashboard/activity-chart.tsx +37 -37
- package/template/src/components/dashboard/add-widget-dialog.tsx +161 -0
- package/template/src/components/dashboard/color-picker.tsx +65 -0
- package/template/src/components/dashboard/contacts-chart.tsx +36 -30
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
- package/template/src/components/dashboard/recent-activity.tsx +79 -86
- package/template/src/components/dashboard/sales-analytics-chart.tsx +4 -8
- package/template/src/components/dashboard/stat-card.tsx +42 -40
- package/template/src/components/dashboard/status-distribution-chart.tsx +64 -27
- package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
- package/template/src/components/dashboard/top-contacts-list.tsx +41 -51
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +71 -78
- package/template/src/components/dashboard/widget-wrapper.tsx +39 -0
- package/template/src/components/header.tsx +21 -12
- package/template/src/components/page-header.tsx +14 -47
- package/template/src/components/sidebar.tsx +3 -4
- package/template/src/contexts/dashboard-theme-context.tsx +58 -0
- package/template/src/lib/audit-log.ts +0 -2
- package/template/src/lib/dashboard-themes.ts +140 -0
- package/template/src/lib/default-widgets.ts +14 -0
- package/template/src/lib/google-drive.ts +38 -30
- package/template/src/lib/permissions.ts +56 -1
- package/template/src/lib/prisma.ts +0 -1
- package/template/src/lib/widget-registry.ts +177 -0
- package/template/src/lib/workflow-executor.ts +7 -13
- 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
|
-
|
|
453
|
-
|
|
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-
|
|
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-
|
|
1053
|
-
<div className="flex items-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
<div className="
|
|
1057
|
-
|
|
1058
|
-
|
|
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
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
<
|
|
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
|
|
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-
|
|
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-
|
|
1076
|
-
|
|
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-
|
|
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-
|
|
1084
|
-
|
|
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-
|
|
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-
|
|
1092
|
-
|
|
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-
|
|
1100
|
-
{/* Filtres
|
|
1101
|
-
<div className="flex
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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">
|
|
1305
|
-
<option value="week">
|
|
1306
|
-
<option value="day">
|
|
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-
|
|
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-[
|
|
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-
|
|
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
|
-
|
|
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: '
|
|
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-
|
|
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(
|
|
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-
|
|
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-
|
|
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: '
|
|
1514
|
+
className="relative grid min-w-[600px]"
|
|
1515
|
+
style={{ gridTemplateColumns: '40px repeat(7, 1fr)' }}
|
|
1465
1516
|
>
|
|
1466
1517
|
{/* Colonne des heures */}
|
|
1467
|
-
<div
|
|
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-
|
|
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
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
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
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2176
|
+
.toISOString()
|
|
2177
|
+
.split('T')[1]
|
|
2178
|
+
.slice(0, 5)
|
|
2125
2179
|
}
|
|
2126
2180
|
onChange={(e) => {
|
|
2127
2181
|
const datePart =
|