@hed-hog/lms 0.0.312 → 0.0.315
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/dist/class-group/class-group.controller.d.ts +2 -2
- package/dist/class-group/class-group.service.d.ts +2 -2
- package/dist/enterprise/dto/enterprise-profile.dto.d.ts +13 -0
- package/dist/enterprise/dto/enterprise-profile.dto.d.ts.map +1 -0
- package/dist/enterprise/dto/enterprise-profile.dto.js +3 -0
- package/dist/enterprise/dto/enterprise-profile.dto.js.map +1 -0
- package/dist/enterprise/enterprise.controller.d.ts +3 -0
- package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
- package/dist/enterprise/enterprise.controller.js +14 -0
- package/dist/enterprise/enterprise.controller.js.map +1 -1
- package/dist/enterprise/enterprise.service.d.ts +3 -0
- package/dist/enterprise/enterprise.service.d.ts.map +1 -1
- package/dist/enterprise/enterprise.service.js +128 -1
- package/dist/enterprise/enterprise.service.js.map +1 -1
- package/dist/instructor/instructor.controller.d.ts +23 -0
- package/dist/instructor/instructor.controller.d.ts.map +1 -1
- package/dist/instructor/instructor.controller.js +41 -0
- package/dist/instructor/instructor.controller.js.map +1 -1
- package/dist/instructor/instructor.service.d.ts +25 -0
- package/dist/instructor/instructor.service.d.ts.map +1 -1
- package/dist/instructor/instructor.service.js +126 -8
- package/dist/instructor/instructor.service.js.map +1 -1
- package/hedhog/data/menu.yaml +23 -7
- package/hedhog/data/role.yaml +9 -1
- package/hedhog/data/route.yaml +54 -0
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +44 -44
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -362
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -111
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -134
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -113
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -314
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -62
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +173 -173
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -58
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +51 -51
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -276
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -1216
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1824 -1824
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -443
- package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +40 -40
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -185
- package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -264
- package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +95 -95
- package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +73 -73
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -136
- package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -80
- package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +949 -949
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -525
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +181 -181
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +51 -51
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -271
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -167
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -108
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -318
- package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -10
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +2 -1
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +438 -0
- package/hedhog/frontend/app/instructors/_components/instructor-types.ts.ejs +40 -0
- package/hedhog/frontend/app/instructors/page.tsx.ejs +696 -0
- package/hedhog/frontend/app/training/page.tsx.ejs +2339 -0
- package/hedhog/table/enterprise_user.yaml +1 -1
- package/package.json +8 -8
- package/src/enterprise/dto/enterprise-profile.dto.ts +17 -0
- package/src/enterprise/enterprise.controller.ts +9 -1
- package/src/enterprise/enterprise.service.ts +147 -4
- package/src/instructor/instructor.controller.ts +36 -9
- package/src/instructor/instructor.service.ts +140 -10
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
EmptyState,
|
|
5
|
+
Page,
|
|
6
|
+
PageHeader,
|
|
7
|
+
PaginationFooter,
|
|
8
|
+
SearchBar,
|
|
9
|
+
type SearchBarControl,
|
|
10
|
+
} from '@/components/entity-list';
|
|
11
|
+
import {
|
|
12
|
+
AlertDialog,
|
|
13
|
+
AlertDialogAction,
|
|
14
|
+
AlertDialogCancel,
|
|
15
|
+
AlertDialogContent,
|
|
16
|
+
AlertDialogDescription,
|
|
17
|
+
AlertDialogHeader,
|
|
18
|
+
AlertDialogTitle,
|
|
19
|
+
} from '@/components/ui/alert-dialog';
|
|
20
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
21
|
+
import { Badge } from '@/components/ui/badge';
|
|
22
|
+
import { Button } from '@/components/ui/button';
|
|
23
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
24
|
+
import {
|
|
25
|
+
DropdownMenu,
|
|
26
|
+
DropdownMenuContent,
|
|
27
|
+
DropdownMenuItem,
|
|
28
|
+
DropdownMenuSeparator,
|
|
29
|
+
DropdownMenuTrigger,
|
|
30
|
+
} from '@/components/ui/dropdown-menu';
|
|
31
|
+
import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
|
|
32
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
33
|
+
import {
|
|
34
|
+
Table,
|
|
35
|
+
TableBody,
|
|
36
|
+
TableCell,
|
|
37
|
+
TableHead,
|
|
38
|
+
TableHeader,
|
|
39
|
+
TableRow,
|
|
40
|
+
} from '@/components/ui/table';
|
|
41
|
+
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
|
42
|
+
import { cn } from '@/lib/utils';
|
|
43
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
44
|
+
import {
|
|
45
|
+
LayoutGrid,
|
|
46
|
+
List,
|
|
47
|
+
Mail,
|
|
48
|
+
MoreHorizontal,
|
|
49
|
+
Pencil,
|
|
50
|
+
Phone,
|
|
51
|
+
Plus,
|
|
52
|
+
Trash2,
|
|
53
|
+
UserRoundPen,
|
|
54
|
+
Users,
|
|
55
|
+
} from 'lucide-react';
|
|
56
|
+
import { useEffect, useState } from 'react';
|
|
57
|
+
import { toast } from 'sonner';
|
|
58
|
+
import { InstructorFormSheet } from './_components/instructor-form-sheet';
|
|
59
|
+
import type {
|
|
60
|
+
InstructorPaginatedResult,
|
|
61
|
+
InstructorRow,
|
|
62
|
+
InstructorStats,
|
|
63
|
+
} from './_components/instructor-types';
|
|
64
|
+
|
|
65
|
+
const VIEW_STORAGE_KEY = 'lms:instructors:view-mode';
|
|
66
|
+
|
|
67
|
+
type ViewMode = 'table' | 'cards';
|
|
68
|
+
|
|
69
|
+
const QUALIFICATION_LABELS: Record<string, string> = {
|
|
70
|
+
'course-lessons': 'Aulas de curso',
|
|
71
|
+
'class-sessions': 'Sessões de turma',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function getInstructorInitials(name: string) {
|
|
75
|
+
return name
|
|
76
|
+
.split(' ')
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.slice(0, 2)
|
|
79
|
+
.map((part) => part[0]?.toUpperCase() || '')
|
|
80
|
+
.join('');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getAvatarUrl(avatarId?: number | null) {
|
|
84
|
+
return typeof avatarId === 'number' && avatarId > 0
|
|
85
|
+
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
|
|
86
|
+
: undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default function InstructorsPage() {
|
|
90
|
+
const { request } = useApp();
|
|
91
|
+
|
|
92
|
+
const [page, setPage] = useState(1);
|
|
93
|
+
const [pageSize, setPageSize] = useState(12);
|
|
94
|
+
const [searchInput, setSearchInput] = useState('');
|
|
95
|
+
const [debouncedSearch, setDebouncedSearch] = useState('');
|
|
96
|
+
const [statusFilter, setStatusFilter] = useState('all');
|
|
97
|
+
const [qualificationFilter, setQualificationFilter] = useState('all');
|
|
98
|
+
const [viewMode, setViewMode] = useState<ViewMode>('table');
|
|
99
|
+
const [sheetOpen, setSheetOpen] = useState(false);
|
|
100
|
+
const [instructorToEdit, setInstructorToEdit] =
|
|
101
|
+
useState<InstructorRow | null>(null);
|
|
102
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
103
|
+
const [instructorToDelete, setInstructorToDelete] =
|
|
104
|
+
useState<InstructorRow | null>(null);
|
|
105
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
106
|
+
|
|
107
|
+
// Debounce search
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const timeout = setTimeout(() => {
|
|
110
|
+
setDebouncedSearch(searchInput.trim());
|
|
111
|
+
setPage(1);
|
|
112
|
+
}, 300);
|
|
113
|
+
return () => clearTimeout(timeout);
|
|
114
|
+
}, [searchInput]);
|
|
115
|
+
|
|
116
|
+
// Restore view mode from localStorage
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
try {
|
|
119
|
+
const saved = window.localStorage.getItem(VIEW_STORAGE_KEY);
|
|
120
|
+
if (saved === 'table' || saved === 'cards') {
|
|
121
|
+
setViewMode(saved);
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// Ignore
|
|
125
|
+
}
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
const handleViewModeChange = (value: string) => {
|
|
129
|
+
if (value !== 'table' && value !== 'cards') return;
|
|
130
|
+
setViewMode(value);
|
|
131
|
+
try {
|
|
132
|
+
window.localStorage.setItem(VIEW_STORAGE_KEY, value);
|
|
133
|
+
} catch {
|
|
134
|
+
// Ignore
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Stats
|
|
139
|
+
const {
|
|
140
|
+
data: stats = { total: 0, active: 0, inactive: 0 },
|
|
141
|
+
refetch: refetchStats,
|
|
142
|
+
} = useQuery<InstructorStats>({
|
|
143
|
+
queryKey: ['lms-instructor-stats'],
|
|
144
|
+
queryFn: async () => {
|
|
145
|
+
const response = await request<InstructorStats>({
|
|
146
|
+
url: '/lms/instructors/stats',
|
|
147
|
+
method: 'GET',
|
|
148
|
+
});
|
|
149
|
+
return response.data;
|
|
150
|
+
},
|
|
151
|
+
placeholderData: (prev) => prev ?? { total: 0, active: 0, inactive: 0 },
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// List
|
|
155
|
+
const {
|
|
156
|
+
data: paginate = {
|
|
157
|
+
data: [],
|
|
158
|
+
total: 0,
|
|
159
|
+
page: 1,
|
|
160
|
+
pageSize,
|
|
161
|
+
lastPage: 1,
|
|
162
|
+
},
|
|
163
|
+
isLoading,
|
|
164
|
+
refetch: refetchList,
|
|
165
|
+
} = useQuery<InstructorPaginatedResult>({
|
|
166
|
+
queryKey: [
|
|
167
|
+
'lms-instructors',
|
|
168
|
+
page,
|
|
169
|
+
pageSize,
|
|
170
|
+
debouncedSearch,
|
|
171
|
+
statusFilter,
|
|
172
|
+
qualificationFilter,
|
|
173
|
+
],
|
|
174
|
+
queryFn: async () => {
|
|
175
|
+
const params = new URLSearchParams({
|
|
176
|
+
page: String(page),
|
|
177
|
+
pageSize: String(pageSize),
|
|
178
|
+
});
|
|
179
|
+
if (debouncedSearch) params.set('search', debouncedSearch);
|
|
180
|
+
if (statusFilter !== 'all') params.set('status', statusFilter);
|
|
181
|
+
if (qualificationFilter !== 'all')
|
|
182
|
+
params.set('qualificationSlugs', qualificationFilter);
|
|
183
|
+
|
|
184
|
+
const response = await request<InstructorPaginatedResult>({
|
|
185
|
+
url: `/lms/instructors?${params.toString()}`,
|
|
186
|
+
method: 'GET',
|
|
187
|
+
});
|
|
188
|
+
return response.data;
|
|
189
|
+
},
|
|
190
|
+
placeholderData: (prev) =>
|
|
191
|
+
prev ?? { data: [], total: 0, page: 1, pageSize, lastPage: 1 },
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const totalPages = Math.max(
|
|
195
|
+
1,
|
|
196
|
+
(paginate.lastPage ?? Math.ceil((paginate.total || 0) / pageSize)) || 1
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (page > totalPages) setPage(totalPages);
|
|
201
|
+
}, [page, totalPages]);
|
|
202
|
+
|
|
203
|
+
const openCreateSheet = () => {
|
|
204
|
+
setInstructorToEdit(null);
|
|
205
|
+
setSheetOpen(true);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const openEditSheet = (instructor: InstructorRow) => {
|
|
209
|
+
setInstructorToEdit(instructor);
|
|
210
|
+
setSheetOpen(true);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const handleSaved = async () => {
|
|
214
|
+
await Promise.all([refetchList(), refetchStats()]);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const handleDeleteConfirm = async () => {
|
|
218
|
+
if (!instructorToDelete) return;
|
|
219
|
+
try {
|
|
220
|
+
setIsDeleting(true);
|
|
221
|
+
await request({
|
|
222
|
+
url: `/lms/instructors/${instructorToDelete.id}`,
|
|
223
|
+
method: 'DELETE',
|
|
224
|
+
});
|
|
225
|
+
toast.success('Instrutor removido com sucesso.');
|
|
226
|
+
setDeleteDialogOpen(false);
|
|
227
|
+
setInstructorToDelete(null);
|
|
228
|
+
await Promise.all([refetchList(), refetchStats()]);
|
|
229
|
+
} catch {
|
|
230
|
+
toast.error('Erro ao remover instrutor. Tente novamente.');
|
|
231
|
+
} finally {
|
|
232
|
+
setIsDeleting(false);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const statsCards = [
|
|
237
|
+
{
|
|
238
|
+
key: 'total',
|
|
239
|
+
title: 'Total de instrutores',
|
|
240
|
+
value: stats.total,
|
|
241
|
+
icon: Users,
|
|
242
|
+
accentClassName: 'from-slate-500/20 via-slate-400/10 to-transparent',
|
|
243
|
+
iconContainerClassName: 'bg-slate-100 text-slate-700',
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
key: 'active',
|
|
247
|
+
title: 'Ativos',
|
|
248
|
+
value: stats.active,
|
|
249
|
+
icon: UserRoundPen,
|
|
250
|
+
accentClassName: 'from-green-500/20 via-emerald-500/10 to-transparent',
|
|
251
|
+
iconContainerClassName: 'bg-green-50 text-green-600',
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
key: 'inactive',
|
|
255
|
+
title: 'Inativos',
|
|
256
|
+
value: stats.inactive,
|
|
257
|
+
icon: UserRoundPen,
|
|
258
|
+
accentClassName: 'from-gray-500/20 via-gray-400/10 to-transparent',
|
|
259
|
+
iconContainerClassName: 'bg-gray-100 text-gray-600',
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
const searchControls: SearchBarControl[] = [
|
|
264
|
+
{
|
|
265
|
+
id: 'status-filter',
|
|
266
|
+
type: 'select',
|
|
267
|
+
value: statusFilter,
|
|
268
|
+
onChange: (value: string) => {
|
|
269
|
+
setStatusFilter(value);
|
|
270
|
+
setPage(1);
|
|
271
|
+
},
|
|
272
|
+
placeholder: 'Filtrar por status',
|
|
273
|
+
options: [
|
|
274
|
+
{ value: 'all', label: 'Todos os status' },
|
|
275
|
+
{ value: 'active', label: 'Ativo' },
|
|
276
|
+
{ value: 'inactive', label: 'Inativo' },
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: 'qualification-filter',
|
|
281
|
+
type: 'select',
|
|
282
|
+
value: qualificationFilter,
|
|
283
|
+
onChange: (value: string) => {
|
|
284
|
+
setQualificationFilter(value);
|
|
285
|
+
setPage(1);
|
|
286
|
+
},
|
|
287
|
+
placeholder: 'Filtrar por qualificação',
|
|
288
|
+
options: [
|
|
289
|
+
{ value: 'all', label: 'Todas as qualificações' },
|
|
290
|
+
{ value: 'course-lessons', label: 'Aulas de curso' },
|
|
291
|
+
{ value: 'class-sessions', label: 'Sessões de turma' },
|
|
292
|
+
],
|
|
293
|
+
},
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<Page>
|
|
298
|
+
<PageHeader
|
|
299
|
+
breadcrumbs={[
|
|
300
|
+
{ label: 'Home', href: '/' },
|
|
301
|
+
{ label: 'LMS', href: '/lms' },
|
|
302
|
+
{ label: 'Instrutores' },
|
|
303
|
+
]}
|
|
304
|
+
title="Instrutores"
|
|
305
|
+
description="Gerencie os perfis de instrutores da plataforma."
|
|
306
|
+
actions={[
|
|
307
|
+
{
|
|
308
|
+
label: 'Novo Instrutor',
|
|
309
|
+
onClick: openCreateSheet,
|
|
310
|
+
icon: <Plus className="h-4 w-4" />,
|
|
311
|
+
},
|
|
312
|
+
]}
|
|
313
|
+
/>
|
|
314
|
+
|
|
315
|
+
<KpiCardsGrid items={statsCards} />
|
|
316
|
+
|
|
317
|
+
<div className="flex flex-col gap-4 xl:flex-row xl:items-center">
|
|
318
|
+
<div className="flex-1">
|
|
319
|
+
<SearchBar
|
|
320
|
+
searchQuery={searchInput}
|
|
321
|
+
onSearchChange={(value) => {
|
|
322
|
+
setSearchInput(value);
|
|
323
|
+
}}
|
|
324
|
+
onSearch={() => setPage(1)}
|
|
325
|
+
placeholder="Buscar por nome ou e-mail..."
|
|
326
|
+
controls={searchControls}
|
|
327
|
+
/>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
<div className="flex items-center justify-between gap-3 sm:justify-start xl:justify-end">
|
|
331
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
332
|
+
Visualização
|
|
333
|
+
</span>
|
|
334
|
+
<ToggleGroup
|
|
335
|
+
type="single"
|
|
336
|
+
value={viewMode}
|
|
337
|
+
onValueChange={handleViewModeChange}
|
|
338
|
+
variant="outline"
|
|
339
|
+
size="sm"
|
|
340
|
+
aria-label="Modo de visualização"
|
|
341
|
+
>
|
|
342
|
+
<ToggleGroupItem
|
|
343
|
+
value="table"
|
|
344
|
+
className="gap-1.5 px-2.5"
|
|
345
|
+
aria-label="Tabela"
|
|
346
|
+
>
|
|
347
|
+
<List className="h-4 w-4" />
|
|
348
|
+
<span className="hidden sm:inline">Tabela</span>
|
|
349
|
+
</ToggleGroupItem>
|
|
350
|
+
<ToggleGroupItem
|
|
351
|
+
value="cards"
|
|
352
|
+
className="gap-1.5 px-2.5"
|
|
353
|
+
aria-label="Cards"
|
|
354
|
+
>
|
|
355
|
+
<LayoutGrid className="h-4 w-4" />
|
|
356
|
+
<span className="hidden sm:inline">Cards</span>
|
|
357
|
+
</ToggleGroupItem>
|
|
358
|
+
</ToggleGroup>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
{isLoading ? (
|
|
363
|
+
viewMode === 'cards' ? (
|
|
364
|
+
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
|
365
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
366
|
+
<Card key={i} className="overflow-hidden py-0">
|
|
367
|
+
<CardContent className="space-y-3 p-4">
|
|
368
|
+
<div className="flex items-center gap-2.5">
|
|
369
|
+
<Skeleton className="h-10 w-10 rounded-full" />
|
|
370
|
+
<div className="flex-1 space-y-2">
|
|
371
|
+
<Skeleton className="h-4 w-2/3" />
|
|
372
|
+
<Skeleton className="h-3 w-1/2" />
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
<div className="flex gap-2">
|
|
376
|
+
<Skeleton className="h-5 w-24 rounded-full" />
|
|
377
|
+
<Skeleton className="h-5 w-16 rounded-full" />
|
|
378
|
+
</div>
|
|
379
|
+
</CardContent>
|
|
380
|
+
</Card>
|
|
381
|
+
))}
|
|
382
|
+
</div>
|
|
383
|
+
) : (
|
|
384
|
+
<div className="space-y-3 p-4">
|
|
385
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
386
|
+
<Skeleton key={i} className="h-14 w-full" />
|
|
387
|
+
))}
|
|
388
|
+
</div>
|
|
389
|
+
)
|
|
390
|
+
) : paginate.data.length === 0 ? (
|
|
391
|
+
<EmptyState
|
|
392
|
+
icon={<UserRoundPen className="h-12 w-12" />}
|
|
393
|
+
title="Nenhum instrutor encontrado"
|
|
394
|
+
description="Crie um novo instrutor ou ajuste os filtros de busca."
|
|
395
|
+
actionLabel="Novo Instrutor"
|
|
396
|
+
actionIcon={<Plus className="mr-2 h-4 w-4" />}
|
|
397
|
+
onAction={openCreateSheet}
|
|
398
|
+
/>
|
|
399
|
+
) : (
|
|
400
|
+
<>
|
|
401
|
+
{viewMode === 'table' ? (
|
|
402
|
+
<div className="overflow-x-auto">
|
|
403
|
+
<Table>
|
|
404
|
+
<TableHeader>
|
|
405
|
+
<TableRow>
|
|
406
|
+
<TableHead>Instrutor</TableHead>
|
|
407
|
+
<TableHead>E-mail</TableHead>
|
|
408
|
+
<TableHead>Telefone</TableHead>
|
|
409
|
+
<TableHead>Qualificações</TableHead>
|
|
410
|
+
<TableHead>Training</TableHead>
|
|
411
|
+
<TableHead>Status</TableHead>
|
|
412
|
+
<TableHead className="w-10" />
|
|
413
|
+
</TableRow>
|
|
414
|
+
</TableHeader>
|
|
415
|
+
<TableBody>
|
|
416
|
+
{paginate.data.map((instructor) => (
|
|
417
|
+
<TableRow
|
|
418
|
+
key={instructor.id}
|
|
419
|
+
className="cursor-pointer"
|
|
420
|
+
onDoubleClick={() => openEditSheet(instructor)}
|
|
421
|
+
>
|
|
422
|
+
<TableCell>
|
|
423
|
+
<div className="flex items-center gap-3">
|
|
424
|
+
<Avatar className="h-9 w-9 rounded-full">
|
|
425
|
+
{instructor.avatarId ? (
|
|
426
|
+
<AvatarImage
|
|
427
|
+
src={getAvatarUrl(instructor.avatarId)}
|
|
428
|
+
alt={instructor.name}
|
|
429
|
+
className="object-cover"
|
|
430
|
+
/>
|
|
431
|
+
) : null}
|
|
432
|
+
<AvatarFallback className="bg-slate-100 text-xs font-semibold uppercase text-slate-700">
|
|
433
|
+
{getInstructorInitials(instructor.name)}
|
|
434
|
+
</AvatarFallback>
|
|
435
|
+
</Avatar>
|
|
436
|
+
<span className="font-medium">{instructor.name}</span>
|
|
437
|
+
</div>
|
|
438
|
+
</TableCell>
|
|
439
|
+
<TableCell>
|
|
440
|
+
<span className="text-sm text-muted-foreground">
|
|
441
|
+
{instructor.email ? (
|
|
442
|
+
<span className="flex items-center gap-1">
|
|
443
|
+
<Mail className="h-3 w-3" />
|
|
444
|
+
{instructor.email}
|
|
445
|
+
</span>
|
|
446
|
+
) : (
|
|
447
|
+
'-'
|
|
448
|
+
)}
|
|
449
|
+
</span>
|
|
450
|
+
</TableCell>
|
|
451
|
+
<TableCell>
|
|
452
|
+
<span className="text-sm text-muted-foreground">
|
|
453
|
+
{instructor.phone ? (
|
|
454
|
+
<span className="flex items-center gap-1">
|
|
455
|
+
<Phone className="h-3 w-3" />
|
|
456
|
+
{instructor.phone}
|
|
457
|
+
</span>
|
|
458
|
+
) : (
|
|
459
|
+
'-'
|
|
460
|
+
)}
|
|
461
|
+
</span>
|
|
462
|
+
</TableCell>
|
|
463
|
+
<TableCell>
|
|
464
|
+
<div className="flex flex-wrap gap-1">
|
|
465
|
+
{instructor.qualificationSlugs.map((slug) => (
|
|
466
|
+
<Badge
|
|
467
|
+
key={slug}
|
|
468
|
+
variant="outline"
|
|
469
|
+
className="border-blue-500/20 bg-blue-500/10 px-2 py-0.5 text-[11px] font-medium text-blue-600"
|
|
470
|
+
>
|
|
471
|
+
{QUALIFICATION_LABELS[slug] ?? slug}
|
|
472
|
+
</Badge>
|
|
473
|
+
))}
|
|
474
|
+
</div>
|
|
475
|
+
</TableCell>
|
|
476
|
+
<TableCell>
|
|
477
|
+
{instructor.hasTrainingAccess ? (
|
|
478
|
+
<Badge
|
|
479
|
+
variant="outline"
|
|
480
|
+
className="border-amber-500/20 bg-amber-500/10 px-2.5 py-1 text-xs font-medium text-amber-600"
|
|
481
|
+
>
|
|
482
|
+
Ativo
|
|
483
|
+
</Badge>
|
|
484
|
+
) : instructor.userId ? (
|
|
485
|
+
<Badge
|
|
486
|
+
variant="outline"
|
|
487
|
+
className="border-gray-500/20 bg-gray-500/10 px-2.5 py-1 text-xs font-medium text-gray-500"
|
|
488
|
+
>
|
|
489
|
+
Desabilitado
|
|
490
|
+
</Badge>
|
|
491
|
+
) : (
|
|
492
|
+
<span className="text-xs text-muted-foreground">
|
|
493
|
+
—
|
|
494
|
+
</span>
|
|
495
|
+
)}
|
|
496
|
+
</TableCell>
|
|
497
|
+
<TableCell>
|
|
498
|
+
<Badge
|
|
499
|
+
variant="outline"
|
|
500
|
+
className={cn(
|
|
501
|
+
'border px-2.5 py-1 text-xs font-medium',
|
|
502
|
+
instructor.status === 'active'
|
|
503
|
+
? 'border-green-500/20 bg-green-500/10 text-green-600'
|
|
504
|
+
: 'border-gray-500/20 bg-gray-500/10 text-gray-600'
|
|
505
|
+
)}
|
|
506
|
+
>
|
|
507
|
+
{instructor.status === 'active' ? 'Ativo' : 'Inativo'}
|
|
508
|
+
</Badge>
|
|
509
|
+
</TableCell>
|
|
510
|
+
<TableCell>
|
|
511
|
+
<DropdownMenu>
|
|
512
|
+
<DropdownMenuTrigger asChild>
|
|
513
|
+
<Button
|
|
514
|
+
variant="ghost"
|
|
515
|
+
size="icon"
|
|
516
|
+
className="h-8 w-8"
|
|
517
|
+
>
|
|
518
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
519
|
+
</Button>
|
|
520
|
+
</DropdownMenuTrigger>
|
|
521
|
+
<DropdownMenuContent align="end">
|
|
522
|
+
<DropdownMenuItem
|
|
523
|
+
onClick={() => openEditSheet(instructor)}
|
|
524
|
+
>
|
|
525
|
+
<Pencil className="mr-2 h-4 w-4" />
|
|
526
|
+
Editar
|
|
527
|
+
</DropdownMenuItem>
|
|
528
|
+
<DropdownMenuSeparator />
|
|
529
|
+
<DropdownMenuItem
|
|
530
|
+
className="text-red-600"
|
|
531
|
+
onClick={() => {
|
|
532
|
+
setInstructorToDelete(instructor);
|
|
533
|
+
setDeleteDialogOpen(true);
|
|
534
|
+
}}
|
|
535
|
+
>
|
|
536
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
537
|
+
Excluir
|
|
538
|
+
</DropdownMenuItem>
|
|
539
|
+
</DropdownMenuContent>
|
|
540
|
+
</DropdownMenu>
|
|
541
|
+
</TableCell>
|
|
542
|
+
</TableRow>
|
|
543
|
+
))}
|
|
544
|
+
</TableBody>
|
|
545
|
+
</Table>
|
|
546
|
+
</div>
|
|
547
|
+
) : (
|
|
548
|
+
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
|
549
|
+
{paginate.data.map((instructor) => (
|
|
550
|
+
<Card
|
|
551
|
+
key={instructor.id}
|
|
552
|
+
className="group cursor-pointer overflow-hidden border-border/70 py-0 transition-colors hover:border-border hover:shadow-md"
|
|
553
|
+
onDoubleClick={() => openEditSheet(instructor)}
|
|
554
|
+
>
|
|
555
|
+
<CardContent className="flex flex-col gap-3 p-4">
|
|
556
|
+
<div className="flex items-start justify-between gap-3">
|
|
557
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
558
|
+
<Avatar className="h-10 w-10 shrink-0 rounded-full border border-slate-500/20">
|
|
559
|
+
{instructor.avatarId ? (
|
|
560
|
+
<AvatarImage
|
|
561
|
+
src={getAvatarUrl(instructor.avatarId)}
|
|
562
|
+
alt={instructor.name}
|
|
563
|
+
className="object-cover"
|
|
564
|
+
/>
|
|
565
|
+
) : null}
|
|
566
|
+
<AvatarFallback className="bg-slate-500/8 text-sm font-semibold uppercase text-slate-700 dark:text-slate-200">
|
|
567
|
+
{getInstructorInitials(instructor.name)}
|
|
568
|
+
</AvatarFallback>
|
|
569
|
+
</Avatar>
|
|
570
|
+
|
|
571
|
+
<div className="min-w-0">
|
|
572
|
+
<p className="truncate font-medium">
|
|
573
|
+
{instructor.name}
|
|
574
|
+
</p>
|
|
575
|
+
{instructor.email && (
|
|
576
|
+
<p className="flex items-center gap-1 truncate text-xs text-muted-foreground">
|
|
577
|
+
<Mail className="h-3 w-3 shrink-0" />
|
|
578
|
+
{instructor.email}
|
|
579
|
+
</p>
|
|
580
|
+
)}
|
|
581
|
+
{instructor.phone && (
|
|
582
|
+
<p className="flex items-center gap-1 truncate text-xs text-muted-foreground">
|
|
583
|
+
<Phone className="h-3 w-3 shrink-0" />
|
|
584
|
+
{instructor.phone}
|
|
585
|
+
</p>
|
|
586
|
+
)}
|
|
587
|
+
</div>
|
|
588
|
+
</div>
|
|
589
|
+
|
|
590
|
+
<DropdownMenu>
|
|
591
|
+
<DropdownMenuTrigger asChild>
|
|
592
|
+
<Button
|
|
593
|
+
variant="ghost"
|
|
594
|
+
size="icon"
|
|
595
|
+
className="h-8 w-8 shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
|
|
596
|
+
>
|
|
597
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
598
|
+
</Button>
|
|
599
|
+
</DropdownMenuTrigger>
|
|
600
|
+
<DropdownMenuContent align="end">
|
|
601
|
+
<DropdownMenuItem
|
|
602
|
+
onClick={() => openEditSheet(instructor)}
|
|
603
|
+
>
|
|
604
|
+
<Pencil className="mr-2 h-4 w-4" />
|
|
605
|
+
Editar
|
|
606
|
+
</DropdownMenuItem>
|
|
607
|
+
<DropdownMenuSeparator />
|
|
608
|
+
<DropdownMenuItem
|
|
609
|
+
className="text-red-600"
|
|
610
|
+
onClick={() => {
|
|
611
|
+
setInstructorToDelete(instructor);
|
|
612
|
+
setDeleteDialogOpen(true);
|
|
613
|
+
}}
|
|
614
|
+
>
|
|
615
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
616
|
+
Excluir
|
|
617
|
+
</DropdownMenuItem>
|
|
618
|
+
</DropdownMenuContent>
|
|
619
|
+
</DropdownMenu>
|
|
620
|
+
</div>
|
|
621
|
+
|
|
622
|
+
<div className="flex flex-wrap gap-1.5">
|
|
623
|
+
<Badge
|
|
624
|
+
variant="outline"
|
|
625
|
+
className={cn(
|
|
626
|
+
'border px-2 py-0.5 text-[11px] font-medium',
|
|
627
|
+
instructor.status === 'active'
|
|
628
|
+
? 'border-green-500/20 bg-green-500/10 text-green-600'
|
|
629
|
+
: 'border-gray-500/20 bg-gray-500/10 text-gray-600'
|
|
630
|
+
)}
|
|
631
|
+
>
|
|
632
|
+
{instructor.status === 'active' ? 'Ativo' : 'Inativo'}
|
|
633
|
+
</Badge>
|
|
634
|
+
{instructor.qualificationSlugs.map((slug) => (
|
|
635
|
+
<Badge
|
|
636
|
+
key={slug}
|
|
637
|
+
variant="outline"
|
|
638
|
+
className="border-blue-500/20 bg-blue-500/10 px-2 py-0.5 text-[11px] font-medium text-blue-600"
|
|
639
|
+
>
|
|
640
|
+
{QUALIFICATION_LABELS[slug] ?? slug}
|
|
641
|
+
</Badge>
|
|
642
|
+
))}
|
|
643
|
+
</div>
|
|
644
|
+
</CardContent>
|
|
645
|
+
</Card>
|
|
646
|
+
))}
|
|
647
|
+
</div>
|
|
648
|
+
)}
|
|
649
|
+
</>
|
|
650
|
+
)}
|
|
651
|
+
|
|
652
|
+
<PaginationFooter
|
|
653
|
+
currentPage={page}
|
|
654
|
+
pageSize={pageSize}
|
|
655
|
+
totalItems={paginate.total}
|
|
656
|
+
onPageChange={setPage}
|
|
657
|
+
onPageSizeChange={(nextPageSize) => {
|
|
658
|
+
setPageSize(nextPageSize);
|
|
659
|
+
setPage(1);
|
|
660
|
+
}}
|
|
661
|
+
/>
|
|
662
|
+
|
|
663
|
+
<InstructorFormSheet
|
|
664
|
+
open={sheetOpen}
|
|
665
|
+
onOpenChange={setSheetOpen}
|
|
666
|
+
instructorId={instructorToEdit?.id ?? null}
|
|
667
|
+
onSaved={handleSaved}
|
|
668
|
+
/>
|
|
669
|
+
|
|
670
|
+
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
671
|
+
<AlertDialogContent>
|
|
672
|
+
<AlertDialogHeader>
|
|
673
|
+
<AlertDialogTitle>Excluir instrutor</AlertDialogTitle>
|
|
674
|
+
<AlertDialogDescription>
|
|
675
|
+
Tem certeza que deseja excluir o instrutor{' '}
|
|
676
|
+
<strong>{instructorToDelete?.name}</strong>? Esta ação não pode
|
|
677
|
+
ser desfeita.
|
|
678
|
+
</AlertDialogDescription>
|
|
679
|
+
</AlertDialogHeader>
|
|
680
|
+
<div className="flex justify-end gap-2">
|
|
681
|
+
<AlertDialogCancel disabled={isDeleting}>
|
|
682
|
+
Cancelar
|
|
683
|
+
</AlertDialogCancel>
|
|
684
|
+
<AlertDialogAction
|
|
685
|
+
onClick={handleDeleteConfirm}
|
|
686
|
+
disabled={isDeleting}
|
|
687
|
+
className="bg-red-600 hover:bg-red-700"
|
|
688
|
+
>
|
|
689
|
+
{isDeleting ? 'Excluindo...' : 'Excluir'}
|
|
690
|
+
</AlertDialogAction>
|
|
691
|
+
</div>
|
|
692
|
+
</AlertDialogContent>
|
|
693
|
+
</AlertDialog>
|
|
694
|
+
</Page>
|
|
695
|
+
);
|
|
696
|
+
}
|