@hed-hog/lms 0.0.350 → 0.0.351

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 (160) hide show
  1. package/dist/certificate/certificate.controller.d.ts +2 -2
  2. package/dist/certificate/certificate.controller.d.ts.map +1 -1
  3. package/dist/certificate/certificate.controller.js +8 -6
  4. package/dist/certificate/certificate.controller.js.map +1 -1
  5. package/dist/certificate/certificate.service.d.ts +5 -2
  6. package/dist/certificate/certificate.service.d.ts.map +1 -1
  7. package/dist/certificate/certificate.service.js +70 -6
  8. package/dist/certificate/certificate.service.js.map +1 -1
  9. package/dist/course/course-structure.controller.d.ts +24 -10
  10. package/dist/course/course-structure.controller.d.ts.map +1 -1
  11. package/dist/course/course-structure.controller.js +23 -2
  12. package/dist/course/course-structure.controller.js.map +1 -1
  13. package/dist/course/course-structure.service.d.ts +16 -8
  14. package/dist/course/course-structure.service.d.ts.map +1 -1
  15. package/dist/course/course-structure.service.js +61 -30
  16. package/dist/course/course-structure.service.js.map +1 -1
  17. package/dist/course/course-video-conversion.service.d.ts +37 -0
  18. package/dist/course/course-video-conversion.service.d.ts.map +1 -0
  19. package/dist/course/course-video-conversion.service.js +308 -0
  20. package/dist/course/course-video-conversion.service.js.map +1 -0
  21. package/dist/course/course.controller.d.ts +17 -0
  22. package/dist/course/course.controller.d.ts.map +1 -1
  23. package/dist/course/course.controller.js +23 -0
  24. package/dist/course/course.controller.js.map +1 -1
  25. package/dist/course/course.module.d.ts.map +1 -1
  26. package/dist/course/course.module.js +15 -2
  27. package/dist/course/course.module.js.map +1 -1
  28. package/dist/course/course.service.d.ts +15 -0
  29. package/dist/course/course.service.d.ts.map +1 -1
  30. package/dist/course/course.service.js +103 -49
  31. package/dist/course/course.service.js.map +1 -1
  32. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +5 -1
  33. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  34. package/dist/course/dto/create-course-structure-lesson.dto.js +16 -2
  35. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  36. package/dist/course/dto/create-course.dto.d.ts +1 -0
  37. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  38. package/dist/course/dto/create-course.dto.js +9 -0
  39. package/dist/course/dto/create-course.dto.js.map +1 -1
  40. package/dist/enterprise/enterprise.controller.d.ts +3 -3
  41. package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
  42. package/dist/enterprise/enterprise.controller.js +0 -1
  43. package/dist/enterprise/enterprise.controller.js.map +1 -1
  44. package/dist/enterprise/enterprise.service.d.ts +3 -3
  45. package/dist/evaluation/evaluation.service.d.ts.map +1 -1
  46. package/dist/evaluation/evaluation.service.js +9 -2
  47. package/dist/evaluation/evaluation.service.js.map +1 -1
  48. package/dist/index.d.ts +1 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +1 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/lms.module.d.ts.map +1 -1
  53. package/dist/lms.module.js +3 -0
  54. package/dist/lms.module.js.map +1 -1
  55. package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.d.ts +6 -0
  56. package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.d.ts.map +1 -0
  57. package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.js +33 -0
  58. package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.js.map +1 -0
  59. package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.d.ts +6 -0
  60. package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.d.ts.map +1 -0
  61. package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.js +33 -0
  62. package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.js.map +1 -0
  63. package/dist/video-resolution-profile/video-resolution-profile.controller.d.ts +38 -0
  64. package/dist/video-resolution-profile/video-resolution-profile.controller.d.ts.map +1 -0
  65. package/dist/video-resolution-profile/video-resolution-profile.controller.js +89 -0
  66. package/dist/video-resolution-profile/video-resolution-profile.controller.js.map +1 -0
  67. package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.d.ts +26 -0
  68. package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.d.ts.map +1 -0
  69. package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.js +160 -0
  70. package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.js.map +1 -0
  71. package/dist/video-resolution-profile/video-resolution-profile.module.d.ts +3 -0
  72. package/dist/video-resolution-profile/video-resolution-profile.module.d.ts.map +1 -0
  73. package/dist/video-resolution-profile/video-resolution-profile.module.js +26 -0
  74. package/dist/video-resolution-profile/video-resolution-profile.module.js.map +1 -0
  75. package/dist/video-resolution-profile/video-resolution-profile.service.d.ts +45 -0
  76. package/dist/video-resolution-profile/video-resolution-profile.service.d.ts.map +1 -0
  77. package/dist/video-resolution-profile/video-resolution-profile.service.js +117 -0
  78. package/dist/video-resolution-profile/video-resolution-profile.service.js.map +1 -0
  79. package/hedhog/data/menu.yaml +17 -0
  80. package/hedhog/data/route.yaml +133 -0
  81. package/hedhog/data/video_resolution_profile.yaml +7 -0
  82. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +269 -324
  83. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +124 -70
  84. package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +7 -4
  85. package/hedhog/frontend/app/_components/create-lms-person-sheet.tsx.ejs +2 -2
  86. package/hedhog/frontend/app/_components/create-lms-student-person-sheet.tsx.ejs +2 -2
  87. package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +34 -4
  88. package/hedhog/frontend/app/_lib/editor/types.ts.ejs +28 -3
  89. package/hedhog/frontend/app/achievements/page.tsx.ejs +9 -3
  90. package/hedhog/frontend/app/bitcodes/page.tsx.ejs +9 -3
  91. package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +7 -3
  92. package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +29 -8
  93. package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +14 -0
  94. package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +194 -9
  95. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +15 -5
  96. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +9 -5
  97. package/hedhog/frontend/app/classes/page.tsx.ejs +73 -47
  98. package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +19 -9
  99. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +24 -1
  100. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +1 -1
  101. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +1 -1
  102. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +28 -16
  103. package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +11 -6
  104. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +7 -4
  105. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -0
  106. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +24 -87
  107. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +892 -411
  108. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1004 -293
  109. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +11 -11
  110. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +62 -52
  111. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +2 -0
  112. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +19 -6
  113. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +86 -1
  114. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +3 -0
  115. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +1 -0
  116. package/hedhog/frontend/app/courses/page.tsx.ejs +112 -89
  117. package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +1 -1
  118. package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +10 -3
  119. package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +8 -4
  120. package/hedhog/frontend/app/enterprise/_components/enterprise-person-edit-sheet.tsx.ejs +2 -2
  121. package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +10 -4
  122. package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +10 -3
  123. package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +10 -3
  124. package/hedhog/frontend/app/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +10 -3
  125. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +23 -9
  126. package/hedhog/frontend/app/exams/page.tsx.ejs +14 -6
  127. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +9 -3
  128. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +190 -17
  129. package/hedhog/frontend/app/layout.tsx.ejs +5 -1
  130. package/hedhog/frontend/app/paths/page.tsx.ejs +13 -5
  131. package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +10 -10
  132. package/hedhog/frontend/app/training/page.tsx.ejs +13 -5
  133. package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +607 -0
  134. package/hedhog/frontend/messages/en.json +250 -9
  135. package/hedhog/frontend/messages/pt.json +250 -9
  136. package/hedhog/table/course.yaml +4 -0
  137. package/hedhog/table/course_lesson_file.yaml +8 -0
  138. package/hedhog/table/course_video_resolution_profile.yaml +22 -0
  139. package/hedhog/table/video_resolution_profile.yaml +18 -0
  140. package/package.json +7 -6
  141. package/src/certificate/certificate.controller.ts +19 -14
  142. package/src/certificate/certificate.service.ts +106 -11
  143. package/src/course/course-structure.controller.ts +24 -2
  144. package/src/course/course-structure.service.ts +21 -4
  145. package/src/course/course-video-conversion.service.ts +415 -0
  146. package/src/course/course.controller.ts +18 -0
  147. package/src/course/course.module.ts +15 -2
  148. package/src/course/course.service.ts +72 -2
  149. package/src/course/dto/create-course-structure-lesson.dto.ts +13 -2
  150. package/src/course/dto/create-course.dto.ts +8 -0
  151. package/src/enterprise/enterprise.controller.ts +0 -1
  152. package/src/evaluation/evaluation.service.ts +9 -2
  153. package/src/index.ts +1 -0
  154. package/src/lms.module.ts +3 -0
  155. package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +16 -0
  156. package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +16 -0
  157. package/src/video-resolution-profile/video-resolution-profile.controller.ts +62 -0
  158. package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +128 -0
  159. package/src/video-resolution-profile/video-resolution-profile.module.ts +13 -0
  160. package/src/video-resolution-profile/video-resolution-profile.service.ts +117 -0
