@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.
- 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
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { DragOverlay as DndDragOverlay } from '@dnd-kit/core';
|
|
4
|
-
import { cn } from '@/lib/utils';
|
|
5
|
-
import { GripVertical, Layers, Video, FileText, HelpCircle } from 'lucide-react';
|
|
6
|
-
import type { FlatItem } from './types';
|
|
7
|
-
|
|
8
|
-
interface DragOverlayContentProps {
|
|
9
|
-
item: FlatItem;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function DragOverlayContent({ item }: DragOverlayContentProps) {
|
|
13
|
-
const isSession = item.type === 'session';
|
|
14
|
-
const isLesson = item.type === 'lesson';
|
|
15
|
-
|
|
16
|
-
const lessonIconMap = {
|
|
17
|
-
video: <Video className="size-3.5 text-blue-500 shrink-0" />,
|
|
18
|
-
post: <FileText className="size-3.5 text-emerald-500 shrink-0" />,
|
|
19
|
-
questao: <HelpCircle className="size-3.5 text-amber-500 shrink-0" />,
|
|
20
|
-
exercicio: <Layers className="size-3.5 text-rose-500 shrink-0" />,
|
|
21
|
-
} as const;
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<div
|
|
25
|
-
className={cn(
|
|
26
|
-
'flex items-center gap-1.5 px-2 py-1.5 rounded-md text-sm',
|
|
27
|
-
'bg-card border shadow-lg ring-1 ring-primary/20',
|
|
28
|
-
'opacity-95 max-w-[260px]',
|
|
29
|
-
isLesson && 'pl-4'
|
|
30
|
-
)}
|
|
31
|
-
>
|
|
32
|
-
<GripVertical className="size-3.5 text-muted-foreground/60 shrink-0" />
|
|
33
|
-
|
|
34
|
-
{isSession && <Layers className="size-3.5 text-muted-foreground shrink-0" />}
|
|
35
|
-
{isLesson && item.type === 'lesson' && lessonIconMap[item.data.type]}
|
|
36
|
-
|
|
37
|
-
<span className="truncate font-medium text-xs">{item.data.title}</span>
|
|
38
|
-
</div>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface TreeDragOverlayProps {
|
|
43
|
-
activeItem: FlatItem | null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function TreeDragOverlay({ activeItem }: TreeDragOverlayProps) {
|
|
47
|
-
return (
|
|
48
|
-
<DndDragOverlay dropAnimation={{ duration: 150, easing: 'ease' }}>
|
|
49
|
-
{activeItem ? <DragOverlayContent item={activeItem} /> : null}
|
|
50
|
-
</DndDragOverlay>
|
|
51
|
-
);
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { DragOverlay as DndDragOverlay } from '@dnd-kit/core';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
import { GripVertical, Layers, Video, FileText, HelpCircle } from 'lucide-react';
|
|
6
|
+
import type { FlatItem } from './types';
|
|
7
|
+
|
|
8
|
+
interface DragOverlayContentProps {
|
|
9
|
+
item: FlatItem;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function DragOverlayContent({ item }: DragOverlayContentProps) {
|
|
13
|
+
const isSession = item.type === 'session';
|
|
14
|
+
const isLesson = item.type === 'lesson';
|
|
15
|
+
|
|
16
|
+
const lessonIconMap = {
|
|
17
|
+
video: <Video className="size-3.5 text-blue-500 shrink-0" />,
|
|
18
|
+
post: <FileText className="size-3.5 text-emerald-500 shrink-0" />,
|
|
19
|
+
questao: <HelpCircle className="size-3.5 text-amber-500 shrink-0" />,
|
|
20
|
+
exercicio: <Layers className="size-3.5 text-rose-500 shrink-0" />,
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className={cn(
|
|
26
|
+
'flex items-center gap-1.5 px-2 py-1.5 rounded-md text-sm',
|
|
27
|
+
'bg-card border shadow-lg ring-1 ring-primary/20',
|
|
28
|
+
'opacity-95 max-w-[260px]',
|
|
29
|
+
isLesson && 'pl-4'
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
<GripVertical className="size-3.5 text-muted-foreground/60 shrink-0" />
|
|
33
|
+
|
|
34
|
+
{isSession && <Layers className="size-3.5 text-muted-foreground shrink-0" />}
|
|
35
|
+
{isLesson && item.type === 'lesson' && lessonIconMap[item.data.type]}
|
|
36
|
+
|
|
37
|
+
<span className="truncate font-medium text-xs">{item.data.title}</span>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface TreeDragOverlayProps {
|
|
43
|
+
activeItem: FlatItem | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function TreeDragOverlay({ activeItem }: TreeDragOverlayProps) {
|
|
47
|
+
return (
|
|
48
|
+
<DndDragOverlay dropAnimation={{ duration: 150, easing: 'ease' }}>
|
|
49
|
+
{activeItem ? <DragOverlayContent item={activeItem} /> : null}
|
|
50
|
+
</DndDragOverlay>
|
|
51
|
+
);
|
|
52
52
|
}
|
|
@@ -1,276 +1,276 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
Eye,
|
|
5
|
-
EyeOff,
|
|
6
|
-
FolderInput,
|
|
7
|
-
Layers,
|
|
8
|
-
Lock,
|
|
9
|
-
Save,
|
|
10
|
-
Video,
|
|
11
|
-
X,
|
|
12
|
-
} from 'lucide-react';
|
|
13
|
-
import { useState } from 'react';
|
|
14
|
-
import { toast } from 'sonner';
|
|
15
|
-
|
|
16
|
-
import { Badge } from '@/components/ui/badge';
|
|
17
|
-
import { Button } from '@/components/ui/button';
|
|
18
|
-
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
19
|
-
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
20
|
-
import {
|
|
21
|
-
Select,
|
|
22
|
-
SelectContent,
|
|
23
|
-
SelectItem,
|
|
24
|
-
SelectTrigger,
|
|
25
|
-
SelectValue,
|
|
26
|
-
} from '@/components/ui/select';
|
|
27
|
-
import { Separator } from '@/components/ui/separator';
|
|
28
|
-
import { cn } from '@/lib/utils';
|
|
29
|
-
|
|
30
|
-
import { useStructureStore } from './store';
|
|
31
|
-
import type { LessonStatus, Visibility } from './types';
|
|
32
|
-
|
|
33
|
-
// ── Config ────────────────────────────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
const STATUS_LABELS: Record<LessonStatus, string> = {
|
|
36
|
-
preparada: 'Preparada',
|
|
37
|
-
gravada: 'Gravada',
|
|
38
|
-
editada: 'Editada',
|
|
39
|
-
finalizada: 'Finalizada',
|
|
40
|
-
publicada: 'Publicada',
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// ── Component ─────────────────────────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
export function EditorBulk() {
|
|
46
|
-
const selectedIds = useStructureStore((s) => s.selectedIds);
|
|
47
|
-
const sessions = useStructureStore((s) => s.sessions);
|
|
48
|
-
const lessons = useStructureStore((s) => s.lessons);
|
|
49
|
-
const clearSelection = useStructureStore((s) => s.clearSelection);
|
|
50
|
-
|
|
51
|
-
const selectedArray = [...selectedIds];
|
|
52
|
-
const lessonIds = selectedArray.filter((id) =>
|
|
53
|
-
lessons.some((l) => l.id === id)
|
|
54
|
-
);
|
|
55
|
-
const sessionIds = selectedArray.filter((id) =>
|
|
56
|
-
sessions.some((s) => s.id === id)
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
const allLessons = lessonIds.length === selectedArray.length;
|
|
60
|
-
const allSessions = sessionIds.length === selectedArray.length;
|
|
61
|
-
|
|
62
|
-
// Local form state (mock — not wired to store actions yet)
|
|
63
|
-
const [status, setStatus] = useState<LessonStatus | ''>('');
|
|
64
|
-
const [visibility, setVisibility] = useState<Visibility | ''>('');
|
|
65
|
-
const [targetSession, setTargetSession] = useState<string>('');
|
|
66
|
-
|
|
67
|
-
function handleSave() {
|
|
68
|
-
const lines: string[] = [];
|
|
69
|
-
if (status) lines.push(`status → ${STATUS_LABELS[status as LessonStatus]}`);
|
|
70
|
-
if (visibility) lines.push(`visibilidade → ${visibility}`);
|
|
71
|
-
if (targetSession)
|
|
72
|
-
lines.push(
|
|
73
|
-
`mover para sessão ${sessions.find((s) => s.id === targetSession)?.title ?? targetSession}`
|
|
74
|
-
);
|
|
75
|
-
toast.success(
|
|
76
|
-
lines.length
|
|
77
|
-
? `Edição em massa: ${lines.join(', ')} (mock)`
|
|
78
|
-
: 'Nenhuma alteração selecionada'
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<div className="flex flex-col h-full min-h-0">
|
|
84
|
-
{/* ── Header ───────────────────────────────────────────────────────── */}
|
|
85
|
-
<div className="flex items-center gap-3 px-4 py-3 border-b bg-muted/30 shrink-0">
|
|
86
|
-
<div className="flex size-9 items-center justify-center rounded-lg bg-primary/10 shrink-0">
|
|
87
|
-
{allLessons ? (
|
|
88
|
-
<Video className="size-4 text-primary" />
|
|
89
|
-
) : allSessions ? (
|
|
90
|
-
<Layers className="size-4 text-primary" />
|
|
91
|
-
) : (
|
|
92
|
-
<Layers className="size-4 text-primary" />
|
|
93
|
-
)}
|
|
94
|
-
</div>
|
|
95
|
-
<div className="flex-1 min-w-0">
|
|
96
|
-
<p className="text-sm font-semibold">Edição em massa</p>
|
|
97
|
-
<p className="text-[0.65rem] text-muted-foreground">
|
|
98
|
-
{selectedIds.size}{' '}
|
|
99
|
-
{allLessons ? 'aulas' : allSessions ? 'sessões' : 'itens'}{' '}
|
|
100
|
-
selecionados
|
|
101
|
-
</p>
|
|
102
|
-
</div>
|
|
103
|
-
<Button
|
|
104
|
-
variant="ghost"
|
|
105
|
-
size="icon"
|
|
106
|
-
className="size-7 shrink-0"
|
|
107
|
-
onClick={clearSelection}
|
|
108
|
-
title="Limpar seleção"
|
|
109
|
-
>
|
|
110
|
-
<X className="size-3.5" />
|
|
111
|
-
</Button>
|
|
112
|
-
</div>
|
|
113
|
-
|
|
114
|
-
{/* ── Body ─────────────────────────────────────────────────────────── */}
|
|
115
|
-
<ScrollArea className="flex-1 min-h-0">
|
|
116
|
-
<div className="flex flex-col gap-3 p-3">
|
|
117
|
-
{/* Badges dos itens selecionados */}
|
|
118
|
-
<div className="flex flex-wrap gap-1">
|
|
119
|
-
{selectedArray.slice(0, 12).map((id) => {
|
|
120
|
-
const lesson = lessons.find((l) => l.id === id);
|
|
121
|
-
const session = sessions.find((s) => s.id === id);
|
|
122
|
-
const label = lesson?.code ?? session?.code ?? id;
|
|
123
|
-
const isLesson = !!lesson;
|
|
124
|
-
return (
|
|
125
|
-
<Badge
|
|
126
|
-
key={id}
|
|
127
|
-
variant="secondary"
|
|
128
|
-
className={cn(
|
|
129
|
-
'text-[0.65rem] h-5',
|
|
130
|
-
isLesson
|
|
131
|
-
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
|
|
132
|
-
: 'bg-muted'
|
|
133
|
-
)}
|
|
134
|
-
>
|
|
135
|
-
{label}
|
|
136
|
-
</Badge>
|
|
137
|
-
);
|
|
138
|
-
})}
|
|
139
|
-
{selectedArray.length > 12 && (
|
|
140
|
-
<Badge variant="outline" className="text-[0.65rem] h-5">
|
|
141
|
-
+{selectedArray.length - 12} mais
|
|
142
|
-
</Badge>
|
|
143
|
-
)}
|
|
144
|
-
</div>
|
|
145
|
-
|
|
146
|
-
{/* Status — só mostra para aulas */}
|
|
147
|
-
{(allLessons || (!allSessions && lessonIds.length > 0)) && (
|
|
148
|
-
<Card>
|
|
149
|
-
<CardHeader className="px-3 pt-3 pb-2">
|
|
150
|
-
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
151
|
-
Status de produção
|
|
152
|
-
</CardTitle>
|
|
153
|
-
</CardHeader>
|
|
154
|
-
<CardContent className="px-3 pb-3">
|
|
155
|
-
<Select
|
|
156
|
-
value={status}
|
|
157
|
-
onValueChange={(v) => setStatus(v as LessonStatus)}
|
|
158
|
-
>
|
|
159
|
-
<SelectTrigger className="h-8 text-xs w-full">
|
|
160
|
-
<SelectValue placeholder="Manter atual" />
|
|
161
|
-
</SelectTrigger>
|
|
162
|
-
<SelectContent>
|
|
163
|
-
<SelectItem value="preparada">Preparada</SelectItem>
|
|
164
|
-
<SelectItem value="gravada">Gravada</SelectItem>
|
|
165
|
-
<SelectItem value="editada">Editada</SelectItem>
|
|
166
|
-
<SelectItem value="finalizada">Finalizada</SelectItem>
|
|
167
|
-
<SelectItem value="publicada">Publicada</SelectItem>
|
|
168
|
-
</SelectContent>
|
|
169
|
-
</Select>
|
|
170
|
-
</CardContent>
|
|
171
|
-
</Card>
|
|
172
|
-
)}
|
|
173
|
-
|
|
174
|
-
{/* Visibilidade */}
|
|
175
|
-
<Card>
|
|
176
|
-
<CardHeader className="px-3 pt-3 pb-2">
|
|
177
|
-
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
178
|
-
Visibilidade
|
|
179
|
-
</CardTitle>
|
|
180
|
-
</CardHeader>
|
|
181
|
-
<CardContent className="px-3 pb-3">
|
|
182
|
-
<Select
|
|
183
|
-
value={visibility}
|
|
184
|
-
onValueChange={(v) => setVisibility(v as Visibility)}
|
|
185
|
-
>
|
|
186
|
-
<SelectTrigger className="h-8 text-xs w-full">
|
|
187
|
-
<SelectValue placeholder="Manter atual" />
|
|
188
|
-
</SelectTrigger>
|
|
189
|
-
<SelectContent>
|
|
190
|
-
<SelectItem value="publico">
|
|
191
|
-
<span className="flex items-center gap-1.5">
|
|
192
|
-
<Eye className="size-3" /> Público
|
|
193
|
-
</span>
|
|
194
|
-
</SelectItem>
|
|
195
|
-
<SelectItem value="privado">
|
|
196
|
-
<span className="flex items-center gap-1.5">
|
|
197
|
-
<EyeOff className="size-3" /> Privado
|
|
198
|
-
</span>
|
|
199
|
-
</SelectItem>
|
|
200
|
-
<SelectItem value="restrito">
|
|
201
|
-
<span className="flex items-center gap-1.5">
|
|
202
|
-
<Lock className="size-3" /> Restrito
|
|
203
|
-
</span>
|
|
204
|
-
</SelectItem>
|
|
205
|
-
</SelectContent>
|
|
206
|
-
</Select>
|
|
207
|
-
</CardContent>
|
|
208
|
-
</Card>
|
|
209
|
-
|
|
210
|
-
{/* Mover para sessão — só para aulas */}
|
|
211
|
-
{(allLessons || lessonIds.length > 0) && (
|
|
212
|
-
<Card>
|
|
213
|
-
<CardHeader className="px-3 pt-3 pb-2">
|
|
214
|
-
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center gap-1.5">
|
|
215
|
-
<FolderInput className="size-3" />
|
|
216
|
-
Mover para sessão
|
|
217
|
-
</CardTitle>
|
|
218
|
-
</CardHeader>
|
|
219
|
-
<CardContent className="px-3 pb-3">
|
|
220
|
-
<Select value={targetSession} onValueChange={setTargetSession}>
|
|
221
|
-
<SelectTrigger className="h-8 text-xs w-full">
|
|
222
|
-
<SelectValue placeholder="Selecionar sessão…" />
|
|
223
|
-
</SelectTrigger>
|
|
224
|
-
<SelectContent>
|
|
225
|
-
{sessions.map((s) => (
|
|
226
|
-
<SelectItem key={s.id} value={s.id}>
|
|
227
|
-
<span className="font-mono mr-1 text-muted-foreground">
|
|
228
|
-
{s.code}
|
|
229
|
-
</span>
|
|
230
|
-
{s.title}
|
|
231
|
-
</SelectItem>
|
|
232
|
-
))}
|
|
233
|
-
</SelectContent>
|
|
234
|
-
</Select>
|
|
235
|
-
</CardContent>
|
|
236
|
-
</Card>
|
|
237
|
-
)}
|
|
238
|
-
|
|
239
|
-
<div className="rounded-md border border-amber-200 bg-amber-50/60 dark:border-amber-800 dark:bg-amber-950/30 px-3 py-2">
|
|
240
|
-
<p className="text-[0.65rem] text-amber-700 dark:text-amber-400">
|
|
241
|
-
As alterações em massa ainda não estão integradas com a API.
|
|
242
|
-
Clique em salvar para ver a pré-visualização.
|
|
243
|
-
</p>
|
|
244
|
-
</div>
|
|
245
|
-
</div>
|
|
246
|
-
</ScrollArea>
|
|
247
|
-
|
|
248
|
-
{/* ── Footer ───────────────────────────────────────────────────────── */}
|
|
249
|
-
<div className="shrink-0 border-t bg-background">
|
|
250
|
-
<Separator />
|
|
251
|
-
<div className="flex items-center gap-2 px-3 py-2">
|
|
252
|
-
<Button
|
|
253
|
-
type="button"
|
|
254
|
-
variant="ghost"
|
|
255
|
-
size="sm"
|
|
256
|
-
className="h-7 text-xs"
|
|
257
|
-
onClick={clearSelection}
|
|
258
|
-
>
|
|
259
|
-
<X className="size-3 mr-1" />
|
|
260
|
-
Limpar seleção
|
|
261
|
-
</Button>
|
|
262
|
-
<div className="flex-1" />
|
|
263
|
-
<Button
|
|
264
|
-
type="button"
|
|
265
|
-
size="sm"
|
|
266
|
-
className="h-7 text-xs"
|
|
267
|
-
onClick={handleSave}
|
|
268
|
-
>
|
|
269
|
-
<Save className="size-3 mr-1" />
|
|
270
|
-
Aplicar (mock)
|
|
271
|
-
</Button>
|
|
272
|
-
</div>
|
|
273
|
-
</div>
|
|
274
|
-
</div>
|
|
275
|
-
);
|
|
276
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Eye,
|
|
5
|
+
EyeOff,
|
|
6
|
+
FolderInput,
|
|
7
|
+
Layers,
|
|
8
|
+
Lock,
|
|
9
|
+
Save,
|
|
10
|
+
Video,
|
|
11
|
+
X,
|
|
12
|
+
} from 'lucide-react';
|
|
13
|
+
import { useState } from 'react';
|
|
14
|
+
import { toast } from 'sonner';
|
|
15
|
+
|
|
16
|
+
import { Badge } from '@/components/ui/badge';
|
|
17
|
+
import { Button } from '@/components/ui/button';
|
|
18
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
19
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
20
|
+
import {
|
|
21
|
+
Select,
|
|
22
|
+
SelectContent,
|
|
23
|
+
SelectItem,
|
|
24
|
+
SelectTrigger,
|
|
25
|
+
SelectValue,
|
|
26
|
+
} from '@/components/ui/select';
|
|
27
|
+
import { Separator } from '@/components/ui/separator';
|
|
28
|
+
import { cn } from '@/lib/utils';
|
|
29
|
+
|
|
30
|
+
import { useStructureStore } from './store';
|
|
31
|
+
import type { LessonStatus, Visibility } from './types';
|
|
32
|
+
|
|
33
|
+
// ── Config ────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
const STATUS_LABELS: Record<LessonStatus, string> = {
|
|
36
|
+
preparada: 'Preparada',
|
|
37
|
+
gravada: 'Gravada',
|
|
38
|
+
editada: 'Editada',
|
|
39
|
+
finalizada: 'Finalizada',
|
|
40
|
+
publicada: 'Publicada',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// ── Component ─────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export function EditorBulk() {
|
|
46
|
+
const selectedIds = useStructureStore((s) => s.selectedIds);
|
|
47
|
+
const sessions = useStructureStore((s) => s.sessions);
|
|
48
|
+
const lessons = useStructureStore((s) => s.lessons);
|
|
49
|
+
const clearSelection = useStructureStore((s) => s.clearSelection);
|
|
50
|
+
|
|
51
|
+
const selectedArray = [...selectedIds];
|
|
52
|
+
const lessonIds = selectedArray.filter((id) =>
|
|
53
|
+
lessons.some((l) => l.id === id)
|
|
54
|
+
);
|
|
55
|
+
const sessionIds = selectedArray.filter((id) =>
|
|
56
|
+
sessions.some((s) => s.id === id)
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const allLessons = lessonIds.length === selectedArray.length;
|
|
60
|
+
const allSessions = sessionIds.length === selectedArray.length;
|
|
61
|
+
|
|
62
|
+
// Local form state (mock — not wired to store actions yet)
|
|
63
|
+
const [status, setStatus] = useState<LessonStatus | ''>('');
|
|
64
|
+
const [visibility, setVisibility] = useState<Visibility | ''>('');
|
|
65
|
+
const [targetSession, setTargetSession] = useState<string>('');
|
|
66
|
+
|
|
67
|
+
function handleSave() {
|
|
68
|
+
const lines: string[] = [];
|
|
69
|
+
if (status) lines.push(`status → ${STATUS_LABELS[status as LessonStatus]}`);
|
|
70
|
+
if (visibility) lines.push(`visibilidade → ${visibility}`);
|
|
71
|
+
if (targetSession)
|
|
72
|
+
lines.push(
|
|
73
|
+
`mover para sessão ${sessions.find((s) => s.id === targetSession)?.title ?? targetSession}`
|
|
74
|
+
);
|
|
75
|
+
toast.success(
|
|
76
|
+
lines.length
|
|
77
|
+
? `Edição em massa: ${lines.join(', ')} (mock)`
|
|
78
|
+
: 'Nenhuma alteração selecionada'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="flex flex-col h-full min-h-0">
|
|
84
|
+
{/* ── Header ───────────────────────────────────────────────────────── */}
|
|
85
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b bg-muted/30 shrink-0">
|
|
86
|
+
<div className="flex size-9 items-center justify-center rounded-lg bg-primary/10 shrink-0">
|
|
87
|
+
{allLessons ? (
|
|
88
|
+
<Video className="size-4 text-primary" />
|
|
89
|
+
) : allSessions ? (
|
|
90
|
+
<Layers className="size-4 text-primary" />
|
|
91
|
+
) : (
|
|
92
|
+
<Layers className="size-4 text-primary" />
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
<div className="flex-1 min-w-0">
|
|
96
|
+
<p className="text-sm font-semibold">Edição em massa</p>
|
|
97
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
98
|
+
{selectedIds.size}{' '}
|
|
99
|
+
{allLessons ? 'aulas' : allSessions ? 'sessões' : 'itens'}{' '}
|
|
100
|
+
selecionados
|
|
101
|
+
</p>
|
|
102
|
+
</div>
|
|
103
|
+
<Button
|
|
104
|
+
variant="ghost"
|
|
105
|
+
size="icon"
|
|
106
|
+
className="size-7 shrink-0"
|
|
107
|
+
onClick={clearSelection}
|
|
108
|
+
title="Limpar seleção"
|
|
109
|
+
>
|
|
110
|
+
<X className="size-3.5" />
|
|
111
|
+
</Button>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{/* ── Body ─────────────────────────────────────────────────────────── */}
|
|
115
|
+
<ScrollArea className="flex-1 min-h-0">
|
|
116
|
+
<div className="flex flex-col gap-3 p-3">
|
|
117
|
+
{/* Badges dos itens selecionados */}
|
|
118
|
+
<div className="flex flex-wrap gap-1">
|
|
119
|
+
{selectedArray.slice(0, 12).map((id) => {
|
|
120
|
+
const lesson = lessons.find((l) => l.id === id);
|
|
121
|
+
const session = sessions.find((s) => s.id === id);
|
|
122
|
+
const label = lesson?.code ?? session?.code ?? id;
|
|
123
|
+
const isLesson = !!lesson;
|
|
124
|
+
return (
|
|
125
|
+
<Badge
|
|
126
|
+
key={id}
|
|
127
|
+
variant="secondary"
|
|
128
|
+
className={cn(
|
|
129
|
+
'text-[0.65rem] h-5',
|
|
130
|
+
isLesson
|
|
131
|
+
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
|
|
132
|
+
: 'bg-muted'
|
|
133
|
+
)}
|
|
134
|
+
>
|
|
135
|
+
{label}
|
|
136
|
+
</Badge>
|
|
137
|
+
);
|
|
138
|
+
})}
|
|
139
|
+
{selectedArray.length > 12 && (
|
|
140
|
+
<Badge variant="outline" className="text-[0.65rem] h-5">
|
|
141
|
+
+{selectedArray.length - 12} mais
|
|
142
|
+
</Badge>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{/* Status — só mostra para aulas */}
|
|
147
|
+
{(allLessons || (!allSessions && lessonIds.length > 0)) && (
|
|
148
|
+
<Card>
|
|
149
|
+
<CardHeader className="px-3 pt-3 pb-2">
|
|
150
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
151
|
+
Status de produção
|
|
152
|
+
</CardTitle>
|
|
153
|
+
</CardHeader>
|
|
154
|
+
<CardContent className="px-3 pb-3">
|
|
155
|
+
<Select
|
|
156
|
+
value={status}
|
|
157
|
+
onValueChange={(v) => setStatus(v as LessonStatus)}
|
|
158
|
+
>
|
|
159
|
+
<SelectTrigger className="h-8 text-xs w-full">
|
|
160
|
+
<SelectValue placeholder="Manter atual" />
|
|
161
|
+
</SelectTrigger>
|
|
162
|
+
<SelectContent>
|
|
163
|
+
<SelectItem value="preparada">Preparada</SelectItem>
|
|
164
|
+
<SelectItem value="gravada">Gravada</SelectItem>
|
|
165
|
+
<SelectItem value="editada">Editada</SelectItem>
|
|
166
|
+
<SelectItem value="finalizada">Finalizada</SelectItem>
|
|
167
|
+
<SelectItem value="publicada">Publicada</SelectItem>
|
|
168
|
+
</SelectContent>
|
|
169
|
+
</Select>
|
|
170
|
+
</CardContent>
|
|
171
|
+
</Card>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{/* Visibilidade */}
|
|
175
|
+
<Card>
|
|
176
|
+
<CardHeader className="px-3 pt-3 pb-2">
|
|
177
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
178
|
+
Visibilidade
|
|
179
|
+
</CardTitle>
|
|
180
|
+
</CardHeader>
|
|
181
|
+
<CardContent className="px-3 pb-3">
|
|
182
|
+
<Select
|
|
183
|
+
value={visibility}
|
|
184
|
+
onValueChange={(v) => setVisibility(v as Visibility)}
|
|
185
|
+
>
|
|
186
|
+
<SelectTrigger className="h-8 text-xs w-full">
|
|
187
|
+
<SelectValue placeholder="Manter atual" />
|
|
188
|
+
</SelectTrigger>
|
|
189
|
+
<SelectContent>
|
|
190
|
+
<SelectItem value="publico">
|
|
191
|
+
<span className="flex items-center gap-1.5">
|
|
192
|
+
<Eye className="size-3" /> Público
|
|
193
|
+
</span>
|
|
194
|
+
</SelectItem>
|
|
195
|
+
<SelectItem value="privado">
|
|
196
|
+
<span className="flex items-center gap-1.5">
|
|
197
|
+
<EyeOff className="size-3" /> Privado
|
|
198
|
+
</span>
|
|
199
|
+
</SelectItem>
|
|
200
|
+
<SelectItem value="restrito">
|
|
201
|
+
<span className="flex items-center gap-1.5">
|
|
202
|
+
<Lock className="size-3" /> Restrito
|
|
203
|
+
</span>
|
|
204
|
+
</SelectItem>
|
|
205
|
+
</SelectContent>
|
|
206
|
+
</Select>
|
|
207
|
+
</CardContent>
|
|
208
|
+
</Card>
|
|
209
|
+
|
|
210
|
+
{/* Mover para sessão — só para aulas */}
|
|
211
|
+
{(allLessons || lessonIds.length > 0) && (
|
|
212
|
+
<Card>
|
|
213
|
+
<CardHeader className="px-3 pt-3 pb-2">
|
|
214
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center gap-1.5">
|
|
215
|
+
<FolderInput className="size-3" />
|
|
216
|
+
Mover para sessão
|
|
217
|
+
</CardTitle>
|
|
218
|
+
</CardHeader>
|
|
219
|
+
<CardContent className="px-3 pb-3">
|
|
220
|
+
<Select value={targetSession} onValueChange={setTargetSession}>
|
|
221
|
+
<SelectTrigger className="h-8 text-xs w-full">
|
|
222
|
+
<SelectValue placeholder="Selecionar sessão…" />
|
|
223
|
+
</SelectTrigger>
|
|
224
|
+
<SelectContent>
|
|
225
|
+
{sessions.map((s) => (
|
|
226
|
+
<SelectItem key={s.id} value={s.id}>
|
|
227
|
+
<span className="font-mono mr-1 text-muted-foreground">
|
|
228
|
+
{s.code}
|
|
229
|
+
</span>
|
|
230
|
+
{s.title}
|
|
231
|
+
</SelectItem>
|
|
232
|
+
))}
|
|
233
|
+
</SelectContent>
|
|
234
|
+
</Select>
|
|
235
|
+
</CardContent>
|
|
236
|
+
</Card>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
<div className="rounded-md border border-amber-200 bg-amber-50/60 dark:border-amber-800 dark:bg-amber-950/30 px-3 py-2">
|
|
240
|
+
<p className="text-[0.65rem] text-amber-700 dark:text-amber-400">
|
|
241
|
+
As alterações em massa ainda não estão integradas com a API.
|
|
242
|
+
Clique em salvar para ver a pré-visualização.
|
|
243
|
+
</p>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
</ScrollArea>
|
|
247
|
+
|
|
248
|
+
{/* ── Footer ───────────────────────────────────────────────────────── */}
|
|
249
|
+
<div className="shrink-0 border-t bg-background">
|
|
250
|
+
<Separator />
|
|
251
|
+
<div className="flex items-center gap-2 px-3 py-2">
|
|
252
|
+
<Button
|
|
253
|
+
type="button"
|
|
254
|
+
variant="ghost"
|
|
255
|
+
size="sm"
|
|
256
|
+
className="h-7 text-xs"
|
|
257
|
+
onClick={clearSelection}
|
|
258
|
+
>
|
|
259
|
+
<X className="size-3 mr-1" />
|
|
260
|
+
Limpar seleção
|
|
261
|
+
</Button>
|
|
262
|
+
<div className="flex-1" />
|
|
263
|
+
<Button
|
|
264
|
+
type="button"
|
|
265
|
+
size="sm"
|
|
266
|
+
className="h-7 text-xs"
|
|
267
|
+
onClick={handleSave}
|
|
268
|
+
>
|
|
269
|
+
<Save className="size-3 mr-1" />
|
|
270
|
+
Aplicar (mock)
|
|
271
|
+
</Button>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
}
|