@hed-hog/lms 0.0.314 → 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.
Files changed (68) hide show
  1. package/dist/class-group/class-group.controller.d.ts +2 -2
  2. package/dist/class-group/class-group.service.d.ts +2 -2
  3. package/dist/enterprise/dto/enterprise-profile.dto.d.ts +13 -0
  4. package/dist/enterprise/dto/enterprise-profile.dto.d.ts.map +1 -0
  5. package/dist/enterprise/dto/enterprise-profile.dto.js +3 -0
  6. package/dist/enterprise/dto/enterprise-profile.dto.js.map +1 -0
  7. package/dist/enterprise/enterprise.controller.d.ts +3 -0
  8. package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
  9. package/dist/enterprise/enterprise.controller.js +14 -0
  10. package/dist/enterprise/enterprise.controller.js.map +1 -1
  11. package/dist/enterprise/enterprise.service.d.ts +3 -0
  12. package/dist/enterprise/enterprise.service.d.ts.map +1 -1
  13. package/dist/enterprise/enterprise.service.js +128 -1
  14. package/dist/enterprise/enterprise.service.js.map +1 -1
  15. package/dist/instructor/instructor.controller.d.ts +23 -0
  16. package/dist/instructor/instructor.controller.d.ts.map +1 -1
  17. package/dist/instructor/instructor.controller.js +41 -0
  18. package/dist/instructor/instructor.controller.js.map +1 -1
  19. package/dist/instructor/instructor.service.d.ts +25 -0
  20. package/dist/instructor/instructor.service.d.ts.map +1 -1
  21. package/dist/instructor/instructor.service.js +126 -8
  22. package/dist/instructor/instructor.service.js.map +1 -1
  23. package/hedhog/data/menu.yaml +23 -7
  24. package/hedhog/data/role.yaml +9 -1
  25. package/hedhog/data/route.yaml +54 -0
  26. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -1
  27. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +44 -44
  28. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -362
  29. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -111
  30. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -134
  31. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -113
  32. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -314
  33. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -62
  34. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +173 -173
  35. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -58
  36. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +51 -51
  37. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -276
  38. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -1216
  39. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1824 -1824
  40. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -443
  41. package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +40 -40
  42. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -185
  43. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -264
  44. package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +95 -95
  45. package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +73 -73
  46. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -136
  47. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -80
  48. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +949 -949
  49. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -525
  50. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +181 -181
  51. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +51 -51
  52. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -271
  53. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -167
  54. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -108
  55. package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -318
  56. package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -10
  57. package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +2 -1
  58. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +438 -0
  59. package/hedhog/frontend/app/instructors/_components/instructor-types.ts.ejs +40 -0
  60. package/hedhog/frontend/app/instructors/page.tsx.ejs +696 -0
  61. package/hedhog/frontend/app/training/page.tsx.ejs +2339 -0
  62. package/hedhog/table/enterprise_user.yaml +1 -1
  63. package/package.json +8 -8
  64. package/src/enterprise/dto/enterprise-profile.dto.ts +17 -0
  65. package/src/enterprise/enterprise.controller.ts +9 -1
  66. package/src/enterprise/enterprise.service.ts +147 -4
  67. package/src/instructor/instructor.controller.ts +36 -9
  68. package/src/instructor/instructor.service.ts +140 -10