@@ -694,15 +694,26 @@ function buildAPI(canvas: any, fabricMod: any): CanvasAPI {
694
694
  let fObj: any = null;
695
695
 
696
696
  if (type === 'field') {
697
+ const isQrCode = key === 'qrCode';
697
698
  fObj = new fabricMod.Textbox(`{{${key}}}`, {
698
- left: cx - 120,
699
- top: cy - 18,
700
- width: 240,
701
- fontSize: 28,
699
+ left: cx - (isQrCode ? 90 : 120),
700
+ top: cy - (isQrCode ? 45 : 18),
701
+ width: isQrCode ? 180 : 240,
702
+ height: isQrCode ? 90 : undefined,
703
+ fontSize: isQrCode ? 20 : 28,
702
704
  fontFamily: 'Inter',
703
705
  fontWeight: '400',
704
706
  fill: '#1e293b',
707
+ textAlign: 'center',
705
708
  });
709
+ if (isQrCode) {
710
+ fObj.set({
711
+ text: '[QR Code]',
712
+ stroke: '#94a3b8',
713
+ strokeWidth: 1,
714
+ backgroundColor: '#f8fafc',
715
+ });
716
+ }
706
717
  fObj._tplKey = key;
707
718
  } else if (type === 'staticText') {
708
719
  fObj = new fabricMod.Textbox('Texto', {
@@ -732,18 +743,28 @@ function buildAPI(canvas: any, fabricMod: any): CanvasAPI {
732
743
  strokeWidth: 2,
733
744
  });
734
745
  } else if (type === 'image') {
746
+ const isCourseLogo = key === 'courseLogo';
747
+ const isCourseBanner = key === 'courseBanner';
748
+ const boxWidth = isCourseBanner ? 420 : isCourseLogo ? 220 : 150;
749
+ const boxHeight = isCourseBanner ? 140 : isCourseLogo ? 120 : 150;
735
750
  fObj = new fabricMod.Rect({
736
- left: cx - 75,
737
- top: cy - 75,
738
- width: 150,
739
- height: 150,
751
+ left: cx - boxWidth / 2,
752
+ top: cy - boxHeight / 2,
753
+ width: boxWidth,
754
+ height: boxHeight,
740
755
  fill: '#e2e8f0',
741
756
  stroke: '#cbd5e1',
742
757
  strokeWidth: 1,
758
+ strokeDashArray: isCourseLogo || isCourseBanner ? [8, 4] : undefined,
743
759
  rx: 4,
744
760
  ry: 4,
745
761
  });
746
762
  fObj._tplImageSrc = '';
763
+ fObj._tplImageFileId = null;
764
+ fObj._tplImageResizeMode = 'contain';
765
+ if (isCourseLogo || isCourseBanner) {
766
+ fObj._tplImagePlaceholder = true;
767
+ }
747
768
  }
748
769
 
749
770
  if (!fObj) return;
@@ -125,6 +125,20 @@ export default function LeftPanel() {
125
125
  <p className="mt-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
126
126
  {t('leftPanel.sections.image')}
127
127
  </p>
128
+ <div className="grid grid-cols-1 gap-2">
129
+ <ElementCard
130
+ icon={<ImageIcon className="size-4" />}
131
+ label="Logo do Curso"
132
+ onClick={() => add('image', { key: 'courseLogo' })}
133
+ dragPayload={{ type: 'image', key: 'courseLogo' }}
134
+ />
135
+ <ElementCard
136
+ icon={<ImageIcon className="size-4" />}
137
+ label="Banner do Curso"
138
+ onClick={() => add('image', { key: 'courseBanner' })}
139
+ dragPayload={{ type: 'image', key: 'courseBanner' }}
140
+ />
141
+ </div>
128
142
  <ElementCard
129
143
  icon={<ImageIcon className="size-4" />}
130
144
  label={t('leftPanel.elements.image')}
@@ -14,6 +14,7 @@ import { Slider } from '@/components/ui/slider';
14
14
  import { Switch } from '@/components/ui/switch';
15
15
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
16
16
  import { Textarea } from '@/components/ui/textarea';
17
+ import { useApp } from '@hed-hog/next-app-provider';
17
18
  import {
18
19
  ChevronDown,
19
20
  ChevronUp,
@@ -493,16 +494,200 @@ function ImageProperties({
493
494
  obj: TemplateObject;
494
495
  setProp: (p: Record<string, unknown>) => void;
495
496
  }) {
497
+ const { request } = useApp();
498
+ const isCoursePlaceholder =
499
+ obj.key === 'courseLogo' || obj.key === 'courseBanner';
500
+ const [isUploading, setIsUploading] = useState(false);
501
+
502
+ const currentFileId =
503
+ typeof obj.image?.file_id === 'number' && Number.isFinite(obj.image.file_id)
504
+ ? obj.image.file_id
505
+ : null;
506
+ const resizeMode =
507
+ obj.image?.resizeMode === 'cover' ||
508
+ obj.image?.resizeMode === 'stretch' ||
509
+ obj.image?.resizeMode === 'center'
510
+ ? obj.image.resizeMode
511
+ : 'contain';
512
+ const previewUrl = currentFileId ? `/file/open/${currentFileId}` : null;
513
+
514
+ async function handleImageUpload(e: React.ChangeEvent<HTMLInputElement>) {
515
+ const file = e.target.files?.[0];
516
+ if (!file) return;
517
+
518
+ if (!String(file.type || '').startsWith('image/')) {
519
+ toast.error('Selecione um arquivo de imagem válido.');
520
+ return;
521
+ }
522
+
523
+ const formData = new FormData();
524
+ formData.append('file', file);
525
+
526
+ setIsUploading(true);
527
+
528
+ try {
529
+ const response = await request<{
530
+ id: number;
531
+ url: string;
532
+ }>({
533
+ url: '/lms/certificates/templates/background-image',
534
+ method: 'POST',
535
+ data: formData,
536
+ });
537
+
538
+ const newFileId = response?.data?.id;
539
+ if (typeof newFileId !== 'number' || !Number.isFinite(newFileId)) {
540
+ throw new Error('Invalid uploaded file id');
541
+ }
542
+
543
+ setProp({
544
+ _tplImageFileId: newFileId,
545
+ _tplImageSrc: response.data.url ?? `/file/open/${newFileId}`,
546
+ });
547
+
548
+ if (
549
+ typeof currentFileId === 'number' &&
550
+ Number.isFinite(currentFileId) &&
551
+ currentFileId !== newFileId
552
+ ) {
553
+ await request({
554
+ url: '/file',
555
+ method: 'DELETE',
556
+ data: { ids: [currentFileId] },
557
+ });
558
+ }
559
+
560
+ toast.success('Imagem personalizada atualizada.');
561
+ } catch {
562
+ toast.error('Não foi possível enviar a imagem.');
563
+ } finally {
564
+ setIsUploading(false);
565
+ e.target.value = '';
566
+ }
567
+ }
568
+
569
+ async function handleRemoveImage() {
570
+ if (!currentFileId) {
571
+ setProp({ _tplImageFileId: null, _tplImageSrc: '' });
572
+ toast.success('Imagem removida do certificado.');
573
+ return;
574
+ }
575
+
576
+ try {
577
+ await request({
578
+ url: '/file',
579
+ method: 'DELETE',
580
+ data: { ids: [currentFileId] },
581
+ });
582
+ setProp({ _tplImageFileId: null, _tplImageSrc: '' });
583
+ toast.success('Imagem removida do certificado.');
584
+ } catch {
585
+ toast.error('Não foi possível remover a imagem.');
586
+ }
587
+ }
588
+
496
589
  return (
497
- <div className="flex flex-col gap-1.5">
498
- <Label className="text-xs">Rotacao ({Math.round(obj.rotation)}deg)</Label>
499
- <Slider
500
- min={0}
501
- max={360}
502
- step={1}
503
- value={[Math.round(obj.rotation)]}
504
- onValueChange={([v]) => setProp({ angle: v })}
505
- />
590
+ <div className="flex flex-col gap-3">
591
+ <div className="flex flex-col gap-1.5">
592
+ <Label className="text-xs">
593
+ Rotacao ({Math.round(obj.rotation)}deg)
594
+ </Label>
595
+ <Slider
596
+ min={0}
597
+ max={360}
598
+ step={1}
599
+ value={[Math.round(obj.rotation)]}
600
+ onValueChange={([v]) => setProp({ angle: v })}
601
+ />
602
+ </div>
603
+
604
+ {!isCoursePlaceholder ? (
605
+ <div className="flex flex-col gap-1.5">
606
+ <Label className="text-xs">Imagem personalizada</Label>
607
+ <input
608
+ type="file"
609
+ accept="image/*"
610
+ onChange={handleImageUpload}
611
+ disabled={isUploading}
612
+ className="block w-full cursor-pointer rounded-md border border-border bg-background px-2 py-1.5 text-xs"
613
+ />
614
+ <div className="flex items-center gap-2">
615
+ <Button
616
+ type="button"
617
+ variant="outline"
618
+ size="sm"
619
+ className="h-7 text-xs"
620
+ onClick={handleRemoveImage}
621
+ disabled={isUploading || !currentFileId}
622
+ >
623
+ Remover imagem
624
+ </Button>
625
+ {currentFileId ? (
626
+ <span className="text-[11px] text-muted-foreground">
627
+ file_id: {currentFileId}
628
+ </span>
629
+ ) : (
630
+ <span className="text-[11px] text-muted-foreground">
631
+ Sem imagem vinculada
632
+ </span>
633
+ )}
634
+ </div>
635
+
636
+ <div className="flex flex-col gap-1.5">
637
+ <Label className="text-xs">Modo de redimensionamento</Label>
638
+ <Select
639
+ value={resizeMode}
640
+ onValueChange={(value) => setProp({ _tplImageResizeMode: value })}
641
+ >
642
+ <SelectTrigger className="w-full">
643
+ <SelectValue />
644
+ </SelectTrigger>
645
+ <SelectContent>
646
+ <SelectItem value="contain">
647
+ Manter proporção (Contain)
648
+ </SelectItem>
649
+ <SelectItem value="cover">
650
+ Manter proporção preenchendo (Crop)
651
+ </SelectItem>
652
+ <SelectItem value="stretch">
653
+ Esticar para preencher (Sem proporção)
654
+ </SelectItem>
655
+ <SelectItem value="center">
656
+ Tamanho original (Center)
657
+ </SelectItem>
658
+ </SelectContent>
659
+ </Select>
660
+ </div>
661
+
662
+ {previewUrl ? (
663
+ <div className="mt-1 overflow-hidden rounded-md border border-border bg-muted/20">
664
+ <img
665
+ src={previewUrl}
666
+ alt="Preview da imagem do certificado"
667
+ className={`h-32 w-full ${
668
+ resizeMode === 'cover'
669
+ ? 'object-cover'
670
+ : resizeMode === 'stretch'
671
+ ? 'object-fill'
672
+ : resizeMode === 'center'
673
+ ? 'object-none object-center'
674
+ : 'object-contain'
675
+ }`}
676
+ loading="lazy"
677
+ />
678
+ </div>
679
+ ) : null}
680
+
681
+ <p className="text-[11px] text-muted-foreground">
682
+ O template salva apenas o file_id desta imagem.
683
+ </p>
684
+ </div>
685
+ ) : (
686
+ <p className="text-[11px] text-muted-foreground">
687
+ Este item é um placeholder de posicionamento. O conteúdo real (logo ou
688
+ banner) será preenchido na emissão final do certificado.
689
+ </p>
690
+ )}
506
691
  </div>
507
692
  );
508
693
  }
@@ -37,12 +37,12 @@ import {
37
37
  } from '@/components/ui/select';
38
38
  import {
39
39
  Sheet,
40
- SheetContent,
41
40
  SheetDescription,
42
41
  SheetFooter,
43
42
  SheetHeader,
44
43
  SheetTitle,
45
44
  } from '@/components/ui/sheet';
45
+ import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
46
46
  import { Skeleton } from '@/components/ui/skeleton';
47
47
  import { Textarea } from '@/components/ui/textarea';
48
48
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
@@ -726,7 +726,11 @@ export default function ModelsPage() {
726
726
  </div>
727
727
 
728
728
  <Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
729
- <SheetContent
729
+ <ResizableSheetContent
730
+ sheetId="lms-certificates-models-create-template-sheet"
731
+ defaultWidth={560}
732
+ minWidth={420}
733
+ maxWidth={920}
730
734
  side="right"
731
735
  className="flex w-full flex-col sm:max-w-lg overflow-y-auto"
732
736
  >
@@ -809,7 +813,7 @@ export default function ModelsPage() {
809
813
  </Button>
810
814
  </SheetFooter>
811
815
  </form>
812
- </SheetContent>
816
+ </ResizableSheetContent>
813
817
  </Sheet>
814
818
 
815
819
  <Sheet
@@ -821,7 +825,13 @@ export default function ModelsPage() {
821
825
  }
822
826
  }}
823
827
  >
824
- <SheetContent className="overflow-y-auto sm:max-w-xl">
828
+ <ResizableSheetContent
829
+ sheetId="lms-certificates-models-edit-template-sheet"
830
+ defaultWidth={640}
831
+ minWidth={460}
832
+ maxWidth={1080}
833
+ className="overflow-y-auto sm:max-w-xl"
834
+ >
825
835
  <SheetHeader>
826
836
  <SheetTitle>{t('editSheet.title')}</SheetTitle>
827
837
  <SheetDescription>{t('editSheet.description')}</SheetDescription>
@@ -906,7 +916,7 @@ export default function ModelsPage() {
906
916
  </Button>
907
917
  </SheetFooter>
908
918
  </form>
909
- </SheetContent>
919
+ </ResizableSheetContent>
910
920
  </Sheet>
911
921
 
912
922
  <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
@@ -1,11 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { PersonFormSheet } from '@/app/(app)/(libraries)/contact/person/_components/person-form-sheet';
3
+ import { PersonFormSheet } from '@/app/(app)/(libraries)/crm/person/_components/person-form-sheet';
4
4
  import type {
5
5
  Person as ContactPerson,
6
6
  ContactTypeOption,
7
7
  DocumentTypeOption,
8
- } from '@/app/(app)/(libraries)/contact/person/_components/person-types';
8
+ } from '@/app/(app)/(libraries)/crm/person/_components/person-types';
9
9
  import { LmsClassCalendar } from '@/app/(app)/(libraries)/lms/_components/lms-class-calendar';
10
10
  import { CopyButton } from '@/components/copy-button';
11
11
  import { Page, PageHeader } from '@/components/entity-list';
@@ -54,12 +54,12 @@ import {
54
54
  } from '@/components/ui/select';
55
55
  import {
56
56
  Sheet,
57
- SheetContent,
58
57
  SheetDescription,
59
58
  SheetFooter,
60
59
  SheetHeader,
61
60
  SheetTitle,
62
61
  } from '@/components/ui/sheet';
62
+ import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
63
63
  import { Skeleton } from '@/components/ui/skeleton';
64
64
  import { Switch } from '@/components/ui/switch';
65
65
  import {
@@ -4812,7 +4812,11 @@ export default function TurmaDetalhePage() {
4812
4812
 
4813
4813
  {/* ── Sheet Aula ───────────────────────────────────────────────────────── */}
4814
4814
  <Sheet open={aulaSheetOpen} onOpenChange={setAulaSheetOpen}>
4815
- <SheetContent
4815
+ <ResizableSheetContent
4816
+ sheetId="lms-classes-detail-lesson-sheet"
4817
+ defaultWidth={560}
4818
+ minWidth={420}
4819
+ maxWidth={920}
4816
4820
  side="right"
4817
4821
  className="flex w-full flex-col sm:max-w-lg overflow-y-auto"
4818
4822
  >
@@ -5703,7 +5707,7 @@ export default function TurmaDetalhePage() {
5703
5707
  )}
5704
5708
  </TabsContent>
5705
5709
  </Tabs>
5706
- </SheetContent>
5710
+ </ResizableSheetContent>
5707
5711
  </Sheet>
5708
5712
 
5709
5713
  <CreateLmsPersonSheet
@@ -75,6 +75,7 @@ interface Turma {
75
75
  curso: string;
76
76
  cursoId: number;
77
77
  instructorId?: number | null;
78
+ professorAvatarId?: number | null;
78
79
  primaryColor?: string | null;
79
80
  tipo: 'presencial' | 'online' | 'hibrida';
80
81
  dataInicio: string;
@@ -115,6 +116,7 @@ type ApiClass = {
115
116
  capacity: number;
116
117
  courseId: number;
117
118
  instructorId?: number | null;
119
+ instructorAvatarId?: number | null;
118
120
  courseTitle: string;
119
121
  enrolledCount: number;
120
122
  professor?: string | null;
@@ -218,6 +220,7 @@ function mapApiClass(item: ApiClass): Turma {
218
220
  curso: item.courseTitle,
219
221
  cursoId: item.courseId,
220
222
  instructorId: item.instructorId ?? null,
223
+ professorAvatarId: item.instructorAvatarId ?? null,
221
224
  primaryColor: item.primaryColor,
222
225
  tipo: toPtType(item.deliveryMode),
223
226
  dataInicio: getDateOnly(item.startDate),
@@ -247,6 +250,26 @@ const STATUS_VARIANT: Record<
247
250
  cancelada: 'destructive',
248
251
  };
249
252
 
253
+ const STATUS_CLASS: Record<string, string> = {
254
+ aberta:
255
+ 'bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-950 dark:text-emerald-400 dark:border-emerald-800',
256
+ em_andamento:
257
+ 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-950 dark:text-blue-400 dark:border-blue-800',
258
+ concluida:
259
+ 'bg-slate-100 text-slate-600 border-slate-200 dark:bg-slate-800 dark:text-slate-400 dark:border-slate-700',
260
+ cancelada:
261
+ 'bg-red-100 text-red-700 border-red-200 dark:bg-red-950 dark:text-red-400 dark:border-red-800',
262
+ };
263
+
264
+ const TIPO_CLASS: Record<string, string> = {
265
+ presencial:
266
+ 'bg-sky-100 text-sky-700 border-sky-200 dark:bg-sky-950 dark:text-sky-400 dark:border-sky-800',
267
+ online:
268
+ 'bg-violet-100 text-violet-700 border-violet-200 dark:bg-violet-950 dark:text-violet-400 dark:border-violet-800',
269
+ hibrida:
270
+ 'bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-950 dark:text-amber-400 dark:border-amber-800',
271
+ };
272
+
250
273
  const TIPO_ICON: Record<string, LucideIcon> = {
251
274
  presencial: MapPin,
252
275
  online: Monitor,
@@ -853,7 +876,9 @@ export default function TurmasPage() {
853
876
  <motion.div key={turma.id} variants={fadeUp}>
854
877
  <Card
855
878
  className="group relative cursor-pointer overflow-hidden border-border/70 shadow-sm transition-all duration-300 hover:-translate-y-0.5 hover:shadow-md"
856
- onDoubleClick={() => openEditSheet(turma)}
879
+ onDoubleClick={() =>
880
+ router.push(`/lms/classes/${turma.id}`)
881
+ }
857
882
  title={t('cards.tooltip')}
858
883
  >
859
884
  <div
@@ -864,13 +889,13 @@ export default function TurmasPage() {
864
889
  : undefined
865
890
  }
866
891
  />
867
- <CardContent className="p-5">
868
- <div className="mb-4 flex items-start gap-3">
892
+ <CardContent className="px-3 py-2">
893
+ <div className="mb-2 flex items-start gap-2">
869
894
  <CourseAvatar
870
895
  fileId={turma.logoFileId}
871
896
  title={turma.curso}
872
- className="size-12 rounded-xl"
873
- iconSize="size-6"
897
+ className="size-9 rounded-lg"
898
+ iconSize="size-5"
874
899
  />
875
900
  <div className="min-w-0 flex-1">
876
901
  <div className="mb-1 flex items-start justify-between gap-2">
@@ -916,59 +941,68 @@ export default function TurmasPage() {
916
941
  </DropdownMenuContent>
917
942
  </DropdownMenu>
918
943
  </div>
919
- <p className="text-xs text-muted-foreground">
944
+ <div className="mt-1 flex items-center gap-1.5 text-xs text-muted-foreground">
920
945
  <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[10px]">
921
946
  {turma.codigo}
922
947
  </code>
923
- <span className="mx-1.5 text-muted-foreground/50">
924
- |
925
- </span>
948
+ <span className="text-muted-foreground/50">|</span>
949
+ {turma.professorAvatarId ? (
950
+ <img
951
+ src={`${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${turma.professorAvatarId}`}
952
+ alt={turma.professor}
953
+ className="size-4 rounded-full object-cover"
954
+ onError={(e) => {
955
+ (e.currentTarget as HTMLImageElement).style.display = 'none';
956
+ }}
957
+ />
958
+ ) : (
959
+ <span className="inline-flex size-4 items-center justify-center rounded-full bg-muted text-[8px] font-semibold uppercase text-muted-foreground">
960
+ {turma.professor
961
+ .split(' ')
962
+ .slice(0, 2)
963
+ .map((p) => p[0])
964
+ .join('')}
965
+ </span>
966
+ )}
926
967
  <span>{turma.professor}</span>
927
- </p>
968
+ </div>
928
969
  </div>
929
970
  </div>
930
971
 
931
- <div className="mb-4 flex flex-wrap items-center gap-1.5">
972
+ <div className="mb-2 flex flex-wrap items-center gap-1">
932
973
  <Badge
933
- variant={STATUS_VARIANT[turma.status]}
934
- className="text-[11px]"
974
+ variant="outline"
975
+ className={`text-[11px] ${STATUS_CLASS[turma.status]}`}
935
976
  >
936
977
  {t(`status.${turma.status}`)}
937
978
  </Badge>
938
- <span className="inline-flex items-center gap-1 rounded-full border bg-muted px-2.5 py-0.5 text-[11px] font-medium text-foreground">
979
+ <span
980
+ className={`inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-[11px] font-medium ${TIPO_CLASS[turma.tipo]}`}
981
+ >
939
982
  <TipoIcon className="size-3" />
940
983
  {t(`type.${turma.tipo}`)}
941
984
  </span>
942
985
  </div>
943
986
 
944
- <div className="mb-4 rounded-lg bg-muted/40 p-3">
945
- <div className="mb-2 flex items-center justify-between text-sm">
987
+ <div className="mb-2 space-y-1">
988
+ <div className="flex items-center justify-between text-[11px]">
946
989
  <span className="text-muted-foreground">
947
990
  {t('cards.occupancy')}
948
991
  </span>
949
- <span className="font-semibold">
950
- {turma.matriculados}/{turma.vagas}
992
+ <span className="font-semibold text-foreground">
993
+ {turma.matriculados}/{turma.vagas} · {ocupacao}%
951
994
  </span>
952
995
  </div>
953
- <div className="relative h-2 w-full overflow-hidden rounded-full bg-muted">
996
+ <div className="relative h-1 w-full overflow-hidden rounded-full bg-muted">
954
997
  <div
955
998
  className="h-full rounded-full bg-primary transition-all duration-500"
956
999
  style={{ width: `${Math.min(ocupacao, 100)}%` }}
957
1000
  />
958
1001
  </div>
959
- <div className="mt-1.5 flex justify-between text-[11px]">
960
- <span className="text-muted-foreground">
961
- {ocupacao}% {t('cards.occupied')}
962
- </span>
963
- <span className="font-medium text-foreground">
964
- {Math.max(turma.vagas - turma.matriculados, 0)}{' '}
965
- {t('cards.freeVacancies')}
966
- </span>
967
- </div>
968
1002
  </div>
969
1003
 
970
- <div className="mb-4 grid grid-cols-2 gap-2">
971
- <div className="rounded-lg border bg-background p-2.5">
1004
+ <div className="grid grid-cols-2 gap-1.5">
1005
+ <div className="rounded-lg border bg-background p-2">
972
1006
  <div className="mb-1 flex items-center gap-1.5 text-[11px] text-muted-foreground">
973
1007
  <CalendarIcon className="size-3" />{' '}
974
1008
  {t('cards.period')}
@@ -989,25 +1023,13 @@ export default function TurmasPage() {
989
1023
  )}
990
1024
  </p>
991
1025
  </div>
992
- <div className="rounded-lg border bg-background p-2.5">
1026
+ <div className="rounded-lg border bg-background p-2">
993
1027
  <div className="mb-1 flex items-center gap-1.5 text-[11px] text-muted-foreground">
994
1028
  <Clock className="size-3" /> {t('cards.schedule')}
995
1029
  </div>
996
1030
  <p className="text-xs font-medium">{scheduleLabel}</p>
997
1031
  </div>
998
1032
  </div>
999
-
1000
- <div className="flex items-center justify-between rounded-lg bg-muted/40 px-3 py-2.5">
1001
- <div className="flex items-center gap-1.5">
1002
- <Users className="size-4 text-muted-foreground" />
1003
- <span className="text-sm font-medium">
1004
- {turma.matriculados}
1005
- </span>
1006
- <span className="text-xs text-muted-foreground">
1007
- {t('cards.enrolled')}
1008
- </span>
1009
- </div>
1010
- </div>
1011
1033
  </CardContent>
1012
1034
  </Card>
1013
1035
  </motion.div>
@@ -1046,7 +1068,9 @@ export default function TurmasPage() {
1046
1068
  <TableRow
1047
1069
  key={turma.id}
1048
1070
  className="cursor-pointer"
1049
- onDoubleClick={() => openEditSheet(turma)}
1071
+ onDoubleClick={() =>
1072
+ router.push(`/lms/classes/${turma.id}`)
1073
+ }
1050
1074
  title={t('cards.tooltip')}
1051
1075
  >
1052
1076
  <TableCell>
@@ -1075,14 +1099,16 @@ export default function TurmasPage() {
1075
1099
  </TableCell>
1076
1100
  <TableCell>
1077
1101
  <Badge
1078
- variant={STATUS_VARIANT[turma.status]}
1079
- className="text-[11px]"
1102
+ variant="outline"
1103
+ className={`text-[11px] ${STATUS_CLASS[turma.status]}`}
1080
1104
  >
1081
1105
  {t(`status.${turma.status}`)}
1082
1106
  </Badge>
1083
1107
  </TableCell>
1084
1108
  <TableCell>
1085
- <span className="inline-flex items-center gap-1 rounded-full border bg-muted px-2 py-0.5 text-[11px] font-medium text-foreground">
1109
+ <span
1110
+ className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-medium ${TIPO_CLASS[turma.tipo]}`}
1111
+ >
1086
1112
  <TipoIcon className="size-3" />
1087
1113
  {t(`type.${turma.tipo}`)}
1088
1114
  </span>