@hed-hog/lms 0.0.268 → 0.0.270
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/hedhog/data/menu.yaml +8 -1
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +1387 -0
- package/hedhog/frontend/app/classes/page.tsx.ejs +4 -4
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +1237 -0
- package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +2642 -0
- package/hedhog/frontend/app/courses/page.tsx.ejs +825 -727
- package/hedhog/frontend/app/exams/[id]/attempt/page.tsx.ejs +976 -0
- package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +931 -0
- package/hedhog/frontend/app/exams/page.tsx.ejs +9 -7
- package/hedhog/frontend/app/training/page.tsx.ejs +3 -3
- package/hedhog/frontend/messages/en.json +703 -14
- package/hedhog/frontend/messages/pt.json +863 -174
- package/hedhog/query/triggers.sql +0 -0
- package/hedhog/table/certificate.yaml +89 -0
- package/hedhog/table/certificate_template.yaml +24 -0
- package/hedhog/table/course.yaml +67 -1
- package/hedhog/table/course_category.yaml +22 -0
- package/hedhog/table/course_class_attendance.yaml +34 -0
- package/hedhog/table/course_class_group.yaml +58 -0
- package/hedhog/table/course_class_session.yaml +38 -0
- package/hedhog/table/course_class_session_instructor.yaml +27 -0
- package/hedhog/table/course_enrollment.yaml +45 -0
- package/hedhog/table/course_image.yaml +33 -0
- package/hedhog/table/course_lesson.yaml +35 -0
- package/hedhog/table/course_lesson_file.yaml +23 -0
- package/hedhog/table/course_lesson_instructor.yaml +27 -0
- package/hedhog/table/course_lesson_progress.yaml +40 -0
- package/hedhog/table/course_lesson_question.yaml +24 -0
- package/hedhog/table/course_module.yaml +25 -0
- package/hedhog/table/course_prerequisite.yaml +48 -0
- package/hedhog/table/evaluation_rating.yaml +30 -0
- package/hedhog/table/evaluation_topic.yaml +68 -0
- package/hedhog/table/exam.yaml +91 -0
- package/hedhog/table/exam_answer.yaml +40 -0
- package/hedhog/table/exam_attempt.yaml +51 -0
- package/hedhog/table/exam_image.yaml +33 -0
- package/hedhog/table/exam_option.yaml +25 -0
- package/hedhog/table/exam_question.yaml +24 -0
- package/hedhog/table/image_type.yaml +28 -0
- package/hedhog/table/instructor.yaml +23 -0
- package/hedhog/table/learning_path.yaml +49 -0
- package/hedhog/table/learning_path_enrollment.yaml +33 -0
- package/hedhog/table/learning_path_image.yaml +33 -0
- package/hedhog/table/learning_path_step.yaml +43 -0
- package/hedhog/table/question.yaml +15 -0
- package/package.json +9 -6
- package/src/index.ts +1 -1
- package/src/lms.module.ts +15 -15
- package/hedhog/table/classes.yaml +0 -3
- package/hedhog/table/exams.yaml +0 -3
- package/hedhog/table/reports.yaml +0 -3
- package/hedhog/table/training.yaml +0 -3
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Page, PageHeader } from '@/components/entity-list';
|
|
4
|
+
import { Badge } from '@/components/ui/badge';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogDescription,
|
|
11
|
+
DialogFooter,
|
|
12
|
+
DialogHeader,
|
|
13
|
+
DialogTitle,
|
|
14
|
+
} from '@/components/ui/dialog';
|
|
15
|
+
import { Field, FieldLabel } from '@/components/ui/field';
|
|
16
|
+
import { Input } from '@/components/ui/input';
|
|
17
|
+
import { Separator } from '@/components/ui/separator';
|
|
18
|
+
import {
|
|
19
|
+
Sheet,
|
|
20
|
+
SheetContent,
|
|
21
|
+
SheetDescription,
|
|
22
|
+
SheetFooter,
|
|
23
|
+
SheetHeader,
|
|
24
|
+
SheetTitle,
|
|
25
|
+
} from '@/components/ui/sheet';
|
|
26
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
27
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
28
|
+
import {
|
|
29
|
+
closestCenter,
|
|
30
|
+
DndContext,
|
|
31
|
+
KeyboardSensor,
|
|
32
|
+
PointerSensor,
|
|
33
|
+
useSensor,
|
|
34
|
+
useSensors,
|
|
35
|
+
type DragEndEvent,
|
|
36
|
+
} from '@dnd-kit/core';
|
|
37
|
+
import {
|
|
38
|
+
arrayMove,
|
|
39
|
+
SortableContext,
|
|
40
|
+
sortableKeyboardCoordinates,
|
|
41
|
+
useSortable,
|
|
42
|
+
verticalListSortingStrategy,
|
|
43
|
+
} from '@dnd-kit/sortable';
|
|
44
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
45
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
46
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
|
47
|
+
import {
|
|
48
|
+
AlertTriangle,
|
|
49
|
+
Check,
|
|
50
|
+
ChevronDown,
|
|
51
|
+
ChevronUp,
|
|
52
|
+
CircleDot,
|
|
53
|
+
CircleOff,
|
|
54
|
+
Copy,
|
|
55
|
+
FileCheck,
|
|
56
|
+
GripVertical,
|
|
57
|
+
Pencil,
|
|
58
|
+
Plus,
|
|
59
|
+
Save,
|
|
60
|
+
Trash2,
|
|
61
|
+
X,
|
|
62
|
+
} from 'lucide-react';
|
|
63
|
+
import { useTranslations } from 'next-intl';
|
|
64
|
+
import { useParams, usePathname } from 'next/navigation';
|
|
65
|
+
import { useEffect, useState } from 'react';
|
|
66
|
+
import { useForm } from 'react-hook-form';
|
|
67
|
+
import { toast } from 'sonner';
|
|
68
|
+
import { z } from 'zod';
|
|
69
|
+
|
|
70
|
+
interface Alternativa {
|
|
71
|
+
id: string;
|
|
72
|
+
texto: string;
|
|
73
|
+
correta: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface Questao {
|
|
77
|
+
id: string;
|
|
78
|
+
enunciado: string;
|
|
79
|
+
pontuacao: number;
|
|
80
|
+
alternativas: Alternativa[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type QuestaoForm = {
|
|
84
|
+
enunciado: string;
|
|
85
|
+
pontuacao: number;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function generateId() {
|
|
89
|
+
return Math.random().toString(36).substring(2, 9);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const initialQuestoes: Questao[] = [
|
|
93
|
+
{
|
|
94
|
+
id: 'q1',
|
|
95
|
+
enunciado:
|
|
96
|
+
'Qual hook do React e utilizado para gerenciar estado local em componentes funcionais?',
|
|
97
|
+
pontuacao: 2,
|
|
98
|
+
alternativas: [
|
|
99
|
+
{ id: 'a1', texto: 'useEffect', correta: false },
|
|
100
|
+
{ id: 'a2', texto: 'useState', correta: true },
|
|
101
|
+
{ id: 'a3', texto: 'useContext', correta: false },
|
|
102
|
+
{ id: 'a4', texto: 'useReducer', correta: false },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'q2',
|
|
107
|
+
enunciado: 'O que o Virtual DOM faz no React?',
|
|
108
|
+
pontuacao: 2,
|
|
109
|
+
alternativas: [
|
|
110
|
+
{ id: 'a5', texto: 'Substitui o DOM real completamente', correta: false },
|
|
111
|
+
{
|
|
112
|
+
id: 'a6',
|
|
113
|
+
texto:
|
|
114
|
+
'Cria uma copia do DOM para comparar e atualizar apenas o necessario',
|
|
115
|
+
correta: true,
|
|
116
|
+
},
|
|
117
|
+
{ id: 'a7', texto: 'Elimina a necessidade de HTML', correta: false },
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'q3',
|
|
122
|
+
enunciado: 'Qual e a principal diferenca entre props e state?',
|
|
123
|
+
pontuacao: 3,
|
|
124
|
+
alternativas: [
|
|
125
|
+
{
|
|
126
|
+
id: 'a8',
|
|
127
|
+
texto: 'Props sao mutaveis, state sao imutaveis',
|
|
128
|
+
correta: false,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'a9',
|
|
132
|
+
texto: 'Props sao passados pelo pai, state e gerenciado internamente',
|
|
133
|
+
correta: true,
|
|
134
|
+
},
|
|
135
|
+
{ id: 'a10', texto: 'Nao ha diferenca', correta: false },
|
|
136
|
+
{
|
|
137
|
+
id: 'a11',
|
|
138
|
+
texto: 'State so existe em class components',
|
|
139
|
+
correta: false,
|
|
140
|
+
},
|
|
141
|
+
{ id: 'a12', texto: 'Props nao podem ser funcoes', correta: false },
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'q4',
|
|
146
|
+
enunciado:
|
|
147
|
+
'Qual metodo do ciclo de vida e equivalente ao useEffect com array de dependencias vazio?',
|
|
148
|
+
pontuacao: 2,
|
|
149
|
+
alternativas: [
|
|
150
|
+
{ id: 'a13', texto: 'componentDidMount', correta: true },
|
|
151
|
+
{ id: 'a14', texto: 'componentWillUpdate', correta: false },
|
|
152
|
+
{ id: 'a15', texto: 'componentDidUpdate', correta: false },
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'q5',
|
|
157
|
+
enunciado: 'Para que serve o React.memo?',
|
|
158
|
+
pontuacao: 1,
|
|
159
|
+
alternativas: [
|
|
160
|
+
{ id: 'a16', texto: 'Para memorizar valores computados', correta: false },
|
|
161
|
+
{
|
|
162
|
+
id: 'a17',
|
|
163
|
+
texto: 'Para evitar re-renderizacoes desnecessarias de componentes',
|
|
164
|
+
correta: true,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
function SortableAlternativa({
|
|
171
|
+
alt,
|
|
172
|
+
index,
|
|
173
|
+
onToggleCorrect,
|
|
174
|
+
onChangeTexto,
|
|
175
|
+
onRemove,
|
|
176
|
+
canRemove,
|
|
177
|
+
}: {
|
|
178
|
+
alt: Alternativa;
|
|
179
|
+
index: number;
|
|
180
|
+
onToggleCorrect: () => void;
|
|
181
|
+
onChangeTexto: (texto: string) => void;
|
|
182
|
+
onRemove: () => void;
|
|
183
|
+
canRemove: boolean;
|
|
184
|
+
}) {
|
|
185
|
+
const t = useTranslations('lms.QuestionsPage');
|
|
186
|
+
const {
|
|
187
|
+
attributes,
|
|
188
|
+
listeners,
|
|
189
|
+
setNodeRef,
|
|
190
|
+
transform,
|
|
191
|
+
transition,
|
|
192
|
+
isDragging,
|
|
193
|
+
} = useSortable({ id: alt.id });
|
|
194
|
+
|
|
195
|
+
const style = {
|
|
196
|
+
transform: CSS.Transform.toString(transform),
|
|
197
|
+
transition,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div
|
|
202
|
+
ref={setNodeRef}
|
|
203
|
+
style={style}
|
|
204
|
+
className={`flex items-center gap-2 rounded-lg border p-2 transition-colors ${
|
|
205
|
+
isDragging ? 'z-50 bg-muted shadow-lg' : ''
|
|
206
|
+
} ${alt.correta ? 'border-foreground/30 bg-muted/50' : 'bg-background'}`}
|
|
207
|
+
>
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
className="cursor-grab touch-none text-muted-foreground hover:text-foreground"
|
|
211
|
+
{...attributes}
|
|
212
|
+
{...listeners}
|
|
213
|
+
aria-label={t('question.dragAlternative')}
|
|
214
|
+
>
|
|
215
|
+
<GripVertical className="size-4" />
|
|
216
|
+
</button>
|
|
217
|
+
|
|
218
|
+
<span className="flex size-6 shrink-0 items-center justify-center rounded-full border text-xs font-medium">
|
|
219
|
+
{String.fromCharCode(65 + index)}
|
|
220
|
+
</span>
|
|
221
|
+
|
|
222
|
+
<Input
|
|
223
|
+
value={alt.texto}
|
|
224
|
+
onChange={(e) => onChangeTexto(e.target.value)}
|
|
225
|
+
className="flex-1 border-0 bg-transparent p-0 text-sm shadow-none focus-visible:ring-0"
|
|
226
|
+
placeholder={t('sheet.fields.alternativePlaceholder')}
|
|
227
|
+
/>
|
|
228
|
+
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
onClick={onToggleCorrect}
|
|
232
|
+
className={`shrink-0 rounded-full p-1 transition-colors ${
|
|
233
|
+
alt.correta
|
|
234
|
+
? 'text-foreground'
|
|
235
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
236
|
+
}`}
|
|
237
|
+
aria-label={
|
|
238
|
+
alt.correta ? t('question.markIncorrect') : t('question.markCorrect')
|
|
239
|
+
}
|
|
240
|
+
>
|
|
241
|
+
{alt.correta ? (
|
|
242
|
+
<CircleDot className="size-5" />
|
|
243
|
+
) : (
|
|
244
|
+
<CircleOff className="size-5" />
|
|
245
|
+
)}
|
|
246
|
+
</button>
|
|
247
|
+
|
|
248
|
+
{canRemove && (
|
|
249
|
+
<button
|
|
250
|
+
type="button"
|
|
251
|
+
onClick={onRemove}
|
|
252
|
+
className="shrink-0 rounded p-1 text-muted-foreground transition-colors hover:text-destructive"
|
|
253
|
+
aria-label={t('question.removeAlternative')}
|
|
254
|
+
>
|
|
255
|
+
<X className="size-4" />
|
|
256
|
+
</button>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function SortableQuestao({
|
|
263
|
+
questao,
|
|
264
|
+
qIndex,
|
|
265
|
+
isExpanded,
|
|
266
|
+
onToggleExpand,
|
|
267
|
+
onEdit,
|
|
268
|
+
onDuplicate,
|
|
269
|
+
onDelete,
|
|
270
|
+
}: {
|
|
271
|
+
questao: Questao;
|
|
272
|
+
qIndex: number;
|
|
273
|
+
isExpanded: boolean;
|
|
274
|
+
onToggleExpand: () => void;
|
|
275
|
+
onEdit: () => void;
|
|
276
|
+
onDuplicate: () => void;
|
|
277
|
+
onDelete: () => void;
|
|
278
|
+
}) {
|
|
279
|
+
const t = useTranslations('lms.QuestionsPage');
|
|
280
|
+
const {
|
|
281
|
+
attributes,
|
|
282
|
+
listeners,
|
|
283
|
+
setNodeRef,
|
|
284
|
+
transform,
|
|
285
|
+
transition,
|
|
286
|
+
isDragging,
|
|
287
|
+
} = useSortable({ id: questao.id });
|
|
288
|
+
|
|
289
|
+
const style = {
|
|
290
|
+
transform: CSS.Transform.toString(transform),
|
|
291
|
+
transition,
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<motion.div
|
|
296
|
+
ref={setNodeRef}
|
|
297
|
+
style={style}
|
|
298
|
+
layout
|
|
299
|
+
initial={{ opacity: 0, y: 10 }}
|
|
300
|
+
animate={{ opacity: 1, y: 0 }}
|
|
301
|
+
exit={{ opacity: 0, y: -10 }}
|
|
302
|
+
transition={{ duration: 0.2 }}
|
|
303
|
+
className={isDragging ? 'z-50' : ''}
|
|
304
|
+
>
|
|
305
|
+
<Card className="overflow-hidden">
|
|
306
|
+
{/* Question header */}
|
|
307
|
+
<div className="flex items-start gap-3 p-4 transition-colors hover:bg-muted/30">
|
|
308
|
+
<button
|
|
309
|
+
type="button"
|
|
310
|
+
className="cursor-grab touch-none text-muted-foreground hover:text-foreground"
|
|
311
|
+
{...attributes}
|
|
312
|
+
{...listeners}
|
|
313
|
+
aria-label={t('question.dragQuestion')}
|
|
314
|
+
>
|
|
315
|
+
<GripVertical className="size-5" />
|
|
316
|
+
</button>
|
|
317
|
+
|
|
318
|
+
<div
|
|
319
|
+
className="flex min-w-0 flex-1 cursor-pointer items-start gap-3"
|
|
320
|
+
onClick={onToggleExpand}
|
|
321
|
+
role="button"
|
|
322
|
+
aria-expanded={isExpanded}
|
|
323
|
+
tabIndex={0}
|
|
324
|
+
onKeyDown={(e) => {
|
|
325
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
326
|
+
e.preventDefault();
|
|
327
|
+
onToggleExpand();
|
|
328
|
+
}
|
|
329
|
+
}}
|
|
330
|
+
>
|
|
331
|
+
<div className="flex size-8 shrink-0 items-center justify-center rounded-full bg-foreground text-sm font-bold text-background">
|
|
332
|
+
{qIndex + 1}
|
|
333
|
+
</div>
|
|
334
|
+
<div className="min-w-0 flex-1">
|
|
335
|
+
<p className="text-sm font-medium leading-relaxed">
|
|
336
|
+
{questao.enunciado}
|
|
337
|
+
</p>
|
|
338
|
+
<div className="mt-2 flex flex-wrap items-center gap-2">
|
|
339
|
+
<Badge variant="secondary" className="text-xs">
|
|
340
|
+
{t('question.alternatives', {
|
|
341
|
+
count: questao.alternativas.length,
|
|
342
|
+
})}
|
|
343
|
+
</Badge>
|
|
344
|
+
<Badge variant="outline" className="text-xs">
|
|
345
|
+
{questao.pontuacao === 1
|
|
346
|
+
? t('question.points', { score: questao.pontuacao })
|
|
347
|
+
: t('question.pointsPlural', { score: questao.pontuacao })}
|
|
348
|
+
</Badge>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
<div className="shrink-0">
|
|
352
|
+
{isExpanded ? (
|
|
353
|
+
<ChevronUp className="size-4 text-muted-foreground" />
|
|
354
|
+
) : (
|
|
355
|
+
<ChevronDown className="size-4 text-muted-foreground" />
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
{/* Expanded content */}
|
|
362
|
+
<AnimatePresence>
|
|
363
|
+
{isExpanded && (
|
|
364
|
+
<motion.div
|
|
365
|
+
initial={{ height: 0, opacity: 0 }}
|
|
366
|
+
animate={{ height: 'auto', opacity: 1 }}
|
|
367
|
+
exit={{ height: 0, opacity: 0 }}
|
|
368
|
+
transition={{ duration: 0.25 }}
|
|
369
|
+
className="overflow-hidden"
|
|
370
|
+
>
|
|
371
|
+
<Separator />
|
|
372
|
+
<div className="p-4">
|
|
373
|
+
<div className="flex flex-col gap-2">
|
|
374
|
+
{questao.alternativas.map((alt, altIndex) => (
|
|
375
|
+
<div
|
|
376
|
+
key={alt.id}
|
|
377
|
+
className={`flex items-center gap-3 rounded-lg border p-3 text-sm ${
|
|
378
|
+
alt.correta
|
|
379
|
+
? 'border-foreground/20 bg-muted/50 font-medium'
|
|
380
|
+
: ''
|
|
381
|
+
}`}
|
|
382
|
+
>
|
|
383
|
+
<span className="flex size-6 shrink-0 items-center justify-center rounded-full border text-xs font-medium">
|
|
384
|
+
{String.fromCharCode(65 + altIndex)}
|
|
385
|
+
</span>
|
|
386
|
+
<span className="flex-1">{alt.texto}</span>
|
|
387
|
+
{alt.correta && (
|
|
388
|
+
<Check className="size-4 shrink-0 text-foreground" />
|
|
389
|
+
)}
|
|
390
|
+
</div>
|
|
391
|
+
))}
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
<div className="mt-4 flex flex-wrap gap-2">
|
|
395
|
+
<Button
|
|
396
|
+
variant="outline"
|
|
397
|
+
size="sm"
|
|
398
|
+
className="gap-1.5"
|
|
399
|
+
onClick={onEdit}
|
|
400
|
+
>
|
|
401
|
+
<Pencil className="size-3.5" /> {t('question.actions.edit')}
|
|
402
|
+
</Button>
|
|
403
|
+
<Button
|
|
404
|
+
variant="outline"
|
|
405
|
+
size="sm"
|
|
406
|
+
className="gap-1.5"
|
|
407
|
+
onClick={onDuplicate}
|
|
408
|
+
>
|
|
409
|
+
<Copy className="size-3.5" />{' '}
|
|
410
|
+
{t('question.actions.duplicate')}
|
|
411
|
+
</Button>
|
|
412
|
+
<Button
|
|
413
|
+
variant="outline"
|
|
414
|
+
size="sm"
|
|
415
|
+
className="gap-1.5 text-destructive hover:text-destructive"
|
|
416
|
+
onClick={onDelete}
|
|
417
|
+
>
|
|
418
|
+
<Trash2 className="size-3.5" />{' '}
|
|
419
|
+
{t('question.actions.delete')}
|
|
420
|
+
</Button>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
</motion.div>
|
|
424
|
+
)}
|
|
425
|
+
</AnimatePresence>
|
|
426
|
+
</Card>
|
|
427
|
+
</motion.div>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export default function QuestoesPage() {
|
|
432
|
+
const t = useTranslations('lms.QuestionsPage');
|
|
433
|
+
const { id } = useParams();
|
|
434
|
+
const pathname = usePathname();
|
|
435
|
+
const [loading, setLoading] = useState(true);
|
|
436
|
+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
437
|
+
const [questoes, setQuestoes] = useState<Questao[]>(initialQuestoes);
|
|
438
|
+
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
|
439
|
+
const [sheetOpen, setSheetOpen] = useState(false);
|
|
440
|
+
const [editingQuestao, setEditingQuestao] = useState<Questao | null>(null);
|
|
441
|
+
const [sheetAlternativas, setSheetAlternativas] = useState<Alternativa[]>([]);
|
|
442
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
443
|
+
const [questaoToDelete, setQuestaoToDelete] = useState<Questao | null>(null);
|
|
444
|
+
const [hasChanges, setHasChanges] = useState(false);
|
|
445
|
+
|
|
446
|
+
const questaoSchema = z.object({
|
|
447
|
+
enunciado: z.string().min(5, t('sheet.validation.statementMin')),
|
|
448
|
+
pontuacao: z.coerce.number().min(0.5, t('sheet.validation.scoreMin')),
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
const form = useForm<QuestaoForm>({
|
|
452
|
+
resolver: zodResolver(questaoSchema),
|
|
453
|
+
defaultValues: { enunciado: '', pontuacao: 2 },
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const sensors = useSensors(
|
|
457
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
|
458
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
useEffect(() => {
|
|
462
|
+
const timer = setTimeout(() => setLoading(false), 600);
|
|
463
|
+
return () => clearTimeout(timer);
|
|
464
|
+
}, []);
|
|
465
|
+
|
|
466
|
+
function toggleExpand(qId: string) {
|
|
467
|
+
setExpandedIds((prev) => {
|
|
468
|
+
const next = new Set(prev);
|
|
469
|
+
if (next.has(qId)) next.delete(qId);
|
|
470
|
+
else next.add(qId);
|
|
471
|
+
return next;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function openCreateSheet() {
|
|
476
|
+
setEditingQuestao(null);
|
|
477
|
+
form.reset({ enunciado: '', pontuacao: 2 });
|
|
478
|
+
setSheetAlternativas([
|
|
479
|
+
{ id: generateId(), texto: '', correta: true },
|
|
480
|
+
{ id: generateId(), texto: '', correta: false },
|
|
481
|
+
]);
|
|
482
|
+
setSheetOpen(true);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function openEditSheet(questao: Questao) {
|
|
486
|
+
setEditingQuestao(questao);
|
|
487
|
+
form.reset({ enunciado: questao.enunciado, pontuacao: questao.pontuacao });
|
|
488
|
+
setSheetAlternativas(questao.alternativas.map((a) => ({ ...a })));
|
|
489
|
+
setSheetOpen(true);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function addAlternativa() {
|
|
493
|
+
setSheetAlternativas((prev) => [
|
|
494
|
+
...prev,
|
|
495
|
+
{ id: generateId(), texto: '', correta: false },
|
|
496
|
+
]);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function removeAlternativa(altId: string) {
|
|
500
|
+
setSheetAlternativas((prev) => prev.filter((a) => a.id !== altId));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function toggleCorreta(altId: string) {
|
|
504
|
+
setSheetAlternativas((prev) =>
|
|
505
|
+
prev.map((a) => ({ ...a, correta: a.id === altId }))
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function updateAlternativaTexto(altId: string, texto: string) {
|
|
510
|
+
setSheetAlternativas((prev) =>
|
|
511
|
+
prev.map((a) => (a.id === altId ? { ...a, texto } : a))
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function handleAltDragEnd(event: DragEndEvent) {
|
|
516
|
+
const { active, over } = event;
|
|
517
|
+
if (over && active.id !== over.id) {
|
|
518
|
+
setSheetAlternativas((items) => {
|
|
519
|
+
const oldIndex = items.findIndex((i) => i.id === active.id);
|
|
520
|
+
const newIndex = items.findIndex((i) => i.id === over.id);
|
|
521
|
+
return arrayMove(items, oldIndex, newIndex);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function onSubmit(data: QuestaoForm) {
|
|
527
|
+
const hasCorreta = sheetAlternativas.some((a) => a.correta);
|
|
528
|
+
const allHaveText = sheetAlternativas.every(
|
|
529
|
+
(a) => a.texto.trim().length > 0
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
if (!hasCorreta) {
|
|
533
|
+
toast.error(t('toasts.noCorrectAnswer'));
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (!allHaveText) {
|
|
537
|
+
toast.error(t('toasts.allMustHaveText'));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (sheetAlternativas.length < 2) {
|
|
541
|
+
toast.error(t('toasts.minAlternatives'));
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (editingQuestao) {
|
|
546
|
+
setQuestoes((prev) =>
|
|
547
|
+
prev.map((q) =>
|
|
548
|
+
q.id === editingQuestao.id
|
|
549
|
+
? {
|
|
550
|
+
...q,
|
|
551
|
+
enunciado: data.enunciado,
|
|
552
|
+
pontuacao: data.pontuacao,
|
|
553
|
+
alternativas: sheetAlternativas,
|
|
554
|
+
}
|
|
555
|
+
: q
|
|
556
|
+
)
|
|
557
|
+
);
|
|
558
|
+
toast.success(t('toasts.questionUpdated'));
|
|
559
|
+
} else {
|
|
560
|
+
const nova: Questao = {
|
|
561
|
+
id: generateId(),
|
|
562
|
+
enunciado: data.enunciado,
|
|
563
|
+
pontuacao: data.pontuacao,
|
|
564
|
+
alternativas: sheetAlternativas,
|
|
565
|
+
};
|
|
566
|
+
setQuestoes((prev) => [...prev, nova]);
|
|
567
|
+
toast.success(t('toasts.questionCreated'));
|
|
568
|
+
}
|
|
569
|
+
setHasChanges(true);
|
|
570
|
+
setSheetOpen(false);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function confirmDelete() {
|
|
574
|
+
if (questaoToDelete) {
|
|
575
|
+
setQuestoes((prev) => prev.filter((q) => q.id !== questaoToDelete.id));
|
|
576
|
+
setHasChanges(true);
|
|
577
|
+
toast.success(t('toasts.questionRemoved'));
|
|
578
|
+
setQuestaoToDelete(null);
|
|
579
|
+
setDeleteDialogOpen(false);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function duplicateQuestao(questao: Questao) {
|
|
584
|
+
const nova: Questao = {
|
|
585
|
+
...questao,
|
|
586
|
+
id: generateId(),
|
|
587
|
+
enunciado: `${questao.enunciado}${t('copySuffix')}`,
|
|
588
|
+
alternativas: questao.alternativas.map((a) => ({
|
|
589
|
+
...a,
|
|
590
|
+
id: generateId(),
|
|
591
|
+
})),
|
|
592
|
+
};
|
|
593
|
+
setQuestoes((prev) => {
|
|
594
|
+
const idx = prev.findIndex((q) => q.id === questao.id);
|
|
595
|
+
const next = [...prev];
|
|
596
|
+
next.splice(idx + 1, 0, nova);
|
|
597
|
+
return next;
|
|
598
|
+
});
|
|
599
|
+
setHasChanges(true);
|
|
600
|
+
toast.success(t('toasts.questionDuplicated'));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function handleQuestionDragEnd(event: DragEndEvent) {
|
|
604
|
+
const { active, over } = event;
|
|
605
|
+
if (over && active.id !== over.id) {
|
|
606
|
+
setQuestoes((items) => {
|
|
607
|
+
const oldIndex = items.findIndex((i) => i.id === active.id);
|
|
608
|
+
const newIndex = items.findIndex((i) => i.id === over.id);
|
|
609
|
+
return arrayMove(items, oldIndex, newIndex);
|
|
610
|
+
});
|
|
611
|
+
setHasChanges(true);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function handleSave() {
|
|
616
|
+
setHasChanges(false);
|
|
617
|
+
toast.success(t('toasts.changesSaved'));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const totalPontuacao = questoes.reduce((a, q) => a + q.pontuacao, 0);
|
|
621
|
+
|
|
622
|
+
return (
|
|
623
|
+
<Page>
|
|
624
|
+
<PageHeader
|
|
625
|
+
title={t('title')}
|
|
626
|
+
description={t('description', {
|
|
627
|
+
code: 'EX-001',
|
|
628
|
+
count: questoes.length,
|
|
629
|
+
total: totalPontuacao,
|
|
630
|
+
})}
|
|
631
|
+
breadcrumbs={[
|
|
632
|
+
{
|
|
633
|
+
label: t('breadcrumbs.home'),
|
|
634
|
+
href: '/',
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
label: t('breadcrumbs.exams'),
|
|
638
|
+
href: '/lms/exams',
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
label: t('breadcrumbs.questions'),
|
|
642
|
+
},
|
|
643
|
+
]}
|
|
644
|
+
actions={
|
|
645
|
+
<div className="flex items-center gap-2">
|
|
646
|
+
<AnimatePresence>
|
|
647
|
+
{hasChanges && (
|
|
648
|
+
<motion.div
|
|
649
|
+
initial={{ opacity: 0, scale: 0.9 }}
|
|
650
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
651
|
+
exit={{ opacity: 0, scale: 0.9 }}
|
|
652
|
+
>
|
|
653
|
+
<Button
|
|
654
|
+
onClick={handleSave}
|
|
655
|
+
className="gap-2"
|
|
656
|
+
variant="outline"
|
|
657
|
+
>
|
|
658
|
+
<Save className="size-4" /> {t('actions.save')}
|
|
659
|
+
</Button>
|
|
660
|
+
</motion.div>
|
|
661
|
+
)}
|
|
662
|
+
</AnimatePresence>
|
|
663
|
+
<Button onClick={openCreateSheet} className="gap-2">
|
|
664
|
+
<Plus className="size-4" /> {t('actions.newQuestion')}
|
|
665
|
+
</Button>
|
|
666
|
+
</div>
|
|
667
|
+
}
|
|
668
|
+
/>
|
|
669
|
+
|
|
670
|
+
<div className="pb-10">
|
|
671
|
+
{/* Breadcrumb + actions */}
|
|
672
|
+
<motion.div
|
|
673
|
+
initial={{ opacity: 0, y: 20 }}
|
|
674
|
+
animate={{ opacity: 1, y: 0 }}
|
|
675
|
+
transition={{ duration: 0.4 }}
|
|
676
|
+
>
|
|
677
|
+
{/* Stats */}
|
|
678
|
+
{!loading && (
|
|
679
|
+
<div className="mb-6 grid grid-cols-3 gap-4">
|
|
680
|
+
{[
|
|
681
|
+
{ label: t('stats.questions'), valor: questoes.length },
|
|
682
|
+
{ label: t('stats.totalScore'), valor: totalPontuacao },
|
|
683
|
+
{
|
|
684
|
+
label: t('stats.avgAlternatives'),
|
|
685
|
+
valor:
|
|
686
|
+
questoes.length > 0
|
|
687
|
+
? (
|
|
688
|
+
questoes.reduce(
|
|
689
|
+
(a, q) => a + q.alternativas.length,
|
|
690
|
+
0
|
|
691
|
+
) / questoes.length
|
|
692
|
+
).toFixed(1)
|
|
693
|
+
: '0',
|
|
694
|
+
},
|
|
695
|
+
].map((stat, i) => (
|
|
696
|
+
<motion.div
|
|
697
|
+
key={stat.label}
|
|
698
|
+
initial={{ opacity: 0, y: 10 }}
|
|
699
|
+
animate={{ opacity: 1, y: 0 }}
|
|
700
|
+
transition={{ delay: i * 0.08 }}
|
|
701
|
+
>
|
|
702
|
+
<Card>
|
|
703
|
+
<CardContent className="p-4 text-center">
|
|
704
|
+
<p className="text-2xl font-bold">{stat.valor}</p>
|
|
705
|
+
<p className="text-xs text-muted-foreground">
|
|
706
|
+
{stat.label}
|
|
707
|
+
</p>
|
|
708
|
+
</CardContent>
|
|
709
|
+
</Card>
|
|
710
|
+
</motion.div>
|
|
711
|
+
))}
|
|
712
|
+
</div>
|
|
713
|
+
)}
|
|
714
|
+
|
|
715
|
+
{/* Questions list */}
|
|
716
|
+
{loading ? (
|
|
717
|
+
<div className="flex flex-col gap-4">
|
|
718
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
719
|
+
<Card key={i}>
|
|
720
|
+
<CardContent className="p-4">
|
|
721
|
+
<div className="flex items-center gap-3">
|
|
722
|
+
<Skeleton className="size-8 rounded-full" />
|
|
723
|
+
<div className="flex-1">
|
|
724
|
+
<Skeleton className="mb-2 h-5 w-3/4" />
|
|
725
|
+
<Skeleton className="h-4 w-1/4" />
|
|
726
|
+
</div>
|
|
727
|
+
</div>
|
|
728
|
+
</CardContent>
|
|
729
|
+
</Card>
|
|
730
|
+
))}
|
|
731
|
+
</div>
|
|
732
|
+
) : questoes.length === 0 ? (
|
|
733
|
+
<Card>
|
|
734
|
+
<CardContent className="flex flex-col items-center gap-3 py-16">
|
|
735
|
+
<div className="flex size-12 items-center justify-center rounded-full bg-muted">
|
|
736
|
+
<FileCheck className="size-6 text-muted-foreground" />
|
|
737
|
+
</div>
|
|
738
|
+
<p className="font-medium">{t('empty.title')}</p>
|
|
739
|
+
<p className="text-sm text-muted-foreground">
|
|
740
|
+
{t('empty.description')}
|
|
741
|
+
</p>
|
|
742
|
+
<Button onClick={openCreateSheet} className="mt-2 gap-2">
|
|
743
|
+
<Plus className="size-4" /> {t('empty.action')}
|
|
744
|
+
</Button>
|
|
745
|
+
</CardContent>
|
|
746
|
+
</Card>
|
|
747
|
+
) : (
|
|
748
|
+
<DndContext
|
|
749
|
+
sensors={sensors}
|
|
750
|
+
collisionDetection={closestCenter}
|
|
751
|
+
onDragEnd={handleQuestionDragEnd}
|
|
752
|
+
>
|
|
753
|
+
<SortableContext
|
|
754
|
+
items={questoes.map((q) => q.id)}
|
|
755
|
+
strategy={verticalListSortingStrategy}
|
|
756
|
+
>
|
|
757
|
+
<div className="flex flex-col gap-3">
|
|
758
|
+
<AnimatePresence>
|
|
759
|
+
{questoes.map((questao, qIndex) => {
|
|
760
|
+
const isExpanded = expandedIds.has(questao.id);
|
|
761
|
+
return (
|
|
762
|
+
<SortableQuestao
|
|
763
|
+
key={questao.id}
|
|
764
|
+
questao={questao}
|
|
765
|
+
qIndex={qIndex}
|
|
766
|
+
isExpanded={isExpanded}
|
|
767
|
+
onToggleExpand={() => toggleExpand(questao.id)}
|
|
768
|
+
onEdit={() => openEditSheet(questao)}
|
|
769
|
+
onDuplicate={() => duplicateQuestao(questao)}
|
|
770
|
+
onDelete={() => {
|
|
771
|
+
setQuestaoToDelete(questao);
|
|
772
|
+
setDeleteDialogOpen(true);
|
|
773
|
+
}}
|
|
774
|
+
/>
|
|
775
|
+
);
|
|
776
|
+
})}
|
|
777
|
+
</AnimatePresence>
|
|
778
|
+
</div>
|
|
779
|
+
</SortableContext>
|
|
780
|
+
</DndContext>
|
|
781
|
+
)}
|
|
782
|
+
</motion.div>
|
|
783
|
+
</div>
|
|
784
|
+
|
|
785
|
+
{/* Create/Edit Sheet */}
|
|
786
|
+
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
|
787
|
+
<SheetContent className="overflow-y-auto sm:max-w-xl">
|
|
788
|
+
<SheetHeader>
|
|
789
|
+
<SheetTitle>
|
|
790
|
+
{editingQuestao ? t('sheet.titleEdit') : t('sheet.titleCreate')}
|
|
791
|
+
</SheetTitle>
|
|
792
|
+
<SheetDescription>
|
|
793
|
+
{editingQuestao
|
|
794
|
+
? t('sheet.descriptionEdit')
|
|
795
|
+
: t('sheet.descriptionCreate')}
|
|
796
|
+
</SheetDescription>
|
|
797
|
+
</SheetHeader>
|
|
798
|
+
<form
|
|
799
|
+
onSubmit={form.handleSubmit(onSubmit)}
|
|
800
|
+
className="flex flex-col gap-5 px-4 pb-4"
|
|
801
|
+
>
|
|
802
|
+
<Field data-invalid={!!form.formState.errors.enunciado}>
|
|
803
|
+
<FieldLabel htmlFor="enunciado">
|
|
804
|
+
{t('sheet.fields.statement')}
|
|
805
|
+
</FieldLabel>
|
|
806
|
+
<Textarea
|
|
807
|
+
id="enunciado"
|
|
808
|
+
rows={3}
|
|
809
|
+
placeholder={t('sheet.fields.statementPlaceholder')}
|
|
810
|
+
{...form.register('enunciado')}
|
|
811
|
+
/>
|
|
812
|
+
{form.formState.errors.enunciado && (
|
|
813
|
+
<p className="text-sm text-destructive">
|
|
814
|
+
{form.formState.errors.enunciado.message}
|
|
815
|
+
</p>
|
|
816
|
+
)}
|
|
817
|
+
</Field>
|
|
818
|
+
|
|
819
|
+
<Field data-invalid={!!form.formState.errors.pontuacao}>
|
|
820
|
+
<FieldLabel htmlFor="pontuacao">
|
|
821
|
+
{t('sheet.fields.score')}
|
|
822
|
+
</FieldLabel>
|
|
823
|
+
<Input
|
|
824
|
+
id="pontuacao"
|
|
825
|
+
type="number"
|
|
826
|
+
step="0.5"
|
|
827
|
+
placeholder={t('sheet.fields.scorePlaceholder')}
|
|
828
|
+
{...form.register('pontuacao')}
|
|
829
|
+
/>
|
|
830
|
+
{form.formState.errors.pontuacao && (
|
|
831
|
+
<p className="text-sm text-destructive">
|
|
832
|
+
{form.formState.errors.pontuacao.message}
|
|
833
|
+
</p>
|
|
834
|
+
)}
|
|
835
|
+
</Field>
|
|
836
|
+
|
|
837
|
+
<Separator />
|
|
838
|
+
|
|
839
|
+
{/* Alternatives with dnd */}
|
|
840
|
+
<div>
|
|
841
|
+
<div className="mb-3 flex items-center justify-between">
|
|
842
|
+
<div>
|
|
843
|
+
<p className="text-sm font-medium">
|
|
844
|
+
{t('sheet.fields.alternatives')}
|
|
845
|
+
</p>
|
|
846
|
+
<p className="text-xs text-muted-foreground">
|
|
847
|
+
{t('sheet.fields.alternativesDescription')}
|
|
848
|
+
</p>
|
|
849
|
+
</div>
|
|
850
|
+
<Button
|
|
851
|
+
type="button"
|
|
852
|
+
variant="outline"
|
|
853
|
+
size="sm"
|
|
854
|
+
className="gap-1.5"
|
|
855
|
+
onClick={addAlternativa}
|
|
856
|
+
>
|
|
857
|
+
<Plus className="size-3.5" /> {t('sheet.actions.add')}
|
|
858
|
+
</Button>
|
|
859
|
+
</div>
|
|
860
|
+
|
|
861
|
+
<DndContext
|
|
862
|
+
sensors={sensors}
|
|
863
|
+
collisionDetection={closestCenter}
|
|
864
|
+
onDragEnd={handleAltDragEnd}
|
|
865
|
+
>
|
|
866
|
+
<SortableContext
|
|
867
|
+
items={sheetAlternativas.map((a) => a.id)}
|
|
868
|
+
strategy={verticalListSortingStrategy}
|
|
869
|
+
>
|
|
870
|
+
<div className="flex flex-col gap-2">
|
|
871
|
+
{sheetAlternativas.map((alt, i) => (
|
|
872
|
+
<SortableAlternativa
|
|
873
|
+
key={alt.id}
|
|
874
|
+
alt={alt}
|
|
875
|
+
index={i}
|
|
876
|
+
onToggleCorrect={() => toggleCorreta(alt.id)}
|
|
877
|
+
onChangeTexto={(t) => updateAlternativaTexto(alt.id, t)}
|
|
878
|
+
onRemove={() => removeAlternativa(alt.id)}
|
|
879
|
+
canRemove={sheetAlternativas.length > 2}
|
|
880
|
+
/>
|
|
881
|
+
))}
|
|
882
|
+
</div>
|
|
883
|
+
</SortableContext>
|
|
884
|
+
</DndContext>
|
|
885
|
+
|
|
886
|
+
{sheetAlternativas.length < 2 && (
|
|
887
|
+
<p className="mt-2 text-sm text-destructive">
|
|
888
|
+
{t('sheet.validation.minAlternatives')}
|
|
889
|
+
</p>
|
|
890
|
+
)}
|
|
891
|
+
</div>
|
|
892
|
+
|
|
893
|
+
<SheetFooter className="gap-2 p-0 pt-4">
|
|
894
|
+
<Button type="submit">
|
|
895
|
+
{editingQuestao
|
|
896
|
+
? t('sheet.actions.update')
|
|
897
|
+
: t('sheet.actions.create')}
|
|
898
|
+
</Button>
|
|
899
|
+
</SheetFooter>
|
|
900
|
+
</form>
|
|
901
|
+
</SheetContent>
|
|
902
|
+
</Sheet>
|
|
903
|
+
|
|
904
|
+
{/* Delete Dialog */}
|
|
905
|
+
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
906
|
+
<DialogContent>
|
|
907
|
+
<DialogHeader>
|
|
908
|
+
<DialogTitle className="flex items-center gap-2">
|
|
909
|
+
<AlertTriangle className="size-5 text-destructive" />
|
|
910
|
+
{t('deleteDialog.title')}
|
|
911
|
+
</DialogTitle>
|
|
912
|
+
<DialogDescription>
|
|
913
|
+
{t('deleteDialog.description')}
|
|
914
|
+
</DialogDescription>
|
|
915
|
+
</DialogHeader>
|
|
916
|
+
<DialogFooter>
|
|
917
|
+
<Button
|
|
918
|
+
variant="outline"
|
|
919
|
+
onClick={() => setDeleteDialogOpen(false)}
|
|
920
|
+
>
|
|
921
|
+
{t('deleteDialog.actions.cancel')}
|
|
922
|
+
</Button>
|
|
923
|
+
<Button variant="destructive" onClick={confirmDelete}>
|
|
924
|
+
{t('deleteDialog.actions.confirm')}
|
|
925
|
+
</Button>
|
|
926
|
+
</DialogFooter>
|
|
927
|
+
</DialogContent>
|
|
928
|
+
</Dialog>
|
|
929
|
+
</Page>
|
|
930
|
+
);
|
|
931
|
+
}
|