@hed-hog/lms 0.0.365 → 0.0.366
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/class-group/class-group.controller.d.ts +1 -0
- package/dist/class-group/class-group.controller.d.ts.map +1 -1
- package/dist/class-group/class-group.service.d.ts +1 -0
- package/dist/class-group/class-group.service.d.ts.map +1 -1
- package/dist/course/course-structure.controller.d.ts +4 -2
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +6 -3
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-video-agent-pipeline.service.d.ts +70 -0
- package/dist/course/course-video-agent-pipeline.service.d.ts.map +1 -0
- package/dist/course/course-video-agent-pipeline.service.js +398 -0
- package/dist/course/course-video-agent-pipeline.service.js.map +1 -0
- package/dist/course/course-video-hls.service.d.ts +14 -0
- package/dist/course/course-video-hls.service.d.ts.map +1 -1
- package/dist/course/course-video-hls.service.js +25 -8
- package/dist/course/course-video-hls.service.js.map +1 -1
- package/dist/course/course.controller.d.ts +2 -0
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +5 -0
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +2 -0
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +36 -2
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/ffmpeg.util.d.ts +10 -0
- package/dist/course/ffmpeg.util.d.ts.map +1 -0
- package/dist/course/ffmpeg.util.js +79 -0
- package/dist/course/ffmpeg.util.js.map +1 -0
- package/dist/course/lms-bulk-upload-automation.service.d.ts +3 -1
- package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload-automation.service.js +7 -3
- package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
- package/dist/enterprise/training/training-admin.controller.d.ts +2 -0
- package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
- package/dist/enterprise/training/training-admin.service.d.ts +2 -0
- package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +10 -0
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/dto/heartbeat.dto.d.ts +9 -0
- package/dist/platforma/dto/heartbeat.dto.d.ts.map +1 -0
- package/dist/platforma/dto/heartbeat.dto.js +50 -0
- package/dist/platforma/dto/heartbeat.dto.js.map +1 -0
- package/dist/platforma/handlers/emit-certificate.handler.d.ts +27 -0
- package/dist/platforma/handlers/emit-certificate.handler.d.ts.map +1 -0
- package/dist/platforma/handlers/emit-certificate.handler.js +117 -0
- package/dist/platforma/handlers/emit-certificate.handler.js.map +1 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts +31 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts.map +1 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.js +281 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.js.map +1 -0
- package/dist/platforma/platforma-heartbeat.service.d.ts +10 -0
- package/dist/platforma/platforma-heartbeat.service.d.ts.map +1 -0
- package/dist/platforma/platforma-heartbeat.service.js +50 -0
- package/dist/platforma/platforma-heartbeat.service.js.map +1 -0
- package/dist/platforma/platforma-performance.service.d.ts +121 -0
- package/dist/platforma/platforma-performance.service.d.ts.map +1 -0
- package/dist/platforma/platforma-performance.service.js +500 -0
- package/dist/platforma/platforma-performance.service.js.map +1 -0
- package/dist/platforma/platforma-search.service.d.ts +21 -0
- package/dist/platforma/platforma-search.service.d.ts.map +1 -0
- package/dist/platforma/platforma-search.service.js +64 -0
- package/dist/platforma/platforma-search.service.js.map +1 -0
- package/dist/platforma/platforma.controller.d.ts +115 -1
- package/dist/platforma/platforma.controller.d.ts.map +1 -1
- package/dist/platforma/platforma.controller.js +50 -2
- package/dist/platforma/platforma.controller.js.map +1 -1
- package/dist/realtime/lms-realtime.controller.d.ts +2 -0
- package/dist/realtime/lms-realtime.controller.d.ts.map +1 -1
- package/dist/realtime/lms-realtime.controller.js +31 -0
- package/dist/realtime/lms-realtime.controller.js.map +1 -1
- package/dist/realtime/lms-realtime.service.d.ts +1 -1
- package/dist/realtime/lms-realtime.service.d.ts.map +1 -1
- package/dist/realtime/lms-realtime.service.js.map +1 -1
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +182 -29
- package/hedhog/frontend/app/classes/_components/classes-calendar-view.tsx.ejs +277 -0
- package/hedhog/frontend/app/classes/page.tsx.ejs +127 -20
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +141 -30
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +13 -13
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +11 -23
- package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +1 -8
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +2 -0
- package/hedhog/frontend/app/courses/page.tsx.ejs +40 -9
- package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +6 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-classes-calendar-tab.tsx.ejs +264 -0
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +104 -47
- package/hedhog/frontend/app/exams/page.tsx.ejs +38 -4
- package/hedhog/frontend/app/instructors/page.tsx.ejs +87 -46
- package/hedhog/frontend/app/paths/page.tsx.ejs +38 -4
- package/hedhog/frontend/app/training/page.tsx.ejs +38 -4
- package/hedhog/frontend/messages/en.json +18 -0
- package/hedhog/frontend/messages/pt.json +21 -1
- package/hedhog/table/course_enrollment.yaml +3 -0
- package/hedhog/table/lesson_view_event.yaml +66 -0
- package/package.json +9 -8
- package/src/course/course-structure.controller.ts +3 -1
- package/src/course/course-video-agent-pipeline.service.ts +471 -0
- package/src/course/course-video-hls.service.ts +30 -10
- package/src/course/course.module.ts +5 -0
- package/src/course/course.service.ts +46 -1
- package/src/course/ffmpeg.util.ts +65 -0
- package/src/course/lms-bulk-upload-automation.service.ts +4 -1
- package/src/lms.module.ts +10 -0
- package/src/platforma/dto/heartbeat.dto.ts +30 -0
- package/src/platforma/handlers/emit-certificate.handler.ts +117 -0
- package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -0
- package/src/platforma/platforma-heartbeat.service.ts +33 -0
- package/src/platforma/platforma-performance.service.ts +606 -0
- package/src/platforma/platforma-search.service.ts +48 -0
- package/src/platforma/platforma.controller.ts +42 -0
- package/src/realtime/lms-realtime.controller.ts +27 -1
- package/src/realtime/lms-realtime.service.ts +2 -1
package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs
CHANGED
|
@@ -262,7 +262,7 @@ export function CourseXpOverviewTab({ courseId, locale }: Props) {
|
|
|
262
262
|
<div className="rounded-2xl border border-border/70 bg-linear-to-br from-background via-background to-muted/30 px-2.5 py-2 shadow-[0_14px_34px_-28px_rgba(15,23,42,0.45)] backdrop-blur-sm sm:px-3 sm:py-2.5">
|
|
263
263
|
<div className="flex flex-wrap items-start justify-between gap-3 border-b border-border/50 pb-2">
|
|
264
264
|
<div className="min-w-0">
|
|
265
|
-
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-
|
|
265
|
+
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-muted-foreground">
|
|
266
266
|
Course XP overview
|
|
267
267
|
</div>
|
|
268
268
|
<div className="mt-1 text-sm font-semibold text-foreground">
|
|
@@ -301,14 +301,14 @@ export function CourseXpOverviewTab({ courseId, locale }: Props) {
|
|
|
301
301
|
|
|
302
302
|
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.06fr)_minmax(0,0.94fr)]">
|
|
303
303
|
<Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
|
|
304
|
-
<CardHeader className="border-b border-border/70
|
|
304
|
+
<CardHeader className="border-b border-border/70 pt-2.5 pb-1.5!">
|
|
305
305
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
306
306
|
<div className="flex min-w-0 items-start gap-3">
|
|
307
307
|
<div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-teal-500/10 text-teal-600 ring-1 ring-inset ring-teal-500/15 dark:text-teal-400">
|
|
308
308
|
<ChartPie className="size-4" />
|
|
309
309
|
</div>
|
|
310
310
|
<div className="min-w-0">
|
|
311
|
-
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-
|
|
311
|
+
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-muted-foreground">
|
|
312
312
|
Macro areas
|
|
313
313
|
</div>
|
|
314
314
|
<CardTitle className="mt-1 text-sm font-semibold">
|
|
@@ -319,7 +319,7 @@ export function CourseXpOverviewTab({ courseId, locale }: Props) {
|
|
|
319
319
|
</CardDescription>
|
|
320
320
|
</div>
|
|
321
321
|
</div>
|
|
322
|
-
<div className="rounded-full border border-
|
|
322
|
+
<div className="rounded-full border border-border/60 bg-muted/30 px-2.5 py-1 text-[10px] font-semibold text-muted-foreground shadow-sm">
|
|
323
323
|
{data.areas.length} áreas
|
|
324
324
|
</div>
|
|
325
325
|
</div>
|
|
@@ -388,7 +388,7 @@ export function CourseXpOverviewTab({ courseId, locale }: Props) {
|
|
|
388
388
|
{areaData.map((item, index) => (
|
|
389
389
|
<div
|
|
390
390
|
key={item.xpAreaId}
|
|
391
|
-
className="rounded-2xl border border-border/60 bg-
|
|
391
|
+
className="rounded-2xl border border-border/60 bg-card/90 px-3 py-2 shadow-[0_14px_28px_-24px_rgba(15,23,42,0.45)] transition-all duration-200 hover:-translate-y-0.5 hover:border-border/80 hover:shadow-[0_18px_34px_-22px_rgba(15,23,42,0.5)]"
|
|
392
392
|
>
|
|
393
393
|
<div className="flex items-center gap-2">
|
|
394
394
|
<span
|
|
@@ -423,14 +423,14 @@ export function CourseXpOverviewTab({ courseId, locale }: Props) {
|
|
|
423
423
|
</Card>
|
|
424
424
|
|
|
425
425
|
<Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
|
|
426
|
-
<CardHeader className="border-b border-border/70
|
|
426
|
+
<CardHeader className="border-b border-border/70 pt-2.5 pb-1.5!">
|
|
427
427
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
428
428
|
<div className="flex min-w-0 items-start gap-3">
|
|
429
429
|
<div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-orange-500/10 text-orange-600 ring-1 ring-inset ring-orange-500/15 dark:text-orange-400">
|
|
430
430
|
<BarChart3 className="size-4" />
|
|
431
431
|
</div>
|
|
432
432
|
<div className="min-w-0">
|
|
433
|
-
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-
|
|
433
|
+
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-muted-foreground">
|
|
434
434
|
Skill density
|
|
435
435
|
</div>
|
|
436
436
|
<CardTitle className="mt-1 text-sm font-semibold">
|
|
@@ -441,7 +441,7 @@ export function CourseXpOverviewTab({ courseId, locale }: Props) {
|
|
|
441
441
|
</CardDescription>
|
|
442
442
|
</div>
|
|
443
443
|
</div>
|
|
444
|
-
<div className="rounded-full border border-
|
|
444
|
+
<div className="rounded-full border border-border/60 bg-muted/30 px-2.5 py-1 text-[10px] font-semibold text-muted-foreground shadow-sm">
|
|
445
445
|
{data.skills.length} skills
|
|
446
446
|
</div>
|
|
447
447
|
</div>
|
|
@@ -518,7 +518,7 @@ export function CourseXpOverviewTab({ courseId, locale }: Props) {
|
|
|
518
518
|
{skillData.map((item, index) => (
|
|
519
519
|
<div
|
|
520
520
|
key={item.xpSkillId}
|
|
521
|
-
className="rounded-2xl border border-border/60 bg-
|
|
521
|
+
className="rounded-2xl border border-border/60 bg-card/90 px-3 py-2 shadow-[0_14px_28px_-24px_rgba(15,23,42,0.45)] transition-all duration-200 hover:-translate-y-0.5 hover:border-border/80 hover:shadow-[0_18px_34px_-22px_rgba(15,23,42,0.5)]"
|
|
522
522
|
>
|
|
523
523
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
524
524
|
<div className="flex min-w-0 items-center gap-2">
|
|
@@ -556,14 +556,14 @@ export function CourseXpOverviewTab({ courseId, locale }: Props) {
|
|
|
556
556
|
|
|
557
557
|
{learningTypeData.length > 0 && (
|
|
558
558
|
<Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
|
|
559
|
-
<CardHeader className="border-b border-border/70
|
|
559
|
+
<CardHeader className="border-b border-border/70 pt-2.5 pb-1.5!">
|
|
560
560
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
561
561
|
<div className="flex min-w-0 items-start gap-3">
|
|
562
562
|
<div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-violet-500/10 text-violet-600 ring-1 ring-inset ring-violet-500/15 dark:text-violet-400">
|
|
563
563
|
<Sparkles className="size-4" />
|
|
564
564
|
</div>
|
|
565
565
|
<div className="min-w-0">
|
|
566
|
-
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-
|
|
566
|
+
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-muted-foreground">
|
|
567
567
|
Learning mix
|
|
568
568
|
</div>
|
|
569
569
|
<CardTitle className="mt-1 text-sm font-semibold">
|
|
@@ -574,7 +574,7 @@ export function CourseXpOverviewTab({ courseId, locale }: Props) {
|
|
|
574
574
|
</CardDescription>
|
|
575
575
|
</div>
|
|
576
576
|
</div>
|
|
577
|
-
<div className="rounded-full border border-
|
|
577
|
+
<div className="rounded-full border border-border/60 bg-muted/30 px-2.5 py-1 text-[10px] font-semibold text-muted-foreground shadow-sm">
|
|
578
578
|
{learningTypeData.length} tipos
|
|
579
579
|
</div>
|
|
580
580
|
</div>
|
|
@@ -583,7 +583,7 @@ export function CourseXpOverviewTab({ courseId, locale }: Props) {
|
|
|
583
583
|
{learningTypeData.map((item, index) => (
|
|
584
584
|
<div
|
|
585
585
|
key={item.xpLearningTypeId}
|
|
586
|
-
className="flex h-full flex-col justify-start gap-1.5 rounded-2xl border border-border/60 bg-
|
|
586
|
+
className="flex h-full flex-col justify-start gap-1.5 rounded-2xl border border-border/60 bg-card/90 px-3 py-2 shadow-[0_14px_28px_-24px_rgba(15,23,42,0.45)] transition-all duration-200 hover:-translate-y-0.5 hover:border-border/80 hover:shadow-[0_18px_34px_-22px_rgba(15,23,42,0.5)]"
|
|
587
587
|
>
|
|
588
588
|
<div className="flex flex-wrap items-center gap-2">
|
|
589
589
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
@@ -435,7 +435,7 @@ export function LessonXpTab({
|
|
|
435
435
|
icon={<Cpu className="size-4 text-sky-600" />}
|
|
436
436
|
total={xpMap.totalXp}
|
|
437
437
|
items={areaDistribution}
|
|
438
|
-
toneClassName="
|
|
438
|
+
toneClassName=""
|
|
439
439
|
/>
|
|
440
440
|
<DistributionCard
|
|
441
441
|
title="Skills"
|
|
@@ -443,7 +443,7 @@ export function LessonXpTab({
|
|
|
443
443
|
icon={<Zap className="size-4 text-emerald-600" />}
|
|
444
444
|
total={xpMap.totalXp}
|
|
445
445
|
items={skillDistribution}
|
|
446
|
-
toneClassName="
|
|
446
|
+
toneClassName=""
|
|
447
447
|
/>
|
|
448
448
|
<TimelineCard
|
|
449
449
|
segmentViews={segmentViews}
|
|
@@ -670,7 +670,7 @@ function TimelineCard({
|
|
|
670
670
|
|
|
671
671
|
return (
|
|
672
672
|
<Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)] xl:col-span-2">
|
|
673
|
-
<CardHeader className="border-b border-border/60
|
|
673
|
+
<CardHeader className="border-b border-border/60 pt-3 pb-1.5!">
|
|
674
674
|
<div className="flex items-center justify-between gap-3">
|
|
675
675
|
<div>
|
|
676
676
|
<CardTitle className="text-sm font-semibold">
|
|
@@ -761,7 +761,7 @@ function LearningTypesCard({ items }: { items: DistributionItem[] }) {
|
|
|
761
761
|
|
|
762
762
|
return (
|
|
763
763
|
<Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)] xl:col-span-2">
|
|
764
|
-
<CardHeader className="border-b border-border/60
|
|
764
|
+
<CardHeader className="border-b border-border/60 pt-3 pb-1.5!">
|
|
765
765
|
<div className="flex items-center justify-between gap-3">
|
|
766
766
|
<CardTitle className="text-sm font-semibold">
|
|
767
767
|
Tipos de aprendizado
|
|
@@ -775,7 +775,7 @@ function LearningTypesCard({ items }: { items: DistributionItem[] }) {
|
|
|
775
775
|
{items.map((item) => (
|
|
776
776
|
<div
|
|
777
777
|
key={item.id}
|
|
778
|
-
className="flex h-full flex-col justify-start gap-1.5 rounded-xl border border-border/60 bg-muted/20 px-2.5 py-2 text-xs shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:border-
|
|
778
|
+
className="flex h-full flex-col justify-start gap-1.5 rounded-xl border border-border/60 bg-muted/20 px-2.5 py-2 text-xs shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:border-border/80 hover:bg-background/95 hover:shadow-md"
|
|
779
779
|
>
|
|
780
780
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
781
781
|
<span className="min-w-0 flex-1 font-medium">{item.label}</span>
|
|
@@ -813,7 +813,7 @@ function SegmentsSection({
|
|
|
813
813
|
}) {
|
|
814
814
|
return (
|
|
815
815
|
<Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
|
|
816
|
-
<CardHeader className="border-b border-border/60
|
|
816
|
+
<CardHeader className="border-b border-border/60 pt-3 pb-1.5!">
|
|
817
817
|
<div className="flex items-center justify-between gap-3">
|
|
818
818
|
<CardTitle className="text-sm font-semibold">
|
|
819
819
|
Segmentos ({segments.length})
|
|
@@ -829,14 +829,14 @@ function SegmentsSection({
|
|
|
829
829
|
<div
|
|
830
830
|
key={segment.id}
|
|
831
831
|
className={cn(
|
|
832
|
-
'flex flex-col gap-2 rounded-2xl border border-border/60 bg-
|
|
832
|
+
'flex flex-col gap-2 rounded-2xl border border-border/60 bg-card/90 px-3 py-2.5 text-xs shadow-[0_16px_30px_-24px_rgba(15,23,42,0.45)] transition-all duration-200 hover:-translate-y-0.5 hover:border-border/80 hover:shadow-[0_20px_36px_-24px_rgba(15,23,42,0.52)]',
|
|
833
833
|
!segment.shouldGrantXp && 'opacity-50'
|
|
834
834
|
)}
|
|
835
835
|
>
|
|
836
836
|
<div className="flex flex-wrap items-start justify-between gap-2">
|
|
837
837
|
<div className="flex min-w-0 flex-col gap-1">
|
|
838
838
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
839
|
-
<span className="rounded-full border border-
|
|
839
|
+
<span className="rounded-full border border-border/60 bg-muted/30 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
840
840
|
{segment.label}
|
|
841
841
|
</span>
|
|
842
842
|
<span className="rounded-full border border-border/60 bg-background/90 px-2 py-0.5 font-mono text-[10px] text-muted-foreground shadow-sm transition-all duration-200 hover:border-border hover:bg-background hover:text-foreground hover:shadow-md">
|
|
@@ -856,7 +856,7 @@ function SegmentsSection({
|
|
|
856
856
|
</div>
|
|
857
857
|
|
|
858
858
|
<div className="flex flex-wrap items-center justify-end gap-1.5">
|
|
859
|
-
<span className="rounded-full border border-
|
|
859
|
+
<span className="rounded-full border border-border/60 bg-muted/30 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
860
860
|
{segment.xpValue} XP
|
|
861
861
|
</span>
|
|
862
862
|
<DifficultyBadge difficulty={segment.difficulty} />
|
|
@@ -1031,14 +1031,7 @@ function MetricPill({
|
|
|
1031
1031
|
value: string;
|
|
1032
1032
|
tone?: 'default' | 'sky' | 'emerald' | 'violet';
|
|
1033
1033
|
}) {
|
|
1034
|
-
const toneClassName =
|
|
1035
|
-
tone === 'sky'
|
|
1036
|
-
? 'border-sky-500/15 bg-sky-500/10 text-sky-700 dark:text-sky-300'
|
|
1037
|
-
: tone === 'emerald'
|
|
1038
|
-
? 'border-emerald-500/15 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
|
|
1039
|
-
: tone === 'violet'
|
|
1040
|
-
? 'border-violet-500/15 bg-violet-500/10 text-violet-700 dark:text-violet-300'
|
|
1041
|
-
: 'border-border/60 bg-muted/40 text-foreground';
|
|
1034
|
+
const toneClassName = 'border-border/60 bg-muted/40 text-foreground';
|
|
1042
1035
|
|
|
1043
1036
|
return (
|
|
1044
1037
|
<div
|
|
@@ -1069,12 +1062,7 @@ function SegmentBreakdownGroup({
|
|
|
1069
1062
|
color: string;
|
|
1070
1063
|
}>;
|
|
1071
1064
|
}) {
|
|
1072
|
-
const toneClassName =
|
|
1073
|
-
tone === 'sky'
|
|
1074
|
-
? 'border-sky-500/15 bg-sky-500/8'
|
|
1075
|
-
: tone === 'emerald'
|
|
1076
|
-
? 'border-emerald-500/15 bg-emerald-500/8'
|
|
1077
|
-
: 'border-violet-500/15 bg-violet-500/8';
|
|
1065
|
+
const toneClassName = 'border-border/60 bg-muted/20';
|
|
1078
1066
|
|
|
1079
1067
|
if (!items.length) {
|
|
1080
1068
|
return null;
|
|
@@ -11,14 +11,7 @@ export function XpHighlightPill({
|
|
|
11
11
|
value: string;
|
|
12
12
|
tone: 'sky' | 'amber' | 'emerald' | 'violet';
|
|
13
13
|
}) {
|
|
14
|
-
const toneClassName =
|
|
15
|
-
tone === 'sky'
|
|
16
|
-
? 'border-sky-500/15 bg-sky-500/10 text-sky-700 dark:text-sky-300'
|
|
17
|
-
: tone === 'amber'
|
|
18
|
-
? 'border-amber-500/15 bg-amber-500/10 text-amber-700 dark:text-amber-300'
|
|
19
|
-
: tone === 'emerald'
|
|
20
|
-
? 'border-emerald-500/15 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
|
|
21
|
-
: 'border-violet-500/15 bg-violet-500/10 text-violet-700 dark:text-violet-300';
|
|
14
|
+
const toneClassName = 'border-border/60 bg-muted/30 text-muted-foreground';
|
|
22
15
|
|
|
23
16
|
return (
|
|
24
17
|
<div
|
|
@@ -318,7 +318,7 @@ const OFFERING_TYPE_COLOR: Record<string, string> = {
|
|
|
318
318
|
hibrido: 'bg-teal-50 text-teal-700 border-teal-200',
|
|
319
319
|
};
|
|
320
320
|
|
|
321
|
-
const
|
|
321
|
+
const DEFAULT_PAGE_SIZES = [6, 12, 24, 48, 96] as const;
|
|
322
322
|
|
|
323
323
|
// ── Seed Data ─────────────────────────────────────────────────────────────────
|
|
324
324
|
|
|
@@ -388,10 +388,44 @@ export default function CursosPage() {
|
|
|
388
388
|
|
|
389
389
|
// Pagination
|
|
390
390
|
const [currentPage, setCurrentPage] = useState(1);
|
|
391
|
+
|
|
392
|
+
const { data: generalSettings } = useQuery<{
|
|
393
|
+
data: Array<{ slug: string; value: string }>;
|
|
394
|
+
}>({
|
|
395
|
+
queryKey: ['setting-group-general'],
|
|
396
|
+
queryFn: async () => {
|
|
397
|
+
const response = await request<{
|
|
398
|
+
data: Array<{ slug: string; value: string }>;
|
|
399
|
+
}>({
|
|
400
|
+
url: '/setting/group/general',
|
|
401
|
+
method: 'GET',
|
|
402
|
+
});
|
|
403
|
+
return response.data;
|
|
404
|
+
},
|
|
405
|
+
staleTime: 5 * 60 * 1000,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const pageSizeOptions = useMemo(() => {
|
|
409
|
+
const setting = generalSettings?.data?.find(
|
|
410
|
+
(s) => s.slug === 'pagination-page-sizes'
|
|
411
|
+
);
|
|
412
|
+
if (!setting?.value) return DEFAULT_PAGE_SIZES;
|
|
413
|
+
try {
|
|
414
|
+
const parsed = JSON.parse(setting.value) as string[];
|
|
415
|
+
const sizes = parsed
|
|
416
|
+
.map(Number)
|
|
417
|
+
.filter((n) => !isNaN(n) && n > 0)
|
|
418
|
+
.sort((a, b) => a - b);
|
|
419
|
+
return sizes.length > 0 ? sizes : DEFAULT_PAGE_SIZES;
|
|
420
|
+
} catch {
|
|
421
|
+
return DEFAULT_PAGE_SIZES;
|
|
422
|
+
}
|
|
423
|
+
}, [generalSettings]);
|
|
424
|
+
|
|
391
425
|
const [pageSize, setPageSize] = usePersistedPageSize({
|
|
392
426
|
storageKey: 'pagination:global:pageSize',
|
|
393
427
|
defaultValue: 12,
|
|
394
|
-
allowedValues:
|
|
428
|
+
allowedValues: pageSizeOptions,
|
|
395
429
|
});
|
|
396
430
|
|
|
397
431
|
const form = useForm<CourseSheetFormValues>({
|
|
@@ -1091,7 +1125,7 @@ export default function CursosPage() {
|
|
|
1091
1125
|
],
|
|
1092
1126
|
},
|
|
1093
1127
|
]}
|
|
1094
|
-
|
|
1128
|
+
actions={
|
|
1095
1129
|
<ViewModeToggle
|
|
1096
1130
|
viewMode={viewMode}
|
|
1097
1131
|
onViewModeChange={setViewMode}
|
|
@@ -1292,12 +1326,9 @@ export default function CursosPage() {
|
|
|
1292
1326
|
/>
|
|
1293
1327
|
) : (
|
|
1294
1328
|
<div className="relative">
|
|
1295
|
-
{cardsRefreshing && (
|
|
1296
|
-
<div className="absolute inset-0 z-10 rounded-2xl bg-background/55 backdrop-blur-[1px]" />
|
|
1297
|
-
)}
|
|
1298
1329
|
{viewMode === 'cards' ? (
|
|
1299
1330
|
<motion.div
|
|
1300
|
-
className=
|
|
1331
|
+
className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
|
|
1301
1332
|
variants={stagger}
|
|
1302
1333
|
initial="hidden"
|
|
1303
1334
|
animate="show"
|
|
@@ -1464,7 +1495,7 @@ export default function CursosPage() {
|
|
|
1464
1495
|
</motion.div>
|
|
1465
1496
|
) : (
|
|
1466
1497
|
<div
|
|
1467
|
-
className=
|
|
1498
|
+
className="overflow-hidden rounded-xl border border-border/70"
|
|
1468
1499
|
>
|
|
1469
1500
|
<Table>
|
|
1470
1501
|
<TableHeader>
|
|
@@ -1672,7 +1703,7 @@ export default function CursosPage() {
|
|
|
1672
1703
|
setPageSize(nextPageSize);
|
|
1673
1704
|
setCurrentPage(1);
|
|
1674
1705
|
}}
|
|
1675
|
-
pageSizeOptions={
|
|
1706
|
+
pageSizeOptions={pageSizeOptions}
|
|
1676
1707
|
selectedCount={selectedIds.size}
|
|
1677
1708
|
/>
|
|
1678
1709
|
</div>
|
|
@@ -25,6 +25,7 @@ import { notFound, useParams, useRouter } from 'next/navigation';
|
|
|
25
25
|
import { useState } from 'react';
|
|
26
26
|
import { AdministratorsTab } from '../_components/enterprise-administrators-tab';
|
|
27
27
|
import { ClassesTab } from '../_components/enterprise-classes-tab';
|
|
28
|
+
import { ClassesCalendarTab } from '../_components/enterprise-classes-calendar-tab';
|
|
28
29
|
import { CoursesTab } from '../_components/enterprise-courses-tab';
|
|
29
30
|
import { EnterpriseSheet } from '../_components/enterprise-sheet';
|
|
30
31
|
import { StudentsTab } from '../_components/enterprise-students-tab';
|
|
@@ -265,6 +266,7 @@ export default function EnterpriseDetailPage() {
|
|
|
265
266
|
<TabsTrigger value="overview">{t('tabs.overview')}</TabsTrigger>
|
|
266
267
|
<TabsTrigger value="crm">{t('tabs.crm')}</TabsTrigger>
|
|
267
268
|
<TabsTrigger value="classes">{t('tabs.classes')}</TabsTrigger>
|
|
269
|
+
<TabsTrigger value="classesCalendar">{t('tabs.classesCalendar')}</TabsTrigger>
|
|
268
270
|
<TabsTrigger value="courses">{t('tabs.courses')}</TabsTrigger>
|
|
269
271
|
<TabsTrigger value="students">{t('tabs.students')}</TabsTrigger>
|
|
270
272
|
<TabsTrigger value="administrators">
|
|
@@ -288,6 +290,10 @@ export default function EnterpriseDetailPage() {
|
|
|
288
290
|
<ClassesTab enterpriseId={account.id} />
|
|
289
291
|
</TabsContent>
|
|
290
292
|
|
|
293
|
+
<TabsContent value="classesCalendar" className="mt-4 px-1">
|
|
294
|
+
<ClassesCalendarTab enterpriseId={account.id} />
|
|
295
|
+
</TabsContent>
|
|
296
|
+
|
|
291
297
|
<TabsContent value="courses" className="mt-4">
|
|
292
298
|
<CoursesTab enterpriseId={account.id} />
|
|
293
299
|
</TabsContent>
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Badge } from '@/components/ui/badge';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
6
|
+
import {
|
|
7
|
+
addMonths,
|
|
8
|
+
addDays,
|
|
9
|
+
eachDayOfInterval,
|
|
10
|
+
format,
|
|
11
|
+
isAfter,
|
|
12
|
+
isBefore,
|
|
13
|
+
isSameDay,
|
|
14
|
+
isSameMonth,
|
|
15
|
+
isToday,
|
|
16
|
+
parseISO,
|
|
17
|
+
startOfMonth,
|
|
18
|
+
startOfWeek,
|
|
19
|
+
subMonths,
|
|
20
|
+
} from 'date-fns';
|
|
21
|
+
import { ptBR } from 'date-fns/locale/pt-BR';
|
|
22
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
23
|
+
import { useLocale, useTranslations } from 'next-intl';
|
|
24
|
+
import { useMemo, useState } from 'react';
|
|
25
|
+
import { useRouter } from 'next/navigation';
|
|
26
|
+
import type { EnterpriseClass } from './enterprise-types';
|
|
27
|
+
|
|
28
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
29
|
+
open: 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300',
|
|
30
|
+
ongoing:
|
|
31
|
+
'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300',
|
|
32
|
+
completed:
|
|
33
|
+
'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300',
|
|
34
|
+
cancelled:
|
|
35
|
+
'bg-muted text-muted-foreground line-through',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const DAY_HEADERS_PT = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom'];
|
|
39
|
+
const DAY_HEADERS_EN = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
40
|
+
|
|
41
|
+
function parseDate(value: string | null | undefined): Date | null {
|
|
42
|
+
if (!value) return null;
|
|
43
|
+
try {
|
|
44
|
+
const d = parseISO(String(value).slice(0, 10));
|
|
45
|
+
return isNaN(d.getTime()) ? null : d;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isClassActiveOnDay(
|
|
52
|
+
cls: EnterpriseClass,
|
|
53
|
+
day: Date
|
|
54
|
+
): boolean {
|
|
55
|
+
const start = parseDate(cls.startDate);
|
|
56
|
+
const end = parseDate(cls.endDate);
|
|
57
|
+
if (!start) return false;
|
|
58
|
+
const dayStart = new Date(day.getFullYear(), day.getMonth(), day.getDate());
|
|
59
|
+
const startDay = new Date(start.getFullYear(), start.getMonth(), start.getDate());
|
|
60
|
+
const endDay = end
|
|
61
|
+
? new Date(end.getFullYear(), end.getMonth(), end.getDate())
|
|
62
|
+
: startDay;
|
|
63
|
+
return (
|
|
64
|
+
(isSameDay(dayStart, startDay) || isAfter(dayStart, startDay)) &&
|
|
65
|
+
(isSameDay(dayStart, endDay) || isBefore(dayStart, endDay))
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function ClassesCalendarTab({
|
|
70
|
+
enterpriseId,
|
|
71
|
+
}: {
|
|
72
|
+
enterpriseId: number;
|
|
73
|
+
}) {
|
|
74
|
+
const { request } = useApp();
|
|
75
|
+
const router = useRouter();
|
|
76
|
+
const locale = useLocale();
|
|
77
|
+
const t = useTranslations('lms.EnterpriseDetailPage');
|
|
78
|
+
const dateLocale = locale.startsWith('pt') ? ptBR : undefined;
|
|
79
|
+
const dayHeaders = locale.startsWith('pt') ? DAY_HEADERS_PT : DAY_HEADERS_EN;
|
|
80
|
+
|
|
81
|
+
const [currentDate, setCurrentDate] = useState(() => new Date());
|
|
82
|
+
const [selectedDay, setSelectedDay] = useState<Date | null>(null);
|
|
83
|
+
|
|
84
|
+
const { data, isLoading } = useQuery<{ data: EnterpriseClass[] }>({
|
|
85
|
+
queryKey: ['enterprise-classes-calendar', enterpriseId],
|
|
86
|
+
queryFn: async () =>
|
|
87
|
+
request<{ data: EnterpriseClass[] }>({
|
|
88
|
+
url: `/lms/enterprise/${enterpriseId}/classes?page=1&pageSize=500`,
|
|
89
|
+
method: 'GET',
|
|
90
|
+
}).then((r) => r.data),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const classes = useMemo(() => data?.data ?? [], [data]);
|
|
94
|
+
|
|
95
|
+
const monthStart = startOfMonth(currentDate);
|
|
96
|
+
const gridStart = startOfWeek(monthStart, { weekStartsOn: 1 });
|
|
97
|
+
const days = eachDayOfInterval({ start: gridStart, end: addDays(gridStart, 41) });
|
|
98
|
+
|
|
99
|
+
const getClassesForDay = (day: Date) =>
|
|
100
|
+
classes.filter((cls) => isClassActiveOnDay(cls, day));
|
|
101
|
+
|
|
102
|
+
const selectedDayClasses = selectedDay ? getClassesForDay(selectedDay) : [];
|
|
103
|
+
|
|
104
|
+
const monthLabel = format(
|
|
105
|
+
currentDate,
|
|
106
|
+
locale.startsWith('pt') ? "MMMM 'de' yyyy" : 'MMMM yyyy',
|
|
107
|
+
{ locale: dateLocale }
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className="space-y-4 pb-6">
|
|
112
|
+
{/* Month navigation */}
|
|
113
|
+
<div className="flex items-center justify-between rounded-xl border bg-muted/20 px-3 py-2">
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
onClick={() => { setCurrentDate((d) => subMonths(d, 1)); setSelectedDay(null); }}
|
|
117
|
+
className="flex size-8 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
118
|
+
aria-label="Mês anterior"
|
|
119
|
+
>
|
|
120
|
+
<ChevronLeft className="size-4" />
|
|
121
|
+
</button>
|
|
122
|
+
<span className="text-sm font-semibold capitalize">{monthLabel}</span>
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
onClick={() => { setCurrentDate((d) => addMonths(d, 1)); setSelectedDay(null); }}
|
|
126
|
+
className="flex size-8 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
127
|
+
aria-label="Próximo mês"
|
|
128
|
+
>
|
|
129
|
+
<ChevronRight className="size-4" />
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{/* Day headers */}
|
|
134
|
+
<div className="grid grid-cols-7 text-center">
|
|
135
|
+
{dayHeaders.map((h) => (
|
|
136
|
+
<div key={h} className="pb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
137
|
+
{h}
|
|
138
|
+
</div>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{/* Calendar grid */}
|
|
143
|
+
{isLoading ? (
|
|
144
|
+
<div className="grid grid-cols-7 gap-px">
|
|
145
|
+
{Array.from({ length: 42 }).map((_, i) => (
|
|
146
|
+
<div key={i} className="h-16 animate-pulse rounded bg-muted/30" />
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
) : (
|
|
150
|
+
<div className="grid grid-cols-7 gap-px rounded-lg border bg-border/40 overflow-hidden">
|
|
151
|
+
{days.map((day) => {
|
|
152
|
+
const dayClasses = getClassesForDay(day);
|
|
153
|
+
const isCurrentMonth = isSameMonth(day, currentDate);
|
|
154
|
+
const isSelected = selectedDay ? isSameDay(day, selectedDay) : false;
|
|
155
|
+
const todayDay = isToday(day);
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<button
|
|
159
|
+
key={day.toISOString()}
|
|
160
|
+
type="button"
|
|
161
|
+
onClick={() => setSelectedDay(isSelected ? null : day)}
|
|
162
|
+
className={[
|
|
163
|
+
'flex min-h-16 flex-col gap-0.5 bg-background p-1 text-left transition-colors hover:bg-muted/40',
|
|
164
|
+
!isCurrentMonth && 'opacity-40',
|
|
165
|
+
isSelected && 'ring-2 ring-inset ring-primary',
|
|
166
|
+
].filter(Boolean).join(' ')}
|
|
167
|
+
>
|
|
168
|
+
<span
|
|
169
|
+
className={[
|
|
170
|
+
'flex size-5 items-center justify-center self-end rounded-full text-[11px] font-medium',
|
|
171
|
+
todayDay
|
|
172
|
+
? 'bg-primary text-primary-foreground'
|
|
173
|
+
: 'text-foreground',
|
|
174
|
+
].join(' ')}
|
|
175
|
+
>
|
|
176
|
+
{format(day, 'd')}
|
|
177
|
+
</span>
|
|
178
|
+
<div className="flex min-h-0 flex-col gap-0.5 overflow-hidden">
|
|
179
|
+
{dayClasses.slice(0, 3).map((cls) => (
|
|
180
|
+
<span
|
|
181
|
+
key={cls.id}
|
|
182
|
+
className={[
|
|
183
|
+
'truncate rounded px-1 text-[9px] font-medium leading-4',
|
|
184
|
+
STATUS_COLORS[cls.status] ?? STATUS_COLORS.open,
|
|
185
|
+
].join(' ')}
|
|
186
|
+
>
|
|
187
|
+
{cls.courseTitle ?? cls.title ?? `#${cls.id}`}
|
|
188
|
+
</span>
|
|
189
|
+
))}
|
|
190
|
+
{dayClasses.length > 3 && (
|
|
191
|
+
<span className="px-1 text-[9px] text-muted-foreground">
|
|
192
|
+
+{dayClasses.length - 3}
|
|
193
|
+
</span>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</button>
|
|
197
|
+
);
|
|
198
|
+
})}
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{/* Selected day detail */}
|
|
203
|
+
{selectedDay && (
|
|
204
|
+
<div className="space-y-2 rounded-xl border bg-muted/20 p-3">
|
|
205
|
+
<p className="text-xs font-semibold text-muted-foreground">
|
|
206
|
+
{format(selectedDay, locale.startsWith('pt') ? "d 'de' MMMM" : 'MMMM d', { locale: dateLocale })}
|
|
207
|
+
</p>
|
|
208
|
+
{selectedDayClasses.length === 0 ? (
|
|
209
|
+
<p className="text-sm text-muted-foreground">Nenhuma turma neste dia.</p>
|
|
210
|
+
) : (
|
|
211
|
+
<div className="space-y-1.5">
|
|
212
|
+
{selectedDayClasses.map((cls) => (
|
|
213
|
+
<button
|
|
214
|
+
key={cls.id}
|
|
215
|
+
type="button"
|
|
216
|
+
onClick={() => router.push(`/lms/classes/${cls.id}`)}
|
|
217
|
+
className="flex w-full items-center gap-3 rounded-lg border bg-background px-3 py-2 text-left transition-colors hover:bg-muted/50"
|
|
218
|
+
>
|
|
219
|
+
<div className="min-w-0 flex-1">
|
|
220
|
+
<p className="truncate text-sm font-medium">
|
|
221
|
+
{cls.courseTitle ?? cls.title ?? `Turma #${cls.id}`}
|
|
222
|
+
</p>
|
|
223
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
224
|
+
{[cls.code, cls.instructorName].filter(Boolean).join(' • ')}
|
|
225
|
+
</p>
|
|
226
|
+
</div>
|
|
227
|
+
<div className="flex shrink-0 flex-col items-end gap-1">
|
|
228
|
+
<Badge
|
|
229
|
+
variant="outline"
|
|
230
|
+
className={['text-[10px] px-1.5 py-0', STATUS_COLORS[cls.status]].join(' ')}
|
|
231
|
+
>
|
|
232
|
+
{cls.status}
|
|
233
|
+
</Badge>
|
|
234
|
+
{cls.capacity != null && (
|
|
235
|
+
<span className="text-[10px] text-muted-foreground">
|
|
236
|
+
{cls.enrolledCount}/{cls.capacity}
|
|
237
|
+
</span>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
</button>
|
|
241
|
+
))}
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
)}
|
|
246
|
+
|
|
247
|
+
{/* Legend */}
|
|
248
|
+
<div className="flex flex-wrap gap-3">
|
|
249
|
+
{Object.entries({
|
|
250
|
+
open: 'Aberta',
|
|
251
|
+
ongoing: 'Em andamento',
|
|
252
|
+
completed: 'Concluída',
|
|
253
|
+
cancelled: 'Cancelada',
|
|
254
|
+
}).map(([status, label]) => (
|
|
255
|
+
<div key={status} className="flex items-center gap-1.5">
|
|
256
|
+
<span className={['rounded px-1.5 py-0.5 text-[10px] font-medium', STATUS_COLORS[status]].join(' ')}>
|
|
257
|
+
{label}
|
|
258
|
+
</span>
|
|
259
|
+
</div>
|
|
260
|
+
))}
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
}
|