@hed-hog/lms 0.0.306 → 0.0.309
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-structure.controller.d.ts +60 -0
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +79 -0
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +61 -1
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +326 -1
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course.controller.d.ts +52 -4
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.service.d.ts +52 -5
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +78 -57
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js +5 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/dto/create-course.dto.d.ts +1 -1
- package/dist/course/dto/create-course.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course.dto.js +4 -1
- package/dist/course/dto/create-course.dto.js.map +1 -1
- package/dist/course/dto/move-lesson.dto.d.ts +10 -0
- package/dist/course/dto/move-lesson.dto.d.ts.map +1 -0
- package/dist/course/dto/move-lesson.dto.js +28 -0
- package/dist/course/dto/move-lesson.dto.js.map +1 -0
- package/dist/course/dto/paste-lessons.dto.d.ts +4 -0
- package/dist/course/dto/paste-lessons.dto.d.ts.map +1 -0
- package/dist/course/dto/paste-lessons.dto.js +24 -0
- package/dist/course/dto/paste-lessons.dto.js.map +1 -0
- package/dist/course/dto/reorder-lessons.dto.d.ts +5 -0
- package/dist/course/dto/reorder-lessons.dto.d.ts.map +1 -0
- package/dist/course/dto/reorder-lessons.dto.js +24 -0
- package/dist/course/dto/reorder-lessons.dto.js.map +1 -0
- package/dist/course/dto/reorder-sessions.dto.d.ts +5 -0
- package/dist/course/dto/reorder-sessions.dto.d.ts.map +1 -0
- package/dist/course/dto/reorder-sessions.dto.js +24 -0
- package/dist/course/dto/reorder-sessions.dto.js.map +1 -0
- package/dist/training/training.controller.js +1 -1
- package/dist/training/training.controller.js.map +1 -1
- package/hedhog/data/image_type.yaml +20 -0
- package/hedhog/data/menu.yaml +2 -2
- package/hedhog/data/route.yaml +60 -6
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +146 -165
- package/hedhog/frontend/app/_components/course-avatar.tsx.ejs +70 -0
- package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +372 -22
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +437 -77
- package/hedhog/frontend/app/classes/page.tsx.ejs +311 -289
- package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +10 -7
- package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +23 -32
- package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +3 -9
- package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +26 -16
- package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +19 -5
- package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +10 -14
- package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +131 -107
- package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +10 -7
- package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +38 -19
- package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +336 -1057
- package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +45 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-skeleton.tsx.ejs +64 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +52 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1827 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +41 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +184 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +96 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +74 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +948 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +150 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +182 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +52 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +122 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +97 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +347 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/course-structure-contract.ts.ejs +195 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +420 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +254 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +987 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +86 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure.ts.ejs +160 -0
- package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -3212
- package/hedhog/frontend/app/courses/page.tsx.ejs +45 -26
- package/hedhog/frontend/app/{training → paths}/page.tsx.ejs +29 -7
- package/hedhog/frontend/messages/en.json +88 -10
- package/hedhog/frontend/messages/pt.json +88 -10
- package/hedhog/table/course.yaml +1 -1
- package/hedhog/table/image_type.yaml +14 -0
- package/package.json +7 -7
- package/src/course/course-structure.controller.ts +63 -0
- package/src/course/course-structure.service.ts +390 -3
- package/src/course/course.service.ts +59 -27
- package/src/course/dto/create-course-structure-lesson.dto.ts +3 -2
- package/src/course/dto/create-course.dto.ts +4 -1
- package/src/course/dto/move-lesson.dto.ts +17 -0
- package/src/course/dto/paste-lessons.dto.ts +9 -0
- package/src/course/dto/reorder-lessons.dto.ts +10 -0
- package/src/course/dto/reorder-sessions.dto.ts +10 -0
- package/src/training/training.controller.ts +1 -1
|
@@ -73,11 +73,18 @@ import {
|
|
|
73
73
|
TableRow,
|
|
74
74
|
} from '@/components/ui/table';
|
|
75
75
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
76
|
+
import {
|
|
77
|
+
Tooltip,
|
|
78
|
+
TooltipContent,
|
|
79
|
+
TooltipProvider,
|
|
80
|
+
TooltipTrigger,
|
|
81
|
+
} from '@/components/ui/tooltip';
|
|
76
82
|
import { cn } from '@/lib/utils';
|
|
77
83
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
78
84
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
79
85
|
import {
|
|
80
86
|
addMonths,
|
|
87
|
+
differenceInCalendarMonths,
|
|
81
88
|
eachDayOfInterval,
|
|
82
89
|
endOfMonth,
|
|
83
90
|
endOfWeek,
|
|
@@ -121,6 +128,7 @@ import {
|
|
|
121
128
|
useCallback,
|
|
122
129
|
useEffect,
|
|
123
130
|
useMemo,
|
|
131
|
+
useRef,
|
|
124
132
|
useState,
|
|
125
133
|
type ReactNode,
|
|
126
134
|
} from 'react';
|
|
@@ -128,6 +136,14 @@ import { Controller, useForm } from 'react-hook-form';
|
|
|
128
136
|
import { toast } from 'sonner';
|
|
129
137
|
import { z } from 'zod';
|
|
130
138
|
import { ClassFormSheet } from '../../_components/class-form-sheet';
|
|
139
|
+
import { CourseAvatar } from '../../_components/course-avatar';
|
|
140
|
+
import {
|
|
141
|
+
CourseCategoryOption,
|
|
142
|
+
CourseFormSheet,
|
|
143
|
+
CourseSheetFormValues,
|
|
144
|
+
DEFAULT_COURSE_FORM_VALUES,
|
|
145
|
+
getCourseSheetSchema,
|
|
146
|
+
} from '../../_components/course-form-sheet';
|
|
131
147
|
import { CreateLmsPersonSheet } from '../../_components/create-lms-person-sheet';
|
|
132
148
|
import { CreateLmsStudentPersonSheet } from '../../_components/create-lms-student-person-sheet';
|
|
133
149
|
|
|
@@ -345,6 +361,25 @@ type ClassDetail = {
|
|
|
345
361
|
location?: string;
|
|
346
362
|
};
|
|
347
363
|
|
|
364
|
+
type ApiCourseDetail = {
|
|
365
|
+
id: number;
|
|
366
|
+
code?: string;
|
|
367
|
+
slug?: string;
|
|
368
|
+
title: string;
|
|
369
|
+
description?: string;
|
|
370
|
+
level?: 'beginner' | 'intermediate' | 'advanced';
|
|
371
|
+
status?: 'draft' | 'published' | 'archived';
|
|
372
|
+
categories?: string[];
|
|
373
|
+
primaryColor?: string | null;
|
|
374
|
+
secondaryColor?: string | null;
|
|
375
|
+
logoFileId?: number | null;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
type ApiCourseCategoryList = {
|
|
379
|
+
data: Array<{ id: number; slug: string; name: string; status?: string }>;
|
|
380
|
+
total?: number;
|
|
381
|
+
};
|
|
382
|
+
|
|
348
383
|
type AttendanceRecord = {
|
|
349
384
|
student_id: number;
|
|
350
385
|
present: boolean;
|
|
@@ -356,6 +391,36 @@ type Locale = {
|
|
|
356
391
|
name: string;
|
|
357
392
|
};
|
|
358
393
|
|
|
394
|
+
function toCourseFormLevel(level?: string): CourseSheetFormValues['nivel'] {
|
|
395
|
+
if (level === 'intermediate') return 'intermediario';
|
|
396
|
+
if (level === 'advanced') return 'avancado';
|
|
397
|
+
return 'iniciante';
|
|
398
|
+
}
|
|
399
|
+
function toCourseFormStatus(status?: string): CourseSheetFormValues['status'] {
|
|
400
|
+
if (status === 'published') return 'ativo';
|
|
401
|
+
if (status === 'archived') return 'arquivado';
|
|
402
|
+
return 'rascunho';
|
|
403
|
+
}
|
|
404
|
+
function toApiCourseLevel(nivel: CourseSheetFormValues['nivel']) {
|
|
405
|
+
if (nivel === 'intermediario') return 'intermediate';
|
|
406
|
+
if (nivel === 'avancado') return 'advanced';
|
|
407
|
+
return 'beginner';
|
|
408
|
+
}
|
|
409
|
+
function toApiCourseStatus(status: CourseSheetFormValues['status']) {
|
|
410
|
+
if (status === 'ativo') return 'published';
|
|
411
|
+
if (status === 'arquivado') return 'archived';
|
|
412
|
+
return 'draft';
|
|
413
|
+
}
|
|
414
|
+
function getContrastColorCourse(hex: string) {
|
|
415
|
+
const cleaned = hex.replace('#', '');
|
|
416
|
+
if (cleaned.length !== 6) return '#FFFFFF';
|
|
417
|
+
const r = parseInt(cleaned.slice(0, 2), 16);
|
|
418
|
+
const g = parseInt(cleaned.slice(2, 4), 16);
|
|
419
|
+
const b = parseInt(cleaned.slice(4, 6), 16);
|
|
420
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
421
|
+
return luminance > 0.6 ? '#111827' : '#FFFFFF';
|
|
422
|
+
}
|
|
423
|
+
|
|
359
424
|
const EMPTY_AULAS: Aula[] = [];
|
|
360
425
|
|
|
361
426
|
const SESSION_RECURRENCE_FREQUENCIES = [
|
|
@@ -580,6 +645,7 @@ type StudentForm = z.infer<ReturnType<typeof getStudentSchema>>;
|
|
|
580
645
|
export default function TurmaDetalhePage() {
|
|
581
646
|
const t = useTranslations('lms.ClassesPage.DetailPage');
|
|
582
647
|
const tClasses = useTranslations('lms.ClassesPage');
|
|
648
|
+
const tCourses = useTranslations('lms.CoursesPage');
|
|
583
649
|
const locale = useLocale();
|
|
584
650
|
const params = useParams();
|
|
585
651
|
const router = useRouter();
|
|
@@ -656,6 +722,8 @@ export default function TurmaDetalhePage() {
|
|
|
656
722
|
|
|
657
723
|
// ── Edit sheet state ──────────────────────────────────────────────────────
|
|
658
724
|
const [editSheetOpen, setEditSheetOpen] = useState(false);
|
|
725
|
+
const [courseSheetOpen, setCourseSheetOpen] = useState(false);
|
|
726
|
+
const [savingCourse, setSavingCourse] = useState(false);
|
|
659
727
|
|
|
660
728
|
// ── Tab state ─────────────────────────────────────────────────────────────
|
|
661
729
|
const [activeTab, setActiveTab] = useState('alunos');
|
|
@@ -663,6 +731,36 @@ export default function TurmaDetalhePage() {
|
|
|
663
731
|
'single' | 'quarter' | 'year' | 'list'
|
|
664
732
|
>('single');
|
|
665
733
|
const [calendarViewDate, setCalendarViewDate] = useState(() => new Date());
|
|
734
|
+
const calendarInitialized = useRef(false);
|
|
735
|
+
|
|
736
|
+
useEffect(() => {
|
|
737
|
+
if (calendarInitialized.current || !turma) return;
|
|
738
|
+
|
|
739
|
+
const rawStart = turma.startDate ?? turma.dataInicio;
|
|
740
|
+
const rawEnd = turma.endDate ?? turma.dataFim;
|
|
741
|
+
|
|
742
|
+
if (!rawStart) return;
|
|
743
|
+
|
|
744
|
+
const start = new Date(`${String(rawStart).slice(0, 10)}T00:00:00`);
|
|
745
|
+
const end = rawEnd
|
|
746
|
+
? new Date(`${String(rawEnd).slice(0, 10)}T00:00:00`)
|
|
747
|
+
: start;
|
|
748
|
+
|
|
749
|
+
const monthDiff = differenceInCalendarMonths(end, start);
|
|
750
|
+
|
|
751
|
+
let mode: 'single' | 'quarter' | 'year' | 'list';
|
|
752
|
+
if (monthDiff === 0) {
|
|
753
|
+
mode = 'single';
|
|
754
|
+
} else if (monthDiff < 12) {
|
|
755
|
+
mode = 'quarter';
|
|
756
|
+
} else {
|
|
757
|
+
mode = 'list';
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
calendarInitialized.current = true;
|
|
761
|
+
setCalendarViewMode(mode);
|
|
762
|
+
setCalendarViewDate(startOfMonth(start));
|
|
763
|
+
}, [turma]);
|
|
666
764
|
|
|
667
765
|
// ── Students state ────────────────────────────────────────────────────────
|
|
668
766
|
const [alunoSearch, setAlunoSearch] = useState('');
|
|
@@ -772,11 +870,50 @@ export default function TurmaDetalhePage() {
|
|
|
772
870
|
}
|
|
773
871
|
}, [aulaSheetOpen, instructorOpen, refetchInstructorOptions]);
|
|
774
872
|
|
|
873
|
+
const { data: courseDetail, refetch: refetchCourseDetail } =
|
|
874
|
+
useQuery<ApiCourseDetail>({
|
|
875
|
+
queryKey: [
|
|
876
|
+
'lms-course-for-class-sheet',
|
|
877
|
+
turma?.courseId ?? turma?.cursoId,
|
|
878
|
+
],
|
|
879
|
+
queryFn: async () => {
|
|
880
|
+
const cId = turma?.courseId ?? turma?.cursoId;
|
|
881
|
+
const res = await request<ApiCourseDetail>({
|
|
882
|
+
url: `/lms/courses/${cId}`,
|
|
883
|
+
method: 'GET',
|
|
884
|
+
});
|
|
885
|
+
return res.data;
|
|
886
|
+
},
|
|
887
|
+
enabled: !!(turma?.courseId ?? turma?.cursoId),
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
const { data: courseCategoryData } = useQuery<ApiCourseCategoryList>({
|
|
891
|
+
queryKey: ['category-options-for-course-sheet'],
|
|
892
|
+
queryFn: async () => {
|
|
893
|
+
const res = await request<ApiCourseCategoryList>({
|
|
894
|
+
url: '/category',
|
|
895
|
+
method: 'GET',
|
|
896
|
+
params: { page: 1, pageSize: 500, status: 'all' },
|
|
897
|
+
});
|
|
898
|
+
return res.data;
|
|
899
|
+
},
|
|
900
|
+
enabled: courseSheetOpen,
|
|
901
|
+
});
|
|
902
|
+
|
|
775
903
|
// ── Attendance state ──────────────────────────────────────────────────────
|
|
776
904
|
const [presencaList, setPresencaList] = useState<PresencaItem[]>([]);
|
|
777
905
|
const [savingPresenca, setSavingPresenca] = useState(false);
|
|
778
906
|
const [aulasState, setAulasState] = useState<Aula[]>([]);
|
|
779
907
|
|
|
908
|
+
// attendanceMatrix: sessionId -> (studentId -> present)
|
|
909
|
+
const [attendanceMatrix, setAttendanceMatrix] = useState<
|
|
910
|
+
Record<number, Record<number, boolean>>
|
|
911
|
+
>({});
|
|
912
|
+
const [loadingAttendanceMatrix, setLoadingAttendanceMatrix] = useState(false);
|
|
913
|
+
const [savingAttendanceCells, setSavingAttendanceCells] = useState<
|
|
914
|
+
Set<string>
|
|
915
|
+
>(new Set());
|
|
916
|
+
|
|
780
917
|
useEffect(() => {
|
|
781
918
|
setAulasState(
|
|
782
919
|
[...aulasQuery].sort(
|
|
@@ -787,6 +924,44 @@ export default function TurmaDetalhePage() {
|
|
|
787
924
|
);
|
|
788
925
|
}, [aulasQuery]);
|
|
789
926
|
|
|
927
|
+
useEffect(() => {
|
|
928
|
+
if (aulasState.length === 0 || alunos.length === 0) return;
|
|
929
|
+
let cancelled = false;
|
|
930
|
+
const load = async () => {
|
|
931
|
+
setLoadingAttendanceMatrix(true);
|
|
932
|
+
try {
|
|
933
|
+
const results = await Promise.all(
|
|
934
|
+
aulasState.map(async (aula) => {
|
|
935
|
+
try {
|
|
936
|
+
const res = await request<AttendanceRecord[]>({
|
|
937
|
+
url: `/lms/classes/${id}/sessions/${aula.id}/attendance`,
|
|
938
|
+
method: 'GET',
|
|
939
|
+
});
|
|
940
|
+
return { sessionId: aula.id, data: res.data };
|
|
941
|
+
} catch {
|
|
942
|
+
return { sessionId: aula.id, data: [] as AttendanceRecord[] };
|
|
943
|
+
}
|
|
944
|
+
})
|
|
945
|
+
);
|
|
946
|
+
if (cancelled) return;
|
|
947
|
+
const matrix: Record<number, Record<number, boolean>> = {};
|
|
948
|
+
for (const { sessionId, data } of results) {
|
|
949
|
+
matrix[sessionId] = {};
|
|
950
|
+
for (const record of data) {
|
|
951
|
+
matrix[sessionId][record.student_id] = record.present;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
setAttendanceMatrix(matrix);
|
|
955
|
+
} finally {
|
|
956
|
+
if (!cancelled) setLoadingAttendanceMatrix(false);
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
void load();
|
|
960
|
+
return () => {
|
|
961
|
+
cancelled = true;
|
|
962
|
+
};
|
|
963
|
+
}, [aulasState, alunos, id, request]);
|
|
964
|
+
|
|
790
965
|
// ── Form ──────────────────────────────────────────────────────────────────
|
|
791
966
|
const aulaSchema = getAulaSchema(t);
|
|
792
967
|
const studentSchema = getStudentSchema(t);
|
|
@@ -822,6 +997,11 @@ export default function TurmaDetalhePage() {
|
|
|
822
997
|
},
|
|
823
998
|
});
|
|
824
999
|
|
|
1000
|
+
const courseForm = useForm<CourseSheetFormValues>({
|
|
1001
|
+
resolver: zodResolver(getCourseSheetSchema(tCourses)),
|
|
1002
|
+
defaultValues: DEFAULT_COURSE_FORM_VALUES,
|
|
1003
|
+
});
|
|
1004
|
+
|
|
825
1005
|
// ── Derived ───────────────────────────────────────────────────────────────
|
|
826
1006
|
const filteredAlunos = useMemo(() => {
|
|
827
1007
|
if (!alunoSearch.trim()) return alunos;
|
|
@@ -849,6 +1029,15 @@ export default function TurmaDetalhePage() {
|
|
|
849
1029
|
[studentPickerResults]
|
|
850
1030
|
);
|
|
851
1031
|
|
|
1032
|
+
const courseCategoryOptions = useMemo<CourseCategoryOption[]>(
|
|
1033
|
+
() =>
|
|
1034
|
+
(courseCategoryData?.data ?? [])
|
|
1035
|
+
.filter((c) => !!c.slug)
|
|
1036
|
+
.map((c) => ({ value: c.slug, label: c.name || c.slug }))
|
|
1037
|
+
.sort((a, b) => a.label.localeCompare(b.label)),
|
|
1038
|
+
[courseCategoryData]
|
|
1039
|
+
);
|
|
1040
|
+
|
|
852
1041
|
useEffect(() => {
|
|
853
1042
|
if (pessoasElegiveisParaMatricula.length === 0) return;
|
|
854
1043
|
|
|
@@ -892,6 +1081,54 @@ export default function TurmaDetalhePage() {
|
|
|
892
1081
|
window.dispatchEvent(new CustomEvent('lms:dashboard-updated'));
|
|
893
1082
|
};
|
|
894
1083
|
|
|
1084
|
+
function openCourseEditSheet() {
|
|
1085
|
+
if (!courseDetail) return;
|
|
1086
|
+
courseForm.reset({
|
|
1087
|
+
nomeInterno: courseDetail.slug ?? courseDetail.code ?? '',
|
|
1088
|
+
tituloComercial: courseDetail.title ?? '',
|
|
1089
|
+
descricao: courseDetail.description ?? '',
|
|
1090
|
+
nivel: toCourseFormLevel(courseDetail.level),
|
|
1091
|
+
status: toCourseFormStatus(courseDetail.status),
|
|
1092
|
+
categorias: courseDetail.categories ?? [],
|
|
1093
|
+
primaryColor: courseDetail.primaryColor ?? '#1D4ED8',
|
|
1094
|
+
secondaryColor: courseDetail.secondaryColor ?? '#111827',
|
|
1095
|
+
logoFileId: courseDetail.logoFileId ?? null,
|
|
1096
|
+
});
|
|
1097
|
+
setCourseSheetOpen(true);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
async function onSubmitCourse(data: CourseSheetFormValues) {
|
|
1101
|
+
const cId = turma?.courseId ?? turma?.cursoId;
|
|
1102
|
+
if (!cId) return;
|
|
1103
|
+
setSavingCourse(true);
|
|
1104
|
+
try {
|
|
1105
|
+
await request({
|
|
1106
|
+
url: `/lms/courses/${cId}`,
|
|
1107
|
+
method: 'PATCH',
|
|
1108
|
+
data: {
|
|
1109
|
+
slug: data.nomeInterno.trim(),
|
|
1110
|
+
title: data.tituloComercial,
|
|
1111
|
+
description: data.descricao,
|
|
1112
|
+
level: toApiCourseLevel(data.nivel),
|
|
1113
|
+
status: toApiCourseStatus(data.status),
|
|
1114
|
+
categorySlugs: data.categorias,
|
|
1115
|
+
primaryColor: data.primaryColor,
|
|
1116
|
+
primaryContrastColor: getContrastColorCourse(data.primaryColor),
|
|
1117
|
+
secondaryColor: data.secondaryColor,
|
|
1118
|
+
secondaryContrastColor: getContrastColorCourse(data.secondaryColor),
|
|
1119
|
+
logoFileId: data.logoFileId ?? null,
|
|
1120
|
+
},
|
|
1121
|
+
});
|
|
1122
|
+
await refetchCourseDetail();
|
|
1123
|
+
setCourseSheetOpen(false);
|
|
1124
|
+
toast.success('Curso atualizado com sucesso.');
|
|
1125
|
+
} catch {
|
|
1126
|
+
toast.error('Não foi possível salvar o curso.');
|
|
1127
|
+
} finally {
|
|
1128
|
+
setSavingCourse(false);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
895
1132
|
// ── Handlers ─────────────────────────────────────────────────────────────
|
|
896
1133
|
const toggleSelectAluno = (studentId: number, e?: React.MouseEvent) => {
|
|
897
1134
|
if (e?.shiftKey && selectedAlunos.length > 0) {
|
|
@@ -1263,6 +1500,49 @@ export default function TurmaDetalhePage() {
|
|
|
1263
1500
|
openAulaSheet(aula, { initialTab: 'chamada' });
|
|
1264
1501
|
};
|
|
1265
1502
|
|
|
1503
|
+
const toggleAttendanceCell = async (sessionId: number, studentId: number) => {
|
|
1504
|
+
const current = attendanceMatrix[sessionId]?.[studentId] ?? false;
|
|
1505
|
+
const next = !current;
|
|
1506
|
+
const cellKey = `${sessionId}-${studentId}`;
|
|
1507
|
+
|
|
1508
|
+
// Build full session attendance payload before the optimistic update
|
|
1509
|
+
const sessionAttendance = alunos.map((a) => ({
|
|
1510
|
+
studentId: a.id,
|
|
1511
|
+
present:
|
|
1512
|
+
a.id === studentId
|
|
1513
|
+
? next
|
|
1514
|
+
: (attendanceMatrix[sessionId]?.[a.id] ?? false),
|
|
1515
|
+
}));
|
|
1516
|
+
|
|
1517
|
+
// Optimistic update
|
|
1518
|
+
setAttendanceMatrix((prev) => ({
|
|
1519
|
+
...prev,
|
|
1520
|
+
[sessionId]: { ...(prev[sessionId] ?? {}), [studentId]: next },
|
|
1521
|
+
}));
|
|
1522
|
+
setSavingAttendanceCells((prev) => new Set(prev).add(cellKey));
|
|
1523
|
+
|
|
1524
|
+
try {
|
|
1525
|
+
await request({
|
|
1526
|
+
url: `/lms/classes/${id}/sessions/${sessionId}/attendance`,
|
|
1527
|
+
method: 'POST',
|
|
1528
|
+
data: { attendance: sessionAttendance },
|
|
1529
|
+
});
|
|
1530
|
+
} catch {
|
|
1531
|
+
// Rollback on error
|
|
1532
|
+
setAttendanceMatrix((prev) => ({
|
|
1533
|
+
...prev,
|
|
1534
|
+
[sessionId]: { ...(prev[sessionId] ?? {}), [studentId]: current },
|
|
1535
|
+
}));
|
|
1536
|
+
toast.error(t('toasts.error'));
|
|
1537
|
+
} finally {
|
|
1538
|
+
setSavingAttendanceCells((prev) => {
|
|
1539
|
+
const updated = new Set(prev);
|
|
1540
|
+
updated.delete(cellKey);
|
|
1541
|
+
return updated;
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1266
1546
|
const togglePresenca = (alunoId: number) => {
|
|
1267
1547
|
setPresencaList((prev) =>
|
|
1268
1548
|
prev.map((p) =>
|
|
@@ -2346,34 +2626,40 @@ export default function TurmaDetalhePage() {
|
|
|
2346
2626
|
|
|
2347
2627
|
{/* ── Tab Presenca ────────────────────────────────────────────── */}
|
|
2348
2628
|
<TabsContent value="presenca" className="mt-0">
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
<div className="grid gap-3 sm:grid-cols-2">
|
|
2354
|
-
{Array.from({ length: 4 }).map((_, i) => (
|
|
2355
|
-
<Card
|
|
2356
|
-
key={i}
|
|
2357
|
-
className="overflow-hidden border-border/70"
|
|
2358
|
-
>
|
|
2359
|
-
<CardContent className="p-4">
|
|
2360
|
-
<Skeleton className="h-20" />
|
|
2361
|
-
</CardContent>
|
|
2362
|
-
</Card>
|
|
2629
|
+
{loading || loadingAttendanceMatrix ? (
|
|
2630
|
+
<div className="space-y-2">
|
|
2631
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
2632
|
+
<Skeleton key={i} className="h-10 w-full" />
|
|
2363
2633
|
))}
|
|
2364
2634
|
</div>
|
|
2635
|
+
) : alunos.length === 0 ? (
|
|
2636
|
+
<div className="rounded-xl border border-dashed border-border/60 bg-muted/20 px-6 py-10 text-center">
|
|
2637
|
+
<div className="flex flex-col items-center gap-3">
|
|
2638
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted/60 text-muted-foreground">
|
|
2639
|
+
<Users className="size-5" />
|
|
2640
|
+
</div>
|
|
2641
|
+
<div>
|
|
2642
|
+
<p className="font-semibold text-sm">
|
|
2643
|
+
{t('students.empty.notEnrolled')}
|
|
2644
|
+
</p>
|
|
2645
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
2646
|
+
{t('students.empty.notEnrolledDescription')}
|
|
2647
|
+
</p>
|
|
2648
|
+
</div>
|
|
2649
|
+
</div>
|
|
2650
|
+
</div>
|
|
2365
2651
|
) : aulasState.length === 0 ? (
|
|
2366
2652
|
<div className="rounded-xl border border-dashed border-border/60 bg-muted/20 px-6 py-10 text-center">
|
|
2367
2653
|
<div className="flex flex-col items-center gap-3">
|
|
2368
2654
|
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted/60 text-muted-foreground">
|
|
2369
|
-
<
|
|
2655
|
+
<CalendarIcon className="size-5" />
|
|
2370
2656
|
</div>
|
|
2371
2657
|
<div>
|
|
2372
2658
|
<p className="font-semibold text-sm">
|
|
2373
|
-
{t('
|
|
2659
|
+
{t('calendar.empty.title')}
|
|
2374
2660
|
</p>
|
|
2375
2661
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
2376
|
-
{t('
|
|
2662
|
+
{t('calendar.empty.description')}
|
|
2377
2663
|
</p>
|
|
2378
2664
|
</div>
|
|
2379
2665
|
<Button
|
|
@@ -2391,67 +2677,72 @@ export default function TurmaDetalhePage() {
|
|
|
2391
2677
|
</div>
|
|
2392
2678
|
</div>
|
|
2393
2679
|
) : (
|
|
2394
|
-
<div className="
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
<div className="min-w-0">
|
|
2416
|
-
<h4 className="truncate text-sm font-semibold">
|
|
2417
|
-
{aula.titulo}
|
|
2418
|
-
</h4>
|
|
2419
|
-
<p className="text-xs capitalize text-muted-foreground">
|
|
2420
|
-
{format(
|
|
2421
|
-
parseSessionDate(aula.data),
|
|
2422
|
-
'EEE, dd/MM',
|
|
2423
|
-
{ locale: dateLocale }
|
|
2424
|
-
)}
|
|
2425
|
-
{' · '}
|
|
2426
|
-
{aula.horaInicio} – {aula.horaFim}
|
|
2427
|
-
</p>
|
|
2428
|
-
</div>
|
|
2429
|
-
<Badge
|
|
2430
|
-
variant="outline"
|
|
2431
|
-
className={cn(
|
|
2432
|
-
'shrink-0 text-[10px]',
|
|
2433
|
-
isPast
|
|
2434
|
-
? 'border-emerald-200 bg-emerald-50 text-emerald-700'
|
|
2435
|
-
: ''
|
|
2436
|
-
)}
|
|
2437
|
-
>
|
|
2438
|
-
{isPast ? (
|
|
2439
|
-
<CheckCircle2 className="mr-1 size-3" />
|
|
2440
|
-
) : aula.tipo === 'online' ? (
|
|
2441
|
-
<Video className="mr-1 size-3" />
|
|
2442
|
-
) : (
|
|
2443
|
-
<MapPin className="mr-1 size-3" />
|
|
2444
|
-
)}
|
|
2445
|
-
{isPast
|
|
2446
|
-
? t('attendance.register')
|
|
2447
|
-
: tClasses(`type.${aula.tipo}`)}
|
|
2448
|
-
</Badge>
|
|
2680
|
+
<div className="overflow-x-auto rounded-xl border border-border/60">
|
|
2681
|
+
<Table>
|
|
2682
|
+
<TableHeader>
|
|
2683
|
+
<TableRow className="bg-muted/40 hover:bg-muted/40">
|
|
2684
|
+
<TableHead className="sticky left-0 z-10 min-w-[180px] bg-muted/40 font-semibold">
|
|
2685
|
+
{t('tabs.students')}
|
|
2686
|
+
</TableHead>
|
|
2687
|
+
{aulasState.map((aula) => {
|
|
2688
|
+
const d = parseSessionDate(aula.data);
|
|
2689
|
+
return (
|
|
2690
|
+
<TableHead
|
|
2691
|
+
key={aula.id}
|
|
2692
|
+
className="min-w-[68px] text-center text-xs"
|
|
2693
|
+
>
|
|
2694
|
+
<div className="flex flex-col items-center gap-0.5">
|
|
2695
|
+
<span className="capitalize text-muted-foreground">
|
|
2696
|
+
{format(d, 'EEE', {
|
|
2697
|
+
locale: dateLocale,
|
|
2698
|
+
})}
|
|
2699
|
+
</span>
|
|
2700
|
+
<span>{format(d, 'dd/MM')}</span>
|
|
2449
2701
|
</div>
|
|
2450
|
-
</
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2702
|
+
</TableHead>
|
|
2703
|
+
);
|
|
2704
|
+
})}
|
|
2705
|
+
</TableRow>
|
|
2706
|
+
</TableHeader>
|
|
2707
|
+
<TableBody>
|
|
2708
|
+
{alunos.map((aluno) => (
|
|
2709
|
+
<TableRow
|
|
2710
|
+
key={aluno.id}
|
|
2711
|
+
className="hover:bg-muted/30"
|
|
2712
|
+
>
|
|
2713
|
+
<TableCell className="sticky left-0 z-10 border-r border-border/40 bg-background text-sm font-medium">
|
|
2714
|
+
{aluno.nome}
|
|
2715
|
+
</TableCell>
|
|
2716
|
+
{aulasState.map((aula) => {
|
|
2717
|
+
const cellKey = `${aula.id}-${aluno.id}`;
|
|
2718
|
+
const present =
|
|
2719
|
+
attendanceMatrix[aula.id]?.[aluno.id] ??
|
|
2720
|
+
false;
|
|
2721
|
+
const saving =
|
|
2722
|
+
savingAttendanceCells.has(cellKey);
|
|
2723
|
+
return (
|
|
2724
|
+
<TableCell
|
|
2725
|
+
key={aula.id}
|
|
2726
|
+
className="text-center"
|
|
2727
|
+
>
|
|
2728
|
+
<Checkbox
|
|
2729
|
+
checked={present}
|
|
2730
|
+
disabled={saving}
|
|
2731
|
+
onCheckedChange={() =>
|
|
2732
|
+
void toggleAttendanceCell(
|
|
2733
|
+
aula.id,
|
|
2734
|
+
aluno.id
|
|
2735
|
+
)
|
|
2736
|
+
}
|
|
2737
|
+
aria-label={`${aluno.nome} – ${format(parseSessionDate(aula.data), 'dd/MM/yyyy')}`}
|
|
2738
|
+
/>
|
|
2739
|
+
</TableCell>
|
|
2740
|
+
);
|
|
2741
|
+
})}
|
|
2742
|
+
</TableRow>
|
|
2743
|
+
))}
|
|
2744
|
+
</TableBody>
|
|
2745
|
+
</Table>
|
|
2455
2746
|
</div>
|
|
2456
2747
|
)}
|
|
2457
2748
|
</TabsContent>
|
|
@@ -2589,6 +2880,62 @@ export default function TurmaDetalhePage() {
|
|
|
2589
2880
|
title={t('sidebar.classInfo') ?? 'Informações da Turma'}
|
|
2590
2881
|
>
|
|
2591
2882
|
<div className="space-y-2.5">
|
|
2883
|
+
{/* Course */}
|
|
2884
|
+
<div className="flex items-center justify-between gap-2">
|
|
2885
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
2886
|
+
<CourseAvatar
|
|
2887
|
+
fileId={courseDetail?.logoFileId}
|
|
2888
|
+
title={classTitle}
|
|
2889
|
+
className="size-8 shrink-0 rounded-lg"
|
|
2890
|
+
iconSize="size-4"
|
|
2891
|
+
/>
|
|
2892
|
+
<div className="min-w-0">
|
|
2893
|
+
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
2894
|
+
Curso
|
|
2895
|
+
</p>
|
|
2896
|
+
<p className="truncate text-xs font-medium text-foreground">
|
|
2897
|
+
{classTitle || '—'}
|
|
2898
|
+
</p>
|
|
2899
|
+
</div>
|
|
2900
|
+
</div>
|
|
2901
|
+
<TooltipProvider delayDuration={300}>
|
|
2902
|
+
<div className="flex shrink-0 items-center gap-0.5">
|
|
2903
|
+
<Tooltip>
|
|
2904
|
+
<TooltipTrigger asChild>
|
|
2905
|
+
<Button
|
|
2906
|
+
variant="ghost"
|
|
2907
|
+
size="icon"
|
|
2908
|
+
className="size-6"
|
|
2909
|
+
onClick={openCourseEditSheet}
|
|
2910
|
+
disabled={!courseDetail}
|
|
2911
|
+
>
|
|
2912
|
+
<Pencil className="size-3" />
|
|
2913
|
+
</Button>
|
|
2914
|
+
</TooltipTrigger>
|
|
2915
|
+
<TooltipContent side="top" className="text-xs">
|
|
2916
|
+
Editar curso
|
|
2917
|
+
</TooltipContent>
|
|
2918
|
+
</Tooltip>
|
|
2919
|
+
<Tooltip>
|
|
2920
|
+
<TooltipTrigger asChild>
|
|
2921
|
+
<Button
|
|
2922
|
+
variant="ghost"
|
|
2923
|
+
size="icon"
|
|
2924
|
+
className="size-6"
|
|
2925
|
+
onClick={handleViewCourse}
|
|
2926
|
+
>
|
|
2927
|
+
<Eye className="size-3" />
|
|
2928
|
+
</Button>
|
|
2929
|
+
</TooltipTrigger>
|
|
2930
|
+
<TooltipContent side="top" className="text-xs">
|
|
2931
|
+
Ver detalhes do curso
|
|
2932
|
+
</TooltipContent>
|
|
2933
|
+
</Tooltip>
|
|
2934
|
+
</div>
|
|
2935
|
+
</TooltipProvider>
|
|
2936
|
+
</div>
|
|
2937
|
+
<div className="my-0.5 border-t border-border/40" />
|
|
2938
|
+
|
|
2592
2939
|
{startDate && endDate && (
|
|
2593
2940
|
<div className="flex items-start gap-2 text-xs">
|
|
2594
2941
|
<CalendarIcon className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />
|
|
@@ -3531,6 +3878,19 @@ export default function TurmaDetalhePage() {
|
|
|
3531
3878
|
defaultQualificationSlugs={['class-sessions']}
|
|
3532
3879
|
/>
|
|
3533
3880
|
|
|
3881
|
+
<CourseFormSheet
|
|
3882
|
+
key={`course-edit-${courseDetail?.id ?? 'none'}`}
|
|
3883
|
+
open={courseSheetOpen}
|
|
3884
|
+
onOpenChange={setCourseSheetOpen}
|
|
3885
|
+
editing={true}
|
|
3886
|
+
saving={savingCourse}
|
|
3887
|
+
form={courseForm}
|
|
3888
|
+
onSubmit={onSubmitCourse}
|
|
3889
|
+
categories={courseCategoryOptions}
|
|
3890
|
+
initialLogoFileId={courseDetail?.logoFileId ?? null}
|
|
3891
|
+
t={tCourses}
|
|
3892
|
+
/>
|
|
3893
|
+
|
|
3534
3894
|
<ClassFormSheet
|
|
3535
3895
|
open={editSheetOpen}
|
|
3536
3896
|
onOpenChange={setEditSheetOpen}
|