@hed-hog/lms 0.0.350 → 0.0.353
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/certificate/certificate.controller.d.ts +2 -2
- package/dist/certificate/certificate.controller.d.ts.map +1 -1
- package/dist/certificate/certificate.controller.js +8 -6
- package/dist/certificate/certificate.controller.js.map +1 -1
- package/dist/certificate/certificate.service.d.ts +5 -2
- package/dist/certificate/certificate.service.d.ts.map +1 -1
- package/dist/certificate/certificate.service.js +70 -6
- package/dist/certificate/certificate.service.js.map +1 -1
- package/dist/course/course-structure.controller.d.ts +24 -10
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +23 -2
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +16 -8
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +61 -30
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course-video-conversion.service.d.ts +37 -0
- package/dist/course/course-video-conversion.service.d.ts.map +1 -0
- package/dist/course/course-video-conversion.service.js +308 -0
- package/dist/course/course-video-conversion.service.js.map +1 -0
- package/dist/course/course.controller.d.ts +17 -0
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.controller.js +23 -0
- package/dist/course/course.controller.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +15 -2
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +15 -0
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +103 -49
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts +5 -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 +16 -2
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/dto/create-course.dto.d.ts +1 -0
- package/dist/course/dto/create-course.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course.dto.js +9 -0
- package/dist/course/dto/create-course.dto.js.map +1 -1
- package/dist/enterprise/enterprise.controller.d.ts +3 -3
- package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
- package/dist/enterprise/enterprise.controller.js +0 -1
- package/dist/enterprise/enterprise.controller.js.map +1 -1
- package/dist/enterprise/enterprise.service.d.ts +3 -3
- package/dist/evaluation/evaluation.service.d.ts.map +1 -1
- package/dist/evaluation/evaluation.service.js +9 -2
- package/dist/evaluation/evaluation.service.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +3 -0
- package/dist/lms.module.js.map +1 -1
- package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.d.ts +6 -0
- package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.d.ts.map +1 -0
- package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.js +33 -0
- package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.js.map +1 -0
- package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.d.ts +6 -0
- package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.d.ts.map +1 -0
- package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.js +33 -0
- package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.js.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.controller.d.ts +38 -0
- package/dist/video-resolution-profile/video-resolution-profile.controller.d.ts.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.controller.js +89 -0
- package/dist/video-resolution-profile/video-resolution-profile.controller.js.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.d.ts +26 -0
- package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.d.ts.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.js +160 -0
- package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.js.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.module.d.ts +3 -0
- package/dist/video-resolution-profile/video-resolution-profile.module.d.ts.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.module.js +26 -0
- package/dist/video-resolution-profile/video-resolution-profile.module.js.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.service.d.ts +45 -0
- package/dist/video-resolution-profile/video-resolution-profile.service.d.ts.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.service.js +117 -0
- package/dist/video-resolution-profile/video-resolution-profile.service.js.map +1 -0
- package/hedhog/data/menu.yaml +17 -0
- package/hedhog/data/route.yaml +133 -0
- package/hedhog/data/video_resolution_profile.yaml +7 -0
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +269 -324
- package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +124 -70
- package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +7 -4
- package/hedhog/frontend/app/_components/create-lms-person-sheet.tsx.ejs +2 -2
- package/hedhog/frontend/app/_components/create-lms-student-person-sheet.tsx.ejs +2 -2
- package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +34 -4
- package/hedhog/frontend/app/_lib/editor/types.ts.ejs +28 -3
- package/hedhog/frontend/app/achievements/page.tsx.ejs +9 -3
- package/hedhog/frontend/app/bitcodes/page.tsx.ejs +9 -3
- package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +7 -3
- package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +29 -8
- package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +14 -0
- package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +194 -9
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +15 -5
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +9 -5
- package/hedhog/frontend/app/classes/page.tsx.ejs +73 -47
- package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +19 -9
- package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +24 -1
- package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +28 -16
- package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +11 -6
- package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +7 -4
- package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -0
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +24 -87
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +892 -411
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1004 -293
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +11 -11
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +62 -52
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +2 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +19 -6
- package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +86 -1
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +1 -0
- package/hedhog/frontend/app/courses/page.tsx.ejs +112 -89
- package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +10 -3
- package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +8 -4
- package/hedhog/frontend/app/enterprise/_components/enterprise-person-edit-sheet.tsx.ejs +2 -2
- package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +10 -4
- package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +10 -3
- package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +10 -3
- package/hedhog/frontend/app/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +10 -3
- package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +23 -9
- package/hedhog/frontend/app/exams/page.tsx.ejs +14 -6
- package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +9 -3
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +190 -17
- package/hedhog/frontend/app/layout.tsx.ejs +5 -1
- package/hedhog/frontend/app/paths/page.tsx.ejs +13 -5
- package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +10 -10
- package/hedhog/frontend/app/training/page.tsx.ejs +13 -5
- package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +607 -0
- package/hedhog/frontend/messages/en.json +250 -9
- package/hedhog/frontend/messages/pt.json +250 -9
- package/hedhog/table/course.yaml +4 -0
- package/hedhog/table/course_lesson_file.yaml +8 -0
- package/hedhog/table/course_video_resolution_profile.yaml +22 -0
- package/hedhog/table/video_resolution_profile.yaml +18 -0
- package/package.json +9 -8
- package/src/certificate/certificate.controller.ts +19 -14
- package/src/certificate/certificate.service.ts +106 -11
- package/src/course/course-structure.controller.ts +24 -2
- package/src/course/course-structure.service.ts +21 -4
- package/src/course/course-video-conversion.service.ts +415 -0
- package/src/course/course.controller.ts +18 -0
- package/src/course/course.module.ts +15 -2
- package/src/course/course.service.ts +72 -2
- package/src/course/dto/create-course-structure-lesson.dto.ts +13 -2
- package/src/course/dto/create-course.dto.ts +8 -0
- package/src/enterprise/enterprise.controller.ts +0 -1
- package/src/evaluation/evaluation.service.ts +9 -2
- package/src/index.ts +1 -0
- package/src/lms.module.ts +3 -0
- package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +16 -0
- package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +16 -0
- package/src/video-resolution-profile/video-resolution-profile.controller.ts +62 -0
- package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +128 -0
- package/src/video-resolution-profile/video-resolution-profile.module.ts +13 -0
- 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
|
-
|
|
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 -
|
|
737
|
-
top: cy -
|
|
738
|
-
width:
|
|
739
|
-
height:
|
|
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-
|
|
498
|
-
<
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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)/
|
|
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)/
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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={() =>
|
|
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="
|
|
868
|
-
<div className="mb-
|
|
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-
|
|
873
|
-
iconSize="size-
|
|
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
|
-
<
|
|
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="
|
|
924
|
-
|
|
925
|
-
|
|
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
|
-
</
|
|
968
|
+
</div>
|
|
928
969
|
</div>
|
|
929
970
|
</div>
|
|
930
971
|
|
|
931
|
-
<div className="mb-
|
|
972
|
+
<div className="mb-2 flex flex-wrap items-center gap-1">
|
|
932
973
|
<Badge
|
|
933
|
-
variant=
|
|
934
|
-
className=
|
|
974
|
+
variant="outline"
|
|
975
|
+
className={`text-[11px] ${STATUS_CLASS[turma.status]}`}
|
|
935
976
|
>
|
|
936
977
|
{t(`status.${turma.status}`)}
|
|
937
978
|
</Badge>
|
|
938
|
-
<span
|
|
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-
|
|
945
|
-
<div className="
|
|
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-
|
|
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="
|
|
971
|
-
<div className="rounded-lg border bg-background p-2
|
|
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
|
|
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={() =>
|
|
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=
|
|
1079
|
-
className=
|
|
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
|
|
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>
|