@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.
Files changed (115) hide show
  1. package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
  2. package/dist/course/course-audio-transcription.service.js +15 -7
  3. package/dist/course/course-audio-transcription.service.js.map +1 -1
  4. package/dist/course/course-operations-integration.service.d.ts +31 -0
  5. package/dist/course/course-operations-integration.service.d.ts.map +1 -1
  6. package/dist/course/course-operations-integration.service.js +286 -22
  7. package/dist/course/course-operations-integration.service.js.map +1 -1
  8. package/dist/course/course-operations.controller.d.ts +10 -0
  9. package/dist/course/course-operations.controller.d.ts.map +1 -0
  10. package/dist/course/course-operations.controller.js +67 -0
  11. package/dist/course/course-operations.controller.js.map +1 -0
  12. package/dist/course/course-structure.controller.d.ts +15 -1
  13. package/dist/course/course-structure.controller.d.ts.map +1 -1
  14. package/dist/course/course-structure.service.d.ts +25 -1
  15. package/dist/course/course-structure.service.d.ts.map +1 -1
  16. package/dist/course/course-structure.service.js +160 -24
  17. package/dist/course/course-structure.service.js.map +1 -1
  18. package/dist/course/course.module.d.ts.map +1 -1
  19. package/dist/course/course.module.js +4 -1
  20. package/dist/course/course.module.js.map +1 -1
  21. package/dist/course/course.service.d.ts +4 -2
  22. package/dist/course/course.service.d.ts.map +1 -1
  23. package/dist/course/course.service.js +61 -2
  24. package/dist/course/course.service.js.map +1 -1
  25. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +3 -0
  26. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  27. package/dist/course/dto/create-course-structure-lesson.dto.js +15 -0
  28. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  29. package/dist/course/dto/create-course-structure-session.dto.d.ts +1 -0
  30. package/dist/course/dto/create-course-structure-session.dto.d.ts.map +1 -1
  31. package/dist/course/dto/create-course-structure-session.dto.js +5 -0
  32. package/dist/course/dto/create-course-structure-session.dto.js.map +1 -1
  33. package/dist/course/dto/update-course-operations-config.dto.d.ts +6 -0
  34. package/dist/course/dto/update-course-operations-config.dto.d.ts.map +1 -0
  35. package/dist/course/dto/update-course-operations-config.dto.js +33 -0
  36. package/dist/course/dto/update-course-operations-config.dto.js.map +1 -0
  37. package/dist/course/lms-operations-task.subscriber.d.ts +13 -0
  38. package/dist/course/lms-operations-task.subscriber.d.ts.map +1 -0
  39. package/dist/course/lms-operations-task.subscriber.js +57 -0
  40. package/dist/course/lms-operations-task.subscriber.js.map +1 -0
  41. package/dist/enterprise/enterprise.service.js +1 -1
  42. package/dist/enterprise/enterprise.service.js.map +1 -1
  43. package/dist/enterprise/training/training-student.controller.d.ts +0 -95
  44. package/dist/enterprise/training/training-student.controller.d.ts.map +1 -1
  45. package/dist/enterprise/training/training-student.controller.js +1 -34
  46. package/dist/enterprise/training/training-student.controller.js.map +1 -1
  47. package/dist/enterprise/training/training-student.service.d.ts +63 -0
  48. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  49. package/dist/enterprise/training/training-student.service.js +320 -4
  50. package/dist/enterprise/training/training-student.service.js.map +1 -1
  51. package/dist/instructor/instructor.service.d.ts.map +1 -1
  52. package/dist/instructor/instructor.service.js +12 -3
  53. package/dist/instructor/instructor.service.js.map +1 -1
  54. package/dist/lms.module.d.ts.map +1 -1
  55. package/dist/lms.module.js +2 -0
  56. package/dist/lms.module.js.map +1 -1
  57. package/dist/platforma/platforma.controller.d.ts +287 -0
  58. package/dist/platforma/platforma.controller.d.ts.map +1 -0
  59. package/dist/platforma/platforma.controller.js +147 -0
  60. package/dist/platforma/platforma.controller.js.map +1 -0
  61. package/hedhog/data/route.yaml +102 -9
  62. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -2
  63. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +10 -13
  64. package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +26 -4
  65. package/hedhog/frontend/app/_lib/editor/types.ts.ejs +6 -0
  66. package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +447 -31
  67. package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +59 -47
  68. package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +201 -35
  69. package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +36 -5
  70. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +2 -2
  71. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
  72. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +17 -6
  73. package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +382 -0
  74. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +45 -8
  75. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +177 -67
  76. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +25 -60
  77. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -0
  78. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +11 -0
  79. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +22 -0
  80. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +79 -64
  81. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +31 -14
  82. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +4 -4
  83. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +39 -27
  84. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +24 -2
  85. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +41 -6
  86. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +2 -2
  87. package/hedhog/frontend/app/courses/page.tsx.ejs +80 -103
  88. package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +1 -1
  89. package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +2 -2
  90. package/hedhog/frontend/app/enterprise/page.tsx.ejs +18 -6
  91. package/hedhog/frontend/app/exams/[id]/page.tsx.ejs +5 -4
  92. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +16 -10
  93. package/hedhog/frontend/app/paths/page.tsx.ejs +1 -1
  94. package/hedhog/frontend/app/training/page.tsx.ejs +1 -1
  95. package/hedhog/frontend/messages/en.json +7 -2
  96. package/hedhog/frontend/messages/pt.json +7 -2
  97. package/hedhog/table/course_lesson.yaml +2 -2
  98. package/hedhog/table/course_module.yaml +3 -0
  99. package/package.json +8 -8
  100. package/src/course/course-audio-transcription.service.ts +21 -8
  101. package/src/course/course-operations-integration.service.ts +460 -22
  102. package/src/course/course-operations.controller.ts +45 -0
  103. package/src/course/course-structure.service.ts +209 -4
  104. package/src/course/course.module.ts +4 -1
  105. package/src/course/course.service.ts +67 -1
  106. package/src/course/dto/create-course-structure-lesson.dto.ts +17 -0
  107. package/src/course/dto/create-course-structure-session.dto.ts +13 -1
  108. package/src/course/dto/update-course-operations-config.dto.ts +16 -0
  109. package/src/course/lms-operations-task.subscriber.ts +44 -0
  110. package/src/enterprise/enterprise.service.ts +1 -1
  111. package/src/enterprise/training/training-student.controller.ts +3 -27
  112. package/src/enterprise/training/training-student.service.ts +350 -2
  113. package/src/instructor/instructor.service.ts +12 -3
  114. package/src/lms.module.ts +2 -0
  115. 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 = useStructureStore((s) => s.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 open={confirmDialog.open} onOpenChange={(open) => !open && closeConfirm()}>
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>{confirmDialog.description}</AlertDialogDescription>
35
+ <AlertDialogDescription>
36
+ {confirmDialog.description}
37
+ </AlertDialogDescription>
33
38
  )}
34
39
  </AlertDialogHeader>
35
40
  <AlertDialogFooter>
36
- <AlertDialogCancel onClick={closeConfirm}>{t('cancel')}</AlertDialogCancel>
41
+ <AlertDialogCancel onClick={closeConfirm}>
42
+ {t('cancel')}
43
+ </AlertDialogCancel>
37
44
  <AlertDialogAction
38
45
  onClick={handleConfirm}
39
- className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
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 (n === 'published' || n === 'active' || n === 'ativo') return 'ativo';
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 === 'ativo') return 'published';
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(['ativo', 'rascunho', 'arquivado']),
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 === 'ativo',
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: 'ativo', label: t('status.active') },
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 === 'ativo') {
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
- 'mx-3 mt-3 h-auto shrink-0 w-[calc(100%-1.5rem)] rounded-lg bg-muted/80 p-1',
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
- : t('structureEditor.tabs.publish')}
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"