@hed-hog/lms 0.0.355 → 0.0.358
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/course/course-audio-transcription.service.d.ts.map +1 -1
- package/dist/course/course-audio-transcription.service.js +15 -7
- package/dist/course/course-audio-transcription.service.js.map +1 -1
- package/dist/course/course-operations-integration.service.d.ts +31 -0
- package/dist/course/course-operations-integration.service.d.ts.map +1 -1
- package/dist/course/course-operations-integration.service.js +286 -22
- package/dist/course/course-operations-integration.service.js.map +1 -1
- package/dist/course/course-operations.controller.d.ts +10 -0
- package/dist/course/course-operations.controller.d.ts.map +1 -0
- package/dist/course/course-operations.controller.js +67 -0
- package/dist/course/course-operations.controller.js.map +1 -0
- package/dist/course/course-structure.controller.d.ts +15 -1
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.service.d.ts +25 -1
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +160 -24
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +4 -1
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +4 -2
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +61 -2
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts +3 -0
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js +15 -0
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/dto/create-course-structure-session.dto.d.ts +1 -0
- package/dist/course/dto/create-course-structure-session.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-session.dto.js +5 -0
- package/dist/course/dto/create-course-structure-session.dto.js.map +1 -1
- package/dist/course/dto/update-course-operations-config.dto.d.ts +6 -0
- package/dist/course/dto/update-course-operations-config.dto.d.ts.map +1 -0
- package/dist/course/dto/update-course-operations-config.dto.js +33 -0
- package/dist/course/dto/update-course-operations-config.dto.js.map +1 -0
- package/dist/course/lms-operations-task.subscriber.d.ts +13 -0
- package/dist/course/lms-operations-task.subscriber.d.ts.map +1 -0
- package/dist/course/lms-operations-task.subscriber.js +57 -0
- package/dist/course/lms-operations-task.subscriber.js.map +1 -0
- package/dist/enterprise/enterprise.service.js +1 -1
- package/dist/enterprise/enterprise.service.js.map +1 -1
- package/dist/enterprise/training/training-student.controller.d.ts +0 -95
- package/dist/enterprise/training/training-student.controller.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.controller.js +1 -34
- package/dist/enterprise/training/training-student.controller.js.map +1 -1
- package/dist/enterprise/training/training-student.service.d.ts +63 -0
- package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.js +320 -4
- package/dist/enterprise/training/training-student.service.js.map +1 -1
- package/dist/instructor/instructor.service.d.ts.map +1 -1
- package/dist/instructor/instructor.service.js +12 -3
- package/dist/instructor/instructor.service.js.map +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +2 -0
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/platforma.controller.d.ts +287 -0
- package/dist/platforma/platforma.controller.d.ts.map +1 -0
- package/dist/platforma/platforma.controller.js +147 -0
- package/dist/platforma/platforma.controller.js.map +1 -0
- package/hedhog/data/route.yaml +102 -9
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -2
- package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +10 -13
- package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +26 -4
- package/hedhog/frontend/app/_lib/editor/types.ts.ejs +6 -0
- package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +447 -31
- package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +59 -47
- package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +201 -35
- package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +36 -5
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +17 -6
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +382 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +45 -8
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +177 -67
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +25 -60
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +11 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +22 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +79 -64
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +31 -14
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +4 -4
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +39 -27
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +24 -2
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +41 -6
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +2 -2
- package/hedhog/frontend/app/courses/page.tsx.ejs +80 -103
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +1 -1
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +2 -2
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +18 -6
- package/hedhog/frontend/app/exams/[id]/page.tsx.ejs +5 -4
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +16 -10
- package/hedhog/frontend/app/paths/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/training/page.tsx.ejs +1 -1
- package/hedhog/frontend/messages/en.json +7 -2
- package/hedhog/frontend/messages/pt.json +7 -2
- package/hedhog/table/course_lesson.yaml +2 -2
- package/hedhog/table/course_module.yaml +3 -0
- package/package.json +8 -8
- package/src/course/course-audio-transcription.service.ts +21 -8
- package/src/course/course-operations-integration.service.ts +460 -22
- package/src/course/course-operations.controller.ts +45 -0
- package/src/course/course-structure.service.ts +209 -4
- package/src/course/course.module.ts +4 -1
- package/src/course/course.service.ts +67 -1
- package/src/course/dto/create-course-structure-lesson.dto.ts +17 -0
- package/src/course/dto/create-course-structure-session.dto.ts +13 -1
- package/src/course/dto/update-course-operations-config.dto.ts +16 -0
- package/src/course/lms-operations-task.subscriber.ts +44 -0
- package/src/enterprise/enterprise.service.ts +1 -1
- package/src/enterprise/training/training-student.controller.ts +3 -27
- package/src/enterprise/training/training-student.service.ts +350 -2
- package/src/instructor/instructor.service.ts +12 -3
- package/src/lms.module.ts +2 -0
- package/src/platforma/platforma.controller.ts +92 -0
|
@@ -16,7 +16,7 @@ import { useStructureStore } from './store';
|
|
|
16
16
|
export function ConfirmDialog() {
|
|
17
17
|
const t = useTranslations('lms.CoursesPage.StructurePage.confirmDialog');
|
|
18
18
|
const confirmDialog = useStructureStore((s) => s.confirmDialog);
|
|
19
|
-
const closeConfirm
|
|
19
|
+
const closeConfirm = useStructureStore((s) => s.closeConfirm);
|
|
20
20
|
|
|
21
21
|
function handleConfirm() {
|
|
22
22
|
confirmDialog.onConfirm?.();
|
|
@@ -24,21 +24,32 @@ export function ConfirmDialog() {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
return (
|
|
27
|
-
<AlertDialog
|
|
27
|
+
<AlertDialog
|
|
28
|
+
open={confirmDialog.open}
|
|
29
|
+
onOpenChange={(open) => !open && closeConfirm()}
|
|
30
|
+
>
|
|
28
31
|
<AlertDialogContent>
|
|
29
32
|
<AlertDialogHeader>
|
|
30
33
|
<AlertDialogTitle>{confirmDialog.title}</AlertDialogTitle>
|
|
31
34
|
{confirmDialog.description && (
|
|
32
|
-
<AlertDialogDescription>
|
|
35
|
+
<AlertDialogDescription>
|
|
36
|
+
{confirmDialog.description}
|
|
37
|
+
</AlertDialogDescription>
|
|
33
38
|
)}
|
|
34
39
|
</AlertDialogHeader>
|
|
35
40
|
<AlertDialogFooter>
|
|
36
|
-
<AlertDialogCancel onClick={closeConfirm}>
|
|
41
|
+
<AlertDialogCancel onClick={closeConfirm}>
|
|
42
|
+
{t('cancel')}
|
|
43
|
+
</AlertDialogCancel>
|
|
37
44
|
<AlertDialogAction
|
|
38
45
|
onClick={handleConfirm}
|
|
39
|
-
className=
|
|
46
|
+
className={
|
|
47
|
+
confirmDialog.destructive
|
|
48
|
+
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
|
49
|
+
: undefined
|
|
50
|
+
}
|
|
40
51
|
>
|
|
41
|
-
{t('confirm')}
|
|
52
|
+
{confirmDialog.confirmText || t('confirm')}
|
|
42
53
|
</AlertDialogAction>
|
|
43
54
|
</AlertDialogFooter>
|
|
44
55
|
</AlertDialogContent>
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { EntityPicker } from '@/components/ui/entity-picker';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { Label } from '@/components/ui/label';
|
|
8
|
+
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
|
9
|
+
import {
|
|
10
|
+
Select,
|
|
11
|
+
SelectContent,
|
|
12
|
+
SelectItem,
|
|
13
|
+
SelectTrigger,
|
|
14
|
+
SelectValue,
|
|
15
|
+
} from '@/components/ui/select';
|
|
16
|
+
import { Separator } from '@/components/ui/separator';
|
|
17
|
+
import { cn } from '@/lib/utils';
|
|
18
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
19
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
20
|
+
import {
|
|
21
|
+
AlertTriangle,
|
|
22
|
+
Briefcase,
|
|
23
|
+
Loader2,
|
|
24
|
+
RefreshCw,
|
|
25
|
+
} from 'lucide-react';
|
|
26
|
+
import { toast } from 'sonner';
|
|
27
|
+
|
|
28
|
+
type OperationsConfig = {
|
|
29
|
+
isAvailable: boolean;
|
|
30
|
+
projectId: number | null;
|
|
31
|
+
projectCode: string | null;
|
|
32
|
+
projectName: string | null;
|
|
33
|
+
taskMode: 'per_lesson' | 'per_production_status' | null;
|
|
34
|
+
completionStatus: string | null;
|
|
35
|
+
missingTasksCount: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type ProjectOption = {
|
|
39
|
+
id: number;
|
|
40
|
+
code?: string | null;
|
|
41
|
+
name: string;
|
|
42
|
+
clientName?: string | null;
|
|
43
|
+
status?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type PickerOption = {
|
|
47
|
+
value: string;
|
|
48
|
+
label: string;
|
|
49
|
+
description?: string | null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const PRODUCTION_STATUSES: { value: string; label: string }[] = [
|
|
53
|
+
{ value: 'preparada', label: 'Preparada' },
|
|
54
|
+
{ value: 'gravada', label: 'Gravada' },
|
|
55
|
+
{ value: 'editada', label: 'Editada' },
|
|
56
|
+
{ value: 'finalizada', label: 'Finalizada' },
|
|
57
|
+
{ value: 'publicada', label: 'Publicada' },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
61
|
+
preparada: 'bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-300',
|
|
62
|
+
gravada: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300',
|
|
63
|
+
editada: 'bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300',
|
|
64
|
+
finalizada: 'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300',
|
|
65
|
+
publicada: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const PRODUCTION_STAGES = [
|
|
69
|
+
{ stage: 'preparacao', label: 'Preparação' },
|
|
70
|
+
{ stage: 'gravacao', label: 'Gravação' },
|
|
71
|
+
{ stage: 'edicao', label: 'Edição' },
|
|
72
|
+
{ stage: 'finalizacao', label: 'Finalização' },
|
|
73
|
+
{ stage: 'publicacao', label: 'Publicação' },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
type Props = {
|
|
77
|
+
courseId: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export function CourseOperationsTab({ courseId }: Props) {
|
|
81
|
+
const { request } = useApp();
|
|
82
|
+
const queryClient = useQueryClient();
|
|
83
|
+
|
|
84
|
+
const configQueryKey = ['lms-course-operations-config', courseId];
|
|
85
|
+
|
|
86
|
+
const { data: config, isLoading: isLoadingConfig } = useQuery<OperationsConfig>({
|
|
87
|
+
queryKey: configQueryKey,
|
|
88
|
+
enabled: Boolean(courseId),
|
|
89
|
+
queryFn: async () => {
|
|
90
|
+
const res = await request<OperationsConfig>({
|
|
91
|
+
url: `/lms/courses/${courseId}/operations`,
|
|
92
|
+
method: 'GET',
|
|
93
|
+
});
|
|
94
|
+
return res.data;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const { data: projectOptionsData } = useQuery<
|
|
99
|
+
{ data: ProjectOption[] } | ProjectOption[]
|
|
100
|
+
>({
|
|
101
|
+
queryKey: ['lms-operations-project-options-tab'],
|
|
102
|
+
queryFn: async () => {
|
|
103
|
+
try {
|
|
104
|
+
const res = await request<{ data: ProjectOption[] } | ProjectOption[]>({
|
|
105
|
+
url: '/operations/projects/options?pageSize=200&sortField=name&sortOrder=asc',
|
|
106
|
+
method: 'GET',
|
|
107
|
+
});
|
|
108
|
+
return res.data;
|
|
109
|
+
} catch {
|
|
110
|
+
return { data: [] };
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
initialData: { data: [] },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const projectOptions: PickerOption[] = (() => {
|
|
117
|
+
const raw = Array.isArray(projectOptionsData)
|
|
118
|
+
? projectOptionsData
|
|
119
|
+
: (projectOptionsData?.data ?? []);
|
|
120
|
+
|
|
121
|
+
const current =
|
|
122
|
+
config?.projectId && config?.projectName
|
|
123
|
+
? [
|
|
124
|
+
{
|
|
125
|
+
value: String(config.projectId),
|
|
126
|
+
label: config.projectName,
|
|
127
|
+
description: config.projectCode ?? undefined,
|
|
128
|
+
},
|
|
129
|
+
]
|
|
130
|
+
: [];
|
|
131
|
+
|
|
132
|
+
const remote = raw.map((p) => ({
|
|
133
|
+
value: String(p.id),
|
|
134
|
+
label: p.name,
|
|
135
|
+
description: p.code ?? p.clientName ?? undefined,
|
|
136
|
+
}));
|
|
137
|
+
|
|
138
|
+
return [...current, ...remote].filter(
|
|
139
|
+
(item, idx, arr) =>
|
|
140
|
+
arr.findIndex((c) => c.value === item.value) === idx,
|
|
141
|
+
);
|
|
142
|
+
})();
|
|
143
|
+
|
|
144
|
+
const [projectId, setProjectId] = useState<string>('');
|
|
145
|
+
const [taskMode, setTaskMode] = useState<
|
|
146
|
+
'per_lesson' | 'per_production_status' | ''
|
|
147
|
+
>('');
|
|
148
|
+
const [completionStatus, setCompletionStatus] = useState<string>('');
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!config) return;
|
|
152
|
+
setProjectId(config.projectId ? String(config.projectId) : '');
|
|
153
|
+
setTaskMode(config.taskMode ?? '');
|
|
154
|
+
setCompletionStatus(config.completionStatus ?? '');
|
|
155
|
+
}, [config]);
|
|
156
|
+
|
|
157
|
+
const { mutate: saveConfig, isPending: isSaving } = useMutation({
|
|
158
|
+
mutationFn: async () => {
|
|
159
|
+
await request({
|
|
160
|
+
url: `/lms/courses/${courseId}/operations`,
|
|
161
|
+
method: 'PATCH',
|
|
162
|
+
data: {
|
|
163
|
+
projectId: projectId ? Number(projectId) : null,
|
|
164
|
+
taskMode: taskMode || null,
|
|
165
|
+
completionStatus: completionStatus || null,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
onSuccess: () => {
|
|
170
|
+
void queryClient.invalidateQueries({ queryKey: configQueryKey });
|
|
171
|
+
toast.success('Configuração de operações salva.');
|
|
172
|
+
},
|
|
173
|
+
onError: () => {
|
|
174
|
+
toast.error('Erro ao salvar configuração de operações.');
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const { mutate: recreateTasks, isPending: isRecreating } = useMutation({
|
|
179
|
+
mutationFn: async () => {
|
|
180
|
+
const res = await request<{ count?: number }>({
|
|
181
|
+
url: `/lms/courses/${courseId}/operations/recreate-tasks`,
|
|
182
|
+
method: 'POST',
|
|
183
|
+
});
|
|
184
|
+
return res.data;
|
|
185
|
+
},
|
|
186
|
+
onSuccess: (data) => {
|
|
187
|
+
void queryClient.invalidateQueries({ queryKey: configQueryKey });
|
|
188
|
+
const count = typeof data === 'number' ? data : (data?.count ?? 0);
|
|
189
|
+
toast.success(
|
|
190
|
+
count > 0
|
|
191
|
+
? `${count} tarefa(s) recriada(s) com sucesso.`
|
|
192
|
+
: 'Nenhuma tarefa faltante encontrada.',
|
|
193
|
+
);
|
|
194
|
+
},
|
|
195
|
+
onError: () => {
|
|
196
|
+
toast.error('Erro ao recriar tarefas.');
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (isLoadingConfig) {
|
|
201
|
+
return (
|
|
202
|
+
<div className="flex items-center justify-center py-10">
|
|
203
|
+
<Loader2 className="size-5 animate-spin text-muted-foreground" />
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div className="flex flex-col gap-5 p-1">
|
|
210
|
+
{/* ── Projeto vinculado ──────────────────────────────────────────── */}
|
|
211
|
+
<div className="flex flex-col gap-1.5">
|
|
212
|
+
<Label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
213
|
+
Projeto Operations
|
|
214
|
+
</Label>
|
|
215
|
+
<EntityPicker<PickerOption>
|
|
216
|
+
value={projectId}
|
|
217
|
+
onChange={(val) => {
|
|
218
|
+
const next = String(val ?? '');
|
|
219
|
+
setProjectId(next);
|
|
220
|
+
if (!next) {
|
|
221
|
+
setTaskMode('');
|
|
222
|
+
setCompletionStatus('');
|
|
223
|
+
}
|
|
224
|
+
}}
|
|
225
|
+
options={projectOptions}
|
|
226
|
+
placeholder="Selecionar projeto..."
|
|
227
|
+
searchPlaceholder="Buscar projeto..."
|
|
228
|
+
entityLabel="projeto"
|
|
229
|
+
emptyStateDescription="Nenhum projeto encontrado."
|
|
230
|
+
getOptionValue={(o) => o.value}
|
|
231
|
+
getOptionLabel={(o) => o.label}
|
|
232
|
+
getOptionDescription={(o) => o.description ?? undefined}
|
|
233
|
+
/>
|
|
234
|
+
{config?.projectName && projectId === String(config.projectId) && (
|
|
235
|
+
<p className="text-xs text-muted-foreground flex items-center gap-1">
|
|
236
|
+
<Briefcase className="size-3" />
|
|
237
|
+
{[config.projectCode, config.projectName].filter(Boolean).join(' — ')}
|
|
238
|
+
</p>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<Separator />
|
|
243
|
+
|
|
244
|
+
{/* ── Configurações (habilitadas só com projeto) ──────────────────── */}
|
|
245
|
+
<fieldset disabled={!projectId} className="flex flex-col gap-5 disabled:opacity-50">
|
|
246
|
+
{/* Modo de criação de tarefas */}
|
|
247
|
+
<div className="flex flex-col gap-2">
|
|
248
|
+
<Label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
249
|
+
Modo de criação de tarefas
|
|
250
|
+
</Label>
|
|
251
|
+
<RadioGroup
|
|
252
|
+
value={taskMode}
|
|
253
|
+
onValueChange={(val) =>
|
|
254
|
+
setTaskMode(val as 'per_lesson' | 'per_production_status')
|
|
255
|
+
}
|
|
256
|
+
className="flex flex-col gap-2"
|
|
257
|
+
>
|
|
258
|
+
<div className="flex items-start gap-2.5 rounded-md border p-3 cursor-pointer hover:bg-muted/40 transition-colors">
|
|
259
|
+
<RadioGroupItem value="per_lesson" id="mode-per-lesson" className="mt-0.5" />
|
|
260
|
+
<div className="flex flex-col gap-0.5">
|
|
261
|
+
<Label htmlFor="mode-per-lesson" className="cursor-pointer text-sm font-medium">
|
|
262
|
+
Uma tarefa por aula
|
|
263
|
+
</Label>
|
|
264
|
+
<p className="text-xs text-muted-foreground">
|
|
265
|
+
Cria uma única tarefa no projeto para cada aula. Ao concluir a tarefa, o status de publicação da aula é atualizado automaticamente.
|
|
266
|
+
</p>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<div className="flex items-start gap-2.5 rounded-md border p-3 cursor-pointer hover:bg-muted/40 transition-colors">
|
|
271
|
+
<RadioGroupItem value="per_production_status" id="mode-per-status" className="mt-0.5" />
|
|
272
|
+
<div className="flex flex-col gap-0.5">
|
|
273
|
+
<Label htmlFor="mode-per-status" className="cursor-pointer text-sm font-medium">
|
|
274
|
+
Uma tarefa por status de produção
|
|
275
|
+
</Label>
|
|
276
|
+
<p className="text-xs text-muted-foreground">
|
|
277
|
+
Cria 5 tarefas por aula — uma para cada etapa de produção:
|
|
278
|
+
</p>
|
|
279
|
+
<div className="flex flex-wrap gap-1 mt-1">
|
|
280
|
+
{PRODUCTION_STAGES.map((s) => (
|
|
281
|
+
<span
|
|
282
|
+
key={s.stage}
|
|
283
|
+
className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium"
|
|
284
|
+
>
|
|
285
|
+
{s.label}
|
|
286
|
+
</span>
|
|
287
|
+
))}
|
|
288
|
+
</div>
|
|
289
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
290
|
+
Ao concluir cada tarefa, o status correspondente é aplicado à aula automaticamente via tag.
|
|
291
|
+
</p>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
</RadioGroup>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
{/* Status ao concluir (apenas modo per_lesson) */}
|
|
298
|
+
{taskMode === 'per_lesson' && (
|
|
299
|
+
<div className="flex flex-col gap-1.5">
|
|
300
|
+
<Label
|
|
301
|
+
htmlFor="completion-status"
|
|
302
|
+
className="text-xs font-medium text-muted-foreground uppercase tracking-wide"
|
|
303
|
+
>
|
|
304
|
+
Status ao concluir a tarefa
|
|
305
|
+
</Label>
|
|
306
|
+
<Select value={completionStatus} onValueChange={setCompletionStatus}>
|
|
307
|
+
<SelectTrigger id="completion-status" className="w-full">
|
|
308
|
+
<SelectValue placeholder="Selecionar status..." />
|
|
309
|
+
</SelectTrigger>
|
|
310
|
+
<SelectContent>
|
|
311
|
+
{PRODUCTION_STATUSES.map((s) => (
|
|
312
|
+
<SelectItem key={s.value} value={s.value}>
|
|
313
|
+
<span
|
|
314
|
+
className={cn(
|
|
315
|
+
'text-xs px-1.5 py-0.5 rounded',
|
|
316
|
+
STATUS_COLORS[s.value],
|
|
317
|
+
)}
|
|
318
|
+
>
|
|
319
|
+
{s.label}
|
|
320
|
+
</span>
|
|
321
|
+
</SelectItem>
|
|
322
|
+
))}
|
|
323
|
+
</SelectContent>
|
|
324
|
+
</Select>
|
|
325
|
+
<p className="text-xs text-muted-foreground">
|
|
326
|
+
O status de produção desta aula será atualizado para o valor selecionado quando a tarefa for movida para "Concluído" no Operations.
|
|
327
|
+
</p>
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
</fieldset>
|
|
331
|
+
|
|
332
|
+
{/* ── Tarefas faltantes ───────────────────────────────────────────── */}
|
|
333
|
+
{config?.missingTasksCount !== undefined && config.missingTasksCount > 0 && (
|
|
334
|
+
<>
|
|
335
|
+
<Separator />
|
|
336
|
+
<div className="flex flex-col gap-2 rounded-md border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/30 p-3">
|
|
337
|
+
<div className="flex items-center gap-2 text-amber-700 dark:text-amber-400">
|
|
338
|
+
<AlertTriangle className="size-4 shrink-0" />
|
|
339
|
+
<p className="text-sm font-medium">
|
|
340
|
+
{config.missingTasksCount === 1
|
|
341
|
+
? '1 aula sem tarefa vinculada'
|
|
342
|
+
: `${config.missingTasksCount} aulas sem tarefas vinculadas`}
|
|
343
|
+
</p>
|
|
344
|
+
</div>
|
|
345
|
+
<p className="text-xs text-amber-600 dark:text-amber-500">
|
|
346
|
+
As tarefas podem ter sido excluídas manualmente no Operations. Clique abaixo para recriá-las.
|
|
347
|
+
</p>
|
|
348
|
+
<Button
|
|
349
|
+
type="button"
|
|
350
|
+
variant="outline"
|
|
351
|
+
size="sm"
|
|
352
|
+
className="self-start gap-2 border-amber-300 text-amber-700 hover:bg-amber-100 dark:border-amber-700 dark:text-amber-400 dark:hover:bg-amber-900/40"
|
|
353
|
+
onClick={() => recreateTasks()}
|
|
354
|
+
disabled={isRecreating || !projectId}
|
|
355
|
+
>
|
|
356
|
+
{isRecreating ? (
|
|
357
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
358
|
+
) : (
|
|
359
|
+
<RefreshCw className="size-3.5" />
|
|
360
|
+
)}
|
|
361
|
+
Recriar tarefas faltantes
|
|
362
|
+
</Button>
|
|
363
|
+
</div>
|
|
364
|
+
</>
|
|
365
|
+
)}
|
|
366
|
+
|
|
367
|
+
{/* ── Ações ───────────────────────────────────────────────────────── */}
|
|
368
|
+
<div className="flex justify-end">
|
|
369
|
+
<Button
|
|
370
|
+
type="button"
|
|
371
|
+
size="sm"
|
|
372
|
+
className="gap-2"
|
|
373
|
+
onClick={() => saveConfig()}
|
|
374
|
+
disabled={isSaving}
|
|
375
|
+
>
|
|
376
|
+
{isSaving && <Loader2 className="size-3.5 animate-spin" />}
|
|
377
|
+
Salvar configurações
|
|
378
|
+
</Button>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
@@ -95,6 +95,7 @@ import {
|
|
|
95
95
|
import { useCreateSessionMutation } from '../_data/use-course-structure-mutations';
|
|
96
96
|
import { courseStructureQueryKey } from '../_data/use-course-structure-query';
|
|
97
97
|
import { IconActionTooltip } from './icon-action-tooltip';
|
|
98
|
+
import { CourseOperationsTab } from './course-operations-tab';
|
|
98
99
|
import { useStructureStore } from './store';
|
|
99
100
|
import { Resource } from './types';
|
|
100
101
|
|
|
@@ -245,13 +246,20 @@ function toPtStatus(
|
|
|
245
246
|
status: ApiCourseDetail['status']
|
|
246
247
|
): CourseEditFormValues['status'] {
|
|
247
248
|
const n = normalizeEnumValue(status);
|
|
248
|
-
if (
|
|
249
|
+
if (
|
|
250
|
+
n === 'published' ||
|
|
251
|
+
n === 'active' ||
|
|
252
|
+
n === 'ativo' ||
|
|
253
|
+
n === 'publicado'
|
|
254
|
+
) {
|
|
255
|
+
return 'publicado';
|
|
256
|
+
}
|
|
249
257
|
if (n === 'archived' || n === 'arquivado') return 'arquivado';
|
|
250
258
|
return 'rascunho';
|
|
251
259
|
}
|
|
252
260
|
|
|
253
261
|
function toApiStatus(status: CourseEditFormValues['status']) {
|
|
254
|
-
if (status === '
|
|
262
|
+
if (status === 'publicado') return 'published';
|
|
255
263
|
if (status === 'arquivado') return 'archived';
|
|
256
264
|
return 'draft';
|
|
257
265
|
}
|
|
@@ -350,7 +358,7 @@ function buildSchema(t: (key: string) => string) {
|
|
|
350
358
|
objetivos: z.string().optional(),
|
|
351
359
|
publicoAlvo: z.string().optional(),
|
|
352
360
|
nivel: z.enum(['iniciante', 'intermediario', 'avancado']),
|
|
353
|
-
status: z.enum(['
|
|
361
|
+
status: z.enum(['publicado', 'rascunho', 'arquivado']),
|
|
354
362
|
tipoOferta: z.enum(['agendado', 'sob_demanda', 'hibrido']),
|
|
355
363
|
localeId: z.string().optional().default(''),
|
|
356
364
|
categorias: z.array(z.string()).optional().default([]),
|
|
@@ -544,6 +552,25 @@ export function EditorCourse() {
|
|
|
544
552
|
initialData: [],
|
|
545
553
|
});
|
|
546
554
|
|
|
555
|
+
const { data: operationsConfig } = useQuery<{ isAvailable: boolean }>({
|
|
556
|
+
queryKey: ['lms-course-operations-config', courseId],
|
|
557
|
+
enabled: Boolean(courseId),
|
|
558
|
+
queryFn: async () => {
|
|
559
|
+
try {
|
|
560
|
+
const res = await request<{ isAvailable: boolean }>({
|
|
561
|
+
url: `/lms/courses/${courseId}/operations`,
|
|
562
|
+
method: 'GET',
|
|
563
|
+
});
|
|
564
|
+
return res.data;
|
|
565
|
+
} catch {
|
|
566
|
+
return { isAvailable: false };
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
initialData: { isAvailable: false },
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
const showOperationsTab = operationsConfig?.isAvailable === true;
|
|
573
|
+
|
|
547
574
|
// ── Save mutation ───────────────────────────────────────────────────────────
|
|
548
575
|
const { mutate: saveCourse, isPending: saving } = useMutation({
|
|
549
576
|
mutationFn: async (data: CourseEditFormValues) => {
|
|
@@ -596,7 +623,7 @@ export function EditorCourse() {
|
|
|
596
623
|
slug: data.slug,
|
|
597
624
|
name: data.nomeInterno,
|
|
598
625
|
description: data.descricaoPublica,
|
|
599
|
-
published: data.status === '
|
|
626
|
+
published: data.status === 'publicado',
|
|
600
627
|
});
|
|
601
628
|
void queryClient.invalidateQueries({
|
|
602
629
|
queryKey: courseStructureQueryKey(courseId),
|
|
@@ -653,7 +680,7 @@ export function EditorCourse() {
|
|
|
653
680
|
);
|
|
654
681
|
const STATUS_OPTIONS = useMemo(
|
|
655
682
|
() => [
|
|
656
|
-
{ value: '
|
|
683
|
+
{ value: 'publicado', label: t('status.published') },
|
|
657
684
|
{ value: 'rascunho', label: t('status.draft') },
|
|
658
685
|
{ value: 'arquivado', label: t('status.archived') },
|
|
659
686
|
],
|
|
@@ -1366,7 +1393,7 @@ export function EditorCourse() {
|
|
|
1366
1393
|
}
|
|
1367
1394
|
|
|
1368
1395
|
function onSubmit(data: CourseEditFormValues) {
|
|
1369
|
-
if (data.status === '
|
|
1396
|
+
if (data.status === 'publicado') {
|
|
1370
1397
|
const requiredProfileIds = new Set(linkedProfileIds);
|
|
1371
1398
|
const invalidLesson = lessons.find((lesson) => {
|
|
1372
1399
|
if (
|
|
@@ -1537,7 +1564,7 @@ export function EditorCourse() {
|
|
|
1537
1564
|
>
|
|
1538
1565
|
<TabsList
|
|
1539
1566
|
className={cn(
|
|
1540
|
-
'
|
|
1567
|
+
'mt-0 h-auto w-full shrink-0 rounded-none border-b bg-muted/50 px-2 py-1 sm:px-3',
|
|
1541
1568
|
'flex items-center gap-1 overflow-x-auto overflow-y-hidden whitespace-nowrap',
|
|
1542
1569
|
isMobile
|
|
1543
1570
|
? 'touch-pan-x snap-x snap-mandatory [scrollbar-width:thin] [&::-webkit-scrollbar]:h-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-muted-foreground/35'
|
|
@@ -1552,6 +1579,7 @@ export function EditorCourse() {
|
|
|
1552
1579
|
'recursos',
|
|
1553
1580
|
'extra',
|
|
1554
1581
|
...(showVideoTab ? ['videos'] : []),
|
|
1582
|
+
...(showOperationsTab ? ['operacoes'] : []),
|
|
1555
1583
|
'publicacao',
|
|
1556
1584
|
] as const
|
|
1557
1585
|
).map((tab) => (
|
|
@@ -1577,7 +1605,9 @@ export function EditorCourse() {
|
|
|
1577
1605
|
? t('structureEditor.tabs.extra')
|
|
1578
1606
|
: tab === 'videos'
|
|
1579
1607
|
? t('structureEditor.tabs.videoProfiles')
|
|
1580
|
-
:
|
|
1608
|
+
: tab === 'operacoes'
|
|
1609
|
+
? 'Operações'
|
|
1610
|
+
: t('structureEditor.tabs.publish')}
|
|
1581
1611
|
</TabsTrigger>
|
|
1582
1612
|
))}
|
|
1583
1613
|
</TabsList>
|
|
@@ -2264,6 +2294,13 @@ export function EditorCourse() {
|
|
|
2264
2294
|
</TabsContent>
|
|
2265
2295
|
)}
|
|
2266
2296
|
|
|
2297
|
+
{/* ── Tab: Operações ──────────────────────────────────────── */}
|
|
2298
|
+
{showOperationsTab && (
|
|
2299
|
+
<TabsContent value="operacoes" className="mt-0 min-w-0">
|
|
2300
|
+
<CourseOperationsTab courseId={courseId} />
|
|
2301
|
+
</TabsContent>
|
|
2302
|
+
)}
|
|
2303
|
+
|
|
2267
2304
|
{/* ── Tab: Publicação ─────────────────────────────────────── */}
|
|
2268
2305
|
<TabsContent
|
|
2269
2306
|
value="publicacao"
|