@@ -1,443 +1,443 @@
1
- 'use client';
2
-
3
- import { zodResolver } from '@hookform/resolvers/zod';
4
- import {
5
- CircleDot,
6
- Clock,
7
- Eye,
8
- EyeOff,
9
- Layers,
10
- Loader2,
11
- Lock,
12
- Plus,
13
- Save,
14
- Trash2,
15
- Undo2,
16
- Video,
17
- } from 'lucide-react';
18
- import { useEffect, useMemo } from 'react';
19
- import { useForm } from 'react-hook-form';
20
- import { z } from 'zod';
21
-
22
- import { Badge } from '@/components/ui/badge';
23
- import { Button } from '@/components/ui/button';
24
- import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
25
- import {
26
- Form,
27
- FormControl,
28
- FormField,
29
- FormItem,
30
- FormLabel,
31
- FormMessage,
32
- } from '@/components/ui/form';
33
- import { Input } from '@/components/ui/input';
34
- import { ScrollArea } from '@/components/ui/scroll-area';
35
- import {
36
- Select,
37
- SelectContent,
38
- SelectItem,
39
- SelectTrigger,
40
- SelectValue,
41
- } from '@/components/ui/select';
42
- import { Separator } from '@/components/ui/separator';
43
- import { Switch } from '@/components/ui/switch';
44
- import { Textarea } from '@/components/ui/textarea';
45
-
46
- import {
47
- useCreateLessonMutation,
48
- useDeleteSessionMutation,
49
- useUpdateSessionMutation,
50
- } from '../_data/use-course-structure-mutations';
51
- import { useStructureStore } from './store';
52
- import type { Visibility } from './types';
53
-
54
- // ── Schema ────────────────────────────────────────────────────────────────────
55
-
56
- const schema = z.object({
57
- code: z.string().min(1, 'Código obrigatório'),
58
- title: z.string().min(1, 'Título obrigatório'),
59
- description: z.string(),
60
- duration: z.coerce.number().min(0),
61
- visibility: z.enum(['publico', 'privado', 'restrito'] as const),
62
- published: z.boolean(),
63
- });
64
-
65
- type FormValues = z.infer<typeof schema>;
66
-
67
- // ── Component ─────────────────────────────────────────────────────────────────
68
-
69
- interface EditorSessionProps {
70
- sessionId: string;
71
- }
72
-
73
- export function EditorSession({ sessionId }: EditorSessionProps) {
74
- const session = useStructureStore((s) =>
75
- s.sessions.find((ss) => ss.id === sessionId)
76
- );
77
- const allLessons = useStructureStore((s) => s.lessons);
78
- const lessons = useMemo(
79
- () => allLessons.filter((l) => l.sessionId === sessionId),
80
- [allLessons, sessionId]
81
- );
82
- const showConfirm = useStructureStore((s) => s.showConfirm);
83
-
84
- const updateSession = useUpdateSessionMutation();
85
- const createLesson = useCreateLessonMutation();
86
- const deleteSession = useDeleteSessionMutation();
87
-
88
- const totalMinutes = lessons.reduce((sum, l) => sum + l.duration, 0);
89
- const hours = Math.floor(totalMinutes / 60);
90
- const minutes = totalMinutes % 60;
91
-
92
- const defaultValues: FormValues = {
93
- code: session?.code ?? '',
94
- title: session?.title ?? '',
95
- description: session?.description ?? '',
96
- duration: session?.duration ?? 0,
97
- visibility: (session?.visibility ?? 'publico') as Visibility,
98
- published: session?.published ?? true,
99
- };
100
-
101
- const form = useForm<FormValues>({
102
- resolver: zodResolver(schema),
103
- defaultValues,
104
- });
105
-
106
- const { isDirty } = form.formState;
107
-
108
- useEffect(() => {
109
- if (!session) return;
110
- form.reset({
111
- code: session.code,
112
- title: session.title,
113
- description: session.description ?? '',
114
- duration: session.duration,
115
- visibility: session.visibility ?? 'publico',
116
- published: session.published ?? true,
117
- });
118
- }, [session?.id]); // eslint-disable-line react-hooks/exhaustive-deps
119
-
120
- if (!session) return null;
121
-
122
- function onSubmit(values: FormValues) {
123
- updateSession.mutate({ sessionId, formValues: values });
124
- form.reset(values);
125
- }
126
-
127
- function handleDelete() {
128
- showConfirm({
129
- title: `Excluir sessão "${session!.title}"?`,
130
- description:
131
- 'A sessão e todas as suas aulas serão excluídas permanentemente.',
132
- onConfirm: () => deleteSession.mutate({ sessionId }),
133
- });
134
- }
135
-
136
- return (
137
- <Form {...form}>
138
- <form
139
- onSubmit={form.handleSubmit(onSubmit)}
140
- className="flex flex-col h-full min-h-0"
141
- >
142
- {/* ── Header ───────────────────────────────────────────────────────── */}
143
- <div className="flex items-center gap-3 px-4 py-3 border-b bg-muted/30 shrink-0">
144
- <div className="flex size-9 items-center justify-center rounded-lg bg-muted shrink-0">
145
- <Layers className="size-4 text-muted-foreground" />
146
- </div>
147
- <div className="flex-1 min-w-0">
148
- <div className="flex items-center gap-1.5">
149
- <span className="text-sm font-semibold truncate">Sessão</span>
150
- {isDirty && (
151
- <CircleDot className="size-3 text-amber-500 shrink-0" />
152
- )}
153
- </div>
154
- <p className="text-[0.65rem] text-muted-foreground truncate">
155
- {session.code}
156
- </p>
157
- </div>
158
- <Badge
159
- variant={session.published ? 'default' : 'secondary'}
160
- className="shrink-0 text-xs"
161
- >
162
- {session.published ? 'Publicada' : 'Oculta'}
163
- </Badge>
164
- <Button
165
- type="button"
166
- variant="ghost"
167
- size="icon"
168
- className="size-7 text-destructive/60 hover:text-destructive shrink-0"
169
- title="Excluir sessão"
170
- aria-label="Excluir sessão"
171
- disabled={deleteSession.isPending}
172
- onClick={handleDelete}
173
- >
174
- {deleteSession.isPending ? (
175
- <Loader2 className="size-3.5 animate-spin" />
176
- ) : (
177
- <Trash2 className="size-3.5" />
178
- )}
179
- </Button>
180
- </div>
181
-
182
- {/* ── Scrollable body ───────────────────────────────────────────────── */}
183
- <ScrollArea className="flex-1 min-h-0">
184
- <div className="flex flex-col gap-3 p-3">
185
- {/* ── Stats ────────────────────────────────────────────────────── */}
186
- <div className="grid grid-cols-2 gap-2">
187
- <StatChip
188
- icon={<Video className="size-3" />}
189
- label="Aulas"
190
- value={lessons.length}
191
- />
192
- <StatChip
193
- icon={<Clock className="size-3" />}
194
- label="Duração"
195
- value={hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`}
196
- />
197
- </div>
198
-
199
- {/* ── Dados ────────────────────────────────────────────────────── */}
200
- <Card className="bg-muted/20 py-2 gap-2">
201
- <CardHeader className="px-3 pt-2 pb-1">
202
- <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
203
- Dados
204
- </CardTitle>
205
- </CardHeader>
206
- <CardContent className="px-3 pb-2 flex flex-col gap-3">
207
- <div className="grid grid-cols-2 gap-2">
208
- <FormField
209
- control={form.control}
210
- name="code"
211
- render={({ field }) => (
212
- <FormItem>
213
- <FormLabel className="text-xs">Código</FormLabel>
214
- <FormControl>
215
- <Input {...field} className="h-8 text-xs font-mono" />
216
- </FormControl>
217
- <FormMessage className="text-xs" />
218
- </FormItem>
219
- )}
220
- />
221
- <FormField
222
- control={form.control}
223
- name="duration"
224
- render={({ field }) => (
225
- <FormItem>
226
- <FormLabel className="text-xs">Duração (min)</FormLabel>
227
- <FormControl>
228
- <Input
229
- {...field}
230
- type="number"
231
- className="h-8 text-xs"
232
- />
233
- </FormControl>
234
- <FormMessage className="text-xs" />
235
- </FormItem>
236
- )}
237
- />
238
- </div>
239
-
240
- <FormField
241
- control={form.control}
242
- name="title"
243
- render={({ field }) => (
244
- <FormItem>
245
- <FormLabel className="text-xs">Título</FormLabel>
246
- <FormControl>
247
- <Input {...field} className="h-8 text-sm" />
248
- </FormControl>
249
- <FormMessage className="text-xs" />
250
- </FormItem>
251
- )}
252
- />
253
-
254
- <FormField
255
- control={form.control}
256
- name="description"
257
- render={({ field }) => (
258
- <FormItem>
259
- <FormLabel className="text-xs">Descrição</FormLabel>
260
- <FormControl>
261
- <Textarea
262
- {...field}
263
- rows={3}
264
- className="text-sm resize-none"
265
- placeholder="Descrição opcional da sessão…"
266
- />
267
- </FormControl>
268
- <FormMessage className="text-xs" />
269
- </FormItem>
270
- )}
271
- />
272
- </CardContent>
273
- </Card>
274
-
275
- {/* ── Publicação ───────────────────────────────────────────────── */}
276
- <Card className="bg-muted/20 py-2 gap-2">
277
- <CardHeader className="px-3 pt-2 pb-1">
278
- <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
279
- Publicação
280
- </CardTitle>
281
- </CardHeader>
282
- <CardContent className="px-3 pb-2">
283
- <div className="grid grid-cols-2 gap-2 items-end">
284
- <FormField
285
- control={form.control}
286
- name="visibility"
287
- render={({ field }) => (
288
- <FormItem>
289
- <FormLabel className="text-xs">Visibilidade</FormLabel>
290
- <Select
291
- value={field.value}
292
- onValueChange={field.onChange}
293
- >
294
- <FormControl>
295
- <SelectTrigger className="h-8 text-xs w-full">
296
- <SelectValue />
297
- </SelectTrigger>
298
- </FormControl>
299
- <SelectContent>
300
- <SelectItem value="publico">
301
- <span className="flex items-center gap-1.5">
302
- <Eye className="size-3" /> Público
303
- </span>
304
- </SelectItem>
305
- <SelectItem value="privado">
306
- <span className="flex items-center gap-1.5">
307
- <EyeOff className="size-3" /> Privado
308
- </span>
309
- </SelectItem>
310
- <SelectItem value="restrito">
311
- <span className="flex items-center gap-1.5">
312
- <Lock className="size-3" /> Restrito
313
- </span>
314
- </SelectItem>
315
- </SelectContent>
316
- </Select>
317
- <FormMessage className="text-xs" />
318
- </FormItem>
319
- )}
320
- />
321
-
322
- <FormField
323
- control={form.control}
324
- name="published"
325
- render={({ field }) => (
326
- <FormItem className="flex items-center justify-between gap-3 rounded-md border px-3 h-8">
327
- <FormLabel className="text-xs cursor-pointer">
328
- Publicada
329
- </FormLabel>
330
- <FormControl>
331
- <Switch
332
- checked={field.value}
333
- onCheckedChange={field.onChange}
334
- />
335
- </FormControl>
336
- </FormItem>
337
- )}
338
- />
339
- </div>
340
- </CardContent>
341
- </Card>
342
-
343
- {/* ── Aulas da sessão ───────────────────────────────────────────── */}
344
- {lessons.length === 0 && (
345
- <div className="flex flex-col items-center gap-2 py-6 px-2 rounded-lg border border-dashed text-center">
346
- <Video className="size-5 text-muted-foreground/40" />
347
- <p className="text-xs text-muted-foreground leading-relaxed">
348
- Esta sessão ainda não tem aulas.
349
- </p>
350
- <Button
351
- type="button"
352
- variant="outline"
353
- size="sm"
354
- className="h-7 text-xs gap-1.5"
355
- disabled={createLesson.isPending}
356
- onClick={() => createLesson.mutate({ sessionId })}
357
- >
358
- {createLesson.isPending ? (
359
- <Loader2 className="size-3 animate-spin" />
360
- ) : (
361
- <Plus className="size-3" />
362
- )}
363
- Adicionar aula
364
- </Button>
365
- </div>
366
- )}
367
- </div>
368
- </ScrollArea>
369
-
370
- {/* ── Footer ───────────────────────────────────────────────────────── */}
371
- <div className="shrink-0 border-t bg-background">
372
- <Separator />
373
- <div className="flex items-center gap-2 px-3 py-2">
374
- <Button
375
- type="button"
376
- variant="ghost"
377
- size="sm"
378
- className="h-7 text-xs"
379
- disabled={!isDirty || updateSession.isPending}
380
- onClick={() => form.reset()}
381
- >
382
- <Undo2 className="size-3 mr-1" />
383
- Cancelar
384
- </Button>
385
- <div className="flex-1" />
386
- <Button
387
- type="button"
388
- variant="outline"
389
- size="sm"
390
- className="h-7 text-xs"
391
- disabled={createLesson.isPending}
392
- onClick={() => createLesson.mutate({ sessionId })}
393
- >
394
- {createLesson.isPending ? (
395
- <Loader2 className="size-3 mr-1 animate-spin" />
396
- ) : (
397
- <Plus className="size-3 mr-1" />
398
- )}
399
- Nova aula
400
- </Button>
401
- <Button
402
- type="submit"
403
- size="sm"
404
- className="h-7 text-xs"
405
- disabled={!isDirty || updateSession.isPending}
406
- >
407
- {updateSession.isPending ? (
408
- <Loader2 className="size-3 mr-1 animate-spin" />
409
- ) : (
410
- <Save className="size-3 mr-1" />
411
- )}
412
- Salvar
413
- </Button>
414
- </div>
415
- </div>
416
- </form>
417
- </Form>
418
- );
419
- }
420
-
421
- // ── Helpers ───────────────────────────────────────────────────────────────────
422
-
423
- function StatChip({
424
- icon,
425
- label,
426
- value,
427
- }: {
428
- icon: React.ReactNode;
429
- label: string;
430
- value: string | number;
431
- }) {
432
- return (
433
- <div className="flex items-center gap-2 rounded-md border bg-muted/20 px-2.5 py-2">
434
- <span className="text-muted-foreground shrink-0">{icon}</span>
435
- <div className="min-w-0">
436
- <p className="text-[0.65rem] text-muted-foreground truncate">{label}</p>
437
- <p className="text-sm font-semibold tabular-nums leading-none">
438
- {value}
439
- </p>
440
- </div>
441
- </div>
442
- );
443
- }
1
+ 'use client';
2
+
3
+ import { zodResolver } from '@hookform/resolvers/zod';
4
+ import {
5
+ CircleDot,
6
+ Clock,
7
+ Eye,
8
+ EyeOff,
9
+ Layers,
10
+ Loader2,
11
+ Lock,
12
+ Plus,
13
+ Save,
14
+ Trash2,
15
+ Undo2,
16
+ Video,
17
+ } from 'lucide-react';
18
+ import { useEffect, useMemo } from 'react';
19
+ import { useForm } from 'react-hook-form';
20
+ import { z } from 'zod';
21
+
22
+ import { Badge } from '@/components/ui/badge';
23
+ import { Button } from '@/components/ui/button';
24
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
25
+ import {
26
+ Form,
27
+ FormControl,
28
+ FormField,
29
+ FormItem,
30
+ FormLabel,
31
+ FormMessage,
32
+ } from '@/components/ui/form';
33
+ import { Input } from '@/components/ui/input';
34
+ import { ScrollArea } from '@/components/ui/scroll-area';
35
+ import {
36
+ Select,
37
+ SelectContent,
38
+ SelectItem,
39
+ SelectTrigger,
40
+ SelectValue,
41
+ } from '@/components/ui/select';
42
+ import { Separator } from '@/components/ui/separator';
43
+ import { Switch } from '@/components/ui/switch';
44
+ import { Textarea } from '@/components/ui/textarea';
45
+
46
+ import {
47
+ useCreateLessonMutation,
48
+ useDeleteSessionMutation,
49
+ useUpdateSessionMutation,
50
+ } from '../_data/use-course-structure-mutations';
51
+ import { useStructureStore } from './store';
52
+ import type { Visibility } from './types';
53
+
54
+ // ── Schema ────────────────────────────────────────────────────────────────────
55
+
56
+ const schema = z.object({
57
+ code: z.string().min(1, 'Código obrigatório'),
58
+ title: z.string().min(1, 'Título obrigatório'),
59
+ description: z.string(),
60
+ duration: z.coerce.number().min(0),
61
+ visibility: z.enum(['publico', 'privado', 'restrito'] as const),
62
+ published: z.boolean(),
63
+ });
64
+
65
+ type FormValues = z.infer<typeof schema>;
66
+
67
+ // ── Component ─────────────────────────────────────────────────────────────────
68
+
69
+ interface EditorSessionProps {
70
+ sessionId: string;
71
+ }
72
+
73
+ export function EditorSession({ sessionId }: EditorSessionProps) {
74
+ const session = useStructureStore((s) =>
75
+ s.sessions.find((ss) => ss.id === sessionId)
76
+ );
77
+ const allLessons = useStructureStore((s) => s.lessons);
78
+ const lessons = useMemo(
79
+ () => allLessons.filter((l) => l.sessionId === sessionId),
80
+ [allLessons, sessionId]
81
+ );
82
+ const showConfirm = useStructureStore((s) => s.showConfirm);
83
+
84
+ const updateSession = useUpdateSessionMutation();
85
+ const createLesson = useCreateLessonMutation();
86
+ const deleteSession = useDeleteSessionMutation();
87
+
88
+ const totalMinutes = lessons.reduce((sum, l) => sum + l.duration, 0);
89
+ const hours = Math.floor(totalMinutes / 60);
90
+ const minutes = totalMinutes % 60;
91
+
92
+ const defaultValues: FormValues = {
93
+ code: session?.code ?? '',
94
+ title: session?.title ?? '',
95
+ description: session?.description ?? '',
96
+ duration: session?.duration ?? 0,
97
+ visibility: (session?.visibility ?? 'publico') as Visibility,
98
+ published: session?.published ?? true,
99
+ };
100
+
101
+ const form = useForm<FormValues>({
102
+ resolver: zodResolver(schema),
103
+ defaultValues,
104
+ });
105
+
106
+ const { isDirty } = form.formState;
107
+
108
+ useEffect(() => {
109
+ if (!session) return;
110
+ form.reset({
111
+ code: session.code,
112
+ title: session.title,
113
+ description: session.description ?? '',
114
+ duration: session.duration,
115
+ visibility: session.visibility ?? 'publico',
116
+ published: session.published ?? true,
117
+ });
118
+ }, [session?.id]); // eslint-disable-line react-hooks/exhaustive-deps
119
+
120
+ if (!session) return null;
121
+
122
+ function onSubmit(values: FormValues) {
123
+ updateSession.mutate({ sessionId, formValues: values });
124
+ form.reset(values);
125
+ }
126
+
127
+ function handleDelete() {
128
+ showConfirm({
129
+ title: `Excluir sessão "${session!.title}"?`,
130
+ description:
131
+ 'A sessão e todas as suas aulas serão excluídas permanentemente.',
132
+ onConfirm: () => deleteSession.mutate({ sessionId }),
133
+ });
134
+ }
135
+
136
+ return (
137
+ <Form {...form}>
138
+ <form
139
+ onSubmit={form.handleSubmit(onSubmit)}
140
+ className="flex flex-col h-full min-h-0"
141
+ >
142
+ {/* ── Header ───────────────────────────────────────────────────────── */}
143
+ <div className="flex items-center gap-3 px-4 py-3 border-b bg-muted/30 shrink-0">
144
+ <div className="flex size-9 items-center justify-center rounded-lg bg-muted shrink-0">
145
+ <Layers className="size-4 text-muted-foreground" />
146
+ </div>
147
+ <div className="flex-1 min-w-0">
148
+ <div className="flex items-center gap-1.5">
149
+ <span className="text-sm font-semibold truncate">Sessão</span>
150
+ {isDirty && (
151
+ <CircleDot className="size-3 text-amber-500 shrink-0" />
152
+ )}
153
+ </div>
154
+ <p className="text-[0.65rem] text-muted-foreground truncate">
155
+ {session.code}
156
+ </p>
157
+ </div>
158
+ <Badge
159
+ variant={session.published ? 'default' : 'secondary'}
160
+ className="shrink-0 text-xs"
161
+ >
162
+ {session.published ? 'Publicada' : 'Oculta'}
163
+ </Badge>
164
+ <Button
165
+ type="button"
166
+ variant="ghost"
167
+ size="icon"
168
+ className="size-7 text-destructive/60 hover:text-destructive shrink-0"
169
+ title="Excluir sessão"
170
+ aria-label="Excluir sessão"
171
+ disabled={deleteSession.isPending}
172
+ onClick={handleDelete}
173
+ >
174
+ {deleteSession.isPending ? (
175
+ <Loader2 className="size-3.5 animate-spin" />
176
+ ) : (
177
+ <Trash2 className="size-3.5" />
178
+ )}
179
+ </Button>
180
+ </div>
181
+
182
+ {/* ── Scrollable body ───────────────────────────────────────────────── */}
183
+ <ScrollArea className="flex-1 min-h-0">
184
+ <div className="flex flex-col gap-3 p-3">
185
+ {/* ── Stats ────────────────────────────────────────────────────── */}
186
+ <div className="grid grid-cols-2 gap-2">
187
+ <StatChip
188
+ icon={<Video className="size-3" />}
189
+ label="Aulas"
190
+ value={lessons.length}
191
+ />
192
+ <StatChip
193
+ icon={<Clock className="size-3" />}
194
+ label="Duração"
195
+ value={hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`}
196
+ />
197
+ </div>
198
+
199
+ {/* ── Dados ────────────────────────────────────────────────────── */}
200
+ <Card className="bg-muted/20 py-2 gap-2">
201
+ <CardHeader className="px-3 pt-2 pb-1">
202
+ <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
203
+ Dados
204
+ </CardTitle>
205
+ </CardHeader>
206
+ <CardContent className="px-3 pb-2 flex flex-col gap-3">
207
+ <div className="grid grid-cols-2 gap-2">
208
+ <FormField
209
+ control={form.control}
210
+ name="code"
211
+ render={({ field }) => (
212
+ <FormItem>
213
+ <FormLabel className="text-xs">Código</FormLabel>
214
+ <FormControl>
215
+ <Input {...field} className="h-8 text-xs font-mono" />
216
+ </FormControl>
217
+ <FormMessage className="text-xs" />
218
+ </FormItem>
219
+ )}
220
+ />
221
+ <FormField
222
+ control={form.control}
223
+ name="duration"
224
+ render={({ field }) => (
225
+ <FormItem>
226
+ <FormLabel className="text-xs">Duração (min)</FormLabel>
227
+ <FormControl>
228
+ <Input
229
+ {...field}
230
+ type="number"
231
+ className="h-8 text-xs"
232
+ />
233
+ </FormControl>
234
+ <FormMessage className="text-xs" />
235
+ </FormItem>
236
+ )}
237
+ />
238
+ </div>
239
+
240
+ <FormField
241
+ control={form.control}
242
+ name="title"
243
+ render={({ field }) => (
244
+ <FormItem>
245
+ <FormLabel className="text-xs">Título</FormLabel>
246
+ <FormControl>
247
+ <Input {...field} className="h-8 text-sm" />
248
+ </FormControl>
249
+ <FormMessage className="text-xs" />
250
+ </FormItem>
251
+ )}
252
+ />
253
+
254
+ <FormField
255
+ control={form.control}
256
+ name="description"
257
+ render={({ field }) => (
258
+ <FormItem>
259
+ <FormLabel className="text-xs">Descrição</FormLabel>
260
+ <FormControl>
261
+ <Textarea
262
+ {...field}
263
+ rows={3}
264
+ className="text-sm resize-none"
265
+ placeholder="Descrição opcional da sessão…"
266
+ />
267
+ </FormControl>
268
+ <FormMessage className="text-xs" />
269
+ </FormItem>
270
+ )}
271
+ />
272
+ </CardContent>
273
+ </Card>
274
+
275
+ {/* ── Publicação ───────────────────────────────────────────────── */}
276
+ <Card className="bg-muted/20 py-2 gap-2">
277
+ <CardHeader className="px-3 pt-2 pb-1">
278
+ <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
279
+ Publicação
280
+ </CardTitle>
281
+ </CardHeader>
282
+ <CardContent className="px-3 pb-2">
283
+ <div className="grid grid-cols-2 gap-2 items-end">
284
+ <FormField
285
+ control={form.control}
286
+ name="visibility"
287
+ render={({ field }) => (
288
+ <FormItem>
289
+ <FormLabel className="text-xs">Visibilidade</FormLabel>
290
+ <Select
291
+ value={field.value}
292
+ onValueChange={field.onChange}
293
+ >
294
+ <FormControl>
295
+ <SelectTrigger className="h-8 text-xs w-full">
296
+ <SelectValue />
297
+ </SelectTrigger>
298
+ </FormControl>
299
+ <SelectContent>
300
+ <SelectItem value="publico">
301
+ <span className="flex items-center gap-1.5">
302
+ <Eye className="size-3" /> Público
303
+ </span>
304
+ </SelectItem>
305
+ <SelectItem value="privado">
306
+ <span className="flex items-center gap-1.5">
307
+ <EyeOff className="size-3" /> Privado
308
+ </span>
309
+ </SelectItem>
310
+ <SelectItem value="restrito">
311
+ <span className="flex items-center gap-1.5">
312
+ <Lock className="size-3" /> Restrito
313
+ </span>
314
+ </SelectItem>
315
+ </SelectContent>
316
+ </Select>
317
+ <FormMessage className="text-xs" />
318
+ </FormItem>
319
+ )}
320
+ />
321
+
322
+ <FormField
323
+ control={form.control}
324
+ name="published"
325
+ render={({ field }) => (
326
+ <FormItem className="flex items-center justify-between gap-3 rounded-md border px-3 h-8">
327
+ <FormLabel className="text-xs cursor-pointer">
328
+ Publicada
329
+ </FormLabel>
330
+ <FormControl>
331
+ <Switch
332
+ checked={field.value}
333
+ onCheckedChange={field.onChange}
334
+ />
335
+ </FormControl>
336
+ </FormItem>
337
+ )}
338
+ />
339
+ </div>
340
+ </CardContent>
341
+ </Card>
342
+
343
+ {/* ── Aulas da sessão ───────────────────────────────────────────── */}
344
+ {lessons.length === 0 && (
345
+ <div className="flex flex-col items-center gap-2 py-6 px-2 rounded-lg border border-dashed text-center">
346
+ <Video className="size-5 text-muted-foreground/40" />
347
+ <p className="text-xs text-muted-foreground leading-relaxed">
348
+ Esta sessão ainda não tem aulas.
349
+ </p>
350
+ <Button
351
+ type="button"
352
+ variant="outline"
353
+ size="sm"
354
+ className="h-7 text-xs gap-1.5"
355
+ disabled={createLesson.isPending}
356
+ onClick={() => createLesson.mutate({ sessionId })}
357
+ >
358
+ {createLesson.isPending ? (
359
+ <Loader2 className="size-3 animate-spin" />
360
+ ) : (
361
+ <Plus className="size-3" />
362
+ )}
363
+ Adicionar aula
364
+ </Button>
365
+ </div>
366
+ )}
367
+ </div>
368
+ </ScrollArea>
369
+
370
+ {/* ── Footer ───────────────────────────────────────────────────────── */}
371
+ <div className="shrink-0 border-t bg-background">
372
+ <Separator />
373
+ <div className="flex items-center gap-2 px-3 py-2">
374
+ <Button
375
+ type="button"
376
+ variant="ghost"
377
+ size="sm"
378
+ className="h-7 text-xs"
379
+ disabled={!isDirty || updateSession.isPending}
380
+ onClick={() => form.reset()}
381
+ >
382
+ <Undo2 className="size-3 mr-1" />
383
+ Cancelar
384
+ </Button>
385
+ <div className="flex-1" />
386
+ <Button
387
+ type="button"
388
+ variant="outline"
389
+ size="sm"
390
+ className="h-7 text-xs"
391
+ disabled={createLesson.isPending}
392
+ onClick={() => createLesson.mutate({ sessionId })}
393
+ >
394
+ {createLesson.isPending ? (
395
+ <Loader2 className="size-3 mr-1 animate-spin" />
396
+ ) : (
397
+ <Plus className="size-3 mr-1" />
398
+ )}
399
+ Nova aula
400
+ </Button>
401
+ <Button
402
+ type="submit"
403
+ size="sm"
404
+ className="h-7 text-xs"
405
+ disabled={!isDirty || updateSession.isPending}
406
+ >
407
+ {updateSession.isPending ? (
408
+ <Loader2 className="size-3 mr-1 animate-spin" />
409
+ ) : (
410
+ <Save className="size-3 mr-1" />
411
+ )}
412
+ Salvar
413
+ </Button>
414
+ </div>
415
+ </div>
416
+ </form>
417
+ </Form>
418
+ );
419
+ }
420
+
421
+ // ── Helpers ───────────────────────────────────────────────────────────────────
422
+
423
+ function StatChip({
424
+ icon,
425
+ label,
426
+ value,
427
+ }: {
428
+ icon: React.ReactNode;
429
+ label: string;
430
+ value: string | number;
431
+ }) {
432
+ return (
433
+ <div className="flex items-center gap-2 rounded-md border bg-muted/20 px-2.5 py-2">
434
+ <span className="text-muted-foreground shrink-0">{icon}</span>
435
+ <div className="min-w-0">
436
+ <p className="text-[0.65rem] text-muted-foreground truncate">{label}</p>
437
+ <p className="text-sm font-semibold tabular-nums leading-none">
438
+ {value}
439
+ </p>
440
+ </div>
441
+ </div>
442
+ );
443
+ }