@hed-hog/lms 0.0.306 → 0.0.310

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 (120) hide show
  1. package/dist/course/course-structure.controller.d.ts +60 -0
  2. package/dist/course/course-structure.controller.d.ts.map +1 -1
  3. package/dist/course/course-structure.controller.js +79 -0
  4. package/dist/course/course-structure.controller.js.map +1 -1
  5. package/dist/course/course-structure.service.d.ts +61 -1
  6. package/dist/course/course-structure.service.d.ts.map +1 -1
  7. package/dist/course/course-structure.service.js +326 -1
  8. package/dist/course/course-structure.service.js.map +1 -1
  9. package/dist/course/course.controller.d.ts +52 -4
  10. package/dist/course/course.controller.d.ts.map +1 -1
  11. package/dist/course/course.service.d.ts +52 -5
  12. package/dist/course/course.service.d.ts.map +1 -1
  13. package/dist/course/course.service.js +78 -57
  14. package/dist/course/course.service.js.map +1 -1
  15. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  16. package/dist/course/dto/create-course-structure-lesson.dto.js +5 -1
  17. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  18. package/dist/course/dto/create-course.dto.d.ts +1 -1
  19. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  20. package/dist/course/dto/create-course.dto.js +4 -1
  21. package/dist/course/dto/create-course.dto.js.map +1 -1
  22. package/dist/course/dto/move-lesson.dto.d.ts +10 -0
  23. package/dist/course/dto/move-lesson.dto.d.ts.map +1 -0
  24. package/dist/course/dto/move-lesson.dto.js +28 -0
  25. package/dist/course/dto/move-lesson.dto.js.map +1 -0
  26. package/dist/course/dto/paste-lessons.dto.d.ts +4 -0
  27. package/dist/course/dto/paste-lessons.dto.d.ts.map +1 -0
  28. package/dist/course/dto/paste-lessons.dto.js +24 -0
  29. package/dist/course/dto/paste-lessons.dto.js.map +1 -0
  30. package/dist/course/dto/reorder-lessons.dto.d.ts +5 -0
  31. package/dist/course/dto/reorder-lessons.dto.d.ts.map +1 -0
  32. package/dist/course/dto/reorder-lessons.dto.js +24 -0
  33. package/dist/course/dto/reorder-lessons.dto.js.map +1 -0
  34. package/dist/course/dto/reorder-sessions.dto.d.ts +5 -0
  35. package/dist/course/dto/reorder-sessions.dto.d.ts.map +1 -0
  36. package/dist/course/dto/reorder-sessions.dto.js +24 -0
  37. package/dist/course/dto/reorder-sessions.dto.js.map +1 -0
  38. package/dist/training/training.controller.js +1 -1
  39. package/dist/training/training.controller.js.map +1 -1
  40. package/hedhog/data/image_type.yaml +20 -0
  41. package/hedhog/data/menu.yaml +2 -2
  42. package/hedhog/data/route.yaml +60 -6
  43. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +146 -165
  44. package/hedhog/frontend/app/_components/course-avatar.tsx.ejs +70 -0
  45. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +372 -22
  46. package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +10 -1
  47. package/hedhog/frontend/app/certificates/models/TopBar.tsx.ejs +44 -3
  48. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +32 -0
  49. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +437 -77
  50. package/hedhog/frontend/app/classes/page.tsx.ejs +311 -289
  51. package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +10 -7
  52. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +23 -32
  53. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +3 -9
  54. package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +26 -16
  55. package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +19 -5
  56. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +10 -14
  57. package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +131 -107
  58. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +10 -7
  59. package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +38 -19
  60. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +1 -1
  61. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
  62. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +336 -1057
  63. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +45 -0
  64. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -0
  65. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -0
  66. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-skeleton.tsx.ejs +64 -0
  67. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
  68. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
  69. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
  70. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -0
  71. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
  72. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -0
  73. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +52 -0
  74. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -0
  75. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -0
  76. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1827 -0
  77. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -0
  78. package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +41 -0
  79. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +184 -0
  80. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -0
  81. package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +96 -0
  82. package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +74 -0
  83. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -0
  84. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -0
  85. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +948 -0
  86. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -0
  87. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +150 -0
  88. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +182 -0
  89. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +52 -0
  90. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -0
  91. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -0
  92. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -0
  93. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +122 -0
  94. package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -0
  95. package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +97 -0
  96. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +347 -0
  97. package/hedhog/frontend/app/courses/[id]/structure/_data/course-structure-contract.ts.ejs +195 -0
  98. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +420 -0
  99. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +254 -0
  100. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +987 -0
  101. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +86 -0
  102. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure.ts.ejs +160 -0
  103. package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -3212
  104. package/hedhog/frontend/app/courses/page.tsx.ejs +45 -26
  105. package/hedhog/frontend/app/{training → paths}/page.tsx.ejs +29 -7
  106. package/hedhog/frontend/messages/en.json +91 -11
  107. package/hedhog/frontend/messages/pt.json +91 -11
  108. package/hedhog/table/course.yaml +1 -1
  109. package/hedhog/table/image_type.yaml +14 -0
  110. package/package.json +7 -7
  111. package/src/course/course-structure.controller.ts +63 -0
  112. package/src/course/course-structure.service.ts +390 -3
  113. package/src/course/course.service.ts +59 -27
  114. package/src/course/dto/create-course-structure-lesson.dto.ts +3 -2
  115. package/src/course/dto/create-course.dto.ts +4 -1
  116. package/src/course/dto/move-lesson.dto.ts +17 -0
  117. package/src/course/dto/paste-lessons.dto.ts +9 -0
  118. package/src/course/dto/reorder-lessons.dto.ts +10 -0
  119. package/src/course/dto/reorder-sessions.dto.ts +10 -0
  120. package/src/training/training.controller.ts +1 -1
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { CourseAvatar } from '@/app/(app)/(libraries)/lms/_components/course-avatar';
3
4
  import {
4
5
  EmptyState,
5
6
  Page,
@@ -8,18 +9,11 @@ import {
8
9
  SearchBar,
9
10
  ViewModeToggle,
10
11
  } from '@/components/entity-list';
12
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
11
13
  import { Badge } from '@/components/ui/badge';
12
14
  import { Button } from '@/components/ui/button';
13
15
  import { Calendar } from '@/components/ui/calendar';
14
16
  import { Card, CardContent } from '@/components/ui/card';
15
- import {
16
- Command,
17
- CommandEmpty,
18
- CommandGroup,
19
- CommandInput,
20
- CommandItem,
21
- CommandList,
22
- } from '@/components/ui/command';
23
17
  import {
24
18
  Dialog,
25
19
  DialogContent,
@@ -83,7 +77,6 @@ import {
83
77
  AlertTriangle,
84
78
  BarChart3,
85
79
  CalendarIcon,
86
- ChevronsUpDown,
87
80
  Clock,
88
81
  Eye,
89
82
  Laptop,
@@ -135,6 +128,7 @@ interface Turma {
135
128
  professor: string;
136
129
  sessionTitle?: string | null;
137
130
  sessionRecurrenceSummary?: SessionRecurrenceSummary | null;
131
+ logoFileId?: number | null;
138
132
  }
139
133
 
140
134
  type SessionRecurrenceFrequency = 'daily' | 'weekly' | 'monthly' | 'yearly';
@@ -196,7 +190,12 @@ type ApiClassStats = {
196
190
  };
197
191
 
198
192
  type ApiCourseList = {
199
- data: Array<{ id: number; title: string }>;
193
+ data: Array<{
194
+ id: number;
195
+ title: string;
196
+ code?: string;
197
+ logoFileId?: number | null;
198
+ }>;
200
199
  total?: number;
201
200
  page?: number;
202
201
  pageSize?: number;
@@ -226,6 +225,7 @@ type InstructorOption = {
226
225
  id: number;
227
226
  name: string;
228
227
  personId?: number;
228
+ avatarId?: number | null;
229
229
  qualificationSlugs?: string[];
230
230
  };
231
231
 
@@ -239,6 +239,8 @@ type InstructorApiRow = {
239
239
  label?: string;
240
240
  personId?: number | string;
241
241
  person_id?: number | string;
242
+ avatarId?: number | string | null;
243
+ avatar_id?: number | string | null;
242
244
  qualificationSlugs?: string[];
243
245
  };
244
246
 
@@ -260,10 +262,17 @@ function normalizeInstructorOption(
260
262
  return null;
261
263
  }
262
264
 
265
+ const rawAvatarId = item?.avatarId ?? item?.avatar_id;
266
+ const avatarId =
267
+ rawAvatarId !== undefined && rawAvatarId !== null
268
+ ? Number(rawAvatarId) || null
269
+ : null;
270
+
263
271
  return {
264
272
  id,
265
273
  name,
266
274
  personId: Number(item?.personId ?? item?.person_id ?? 0) || undefined,
275
+ avatarId,
267
276
  qualificationSlugs: Array.isArray(item?.qualificationSlugs)
268
277
  ? item.qualificationSlugs
269
278
  : undefined,
@@ -483,22 +492,6 @@ function getTurmaSchema(t: (key: string) => string) {
483
492
  });
484
493
  }
485
494
 
486
- if (values.sessionRecurrenceMode !== 'none') {
487
- if (!values.sessionRecurrenceUntil) {
488
- ctx.addIssue({
489
- code: z.ZodIssueCode.custom,
490
- path: ['sessionRecurrenceUntil'],
491
- message: t('form.validation.sessionRecurrenceUntilRequired'),
492
- });
493
- } else if (values.sessionRecurrenceUntil < values.dataInicio) {
494
- ctx.addIssue({
495
- code: z.ZodIssueCode.custom,
496
- path: ['sessionRecurrenceUntil'],
497
- message: t('form.validation.sessionRecurrenceUntilAfterStart'),
498
- });
499
- }
500
- }
501
-
502
495
  const requiresDays =
503
496
  values.sessionRecurrenceMode === 'weekly' ||
504
497
  values.sessionRecurrenceMode === 'weekdays' ||
@@ -690,8 +683,6 @@ export default function TurmasPage() {
690
683
  useState(false);
691
684
  const [previousRecurrenceMode, setPreviousRecurrenceMode] =
692
685
  useState<SessionRecurrenceMode>('none');
693
- const [professorOpen, setProfessorOpen] = useState(false);
694
- const [professorSearch, setProfessorSearch] = useState('');
695
686
  const [createProfessorDialogOpen, setCreateProfessorDialogOpen] =
696
687
  useState(false);
697
688
 
@@ -798,63 +789,6 @@ export default function TurmasPage() {
798
789
  },
799
790
  });
800
791
 
801
- const {
802
- data: professorOptions = [],
803
- isFetching: loadingProfessores,
804
- refetch: refetchProfessorOptions,
805
- } = useQuery<InstructorOption[]>({
806
- queryKey: ['lms-classes-professors', professorSearch],
807
- queryFn: async () => {
808
- const response = await request<
809
- | InstructorApiRow[]
810
- | {
811
- data?: InstructorApiRow[];
812
- items?: InstructorApiRow[];
813
- rows?: InstructorApiRow[];
814
- }
815
- >({
816
- url: '/lms/instructors',
817
- method: 'GET',
818
- params: {
819
- page: 1,
820
- pageSize: 100,
821
- qualificationSlugs: ['class-sessions'],
822
- ...(professorSearch.trim() ? { search: professorSearch.trim() } : {}),
823
- },
824
- });
825
-
826
- const payload = response.data;
827
- const rows = Array.isArray(payload)
828
- ? payload
829
- : Array.isArray(payload?.data)
830
- ? payload.data
831
- : Array.isArray(payload?.items)
832
- ? payload.items
833
- : Array.isArray(payload?.rows)
834
- ? payload.rows
835
- : [];
836
-
837
- const unique = new Map<number, InstructorOption>();
838
-
839
- for (const row of rows) {
840
- const normalized = normalizeInstructorOption(row);
841
- if (!normalized) continue;
842
- unique.set(normalized.id, normalized);
843
- }
844
-
845
- return Array.from(unique.values()).sort((a, b) =>
846
- a.name.localeCompare(b.name)
847
- );
848
- },
849
- initialData: [],
850
- });
851
-
852
- useEffect(() => {
853
- if (professorOpen) {
854
- void refetchProfessorOptions();
855
- }
856
- }, [professorOpen, refetchProfessorOptions]);
857
-
858
792
  useEffect(() => {
859
793
  if (courseSheetOpen) {
860
794
  void refetchCategoryOptions();
@@ -936,6 +870,7 @@ export default function TurmasPage() {
936
870
 
937
871
  useEffect(() => {
938
872
  if (!sheetOpen || editingTurma || !createCodeSeed) return;
873
+ if (form.getFieldState('codigo').isDirty) return;
939
874
 
940
875
  const nextCode = buildClassCode(
941
876
  watchedFormValues.curso || undefined,
@@ -1019,9 +954,9 @@ export default function TurmasPage() {
1019
954
  );
1020
955
 
1021
956
  const recurrenceSummaryText = useMemo(() => {
1022
- const until = watchedFormValues.sessionRecurrenceUntil
957
+ const until = watchedFormValues.dataFim
1023
958
  ? formatDateLocalized(
1024
- watchedFormValues.sessionRecurrenceUntil,
959
+ watchedFormValues.dataFim,
1025
960
  getSettingValue,
1026
961
  currentLocaleCode
1027
962
  )
@@ -1036,7 +971,7 @@ export default function TurmasPage() {
1036
971
  }, [
1037
972
  t,
1038
973
  watchedFormValues.sessionRecurrenceMode,
1039
- watchedFormValues.sessionRecurrenceUntil,
974
+ watchedFormValues.dataFim,
1040
975
  getSettingValue,
1041
976
  currentLocaleCode,
1042
977
  ]);
@@ -1097,23 +1032,19 @@ export default function TurmasPage() {
1097
1032
  watchedFormValues.sessionRecurrenceMode === 'custom' &&
1098
1033
  customRecurrenceFrequency === 'weekly';
1099
1034
 
1035
+ const weeklyNeedsDays = watchedFormValues.sessionRecurrenceMode === 'weekly';
1036
+
1100
1037
  useEffect(() => {
1101
1038
  if (!watchedFormValues.dataFim) {
1102
1039
  return;
1103
1040
  }
1104
1041
 
1105
- if (!watchedFormValues.sessionRecurrenceUntil) {
1106
- form.setValue('sessionRecurrenceUntil', watchedFormValues.dataFim, {
1107
- shouldDirty: false,
1108
- shouldTouch: false,
1109
- shouldValidate: false,
1110
- });
1111
- }
1112
- }, [
1113
- form,
1114
- watchedFormValues.dataFim,
1115
- watchedFormValues.sessionRecurrenceUntil,
1116
- ]);
1042
+ form.setValue('sessionRecurrenceUntil', watchedFormValues.dataFim, {
1043
+ shouldDirty: false,
1044
+ shouldTouch: false,
1045
+ shouldValidate: false,
1046
+ });
1047
+ }, [form, watchedFormValues.dataFim]);
1117
1048
 
1118
1049
  useEffect(() => {
1119
1050
  if (!watchedFormValues.dataInicio) {
@@ -1122,7 +1053,7 @@ export default function TurmasPage() {
1122
1053
 
1123
1054
  const defaultDay = getDayCodeFromDate(watchedFormValues.dataInicio);
1124
1055
  const recurrenceMode = watchedFormValues.sessionRecurrenceMode;
1125
- const recurrenceDays = watchedFormValues.sessionRecurrenceDaysOfWeek ?? [];
1056
+ const recurrenceDays = form.getValues('sessionRecurrenceDaysOfWeek') ?? [];
1126
1057
 
1127
1058
  if (recurrenceMode === 'weekly' && recurrenceDays.length === 0) {
1128
1059
  form.setValue('sessionRecurrenceDaysOfWeek', [defaultDay], {
@@ -1133,20 +1064,27 @@ export default function TurmasPage() {
1133
1064
  }
1134
1065
 
1135
1066
  if (recurrenceMode === 'weekdays') {
1136
- form.setValue(
1137
- 'sessionRecurrenceDaysOfWeek',
1138
- ['MO', 'TU', 'WE', 'TH', 'FR'],
1139
- {
1140
- shouldDirty: false,
1141
- shouldTouch: false,
1142
- shouldValidate: false,
1143
- }
1144
- );
1067
+ const weekdaySet = ['MO', 'TU', 'WE', 'TH', 'FR'];
1068
+ const alreadySet =
1069
+ recurrenceDays.length === weekdaySet.length &&
1070
+ weekdaySet.every((d) =>
1071
+ recurrenceDays.includes(d as SessionRecurrenceDay)
1072
+ );
1073
+ if (!alreadySet) {
1074
+ form.setValue(
1075
+ 'sessionRecurrenceDaysOfWeek',
1076
+ ['MO', 'TU', 'WE', 'TH', 'FR'],
1077
+ {
1078
+ shouldDirty: false,
1079
+ shouldTouch: false,
1080
+ shouldValidate: false,
1081
+ }
1082
+ );
1083
+ }
1145
1084
  }
1146
1085
  }, [
1147
1086
  form,
1148
1087
  watchedFormValues.dataInicio,
1149
- watchedFormValues.sessionRecurrenceDaysOfWeek,
1150
1088
  watchedFormValues.sessionRecurrenceMode,
1151
1089
  ]);
1152
1090
 
@@ -1182,28 +1120,40 @@ export default function TurmasPage() {
1182
1120
 
1183
1121
  const professorNameById = useMemo<Map<number, string>>(() => {
1184
1122
  const map = new Map<number, string>();
1185
- for (const p of professorOptions) {
1186
- map.set(p.id, p.name);
1123
+ for (const turma of classesResponse?.data ?? []) {
1124
+ if (turma.instructorId && turma.instructorName) {
1125
+ map.set(turma.instructorId, turma.instructorName);
1126
+ }
1127
+ }
1128
+ return map;
1129
+ }, [classesResponse]);
1130
+
1131
+ const courseLogoMap = useMemo<Map<number, number | null>>(() => {
1132
+ const map = new Map<number, number | null>();
1133
+ for (const course of coursesResponse?.data ?? []) {
1134
+ map.set(course.id, course.logoFileId ?? null);
1187
1135
  }
1188
1136
  return map;
1189
- }, [professorOptions]);
1137
+ }, [coursesResponse]);
1190
1138
 
1191
1139
  const turmas = useMemo<Turma[]>(
1192
1140
  () =>
1193
1141
  (classesResponse?.data ?? []).map((item) => {
1194
1142
  const mapped = mapApiClass(item);
1143
+ const logoFileId = courseLogoMap.get(mapped.cursoId) ?? null;
1144
+ const withLogo = { ...mapped, logoFileId };
1195
1145
  if (
1196
- (mapped.professor === '-' || !mapped.professor) &&
1197
- mapped.instructorId
1146
+ (withLogo.professor === '-' || !withLogo.professor) &&
1147
+ withLogo.instructorId
1198
1148
  ) {
1199
- const fallback = professorNameById.get(mapped.instructorId);
1149
+ const fallback = professorNameById.get(withLogo.instructorId);
1200
1150
  if (fallback) {
1201
- return { ...mapped, professor: fallback };
1151
+ return { ...withLogo, professor: fallback };
1202
1152
  }
1203
1153
  }
1204
- return mapped;
1154
+ return withLogo;
1205
1155
  }),
1206
- [classesResponse, professorNameById]
1156
+ [classesResponse, professorNameById, courseLogoMap]
1207
1157
  );
1208
1158
  const previewTurma = useMemo<Turma | null>(() => {
1209
1159
  if (!sheetOpen || !editingTurma) return null;
@@ -1413,7 +1363,7 @@ export default function TurmasPage() {
1413
1363
  sessionRecurrenceDaysOfWeek:
1414
1364
  recurrenceSummary?.daysOfWeek ??
1415
1365
  (recurrenceMode === 'weekly' ? [defaultDay] : []),
1416
- sessionRecurrenceUntil: recurrenceSummary?.until ?? detailedTurma.dataFim,
1366
+ sessionRecurrenceUntil: detailedTurma.dataFim,
1417
1367
  sessionTitleMode:
1418
1368
  response.data.sessionTitle &&
1419
1369
  response.data.sessionTitle !==
@@ -1510,7 +1460,6 @@ export default function TurmasPage() {
1510
1460
  async function handleCustomRecurrenceConfirm() {
1511
1461
  const valid = await form.trigger([
1512
1462
  'sessionRecurrenceInterval',
1513
- 'sessionRecurrenceUntil',
1514
1463
  'sessionRecurrenceDaysOfWeek',
1515
1464
  'dataInicio',
1516
1465
  ]);
@@ -1621,7 +1570,6 @@ export default function TurmasPage() {
1621
1570
  });
1622
1571
 
1623
1572
  toast.success(t('toasts.turmaUpdated'));
1624
- await refetchProfessorOptions();
1625
1573
  } else {
1626
1574
  const response = await request<ApiClass>({
1627
1575
  url: '/lms/classes',
@@ -1643,7 +1591,6 @@ export default function TurmasPage() {
1643
1591
  });
1644
1592
 
1645
1593
  toast.success(t('toasts.turmaCreated'));
1646
- await refetchProfessorOptions();
1647
1594
  await refetchClasses();
1648
1595
  await refetchStats();
1649
1596
  notifyLmsDashboardUpdated();
@@ -1700,7 +1647,6 @@ export default function TurmasPage() {
1700
1647
  shouldValidate: true,
1701
1648
  });
1702
1649
 
1703
- await refetchProfessorOptions();
1704
1650
  await refetchClasses();
1705
1651
  };
1706
1652
 
@@ -2000,16 +1946,21 @@ export default function TurmasPage() {
2000
1946
  title={t('cards.tooltip')}
2001
1947
  >
2002
1948
  <div
2003
- className="absolute inset-x-0 top-0 h-1"
2004
- style={{
2005
- backgroundColor: turma.primaryColor || '#1D4ED8',
2006
- }}
1949
+ className="absolute inset-x-0 top-0 h-1 bg-primary"
1950
+ style={
1951
+ turma.primaryColor
1952
+ ? { backgroundColor: turma.primaryColor }
1953
+ : undefined
1954
+ }
2007
1955
  />
2008
1956
  <CardContent className="p-5">
2009
1957
  <div className="mb-4 flex items-start gap-3">
2010
- <div className="flex size-12 shrink-0 items-center justify-center rounded-xl border bg-muted">
2011
- <TipoIcon className="size-6 text-foreground" />
2012
- </div>
1958
+ <CourseAvatar
1959
+ fileId={turma.logoFileId}
1960
+ title={turma.curso}
1961
+ className="size-12 rounded-xl"
1962
+ iconSize="size-6"
1963
+ />
2013
1964
  <div className="min-w-0 flex-1">
2014
1965
  <div className="mb-1 flex items-start justify-between gap-2">
2015
1966
  <h3 className="line-clamp-2 font-semibold leading-snug text-foreground">
@@ -2188,19 +2139,27 @@ export default function TurmasPage() {
2188
2139
  title={t('cards.tooltip')}
2189
2140
  >
2190
2141
  <TableCell>
2191
- <div className="min-w-0">
2192
- <p className="truncate font-semibold text-foreground">
2193
- {turma.curso}
2194
- </p>
2195
- <p className="mt-1 text-xs text-muted-foreground">
2196
- <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[10px]">
2197
- {turma.codigo}
2198
- </code>
2199
- <span className="mx-1.5 text-muted-foreground/50">
2200
- |
2201
- </span>
2202
- <span>{turma.professor}</span>
2203
- </p>
2142
+ <div className="flex min-w-0 items-center gap-3">
2143
+ <CourseAvatar
2144
+ fileId={turma.logoFileId}
2145
+ title={turma.curso}
2146
+ className="size-8 shrink-0 rounded-lg"
2147
+ iconSize="size-4"
2148
+ />
2149
+ <div className="min-w-0">
2150
+ <p className="truncate font-semibold text-foreground">
2151
+ {turma.curso}
2152
+ </p>
2153
+ <p className="mt-1 text-xs text-muted-foreground">
2154
+ <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[10px]">
2155
+ {turma.codigo}
2156
+ </code>
2157
+ <span className="mx-1.5 text-muted-foreground/50">
2158
+ |
2159
+ </span>
2160
+ <span>{turma.professor}</span>
2161
+ </p>
2162
+ </div>
2204
2163
  </div>
2205
2164
  </TableCell>
2206
2165
  <TableCell>
@@ -2323,11 +2282,17 @@ export default function TurmasPage() {
2323
2282
  <Input
2324
2283
  id="codigo"
2325
2284
  value={watchedFormValues.codigo ?? ''}
2326
- readOnly
2327
2285
  className="uppercase"
2286
+ onChange={(event) =>
2287
+ form.setValue('codigo', event.target.value.toUpperCase(), {
2288
+ shouldDirty: true,
2289
+ shouldTouch: true,
2290
+ shouldValidate: true,
2291
+ })
2292
+ }
2328
2293
  />
2329
2294
  <FieldDescription>
2330
- Codigo gerado automaticamente pelo sistema.
2295
+ Codigo gerado automaticamente, mas pode ser editado.
2331
2296
  </FieldDescription>
2332
2297
  <FieldError>{form.formState.errors.codigo?.message}</FieldError>
2333
2298
  </Field>
@@ -2339,7 +2304,15 @@ export default function TurmasPage() {
2339
2304
  </FieldLabel>
2340
2305
  <div className="flex items-end gap-2">
2341
2306
  <div className="min-w-0 flex-1">
2342
- <EntityPicker<{ id: number; title: string }, TurmaForm>
2307
+ <EntityPicker<
2308
+ {
2309
+ id: number;
2310
+ title: string;
2311
+ code?: string;
2312
+ logoFileId?: number | null;
2313
+ },
2314
+ TurmaForm
2315
+ >
2343
2316
  form={form}
2344
2317
  name="courseId"
2345
2318
  valueType="number"
@@ -2351,6 +2324,33 @@ export default function TurmasPage() {
2351
2324
  loadingLabel="Carregando cursos..."
2352
2325
  noResultsLabel="Nenhum curso encontrado."
2353
2326
  showCreateButton={false}
2327
+ renderOption={({ option }) => (
2328
+ <div className="flex items-center gap-2 py-0.5">
2329
+ <CourseAvatar
2330
+ fileId={option.logoFileId}
2331
+ title={option.title}
2332
+ className="size-8 shrink-0 rounded-lg"
2333
+ iconSize="size-4"
2334
+ />
2335
+ <div className="min-w-0">
2336
+ <p className="truncate text-sm">{option.title}</p>
2337
+ <p className="truncate text-xs text-muted-foreground">
2338
+ {option.code ?? '—'} · #{option.id}
2339
+ </p>
2340
+ </div>
2341
+ </div>
2342
+ )}
2343
+ renderSelectedValue={({ option }) => (
2344
+ <div className="flex items-center gap-2">
2345
+ <CourseAvatar
2346
+ fileId={option?.logoFileId}
2347
+ title={option?.title ?? ''}
2348
+ className="size-5 shrink-0 rounded"
2349
+ iconSize="size-3"
2350
+ />
2351
+ <span className="truncate">{option?.title}</span>
2352
+ </div>
2353
+ )}
2354
2354
  onChange={(value, option) => {
2355
2355
  const courseId =
2356
2356
  typeof value === 'number' ? value : undefined;
@@ -2621,30 +2621,41 @@ export default function TurmasPage() {
2621
2621
  {form.formState.errors.sessionRecurrenceMode?.message}
2622
2622
  </FieldError>
2623
2623
  </Field>
2624
+ </div>
2624
2625
 
2626
+ {weeklyNeedsDays && (
2625
2627
  <Field>
2626
- <FieldLabel>{t('form.recurrence.until')}</FieldLabel>
2627
- <Input
2628
- type="date"
2629
- value={watchedFormValues.sessionRecurrenceUntil ?? ''}
2630
- min={watchedFormValues.dataInicio || undefined}
2631
- onChange={(event) =>
2632
- form.setValue(
2633
- 'sessionRecurrenceUntil',
2634
- event.target.value,
2635
- {
2636
- shouldDirty: true,
2637
- shouldTouch: true,
2638
- shouldValidate: true,
2639
- }
2640
- )
2641
- }
2642
- />
2628
+ <FieldLabel>
2629
+ {t('form.recurrence.customDialog.repeatOn')}
2630
+ </FieldLabel>
2631
+ <div className="flex flex-wrap gap-2">
2632
+ {recurrenceDayOptions.map((day) => {
2633
+ const active = (
2634
+ watchedFormValues.sessionRecurrenceDaysOfWeek ?? []
2635
+ ).includes(day.value);
2636
+
2637
+ return (
2638
+ <Button
2639
+ key={day.value}
2640
+ type="button"
2641
+ variant={active ? 'default' : 'outline'}
2642
+ size="icon"
2643
+ className="rounded-full"
2644
+ onClick={() => toggleCustomRecurrenceDay(day.value)}
2645
+ >
2646
+ {day.label}
2647
+ </Button>
2648
+ );
2649
+ })}
2650
+ </div>
2643
2651
  <FieldError>
2644
- {form.formState.errors.sessionRecurrenceUntil?.message}
2652
+ {
2653
+ form.formState.errors.sessionRecurrenceDaysOfWeek
2654
+ ?.message
2655
+ }
2645
2656
  </FieldError>
2646
2657
  </Field>
2647
- </div>
2658
+ )}
2648
2659
 
2649
2660
  <div className="grid gap-4 md:grid-cols-[minmax(0,0.55fr)_minmax(0,1fr)]">
2650
2661
  <Field>
@@ -2812,107 +2823,139 @@ export default function TurmasPage() {
2812
2823
  {t('form.fields.professor.label')}{' '}
2813
2824
  <span className="text-destructive">*</span>
2814
2825
  </FieldLabel>
2815
- <Controller
2816
- name="professor"
2817
- control={form.control}
2818
- render={({ field }) => (
2819
- <div className="flex items-end gap-2">
2820
- <div className="flex-1">
2821
- <Popover
2822
- open={professorOpen}
2823
- onOpenChange={setProfessorOpen}
2824
- >
2825
- <PopoverTrigger asChild>
2826
- <Button
2827
- type="button"
2828
- variant="outline"
2829
- role="combobox"
2830
- className="w-full justify-between"
2831
- >
2832
- <span className="truncate text-left">
2833
- {field.value ||
2834
- t('form.fields.professor.placeholder')}
2835
- </span>
2836
- {loadingProfessores ? (
2837
- <Loader2 className="ml-2 h-4 w-4 shrink-0 animate-spin opacity-60" />
2838
- ) : (
2839
- <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
2840
- )}
2841
- </Button>
2842
- </PopoverTrigger>
2843
- <PopoverContent className="p-0" align="start">
2844
- <Command shouldFilter={false}>
2845
- <CommandInput
2846
- placeholder={t(
2847
- 'form.fields.professor.placeholder'
2848
- )}
2849
- value={professorSearch}
2850
- onValueChange={setProfessorSearch}
2851
- />
2852
- <CommandList>
2853
- <CommandEmpty>
2854
- <div className="flex flex-col items-center gap-3 py-4 px-2">
2855
- <p className="text-sm text-muted-foreground">
2856
- Nenhum professor encontrado.
2857
- </p>
2858
- <Button
2859
- type="button"
2860
- variant="outline"
2861
- size="sm"
2862
- className="w-full"
2863
- onClick={() => {
2864
- setProfessorOpen(false);
2865
- setCreateProfessorDialogOpen(true);
2866
- }}
2867
- >
2868
- <Plus className="mr-2 h-4 w-4" />
2869
- Cadastrar novo professor
2870
- </Button>
2871
- </div>
2872
- </CommandEmpty>
2873
- <CommandGroup>
2874
- {professorOptions.map((professor) => (
2875
- <CommandItem
2876
- key={professor.id}
2877
- value={`${professor.name}-${professor.id}`}
2878
- onSelect={() => {
2879
- form.setValue(
2880
- 'instructorId',
2881
- professor.id,
2882
- {
2883
- shouldDirty: true,
2884
- shouldTouch: true,
2885
- shouldValidate: true,
2886
- }
2887
- );
2888
- field.onChange(professor.name);
2889
- setProfessorOpen(false);
2890
- setProfessorSearch('');
2891
- }}
2892
- >
2893
- {professor.name}
2894
- </CommandItem>
2895
- ))}
2896
- </CommandGroup>
2897
- </CommandList>
2898
- </Command>
2899
- </PopoverContent>
2900
- </Popover>
2901
- </div>
2902
-
2903
- <Button
2904
- type="button"
2905
- variant="outline"
2906
- size="icon"
2907
- className="shrink-0"
2908
- onClick={() => setCreateProfessorDialogOpen(true)}
2909
- aria-label="Cadastrar novo professor"
2910
- >
2911
- <Plus className="h-4 w-4" />
2912
- </Button>
2913
- </div>
2914
- )}
2915
- />
2826
+ <div className="flex items-end gap-2">
2827
+ <div className="flex-1">
2828
+ <EntityPicker<InstructorOption, TurmaForm>
2829
+ form={form}
2830
+ name="instructorId"
2831
+ valueType="number"
2832
+ placeholder={t('form.fields.professor.placeholder')}
2833
+ initialSelectedLabel={watchedFormValues.professor ?? ''}
2834
+ searchPlaceholder={t('form.fields.professor.placeholder')}
2835
+ emptyStateDescription="Nenhum professor encontrado."
2836
+ noResultsLabel="Nenhum professor encontrado."
2837
+ showCreateButton={false}
2838
+ clearable={false}
2839
+ getOptionValue={(opt) => opt.id}
2840
+ getOptionLabel={(opt) => opt.name}
2841
+ onChange={(value, option) => {
2842
+ form.setValue(
2843
+ 'instructorId',
2844
+ value as number | undefined,
2845
+ {
2846
+ shouldDirty: true,
2847
+ shouldTouch: true,
2848
+ shouldValidate: true,
2849
+ }
2850
+ );
2851
+ form.setValue('professor', option?.name ?? '', {
2852
+ shouldDirty: true,
2853
+ shouldTouch: true,
2854
+ shouldValidate: true,
2855
+ });
2856
+ }}
2857
+ loadOptions={async ({ page, pageSize, search }) => {
2858
+ const response = await request<
2859
+ | InstructorApiRow[]
2860
+ | {
2861
+ data?: InstructorApiRow[];
2862
+ items?: InstructorApiRow[];
2863
+ rows?: InstructorApiRow[];
2864
+ total?: number;
2865
+ lastPage?: number;
2866
+ }
2867
+ >({
2868
+ url: '/lms/instructors',
2869
+ method: 'GET',
2870
+ params: {
2871
+ page,
2872
+ pageSize,
2873
+ qualificationSlugs: ['class-sessions'],
2874
+ ...(search.trim() ? { search: search.trim() } : {}),
2875
+ },
2876
+ });
2877
+
2878
+ const payload = response.data;
2879
+ const rows = Array.isArray(payload)
2880
+ ? payload
2881
+ : Array.isArray(payload?.data)
2882
+ ? payload.data
2883
+ : Array.isArray(payload?.items)
2884
+ ? payload.items
2885
+ : Array.isArray(payload?.rows)
2886
+ ? payload.rows
2887
+ : [];
2888
+ const lastPage =
2889
+ !Array.isArray(payload) && payload?.lastPage
2890
+ ? payload.lastPage
2891
+ : 1;
2892
+
2893
+ const items = rows
2894
+ .map(normalizeInstructorOption)
2895
+ .filter((opt): opt is InstructorOption => opt !== null);
2896
+
2897
+ return { items, hasMore: page < lastPage };
2898
+ }}
2899
+ renderOption={({ option }) => {
2900
+ const initials = option.name
2901
+ .split(' ')
2902
+ .filter(Boolean)
2903
+ .slice(0, 2)
2904
+ .map((p) => p[0]?.toUpperCase() ?? '')
2905
+ .join('');
2906
+ const avatarUrl = option.avatarId
2907
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${option.avatarId}`
2908
+ : undefined;
2909
+ return (
2910
+ <div className="flex min-w-0 items-center gap-3 py-0.5">
2911
+ <Avatar className="h-8 w-8 shrink-0 rounded-lg border border-border/60">
2912
+ <AvatarImage src={avatarUrl} />
2913
+ <AvatarFallback className="rounded-lg bg-muted text-[11px] font-semibold text-foreground">
2914
+ {initials}
2915
+ </AvatarFallback>
2916
+ </Avatar>
2917
+ <span className="truncate text-sm">
2918
+ {option.name}
2919
+ </span>
2920
+ </div>
2921
+ );
2922
+ }}
2923
+ renderSelectedValue={({ option, label }) => {
2924
+ const name = option?.name ?? label;
2925
+ const initials = name
2926
+ .split(' ')
2927
+ .filter(Boolean)
2928
+ .slice(0, 2)
2929
+ .map((p) => p[0]?.toUpperCase() ?? '')
2930
+ .join('');
2931
+ const avatarUrl = option?.avatarId
2932
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${option.avatarId}`
2933
+ : undefined;
2934
+ return (
2935
+ <div className="flex items-center gap-2">
2936
+ <Avatar className="h-5 w-5 shrink-0 rounded">
2937
+ <AvatarImage src={avatarUrl} />
2938
+ <AvatarFallback className="rounded bg-muted text-[10px] font-semibold">
2939
+ {initials}
2940
+ </AvatarFallback>
2941
+ </Avatar>
2942
+ <span className="truncate">{name}</span>
2943
+ </div>
2944
+ );
2945
+ }}
2946
+ />
2947
+ </div>
2948
+ <Button
2949
+ type="button"
2950
+ variant="outline"
2951
+ size="icon"
2952
+ className="shrink-0"
2953
+ onClick={() => setCreateProfessorDialogOpen(true)}
2954
+ aria-label="Cadastrar novo professor"
2955
+ >
2956
+ <Plus className="h-4 w-4" />
2957
+ </Button>
2958
+ </div>
2916
2959
  <FieldError>
2917
2960
  {form.formState.errors.professor?.message}
2918
2961
  </FieldError>
@@ -3088,27 +3131,6 @@ export default function TurmasPage() {
3088
3131
  </FieldError>
3089
3132
  </Field>
3090
3133
  )}
3091
-
3092
- <Field>
3093
- <FieldLabel>
3094
- {t('form.recurrence.customDialog.endDate')}
3095
- </FieldLabel>
3096
- <Input
3097
- type="date"
3098
- min={watchedFormValues.dataInicio || undefined}
3099
- value={watchedFormValues.sessionRecurrenceUntil ?? ''}
3100
- onChange={(event) =>
3101
- form.setValue('sessionRecurrenceUntil', event.target.value, {
3102
- shouldDirty: true,
3103
- shouldTouch: true,
3104
- shouldValidate: true,
3105
- })
3106
- }
3107
- />
3108
- <FieldError>
3109
- {form.formState.errors.sessionRecurrenceUntil?.message}
3110
- </FieldError>
3111
- </Field>
3112
3134
  </div>
3113
3135
 
3114
3136
  <DialogFooter className="gap-2">