@fayz-ai/plugin-courses 0.10.49 → 0.10.51

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/index.js CHANGED
@@ -1,16 +1,13 @@
1
+ import { navigateTo } from './chunk-4DMJIPDN.js';
2
+ import { formatPercent } from './chunk-A4BEN6VV.js';
1
3
  import * as React3 from 'react';
2
4
  import { useState, useEffect, useMemo } from 'react';
3
- import { hashRouterAdapter, createConnectionStore, connectionSettings, connectionStatus, settingsStatus, registerPluginFactory, registerTranslations, useTranslation, formatCurrency, getSupabaseClientOptional, connectorRuntimeToken, CONNECTOR_RUNTIME_TOKEN_HEADER } from '@fayz-ai/core';
5
+ import { createConnectionStore, connectionSettings, connectionStatus, settingsStatus, lazyComponent, registerPluginFactory, registerTranslations, useTranslation, formatCurrency, getSupabaseClientOptional, connectorRuntimeToken, CONNECTOR_RUNTIME_TOKEN_HEADER } from '@fayz-ai/core';
4
6
  import { setCoursesTenantResolver, getCoursesProviderOptional, setCoursesProvider, createMockCoursesProvider, getCoursesTenantId, COURSES_TABLES, getCoursesProvider } from '@fayz-ai/courses';
5
- import { useOrganizationStore, useLimitGuard, usePermissionOptional, PermissionGate, OrderListView, OrderDetailSkeleton, PersonLink, OrderDetailView, invalidateLimit, PluginSettingsPanel, SettingsGroup, SelectRow, ToggleRow } from '@fayz-ai/admin';
6
- import { Button, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, DatePicker, defineKpiWidget, toast, useModuleLayout, PageHeaderActions, Badge, useSaveBar, PageHeader, Separator, Tabs, TabsList, TabsTrigger, TabsContent, Input, Textarea, SegmentedControl, Checkbox, ConfirmDialog, KpiCard, Skeleton, HeaderTabs, Card, DataTable } from '@fayz-ai/ui';
7
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
8
- import { Loader2, Plug, Link2, Download, History, CheckCircle2, AlertCircle, ImagePlus, Users, ShoppingCart, Layers, Tag, UploadCloud, Info, Plus, Trash2, Clock, Lock, Check, GripVertical, ChevronDown, Video, FileText, PlayCircle } from 'lucide-react';
9
- import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCenter } from '@dnd-kit/core';
10
- import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, arrayMove, useSortable } from '@dnd-kit/sortable';
11
- import { CSS } from '@dnd-kit/utilities';
12
-
13
- // src/index.ts
7
+ import { useOrganizationStore } from '@fayz-ai/admin';
8
+ import { Button, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, DatePicker, defineKpiWidget, toast, KpiCard } from '@fayz-ai/ui';
9
+ import { jsxs, jsx } from 'react/jsx-runtime';
10
+ import { Loader2, Plug, Link2, Download, History, CheckCircle2, AlertCircle } from 'lucide-react';
14
11
 
15
12
  // src/locales/en.ts
16
13
  var en = {
@@ -421,1471 +418,6 @@ var coursesLocales = {
421
418
  en,
422
419
  "pt-BR": ptBR
423
420
  };
424
- var adapter = hashRouterAdapter();
425
- function navigateTo(to) {
426
- adapter.navigate(to);
427
- }
428
- function statusVariant(status) {
429
- if (status === "published") return "success";
430
- if (status === "archived") return "outline";
431
- return "secondary";
432
- }
433
- function CoursesListPage() {
434
- const t = useTranslation();
435
- const shellOwnsHeader = useModuleLayout() === "tabs";
436
- const [courses, setCourses] = React3.useState(null);
437
- const [creating, setCreating] = React3.useState(false);
438
- const guardCourses = useLimitGuard("courses");
439
- const can = usePermissionOptional();
440
- const load = React3.useCallback(async () => {
441
- setCourses(await getCoursesProvider().listCourses());
442
- }, []);
443
- React3.useEffect(() => {
444
- void load();
445
- }, [load]);
446
- async function onCreate() {
447
- if (!can("courses", "create")) return;
448
- if (await guardCourses() === "blocked") return;
449
- setCreating(true);
450
- try {
451
- const course = await getCoursesProvider().createCourse({
452
- title: t("courses.editor.untitledCourse") || "Untitled course",
453
- status: "draft"
454
- });
455
- invalidateLimit("courses");
456
- navigateTo(`/courses/${course.id}`);
457
- } finally {
458
- setCreating(false);
459
- }
460
- }
461
- return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-6xl p-6 md:p-8", children: [
462
- shellOwnsHeader ? /* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsx(PermissionGate, { feature: "courses", action: "create", children: /* @__PURE__ */ jsx(Button, { onClick: onCreate, disabled: creating, children: t("courses.new") || "New course" }) }) }) : /* @__PURE__ */ jsxs("div", { className: "mb-6 flex items-center justify-between", children: [
463
- /* @__PURE__ */ jsxs("div", { children: [
464
- /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold text-foreground", children: t("courses.title") || "Courses" }),
465
- courses && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: (t("courses.count") || "{count} courses").replace("{count}", String(courses.length)) })
466
- ] }),
467
- /* @__PURE__ */ jsx(PermissionGate, { feature: "courses", action: "create", children: /* @__PURE__ */ jsx(Button, { onClick: onCreate, disabled: creating, children: t("courses.new") || "New course" }) })
468
- ] }),
469
- courses && courses.length === 0 && /* @__PURE__ */ jsx("div", { className: "rounded-xl border border-dashed border-border p-12 text-center text-muted-foreground", children: t("courses.empty") || "No courses yet. Create your first one." }),
470
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3", children: [
471
- courses === null && Array.from({ length: 6 }).map((_, i) => /* @__PURE__ */ jsx(CourseCardSkeleton, {}, i)),
472
- courses?.map((course) => /* @__PURE__ */ jsxs(
473
- Button,
474
- {
475
- variant: "bare",
476
- size: "bare",
477
- onClick: () => navigateTo(`/courses/${course.id}`),
478
- className: "group overflow-hidden rounded-card border border-border bg-card text-left transition hover:border-primary hover:shadow-md block whitespace-normal font-normal",
479
- children: [
480
- /* @__PURE__ */ jsx("div", { className: "aspect-video w-full overflow-hidden bg-muted", children: course.thumbnailUrl && // eslint-disable-next-line @next/next/no-img-element
481
- /* @__PURE__ */ jsx("img", { src: course.thumbnailUrl, alt: "", className: "h-full w-full object-cover" }) }),
482
- /* @__PURE__ */ jsxs("div", { className: "space-y-2 p-4", children: [
483
- /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
484
- /* @__PURE__ */ jsx("h3", { className: "font-semibold text-foreground line-clamp-2", children: course.title }),
485
- /* @__PURE__ */ jsx(Badge, { variant: statusVariant(course.status), children: t(`courses.status.${course.status}`) || course.status })
486
- ] }),
487
- course.subtitle && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground line-clamp-2", children: course.subtitle }),
488
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: course.price > 0 ? `${course.currency} ${course.price.toFixed(2)}` : "\u2014" })
489
- ] })
490
- ]
491
- },
492
- course.id
493
- ))
494
- ] })
495
- ] });
496
- }
497
- CoursesListPage.displayName = "CoursesListPage";
498
- function CourseCardSkeleton() {
499
- return /* @__PURE__ */ jsxs("div", { className: "overflow-hidden rounded-card border border-border bg-card", children: [
500
- /* @__PURE__ */ jsx(Skeleton, { className: "aspect-video w-full rounded-none" }),
501
- /* @__PURE__ */ jsxs("div", { className: "space-y-3 p-4", children: [
502
- /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
503
- /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-2/3" }),
504
- /* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-16 rounded-full" })
505
- ] }),
506
- /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-full" }),
507
- /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-20" })
508
- ] })
509
- ] });
510
- }
511
- function PageContainer({ title, subtitle, actions, navTabs, activeNavTab, onNavTabChange, children }) {
512
- return /* @__PURE__ */ jsxs("div", { children: [
513
- navTabs && navTabs.length > 0 ? (
514
- // Mesma linha de CrudListView/OrderListView: abas à esquerda, ações à
515
- // direita. Com abas o <h1> sai: a aba ativa já nomeia a tela, e a barra
516
- // de topo nomeia o grupo.
517
- /* @__PURE__ */ jsxs("div", { className: "mb-6 flex flex-wrap items-center justify-between gap-3", children: [
518
- /* @__PURE__ */ jsx(
519
- HeaderTabs,
520
- {
521
- tabs: navTabs,
522
- value: activeNavTab ?? navTabs[0].value,
523
- onChange: (v) => onNavTabChange?.(v)
524
- }
525
- ),
526
- actions
527
- ] })
528
- ) : /* @__PURE__ */ jsxs("div", { className: "mb-6 flex items-start justify-between gap-4", children: [
529
- /* @__PURE__ */ jsxs("div", { children: [
530
- /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold text-foreground", children: title }),
531
- subtitle && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: subtitle })
532
- ] }),
533
- actions
534
- ] }),
535
- children
536
- ] });
537
- }
538
- function StatCard({ label, value, hint }) {
539
- return /* @__PURE__ */ jsxs(Card, { className: "p-5", children: [
540
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: label }),
541
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-2xl font-bold text-foreground", children: value }),
542
- hint && /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: hint })
543
- ] });
544
- }
545
- function StatGrid({ children }) {
546
- return /* @__PURE__ */ jsx("div", { className: "mb-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children });
547
- }
548
- function EmptyState({ icon, title, description, action }) {
549
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center rounded-xl border border-dashed border-border bg-card/40 px-6 py-12 text-center", children: [
550
- icon && /* @__PURE__ */ jsx("div", { className: "mb-3 flex h-11 w-11 items-center justify-center rounded-full bg-muted text-muted-foreground", children: icon }),
551
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: title }),
552
- description && /* @__PURE__ */ jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: description }),
553
- action && /* @__PURE__ */ jsx("div", { className: "mt-4", children: action })
554
- ] });
555
- }
556
- function InfoNote({ icon, children, className }) {
557
- return /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2.5 rounded-lg border border-border bg-muted/40 px-3.5 py-3 text-sm text-muted-foreground " + (className ?? ""), children: [
558
- icon && /* @__PURE__ */ jsx("span", { className: "mt-0.5 shrink-0 text-muted-foreground/80", children: icon }),
559
- /* @__PURE__ */ jsx("div", { className: "min-w-0 [&_strong]:font-medium [&_strong]:text-foreground", children })
560
- ] });
561
- }
562
- function SectionCard({ title, description, actions, children, className }) {
563
- return /* @__PURE__ */ jsxs(Card, { className: "p-5 md:p-6 " + (className ?? ""), children: [
564
- (title || actions) && /* @__PURE__ */ jsxs("div", { className: "mb-4 flex items-start justify-between gap-4", children: [
565
- /* @__PURE__ */ jsxs("div", { children: [
566
- title && /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold text-foreground", children: title }),
567
- description && /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-sm text-muted-foreground", children: description })
568
- ] }),
569
- actions
570
- ] }),
571
- children
572
- ] });
573
- }
574
- function SimpleTable({ columns, rows, empty, loading, onRowClick }) {
575
- const cols = React3.useMemo(
576
- () => columns.map((c) => ({
577
- id: c.key,
578
- header: () => /* @__PURE__ */ jsx("div", { className: c.align === "right" ? "text-right" : "", children: c.header }),
579
- cell: ({ row }) => /* @__PURE__ */ jsx("div", { className: c.align === "right" ? "text-right" : "", children: c.render(row.original) })
580
- })),
581
- [columns]
582
- );
583
- return /* @__PURE__ */ jsx(
584
- DataTable,
585
- {
586
- columns: cols,
587
- data: rows,
588
- emptyMessage: empty,
589
- loading,
590
- onRowClick: onRowClick ? (row) => onRowClick(row) : void 0
591
- }
592
- );
593
- }
594
- function buildCourseStudents(orders) {
595
- const byKey = /* @__PURE__ */ new Map();
596
- for (const o of orders) {
597
- const key = o.customerId ?? o.customerEmail ?? o.id;
598
- const existing = byKey.get(key);
599
- if (!existing || o.createdAt && existing.since && o.createdAt > existing.since) {
600
- byKey.set(key, {
601
- key,
602
- name: o.customerName ?? o.customerEmail ?? "\u2014",
603
- email: o.customerEmail ?? null,
604
- since: o.createdAt ?? null
605
- });
606
- }
607
- }
608
- return [...byKey.values()].sort((a, b) => (b.since ?? "").localeCompare(a.since ?? ""));
609
- }
610
- var STATUSES = ["draft", "published", "archived"];
611
- var DELIVERY_TYPES = ["online", "in_person", "hybrid"];
612
- var TABS = ["details", "curriculum", "offers", "members"];
613
- var TAB_ICON = {
614
- details: Tag,
615
- curriculum: Layers,
616
- offers: ShoppingCart,
617
- members: Users
618
- };
619
- function editSnapshot(c) {
620
- return JSON.stringify({
621
- title: c.title,
622
- subtitle: c.subtitle,
623
- description: c.description,
624
- price: c.price,
625
- status: c.status,
626
- deliveryType: c.deliveryType,
627
- thumbnailUrl: c.thumbnailUrl
628
- });
629
- }
630
- function CourseEditorPage({ id }) {
631
- const t = useTranslation();
632
- const [tab, setTab] = React3.useState("details");
633
- const [course, setCourse] = React3.useState(null);
634
- const [modules, setModules] = React3.useState([]);
635
- const [lessons, setLessons] = React3.useState([]);
636
- const [offers, setOffers] = React3.useState([]);
637
- const [students, setStudents] = React3.useState([]);
638
- const [loading, setLoading] = React3.useState(true);
639
- const [savedSnap, setSavedSnap] = React3.useState("");
640
- const [saving, setSaving] = React3.useState(false);
641
- const [uploading, setUploading] = React3.useState(false);
642
- const fileRef = React3.useRef(null);
643
- const [pendingDelete, setPendingDelete] = React3.useState(null);
644
- const [autoFocusId, setAutoFocusId] = React3.useState(null);
645
- const reload = React3.useCallback(async () => {
646
- if (!id) return;
647
- const provider = getCoursesProvider();
648
- const c = await provider.getCourse(id);
649
- setCourse(c);
650
- if (c) {
651
- setSavedSnap(editSnapshot(c));
652
- const [m, l, o, paidOrders] = await Promise.all([
653
- provider.listModules(c.id),
654
- provider.listLessons(c.id),
655
- provider.listOffers(c.id),
656
- provider.listOrders({ courseId: c.id, financialStatus: "paid" })
657
- ]);
658
- setModules(m);
659
- setLessons(l);
660
- setOffers(o);
661
- setStudents(buildCourseStudents(paidOrders));
662
- }
663
- setLoading(false);
664
- }, [id]);
665
- React3.useEffect(() => {
666
- void reload();
667
- }, [reload]);
668
- const dirty = !!course && editSnapshot(course) !== savedSnap;
669
- useSaveBar({
670
- dirty,
671
- saving,
672
- onSave: () => {
673
- void saveDetails();
674
- },
675
- onDiscard: () => {
676
- void reload();
677
- },
678
- saveLabel: t("courses.editor.save") || "Salvar"
679
- });
680
- const header = /* @__PURE__ */ jsx(
681
- PageHeader,
682
- {
683
- title: course?.title ?? "",
684
- parentLabel: t("courses.title") || "Courses",
685
- onBack: () => navigateTo("/courses")
686
- }
687
- );
688
- if (loading || !course) return /* @__PURE__ */ jsxs(Fragment, { children: [
689
- header,
690
- /* @__PURE__ */ jsx(EditorSkeleton, {})
691
- ] });
692
- function patchCourse(patch) {
693
- setCourse((c) => c ? { ...c, ...patch } : c);
694
- }
695
- async function saveDetails() {
696
- if (!course) return;
697
- setSaving(true);
698
- try {
699
- await getCoursesProvider().updateCourse(course.id, {
700
- title: course.title,
701
- subtitle: course.subtitle ?? void 0,
702
- description: course.description ?? void 0,
703
- thumbnailUrl: course.thumbnailUrl ?? void 0,
704
- price: course.price,
705
- status: course.status,
706
- deliveryType: course.deliveryType
707
- });
708
- setSavedSnap(editSnapshot(course));
709
- toast.success(t("courses.editor.saved") || "Course saved");
710
- } catch {
711
- toast.error(t("courses.editor.saveFailed") || "Could not save the course");
712
- } finally {
713
- setSaving(false);
714
- }
715
- }
716
- async function onPickThumbnail(e) {
717
- const file = e.target.files?.[0];
718
- if (!file || !course) return;
719
- const supabase = getSupabaseClientOptional();
720
- if (!supabase?.storage) {
721
- toast.error(t("courses.editor.uploadUnavailable") || "Upload unavailable \u2014 paste an image URL instead.");
722
- if (fileRef.current) fileRef.current.value = "";
723
- return;
724
- }
725
- setUploading(true);
726
- try {
727
- const ext = file.name.split(".").pop() || "jpg";
728
- const path = `${course.id}/cover-${Date.now()}.${ext}`;
729
- const bucket = supabase.storage.from("course-media");
730
- const { error } = await bucket.upload(path, file, { upsert: true, contentType: file.type });
731
- if (error) throw error;
732
- const { data } = bucket.getPublicUrl(path);
733
- patchCourse({ thumbnailUrl: data.publicUrl });
734
- toast.success(t("courses.editor.thumbnailUploaded") || "Image uploaded \u2014 remember to save.");
735
- } catch {
736
- toast.error(t("courses.editor.uploadFailed") || "Could not upload the image");
737
- } finally {
738
- setUploading(false);
739
- if (fileRef.current) fileRef.current.value = "";
740
- }
741
- }
742
- async function addModule() {
743
- const mod = await getCoursesProvider().createModule({ courseId: course.id, title: t("courses.editor.untitledModule") || "New module" });
744
- toast.success(t("courses.editor.moduleAdded") || "Module added");
745
- await reload();
746
- setAutoFocusId(mod.id);
747
- }
748
- async function renameModule(moduleId, title) {
749
- await getCoursesProvider().updateModule(moduleId, { title });
750
- }
751
- async function addLesson(moduleId) {
752
- const tempId = `tmp-${crypto.randomUUID()}`;
753
- const nextOrder = lessons.filter((l) => l.moduleId === moduleId).reduce((max, l) => Math.max(max, l.sortOrder), -1) + 1;
754
- const optimistic = {
755
- id: tempId,
756
- courseId: course.id,
757
- moduleId,
758
- title: t("courses.editor.untitledLesson") || "New lesson",
759
- description: null,
760
- videoUrl: "",
761
- durationSec: 0,
762
- sortOrder: nextOrder
763
- };
764
- setLessons((ls) => [...ls, optimistic]);
765
- setAutoFocusId(tempId);
766
- try {
767
- const real = await getCoursesProvider().createLesson({
768
- courseId: course.id,
769
- moduleId,
770
- title: optimistic.title,
771
- videoUrl: ""
772
- });
773
- setLessons((ls) => ls.map((l) => l.id === tempId ? { ...l, id: real.id } : l));
774
- setAutoFocusId((cur) => cur === tempId ? real.id : cur);
775
- } catch {
776
- setLessons((ls) => ls.filter((l) => l.id !== tempId));
777
- toast.error(t("courses.editor.saveFailed") || "Could not add the lesson");
778
- }
779
- }
780
- async function patchLesson(lessonId, patch) {
781
- await getCoursesProvider().updateLesson(lessonId, {
782
- title: patch.title,
783
- videoUrl: patch.videoUrl,
784
- durationSec: patch.durationSec,
785
- description: patch.description ?? void 0
786
- });
787
- }
788
- async function reorderModules(orderedIds) {
789
- const prev = modules;
790
- setModules(orderedIds.map((mid, i) => ({ ...prev.find((m) => m.id === mid), sortOrder: i })));
791
- try {
792
- await Promise.all(orderedIds.map((mid, i) => getCoursesProvider().updateModule(mid, { sortOrder: i })));
793
- } catch {
794
- toast.error(t("courses.editor.reorderFailed") || "Could not reorder");
795
- } finally {
796
- await reload();
797
- }
798
- }
799
- async function reorderLessons(moduleId, orderedIds) {
800
- const prev = lessons;
801
- const others = prev.filter((l) => l.moduleId !== moduleId);
802
- const reordered = orderedIds.map((lid, i) => ({ ...prev.find((l) => l.id === lid), sortOrder: i }));
803
- setLessons([...others, ...reordered]);
804
- try {
805
- await Promise.all(orderedIds.map((lid, i) => getCoursesProvider().updateLesson(lid, { sortOrder: i })));
806
- } catch {
807
- toast.error(t("courses.editor.reorderFailed") || "Could not reorder");
808
- } finally {
809
- await reload();
810
- }
811
- }
812
- async function confirmDelete() {
813
- if (!pendingDelete) return;
814
- const provider = getCoursesProvider();
815
- if (pendingDelete.kind === "module") await provider.deleteModule(pendingDelete.id);
816
- else await provider.deleteLesson(pendingDelete.id);
817
- toast.success(t("courses.editor.deleted") || "Removed");
818
- setPendingDelete(null);
819
- await reload();
820
- }
821
- async function addOffer() {
822
- await getCoursesProvider().createOffer({
823
- courseId: course.id,
824
- name: t("courses.offers.newName") || "New offer",
825
- price: course.price,
826
- kind: "one_time"
827
- });
828
- toast.success(t("courses.offers.added") || "Offer added");
829
- await reload();
830
- }
831
- async function patchOffer(offerId, patch) {
832
- await getCoursesProvider().updateOffer(offerId, {
833
- name: patch.name,
834
- price: patch.price,
835
- kind: patch.kind,
836
- recurringInterval: patch.recurringInterval,
837
- isOrderBump: patch.isOrderBump,
838
- isDefault: patch.isDefault
839
- });
840
- await reload();
841
- }
842
- async function deleteOffer(offerId) {
843
- await getCoursesProvider().deleteOffer(offerId);
844
- toast.success(t("courses.offers.removed") || "Offer removed");
845
- await reload();
846
- }
847
- const deliveryOptions = DELIVERY_TYPES.map((d) => ({
848
- value: d,
849
- label: t(`courses.editor.delivery.${d}`) || d
850
- }));
851
- return (
852
- // Sem padding e sem max-width próprios: a shell já envolve toda página em
853
- // `p-3 md:p-6` (AdminShell). Reaplicar `p-6 md:p-8` aqui empurrava o editor
854
- // 32px à direita do título na barra de topo e de todas as outras telas, e o
855
- // `mx-auto max-w-4xl` ainda o centralizava enquanto a barra ficava à esquerda.
856
- /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
857
- /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-5", children: [
858
- /* @__PURE__ */ jsx("div", { className: "aspect-video w-36 shrink-0 overflow-hidden rounded-2xl border border-border bg-muted shadow-sm", children: course.thumbnailUrl ? (
859
- // eslint-disable-next-line @next/next/no-img-element
860
- /* @__PURE__ */ jsx("img", { src: course.thumbnailUrl, alt: "", className: "h-full w-full object-cover" })
861
- ) : /* @__PURE__ */ jsx("div", { className: "flex h-full w-full items-center justify-center text-muted-foreground", children: /* @__PURE__ */ jsx(ImagePlus, { className: "h-6 w-6" }) }) }),
862
- /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2", children: [
863
- /* @__PURE__ */ jsx("h1", { className: "truncate text-2xl font-bold text-foreground", children: course.title || (t("courses.editor.untitledCourse") || "Untitled course") }),
864
- /* @__PURE__ */ jsx(Badge, { variant: course.status === "published" ? "success" : course.status === "archived" ? "secondary" : "outline", children: t(`courses.status.${course.status}`) || course.status }),
865
- /* @__PURE__ */ jsx(Badge, { variant: "outline", children: t(`courses.editor.delivery.${course.deliveryType}`) || course.deliveryType })
866
- ] }) })
867
- ] }),
868
- /* @__PURE__ */ jsx(Separator, {}),
869
- /* @__PURE__ */ jsxs(Tabs, { value: tab, onValueChange: (v) => setTab(v), children: [
870
- /* @__PURE__ */ jsx(TabsList, { className: "max-w-full justify-start overflow-x-auto scrollbar-none", children: TABS.map((tk) => {
871
- const Icon = TAB_ICON[tk];
872
- return /* @__PURE__ */ jsxs(TabsTrigger, { value: tk, className: "gap-1.5", children: [
873
- /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" }),
874
- t(`courses.editor.tab.${tk}`) || tk
875
- ] }, tk);
876
- }) }),
877
- /* @__PURE__ */ jsxs(TabsContent, { value: "details", className: "mt-4 space-y-5", children: [
878
- /* @__PURE__ */ jsx(SectionCard, { title: t("courses.editor.sectionBasics") || "Basics", children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
879
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldTitle") || "Title", children: /* @__PURE__ */ jsx(Input, { value: course.title, onChange: (e) => patchCourse({ title: e.target.value }) }) }),
880
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldSubtitle") || "Subtitle", children: /* @__PURE__ */ jsx(Input, { value: course.subtitle ?? "", onChange: (e) => patchCourse({ subtitle: e.target.value }) }) }),
881
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldDescription") || "Description", children: /* @__PURE__ */ jsx(
882
- Textarea,
883
- {
884
- className: "min-h-[96px] px-2 py-1.5",
885
- value: course.description ?? "",
886
- onChange: (e) => patchCourse({ description: e.target.value }),
887
- placeholder: t("courses.editor.descriptionPlaceholder") || "What will students learn?"
888
- }
889
- ) }),
890
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldDelivery") || "Format", hint: t("courses.editor.deliveryHint") || "How this course is delivered.", children: /* @__PURE__ */ jsx(
891
- SegmentedControl,
892
- {
893
- size: "md",
894
- options: deliveryOptions,
895
- value: course.deliveryType,
896
- onChange: (v) => patchCourse({ deliveryType: v }),
897
- "aria-label": t("courses.editor.fieldDelivery") || "Format"
898
- }
899
- ) })
900
- ] }) }),
901
- /* @__PURE__ */ jsx(SectionCard, { title: t("courses.editor.sectionThumbnail") || "Cover image", description: t("courses.editor.thumbnailHint") || "Shown on the storefront and in listings. 1280\xD7720 recommended.", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 sm:flex-row sm:items-start", children: [
902
- /* @__PURE__ */ jsx("div", { className: "aspect-video w-full shrink-0 overflow-hidden rounded-lg border border-border bg-muted sm:w-56", children: course.thumbnailUrl ? (
903
- // eslint-disable-next-line @next/next/no-img-element
904
- /* @__PURE__ */ jsx("img", { src: course.thumbnailUrl, alt: "", className: "h-full w-full object-cover" })
905
- ) : /* @__PURE__ */ jsxs("div", { className: "flex h-full w-full flex-col items-center justify-center gap-1 text-muted-foreground", children: [
906
- /* @__PURE__ */ jsx(ImagePlus, { className: "h-6 w-6" }),
907
- /* @__PURE__ */ jsx("span", { className: "text-xs", children: t("courses.editor.thumbnailEmpty") || "No image" })
908
- ] }) }),
909
- /* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-3", children: [
910
- /* @__PURE__ */ jsx("input", { ref: fileRef, type: "file", accept: "image/*", className: "hidden", onChange: onPickThumbnail }),
911
- /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", disabled: uploading, onClick: () => fileRef.current?.click(), children: [
912
- uploading ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(UploadCloud, { className: "h-4 w-4" }),
913
- uploading ? t("courses.editor.uploading") || "Uploading\u2026" : t("courses.editor.thumbnailUpload") || "Upload image"
914
- ] }),
915
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.thumbnailUrl") || "Or paste an image URL", children: /* @__PURE__ */ jsx(
916
- Input,
917
- {
918
- value: course.thumbnailUrl ?? "",
919
- placeholder: "https://\u2026",
920
- onChange: (e) => patchCourse({ thumbnailUrl: e.target.value || null })
921
- }
922
- ) })
923
- ] })
924
- ] }) }),
925
- /* @__PURE__ */ jsx(SectionCard, { title: t("courses.editor.sectionPricing") || "Pricing & status", children: /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2", children: [
926
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldPrice") || "Price", hint: course.currency, children: /* @__PURE__ */ jsx(
927
- Input,
928
- {
929
- type: "number",
930
- min: 0,
931
- step: "0.01",
932
- inputMode: "decimal",
933
- value: String(course.price),
934
- onChange: (e) => patchCourse({ price: Number(e.target.value) || 0 })
935
- }
936
- ) }),
937
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldStatus") || "Status", children: /* @__PURE__ */ jsxs(Select, { value: course.status, onValueChange: (v) => patchCourse({ status: v }), children: [
938
- /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
939
- /* @__PURE__ */ jsx(SelectContent, { children: STATUSES.map((s) => /* @__PURE__ */ jsx(SelectItem, { value: s, children: t(`courses.status.${s}`) || s }, s)) })
940
- ] }) })
941
- ] }) })
942
- ] }),
943
- /* @__PURE__ */ jsx(TabsContent, { value: "curriculum", className: "mt-4 space-y-4", children: /* @__PURE__ */ jsx(
944
- CurriculumBuilder,
945
- {
946
- t,
947
- modules,
948
- lessons,
949
- autoFocusId,
950
- onAutoFocused: () => setAutoFocusId(null),
951
- onAddModule: addModule,
952
- onRenameModule: renameModule,
953
- onAddLesson: addLesson,
954
- onPatchLesson: patchLesson,
955
- onRequestDelete: (kind, delId) => setPendingDelete({ kind, id: delId }),
956
- onReorderModules: reorderModules,
957
- onReorderLessons: reorderLessons
958
- }
959
- ) }),
960
- /* @__PURE__ */ jsxs(TabsContent, { value: "offers", className: "mt-4 space-y-4", children: [
961
- /* @__PURE__ */ jsx(InfoNote, { icon: /* @__PURE__ */ jsx(Info, { className: "h-4 w-4" }), children: t("courses.offers.help") || "Price points for this course. Sales run through orders and the Financeiro module." }),
962
- offers.length === 0 ? /* @__PURE__ */ jsx(
963
- EmptyState,
964
- {
965
- icon: /* @__PURE__ */ jsx(Tag, { className: "h-5 w-5" }),
966
- title: t("courses.offers.empty") || "No offers yet",
967
- description: t("courses.offers.emptyHint") || "Create an offer to sell this course.",
968
- action: /* @__PURE__ */ jsxs(Button, { onClick: addOffer, children: [
969
- /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
970
- t("courses.offers.firstOffer") || "Create first offer"
971
- ] })
972
- }
973
- ) : /* @__PURE__ */ jsxs(Fragment, { children: [
974
- offers.map((offer) => /* @__PURE__ */ jsxs("div", { className: "rounded-card border border-border bg-card p-4 shadow-sm md:p-5", children: [
975
- /* @__PURE__ */ jsxs("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
976
- /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "gap-1", children: offer.kind === "subscription" ? t("courses.offers.subscription") || "Subscription" : t("courses.offers.oneTime") || "One-time" }),
977
- /* @__PURE__ */ jsx(
978
- Button,
979
- {
980
- variant: "bare",
981
- size: "bare",
982
- onClick: () => deleteOffer(offer.id),
983
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground/60 transition-colors hover:bg-destructive/10 hover:text-destructive",
984
- "aria-label": t("courses.offers.remove") || "Remove",
985
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
986
- }
987
- )
988
- ] }),
989
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-[1fr_140px_160px]", children: [
990
- /* @__PURE__ */ jsx(Field, { label: t("courses.offers.name") || "Offer name", children: /* @__PURE__ */ jsx(Input, { defaultValue: offer.name, onBlur: (e) => patchOffer(offer.id, { name: e.target.value }) }) }),
991
- /* @__PURE__ */ jsx(Field, { label: t("courses.offers.price") || "Price", hint: offer.currency, children: /* @__PURE__ */ jsx(Input, { type: "number", min: 0, step: "0.01", inputMode: "decimal", defaultValue: String(offer.price), onBlur: (e) => patchOffer(offer.id, { price: Number(e.target.value) || 0 }) }) }),
992
- /* @__PURE__ */ jsx(Field, { label: t("courses.offers.kind") || "Type", children: /* @__PURE__ */ jsxs(
993
- Select,
994
- {
995
- value: offer.kind,
996
- onValueChange: (v) => patchOffer(offer.id, { kind: v, recurringInterval: v === "subscription" ? offer.recurringInterval ?? "month" : null }),
997
- children: [
998
- /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
999
- /* @__PURE__ */ jsxs(SelectContent, { children: [
1000
- /* @__PURE__ */ jsx(SelectItem, { value: "one_time", children: t("courses.offers.oneTime") || "One-time" }),
1001
- /* @__PURE__ */ jsx(SelectItem, { value: "subscription", children: t("courses.offers.subscription") || "Subscription" })
1002
- ] })
1003
- ]
1004
- }
1005
- ) })
1006
- ] }),
1007
- offer.kind === "subscription" && /* @__PURE__ */ jsx("div", { className: "mt-3 max-w-[200px]", children: /* @__PURE__ */ jsx(Field, { label: t("courses.offers.interval") || "Billing interval", children: /* @__PURE__ */ jsxs(
1008
- Select,
1009
- {
1010
- value: offer.recurringInterval ?? "month",
1011
- onValueChange: (v) => patchOffer(offer.id, { recurringInterval: v }),
1012
- children: [
1013
- /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
1014
- /* @__PURE__ */ jsxs(SelectContent, { children: [
1015
- /* @__PURE__ */ jsx(SelectItem, { value: "month", children: t("courses.interval.month") || "month" }),
1016
- /* @__PURE__ */ jsx(SelectItem, { value: "year", children: t("courses.interval.year") || "year" })
1017
- ] })
1018
- ]
1019
- }
1020
- ) }) }),
1021
- /* @__PURE__ */ jsxs("div", { className: "mt-4 rounded-lg border border-dashed border-border bg-muted/30 p-3", children: [
1022
- /* @__PURE__ */ jsxs("p", { className: "mb-2.5 flex items-center gap-1.5 text-xs text-muted-foreground", children: [
1023
- /* @__PURE__ */ jsx(Clock, { className: "h-3.5 w-3.5" }),
1024
- t("courses.offers.checkoutFlagsNote") || "Apply once the public checkout ships (coming soon)."
1025
- ] }),
1026
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-5 gap-y-2", children: [
1027
- /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 text-sm text-muted-foreground", children: [
1028
- /* @__PURE__ */ jsx(Checkbox, { checked: offer.isDefault, onChange: (v) => patchOffer(offer.id, { isDefault: v }) }),
1029
- t("courses.offers.default") || "Default"
1030
- ] }),
1031
- /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 text-sm text-muted-foreground", children: [
1032
- /* @__PURE__ */ jsx(Checkbox, { checked: offer.isOrderBump, onChange: (v) => patchOffer(offer.id, { isOrderBump: v }) }),
1033
- t("courses.offers.bump") || "Order bump"
1034
- ] })
1035
- ] })
1036
- ] })
1037
- ] }, offer.id)),
1038
- /* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: addOffer, className: "w-full border-dashed", children: [
1039
- /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
1040
- t("courses.offers.add") || "Add offer"
1041
- ] })
1042
- ] })
1043
- ] }),
1044
- /* @__PURE__ */ jsxs(TabsContent, { value: "members", className: "mt-4 space-y-4", children: [
1045
- /* @__PURE__ */ jsx(InfoNote, { icon: /* @__PURE__ */ jsx(Users, { className: "h-4 w-4" }), children: t("courses.editor.membersHelp") || "Buyers are enrolled automatically through orders and get access to all of this course\u2019s content." }),
1046
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1047
- /* @__PURE__ */ jsxs("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [
1048
- t("courses.editor.enrolledStudents") || "Enrolled students",
1049
- " (",
1050
- students.length,
1051
- ")"
1052
- ] }),
1053
- students.length === 0 ? /* @__PURE__ */ jsx(
1054
- EmptyState,
1055
- {
1056
- icon: /* @__PURE__ */ jsx(Users, { className: "h-5 w-5" }),
1057
- title: t("courses.editor.membersEmpty") || "No students yet",
1058
- description: t("courses.editor.membersEmptyHint") || "Buyers are enrolled here automatically after a paid order."
1059
- }
1060
- ) : /* @__PURE__ */ jsx("div", { className: "divide-y divide-border overflow-hidden rounded-card border border-border bg-card", children: students.map((s) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
1061
- /* @__PURE__ */ jsx("span", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-semibold uppercase text-muted-foreground", "aria-hidden": true, children: (s.name || s.email || "?").charAt(0) }),
1062
- /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
1063
- /* @__PURE__ */ jsx("span", { className: "truncate font-medium text-foreground", children: s.name }),
1064
- s.email && /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-muted-foreground", children: s.email })
1065
- ] }),
1066
- s.since && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: new Date(s.since).toLocaleDateString() })
1067
- ] }, s.key)) })
1068
- ] }),
1069
- offers.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1070
- /* @__PURE__ */ jsx("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: t("courses.editor.accessUnlockedBy") || "Access is unlocked by" }),
1071
- /* @__PURE__ */ jsx("div", { className: "divide-y divide-border overflow-hidden rounded-card border border-border bg-card", children: offers.map((offer) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
1072
- /* @__PURE__ */ jsx("span", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground", "aria-hidden": true, children: /* @__PURE__ */ jsx(Lock, { className: "h-4 w-4" }) }),
1073
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate font-medium text-foreground", children: offer.name }),
1074
- /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm tabular-nums text-muted-foreground", children: formatCurrency(offer.price, offer.currency) })
1075
- ] }, offer.id)) })
1076
- ] })
1077
- ] })
1078
- ] }),
1079
- /* @__PURE__ */ jsx(
1080
- ConfirmDialog,
1081
- {
1082
- open: pendingDelete !== null,
1083
- variant: "destructive",
1084
- title: pendingDelete?.kind === "module" ? t("courses.editor.deleteModuleConfirm") || "Delete this module?" : t("courses.editor.deleteLessonConfirm") || "Remove this lesson?",
1085
- description: pendingDelete?.kind === "module" ? t("courses.editor.deleteModuleConfirmBody") || "Its lessons will be removed too. This cannot be undone." : t("courses.editor.deleteLessonConfirmBody") || "This cannot be undone.",
1086
- confirmLabel: t("courses.editor.confirmDelete") || "Delete",
1087
- cancelLabel: t("courses.editor.cancel") || "Cancel",
1088
- onConfirm: confirmDelete,
1089
- onCancel: () => setPendingDelete(null)
1090
- }
1091
- )
1092
- ] })
1093
- );
1094
- }
1095
- CourseEditorPage.displayName = "CourseEditorPage";
1096
- function formatDuration(sec, t) {
1097
- const min = Math.max(0, Math.round(sec / 60));
1098
- const mLabel = t("courses.editor.minShort") || "min";
1099
- if (min < 60) return `${min} ${mLabel}`;
1100
- const h = Math.floor(min / 60);
1101
- const rem = min % 60;
1102
- const hLabel = t("courses.editor.hourShort") || "h";
1103
- return rem ? `${h} ${hLabel} ${rem} ${mLabel}` : `${h} ${hLabel}`;
1104
- }
1105
- function CurriculumBuilder({
1106
- t,
1107
- modules,
1108
- lessons,
1109
- autoFocusId,
1110
- onAutoFocused,
1111
- onAddModule,
1112
- onRenameModule,
1113
- onAddLesson,
1114
- onPatchLesson,
1115
- onRequestDelete,
1116
- onReorderModules,
1117
- onReorderLessons
1118
- }) {
1119
- const [collapsed, setCollapsed] = React3.useState(/* @__PURE__ */ new Set());
1120
- const sensors = useSensors(
1121
- useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
1122
- useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
1123
- );
1124
- function toggleModule(id) {
1125
- setCollapsed((prev) => {
1126
- const next = new Set(prev);
1127
- if (next.has(id)) next.delete(id);
1128
- else next.add(id);
1129
- return next;
1130
- });
1131
- }
1132
- function handleModuleDragEnd(e) {
1133
- const { active, over } = e;
1134
- if (!over || active.id === over.id) return;
1135
- const ids = modules.map((m) => m.id);
1136
- const from = ids.indexOf(String(active.id));
1137
- const to = ids.indexOf(String(over.id));
1138
- if (from < 0 || to < 0) return;
1139
- onReorderModules(arrayMove(ids, from, to));
1140
- }
1141
- if (modules.length === 0) {
1142
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1143
- /* @__PURE__ */ jsx(AutosaveHint, { t }),
1144
- /* @__PURE__ */ jsx(
1145
- EmptyState,
1146
- {
1147
- icon: /* @__PURE__ */ jsx(Layers, { className: "h-5 w-5" }),
1148
- title: t("courses.editor.modulesEmpty") || "No modules yet",
1149
- description: t("courses.editor.modulesEmptyHint") || "Create a module to start organizing your lessons.",
1150
- action: /* @__PURE__ */ jsxs(Button, { onClick: onAddModule, children: [
1151
- /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
1152
- t("courses.editor.firstModule") || "Create first module"
1153
- ] })
1154
- }
1155
- )
1156
- ] });
1157
- }
1158
- const totalLessons = lessons.length;
1159
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1160
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
1161
- /* @__PURE__ */ jsx(AutosaveHint, { t }),
1162
- /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground tabular-nums", children: [
1163
- modules.length,
1164
- " ",
1165
- modules.length === 1 ? t("courses.editor.moduleLabel") || "module" : t("courses.editor.moduleLabelPlural") || "modules",
1166
- " \xB7 ",
1167
- totalLessons,
1168
- " ",
1169
- totalLessons === 1 ? t("courses.editor.lessonUnitOne") || "lesson" : t("courses.editor.lessonUnitOther") || "lessons"
1170
- ] })
1171
- ] }),
1172
- /* @__PURE__ */ jsx(DndContext, { sensors, collisionDetection: closestCenter, onDragEnd: handleModuleDragEnd, children: /* @__PURE__ */ jsx(SortableContext, { items: modules.map((m) => m.id), strategy: verticalListSortingStrategy, children: /* @__PURE__ */ jsx("div", { className: "space-y-3", children: modules.map((mod, i) => /* @__PURE__ */ jsx(
1173
- SortableModuleCard,
1174
- {
1175
- t,
1176
- index: i,
1177
- mod,
1178
- lessons: lessons.filter((l) => l.moduleId === mod.id),
1179
- expanded: !collapsed.has(mod.id),
1180
- onToggle: () => toggleModule(mod.id),
1181
- autoFocusId,
1182
- onAutoFocused,
1183
- onRenameModule,
1184
- onAddLesson,
1185
- onPatchLesson,
1186
- onRequestDelete,
1187
- onReorderLessons
1188
- },
1189
- mod.id
1190
- )) }) }) }),
1191
- /* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: onAddModule, className: "w-full border-dashed", children: [
1192
- /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
1193
- t("courses.editor.newModule") || "New module"
1194
- ] })
1195
- ] });
1196
- }
1197
- function SortableModuleCard({
1198
- t,
1199
- index,
1200
- mod,
1201
- lessons,
1202
- expanded,
1203
- onToggle,
1204
- autoFocusId,
1205
- onAutoFocused,
1206
- onRenameModule,
1207
- onAddLesson,
1208
- onPatchLesson,
1209
- onRequestDelete,
1210
- onReorderLessons
1211
- }) {
1212
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: mod.id });
1213
- const sensors = useSensors(
1214
- useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
1215
- useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
1216
- );
1217
- const style = {
1218
- transform: CSS.Transform.toString(transform),
1219
- transition,
1220
- opacity: isDragging ? 0.6 : 1,
1221
- zIndex: isDragging ? 20 : void 0
1222
- };
1223
- const count = lessons.length;
1224
- const totalSec = lessons.reduce((s, l) => s + (l.durationSec || 0), 0);
1225
- function handleLessonDragEnd(e) {
1226
- const { active, over } = e;
1227
- if (!over || active.id === over.id) return;
1228
- const ids = lessons.map((l) => l.id);
1229
- const from = ids.indexOf(String(active.id));
1230
- const to = ids.indexOf(String(over.id));
1231
- if (from < 0 || to < 0) return;
1232
- onReorderLessons(mod.id, arrayMove(ids, from, to));
1233
- }
1234
- return /* @__PURE__ */ jsxs(
1235
- "div",
1236
- {
1237
- ref: setNodeRef,
1238
- style,
1239
- className: "overflow-hidden rounded-card border border-border bg-card shadow-sm",
1240
- children: [
1241
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-2 py-2 sm:gap-2 sm:px-3", children: [
1242
- /* @__PURE__ */ jsx(
1243
- Button,
1244
- {
1245
- variant: "bare",
1246
- size: "bare",
1247
- ...attributes,
1248
- ...listeners,
1249
- className: "hidden h-8 w-6 shrink-0 cursor-grab touch-none items-center justify-center rounded-md text-muted-foreground/40 transition-colors hover:text-muted-foreground active:cursor-grabbing sm:flex",
1250
- "aria-label": t("courses.editor.dragToReorder") || "Drag to reorder",
1251
- children: /* @__PURE__ */ jsx(GripVertical, { className: "h-4 w-4" })
1252
- }
1253
- ),
1254
- /* @__PURE__ */ jsx(
1255
- Button,
1256
- {
1257
- variant: "bare",
1258
- size: "bare",
1259
- onClick: onToggle,
1260
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
1261
- "aria-label": expanded ? t("courses.editor.collapse") || "Collapse" : t("courses.editor.expand") || "Expand",
1262
- "aria-expanded": expanded,
1263
- children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 transition-transform " + (expanded ? "" : "-rotate-90") })
1264
- }
1265
- ),
1266
- /* @__PURE__ */ jsx("span", { className: "hidden shrink-0 items-center rounded-md bg-muted px-2 py-1 text-xs font-semibold tabular-nums text-muted-foreground sm:inline-flex", children: String(index + 1).padStart(2, "0") }),
1267
- /* @__PURE__ */ jsx(
1268
- TitleField,
1269
- {
1270
- value: mod.title,
1271
- autoFocus: autoFocusId === mod.id,
1272
- onAutoFocused,
1273
- onCommit: (v) => onRenameModule(mod.id, v),
1274
- placeholder: t("courses.editor.moduleTitle") || "Module title",
1275
- ariaLabel: t("courses.editor.moduleTitle") || "Module title",
1276
- className: "text-sm font-semibold"
1277
- }
1278
- ),
1279
- /* @__PURE__ */ jsx("span", { className: "hidden shrink-0 text-xs text-muted-foreground tabular-nums md:inline", children: count > 0 ? `${count} ${count === 1 ? t("courses.editor.lessonUnitOne") || "lesson" : t("courses.editor.lessonUnitOther") || "lessons"} \xB7 ${formatDuration(totalSec, t)}` : t("courses.editor.noLessons") || "No lessons" }),
1280
- /* @__PURE__ */ jsx(
1281
- Button,
1282
- {
1283
- variant: "bare",
1284
- size: "bare",
1285
- onClick: () => onRequestDelete("module", mod.id),
1286
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground/60 transition-colors hover:bg-destructive/10 hover:text-destructive",
1287
- "aria-label": t("courses.editor.deleteModule") || "Delete module",
1288
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
1289
- }
1290
- )
1291
- ] }),
1292
- expanded && /* @__PURE__ */ jsxs("div", { className: "border-t border-border/60 bg-muted/20 p-2 sm:p-3", children: [
1293
- count === 0 ? /* @__PURE__ */ jsxs(
1294
- Button,
1295
- {
1296
- variant: "bare",
1297
- size: "bare",
1298
- onClick: () => onAddLesson(mod.id),
1299
- className: "flex w-full items-center justify-center gap-2 rounded-lg border border-dashed border-border px-3 py-4 text-sm text-muted-foreground transition-colors hover:border-primary/40 hover:bg-card hover:text-foreground font-normal",
1300
- children: [
1301
- /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
1302
- " ",
1303
- t("courses.editor.firstLesson") || "Add the first lesson"
1304
- ]
1305
- }
1306
- ) : /* @__PURE__ */ jsx(DndContext, { sensors, collisionDetection: closestCenter, onDragEnd: handleLessonDragEnd, children: /* @__PURE__ */ jsx(SortableContext, { items: lessons.map((l) => l.id), strategy: verticalListSortingStrategy, children: /* @__PURE__ */ jsx("div", { className: "space-y-1.5", children: lessons.map((lesson) => /* @__PURE__ */ jsx(
1307
- SortableLessonRow,
1308
- {
1309
- t,
1310
- lesson,
1311
- autoFocusId,
1312
- onAutoFocused,
1313
- onPatchLesson,
1314
- onRequestDelete
1315
- },
1316
- lesson.id
1317
- )) }) }) }),
1318
- count > 0 && /* @__PURE__ */ jsxs(
1319
- Button,
1320
- {
1321
- variant: "bare",
1322
- size: "bare",
1323
- onClick: () => onAddLesson(mod.id),
1324
- className: "mt-2 flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm font-medium text-primary transition-colors hover:bg-primary/10",
1325
- children: [
1326
- /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
1327
- " ",
1328
- t("courses.editor.newLesson") || "New lesson"
1329
- ]
1330
- }
1331
- )
1332
- ] })
1333
- ]
1334
- }
1335
- );
1336
- }
1337
- function SortableLessonRow({ t, lesson, autoFocusId, onAutoFocused, onPatchLesson, onRequestDelete }) {
1338
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: lesson.id });
1339
- const [open, setOpen] = React3.useState(false);
1340
- const [videoUrl, setVideoUrl] = React3.useState(lesson.videoUrl);
1341
- const [durationSec, setDurationSec] = React3.useState(lesson.durationSec);
1342
- React3.useEffect(() => {
1343
- setVideoUrl(lesson.videoUrl);
1344
- }, [lesson.videoUrl]);
1345
- React3.useEffect(() => {
1346
- setDurationSec(lesson.durationSec);
1347
- }, [lesson.durationSec]);
1348
- const hasVideo = videoUrl.trim().length > 0;
1349
- const style = {
1350
- transform: CSS.Transform.toString(transform),
1351
- transition,
1352
- opacity: isDragging ? 0.6 : 1,
1353
- zIndex: isDragging ? 10 : void 0
1354
- };
1355
- return /* @__PURE__ */ jsxs(
1356
- "div",
1357
- {
1358
- ref: setNodeRef,
1359
- style,
1360
- className: "group rounded-card border bg-card transition-colors " + (open ? "border-border" : "border-transparent hover:border-border"),
1361
- children: [
1362
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-1.5 py-1.5 sm:gap-2 sm:px-2", children: [
1363
- /* @__PURE__ */ jsx(
1364
- Button,
1365
- {
1366
- variant: "bare",
1367
- size: "bare",
1368
- ...attributes,
1369
- ...listeners,
1370
- className: "hidden h-8 w-5 shrink-0 cursor-grab touch-none items-center justify-center text-muted-foreground/40 transition-colors hover:text-muted-foreground active:cursor-grabbing sm:flex",
1371
- "aria-label": t("courses.editor.dragToReorder") || "Drag to reorder",
1372
- children: /* @__PURE__ */ jsx(GripVertical, { className: "h-4 w-4" })
1373
- }
1374
- ),
1375
- /* @__PURE__ */ jsx(
1376
- "span",
1377
- {
1378
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg " + (hasVideo ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground"),
1379
- "aria-hidden": true,
1380
- children: hasVideo ? /* @__PURE__ */ jsx(Video, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(FileText, { className: "h-4 w-4" })
1381
- }
1382
- ),
1383
- /* @__PURE__ */ jsx(
1384
- TitleField,
1385
- {
1386
- value: lesson.title,
1387
- autoFocus: autoFocusId === lesson.id,
1388
- onAutoFocused,
1389
- onCommit: (v) => onPatchLesson(lesson.id, { title: v }),
1390
- placeholder: t("courses.editor.lessonTitle") || "Lesson title",
1391
- ariaLabel: t("courses.editor.lessonTitle") || "Lesson title",
1392
- className: "text-sm"
1393
- }
1394
- ),
1395
- /* @__PURE__ */ jsxs("span", { className: "hidden shrink-0 items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs tabular-nums text-muted-foreground sm:inline-flex", children: [
1396
- /* @__PURE__ */ jsx(Clock, { className: "h-3 w-3" }),
1397
- Math.round(durationSec / 60),
1398
- " ",
1399
- t("courses.editor.minShort") || "min"
1400
- ] }),
1401
- /* @__PURE__ */ jsxs(
1402
- Button,
1403
- {
1404
- variant: "bare",
1405
- size: "bare",
1406
- onClick: () => setOpen((v) => !v),
1407
- className: "inline-flex shrink-0 items-center gap-1 rounded-full border px-2 py-1 text-xs font-medium transition-colors " + (hasVideo ? "border-border text-muted-foreground hover:text-foreground" : "border-dashed border-border text-muted-foreground hover:border-primary/40 hover:text-foreground") + " opacity-100 sm:opacity-0 sm:group-hover:opacity-100 " + (open ? "sm:opacity-100" : ""),
1408
- "aria-label": t("courses.editor.editLesson") || "Edit lesson",
1409
- "aria-expanded": open,
1410
- children: [
1411
- /* @__PURE__ */ jsx(PlayCircle, { className: "h-3.5 w-3.5" }),
1412
- /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: hasVideo ? t("courses.editor.videoSet") || "Video" : t("courses.editor.addVideo") || "Add video" })
1413
- ]
1414
- }
1415
- ),
1416
- /* @__PURE__ */ jsx(
1417
- Button,
1418
- {
1419
- variant: "bare",
1420
- size: "bare",
1421
- onClick: () => onRequestDelete("lesson", lesson.id),
1422
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground/60 transition-colors hover:bg-destructive/10 hover:text-destructive sm:opacity-0 sm:group-hover:opacity-100",
1423
- "aria-label": t("courses.editor.deleteLesson") || "Remove lesson",
1424
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
1425
- }
1426
- )
1427
- ] }),
1428
- open && /* @__PURE__ */ jsxs("div", { className: "space-y-3 border-t border-border/60 px-3 py-3 sm:pl-11", children: [
1429
- /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1430
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("courses.editor.videoUrlLabel") || "Video URL" }),
1431
- /* @__PURE__ */ jsx(
1432
- Input,
1433
- {
1434
- defaultValue: lesson.videoUrl,
1435
- placeholder: t("courses.editor.videoUrlPlaceholder") || "Paste the video link (YouTube, Vimeo\u2026)",
1436
- onBlur: (e) => {
1437
- setVideoUrl(e.target.value);
1438
- onPatchLesson(lesson.id, { videoUrl: e.target.value });
1439
- },
1440
- "aria-label": t("courses.editor.videoUrlLabel") || "Video URL"
1441
- }
1442
- )
1443
- ] }),
1444
- /* @__PURE__ */ jsxs("div", { className: "max-w-[160px] space-y-1.5", children: [
1445
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("courses.editor.lessonDuration") || "Duration (min)" }),
1446
- /* @__PURE__ */ jsx(
1447
- Input,
1448
- {
1449
- type: "number",
1450
- min: 0,
1451
- inputMode: "numeric",
1452
- defaultValue: String(Math.round(lesson.durationSec / 60)),
1453
- onBlur: (e) => {
1454
- const s = (Number(e.target.value) || 0) * 60;
1455
- setDurationSec(s);
1456
- onPatchLesson(lesson.id, { durationSec: s });
1457
- },
1458
- "aria-label": t("courses.editor.lessonDuration") || "Duration (min)"
1459
- }
1460
- )
1461
- ] })
1462
- ] })
1463
- ]
1464
- }
1465
- );
1466
- }
1467
- function TitleField({ value, autoFocus, onAutoFocused, onCommit, placeholder, ariaLabel, className }) {
1468
- const ref = React3.useRef(null);
1469
- React3.useEffect(() => {
1470
- if (autoFocus && ref.current) {
1471
- ref.current.focus();
1472
- ref.current.select();
1473
- onAutoFocused?.();
1474
- }
1475
- }, [autoFocus]);
1476
- return /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(
1477
- Input,
1478
- {
1479
- ref,
1480
- defaultValue: value,
1481
- placeholder,
1482
- "aria-label": ariaLabel,
1483
- onBlur: (e) => onCommit(e.target.value),
1484
- onKeyDown: (e) => {
1485
- if (e.key === "Enter") e.currentTarget.blur();
1486
- },
1487
- className: "h-auto rounded-md border-0 bg-transparent px-2 py-1.5 shadow-none transition-colors placeholder:text-muted-foreground/60 hover:bg-muted/60 focus:bg-background focus-visible:ring-offset-0 " + (className ?? "")
1488
- }
1489
- ) });
1490
- }
1491
- function Field({ label, hint, children }) {
1492
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1493
- /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [
1494
- /* @__PURE__ */ jsx("label", { className: "text-sm font-medium text-foreground", children: label }),
1495
- hint && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: hint })
1496
- ] }),
1497
- children
1498
- ] });
1499
- }
1500
- function AutosaveHint({ t }) {
1501
- return /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
1502
- /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 text-success" }),
1503
- t("courses.editor.autosaved") || "Changes are saved automatically."
1504
- ] });
1505
- }
1506
- function EditorSkeleton() {
1507
- return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-4xl p-6 md:p-8", children: [
1508
- /* @__PURE__ */ jsx(Skeleton, { className: "mb-4 h-4 w-28" }),
1509
- /* @__PURE__ */ jsx(Skeleton, { className: "mb-6 h-8 w-64" }),
1510
- /* @__PURE__ */ jsx(Skeleton, { className: "mb-6 h-10 w-full" }),
1511
- /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1512
- /* @__PURE__ */ jsx(Skeleton, { className: "h-40 w-full rounded-xl" }),
1513
- /* @__PURE__ */ jsx(Skeleton, { className: "h-40 w-full rounded-xl" })
1514
- ] })
1515
- ] });
1516
- }
1517
-
1518
- // src/lib/format.ts
1519
- function formatDate(iso) {
1520
- try {
1521
- return new Intl.DateTimeFormat("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" }).format(new Date(iso));
1522
- } catch {
1523
- return iso.slice(0, 10);
1524
- }
1525
- }
1526
- function formatPercent(fraction, digits = 0) {
1527
- return `${(fraction * 100).toFixed(digits)}%`;
1528
- }
1529
- function statusVariant2(s) {
1530
- if (s === "paid") return "success";
1531
- if (s === "pending") return "secondary";
1532
- if (s === "refunded" || s === "chargeback") return "destructive";
1533
- return "outline";
1534
- }
1535
- var salesCache = null;
1536
- function SalesPage({ navTabs, activeNavTab, ordersRoute = "/courses/sales" } = {}) {
1537
- const t = useTranslation();
1538
- const [orders, setOrders] = React3.useState(salesCache);
1539
- const tabs = navTabs?.();
1540
- React3.useEffect(() => {
1541
- if (salesCache) return;
1542
- let alive = true;
1543
- const p = getCoursesProvider();
1544
- void Promise.all([
1545
- p.listCoreSales().catch(() => []),
1546
- p.listOrders().catch(() => [])
1547
- ]).then(([core, store2]) => {
1548
- if (!alive) return;
1549
- const byId = /* @__PURE__ */ new Map();
1550
- for (const o of store2) byId.set(o.id, o);
1551
- for (const o of core) byId.set(o.id, o);
1552
- const list = [...byId.values()].sort((a, b) => a.createdAt < b.createdAt ? 1 : -1);
1553
- salesCache = list;
1554
- setOrders(list);
1555
- });
1556
- return () => {
1557
- alive = false;
1558
- };
1559
- }, []);
1560
- const paid = (orders ?? []).filter((o) => o.financialStatus === "paid");
1561
- const revenue = paid.reduce((s, o) => s + o.total, 0);
1562
- const currency = orders?.[0]?.currency ?? "BRL";
1563
- const columns = [
1564
- { key: "date", header: t("courses.sales.date") || "Data", render: (o) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatDate(o.createdAt) }) },
1565
- { key: "customer", header: t("courses.sales.customer") || "Cliente", render: (o) => /* @__PURE__ */ jsxs("div", { children: [
1566
- /* @__PURE__ */ jsx("div", { className: "font-medium text-foreground", children: o.customerName ?? "\u2014" }),
1567
- /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: o.customerEmail })
1568
- ] }) },
1569
- { key: "method", header: t("courses.sales.method") || "Forma", render: (o) => /* @__PURE__ */ jsxs("span", { className: "capitalize", children: [
1570
- o.paymentMethod ?? "\u2014",
1571
- o.installmentCount && o.installmentCount > 1 ? ` \xB7 ${o.installmentCount}x` : ""
1572
- ] }) },
1573
- { key: "status", header: t("courses.sales.status") || "Status", render: (o) => /* @__PURE__ */ jsx(Badge, { variant: statusVariant2(o.financialStatus), children: t(`courses.financialStatus.${o.financialStatus}`) || o.financialStatus }) },
1574
- { key: "fee", header: t("courses.sales.fee") || "Taxa", align: "right", render: (o) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatCurrency(o.platformFee, o.currency) }) },
1575
- { key: "total", header: t("courses.sales.amount") || "Valor", align: "right", render: (o) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: formatCurrency(o.total, o.currency) }) }
1576
- ];
1577
- return /* @__PURE__ */ jsx(
1578
- OrderListView,
1579
- {
1580
- title: t("courses.sales.title") || "Vendas",
1581
- subtitle: t("courses.sales.subtitle") || "Todas as transa\xE7\xF5es dos seus cursos.",
1582
- navTabs: tabs?.map((tb) => ({ value: tb.value, label: tb.label })),
1583
- activeNavTab,
1584
- onNavTabChange: (v) => {
1585
- const target = tabs?.find((tb) => tb.value === v);
1586
- if (target) navigateTo(target.route);
1587
- },
1588
- stats: [
1589
- { label: t("courses.sales.count") || "Vendas", value: String(paid.length) },
1590
- { label: t("courses.sales.revenue") || "Receita", value: formatCurrency(revenue, currency) }
1591
- ],
1592
- columns,
1593
- rows: orders ?? [],
1594
- loading: orders === null,
1595
- empty: t("courses.sales.empty") || "Nenhuma venda ainda.",
1596
- onRowClick: (o) => navigateTo(`${ordersRoute}/${o.id}`)
1597
- }
1598
- );
1599
- }
1600
- SalesPage.displayName = "SalesPage";
1601
- var Page = ({ children }) => /* @__PURE__ */ jsx("div", { className: "mx-auto max-w-6xl p-6 md:p-8", children });
1602
- var orderCache = /* @__PURE__ */ new Map();
1603
- function statusTone(s) {
1604
- if (s === "paid") return "success";
1605
- if (s === "pending") return "warning";
1606
- if (s === "refunded" || s === "chargeback") return "destructive";
1607
- return "muted";
1608
- }
1609
- function instTone(s) {
1610
- if (s === "paid") return "success";
1611
- if (s === "overdue") return "destructive";
1612
- return "warning";
1613
- }
1614
- function CourseOrderDetailView({ id, ordersRoute = "/courses/sales" }) {
1615
- const t = useTranslation();
1616
- const [order, setOrder] = React3.useState(() => id ? orderCache.get(id) ?? null : null);
1617
- const [loading, setLoading] = React3.useState(() => !(id && orderCache.has(id)));
1618
- React3.useEffect(() => {
1619
- let alive = true;
1620
- if (!id) {
1621
- setLoading(false);
1622
- return;
1623
- }
1624
- const cached = orderCache.get(id);
1625
- if (cached) {
1626
- setOrder(cached);
1627
- setLoading(false);
1628
- return;
1629
- }
1630
- setLoading(true);
1631
- void getCoursesProvider().getOrder(id).then((o) => {
1632
- if (!alive) return;
1633
- if (o) orderCache.set(id, o);
1634
- setOrder(o);
1635
- setLoading(false);
1636
- });
1637
- return () => {
1638
- alive = false;
1639
- };
1640
- }, [id]);
1641
- const back = () => navigateTo(ordersRoute);
1642
- if (loading) return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(OrderDetailSkeleton, {}) });
1643
- if (!order) {
1644
- return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(
1645
- EmptyState,
1646
- {
1647
- title: t("courses.order.notFound") || "Pedido n\xE3o encontrado",
1648
- description: "",
1649
- action: /* @__PURE__ */ jsx(Button, { variant: "bare", size: "bare", className: "text-sm text-primary underline font-normal", onClick: back, children: t("common.back") || "Voltar" })
1650
- }
1651
- ) });
1652
- }
1653
- const cur = order.currency;
1654
- const installments = order.installments.map((m) => ({
1655
- number: m.installmentNumber,
1656
- amount: formatCurrency(m.amount, cur),
1657
- status: m.status,
1658
- statusTone: instTone(m.status),
1659
- dueDate: m.dueDate ? formatDate(m.dueDate) : null,
1660
- paymentDate: m.paymentDate ? formatDate(m.paymentDate) : null,
1661
- note: m.cardBrand ? `${m.cardBrand}${m.cardInstallments ? ` ${m.cardInstallments}x` : ""}` : null
1662
- }));
1663
- const data = {
1664
- title: `${t("courses.order.title") || "Pedido"} #${order.id.slice(0, 8)}`,
1665
- subtitle: formatDate(order.createdAt),
1666
- badges: [
1667
- { label: t(`courses.financialStatus.${order.financialStatus}`) || order.financialStatus, tone: statusTone(order.financialStatus) },
1668
- ...order.installmentCount && order.installmentCount > 1 ? [{ label: `${order.installmentCount}x`, tone: "muted" }] : []
1669
- ],
1670
- customer: { name: order.customerName, email: order.customerEmail },
1671
- customerNode: /* @__PURE__ */ jsxs("div", { className: "space-y-1 text-sm", children: [
1672
- order.customerId ? /* @__PURE__ */ jsx(
1673
- PersonLink,
1674
- {
1675
- personId: order.customerId,
1676
- name: order.customerName ?? order.customerEmail ?? "Cliente",
1677
- className: "font-medium"
1678
- }
1679
- ) : /* @__PURE__ */ jsx("p", { className: "font-medium", children: order.customerName ?? "\u2014" }),
1680
- order.customerEmail && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: order.customerEmail })
1681
- ] }),
1682
- items: [{
1683
- name: order.courseTitle ?? "\u2014",
1684
- description: order.offerName,
1685
- quantity: 1,
1686
- unitPrice: formatCurrency(order.total, cur),
1687
- total: formatCurrency(order.total, cur)
1688
- }],
1689
- totals: [
1690
- { label: "Total", value: formatCurrency(order.total, cur), strong: true },
1691
- ...order.platformFee > 0 ? [{ label: "Taxa", value: `\u2212 ${formatCurrency(order.platformFee, cur)}`, muted: true }] : [],
1692
- { label: "L\xEDquido", value: formatCurrency(order.netValue, cur) }
1693
- ],
1694
- paymentMethod: order.paymentMethod ?? void 0,
1695
- receivable: {
1696
- rows: [
1697
- { label: "Recebido", value: formatCurrency(order.paidAmount, cur) },
1698
- { label: "Saldo", value: formatCurrency(order.balance, cur), strong: order.balance > 0 }
1699
- ],
1700
- installments,
1701
- emptyLabel: t("courses.order.noInstallments") || "Sem parcelas."
1702
- },
1703
- onBack: back,
1704
- backLabel: t("courses.sales.title") || "Vendas"
1705
- };
1706
- return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(OrderDetailView, { ...data }) });
1707
- }
1708
- CourseOrderDetailView.displayName = "CourseOrderDetailView";
1709
- function statusVariant3(s) {
1710
- if (s === "active") return "success";
1711
- if (s === "past_due") return "secondary";
1712
- return "destructive";
1713
- }
1714
- function SubscriptionsPage({ navTabs, activeNavTab } = {}) {
1715
- const t = useTranslation();
1716
- const [subs, setSubs] = React3.useState(null);
1717
- const tabs = navTabs?.();
1718
- React3.useEffect(() => {
1719
- void getCoursesProvider().listSubscriptions().then(setSubs);
1720
- }, []);
1721
- const active = (subs ?? []).filter((s) => s.status === "active");
1722
- const mrr = active.reduce((sum, s) => sum + (s.interval === "year" ? s.netValue / 12 : s.netValue), 0);
1723
- const currency = subs?.[0]?.currency ?? "BRL";
1724
- const columns = [
1725
- { key: "start", header: t("courses.subs.start") || "Start date", render: (s) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatDate(s.startedAt) }) },
1726
- { key: "customer", header: t("courses.subs.customer") || "Customer", render: (s) => /* @__PURE__ */ jsxs("div", { children: [
1727
- /* @__PURE__ */ jsx("div", { className: "font-medium text-foreground", children: s.customerName ?? "\u2014" }),
1728
- /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: s.customerEmail })
1729
- ] }) },
1730
- { key: "status", header: t("courses.subs.status") || "Status", render: (s) => /* @__PURE__ */ jsx(Badge, { variant: statusVariant3(s.status), children: t(`courses.subStatus.${s.status}`) || s.status }) },
1731
- { key: "value", header: t("courses.subs.value") || "Net value", align: "right", render: (s) => /* @__PURE__ */ jsxs("span", { className: "font-medium text-foreground", children: [
1732
- formatCurrency(s.netValue, s.currency),
1733
- /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
1734
- " / ",
1735
- t(`courses.interval.${s.interval}`) || s.interval
1736
- ] })
1737
- ] }) }
1738
- ];
1739
- return /* @__PURE__ */ jsx(
1740
- OrderListView,
1741
- {
1742
- title: t("courses.subs.title") || "Subscriptions",
1743
- subtitle: t("courses.subs.subtitle") || "Recurring plans and MRR.",
1744
- navTabs: tabs?.map((tb) => ({ value: tb.value, label: tb.label })),
1745
- activeNavTab,
1746
- onNavTabChange: (v) => {
1747
- const target = tabs?.find((tb) => tb.value === v);
1748
- if (target) navigateTo(target.route);
1749
- },
1750
- stats: [
1751
- { label: t("courses.subs.active") || "Active subscriptions", value: String(active.length) },
1752
- { label: t("courses.subs.mrr") || "Monthly recurring revenue", value: formatCurrency(mrr, currency) }
1753
- ],
1754
- columns,
1755
- rows: subs ?? [],
1756
- loading: subs === null,
1757
- empty: t("courses.subs.empty") || "No subscriptions yet."
1758
- }
1759
- );
1760
- }
1761
- SubscriptionsPage.displayName = "SubscriptionsPage";
1762
- function FinancialPage() {
1763
- const t = useTranslation();
1764
- const [summary, setSummary] = React3.useState(null);
1765
- const [payouts, setPayouts] = React3.useState(null);
1766
- React3.useEffect(() => {
1767
- const p = getCoursesProvider();
1768
- void p.getFinancialSummary().then(setSummary);
1769
- void p.listPayouts().then(setPayouts);
1770
- }, []);
1771
- const currency = summary?.currency ?? "BRL";
1772
- const columns = [
1773
- { key: "date", header: t("courses.financial.date") || "Date", render: (p) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatDate(p.createdAt) }) },
1774
- { key: "amount", header: t("courses.financial.amount") || "Amount", align: "right", render: (p) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: formatCurrency(p.amount, p.currency) }) },
1775
- { key: "status", header: t("courses.financial.status") || "Status", align: "right", render: (p) => /* @__PURE__ */ jsx(Badge, { variant: p.status === "paid" ? "success" : p.status === "pending" ? "secondary" : "destructive", children: t(`courses.payoutStatus.${p.status}`) || p.status }) }
1776
- ];
1777
- return /* @__PURE__ */ jsxs(
1778
- PageContainer,
1779
- {
1780
- title: t("courses.financial.title") || "Financial settings",
1781
- subtitle: t("courses.financial.subtitle") || "Balance and payouts.",
1782
- children: [
1783
- /* @__PURE__ */ jsxs(StatGrid, { children: [
1784
- /* @__PURE__ */ jsx(StatCard, { label: t("courses.financial.available") || "Available balance", value: formatCurrency(summary?.availableBalance ?? 0, currency) }),
1785
- /* @__PURE__ */ jsx(StatCard, { label: t("courses.financial.pending") || "Pending balance", value: formatCurrency(summary?.pendingBalance ?? 0, currency) }),
1786
- /* @__PURE__ */ jsx(StatCard, { label: t("courses.financial.revenue") || "Total revenue", value: formatCurrency(summary?.totalRevenue ?? 0, currency) }),
1787
- /* @__PURE__ */ jsx(StatCard, { label: t("courses.financial.feeCollected") || "Platform fees", value: formatCurrency(summary?.platformFeeTotal ?? 0, currency), hint: summary ? formatPercent(summary.refundRate, 1) + " " + (t("courses.financial.refundRate") || "refund rate") : void 0 })
1788
- ] }),
1789
- /* @__PURE__ */ jsx("h2", { className: "mb-3 text-sm font-semibold text-foreground", children: t("courses.financial.payouts") || "Payouts" }),
1790
- /* @__PURE__ */ jsx(
1791
- SimpleTable,
1792
- {
1793
- columns,
1794
- rows: payouts ?? [],
1795
- empty: t("courses.financial.noPayouts") || "No payouts yet."
1796
- }
1797
- )
1798
- ]
1799
- }
1800
- );
1801
- }
1802
- FinancialPage.displayName = "FinancialPage";
1803
- function ReportsPage() {
1804
- const t = useTranslation();
1805
- const [rows, setRows] = React3.useState(null);
1806
- React3.useEffect(() => {
1807
- const p = getCoursesProvider();
1808
- void Promise.all([p.listCourses(), p.listOrders({ financialStatus: "paid" })]).then(([courses, orders]) => {
1809
- setRows(buildRevenueByCourse(courses, orders));
1810
- });
1811
- }, []);
1812
- const columns = [
1813
- { key: "title", header: t("courses.reports.course") || "Course", render: (r) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: r.title }) },
1814
- { key: "sales", header: t("courses.reports.sales") || "Sales", align: "right", render: (r) => /* @__PURE__ */ jsx("span", { children: r.sales }) },
1815
- { key: "revenue", header: t("courses.reports.revenue") || "Revenue", align: "right", render: (r) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: formatCurrency(r.revenue, r.currency) }) }
1816
- ];
1817
- return /* @__PURE__ */ jsxs(PageContainer, { title: t("courses.reports.title") || "Reports", subtitle: t("courses.reports.subtitle") || "Revenue by course.", children: [
1818
- /* @__PURE__ */ jsx("h2", { className: "mb-3 text-sm font-semibold text-foreground", children: t("courses.reports.byCourse") || "Revenue by course" }),
1819
- /* @__PURE__ */ jsx(
1820
- SimpleTable,
1821
- {
1822
- columns,
1823
- rows: rows ?? [],
1824
- empty: t("courses.reports.empty") || "No revenue yet."
1825
- }
1826
- )
1827
- ] });
1828
- }
1829
- ReportsPage.displayName = "ReportsPage";
1830
- function buildRevenueByCourse(courses, orders) {
1831
- const byCourse = /* @__PURE__ */ new Map();
1832
- for (const o of orders) {
1833
- const agg = byCourse.get(o.courseId) ?? { sales: 0, revenue: 0, currency: o.currency };
1834
- agg.sales += 1;
1835
- agg.revenue = Math.round((agg.revenue + o.total) * 100) / 100;
1836
- byCourse.set(o.courseId, agg);
1837
- }
1838
- return courses.map((c) => {
1839
- const agg = byCourse.get(c.id);
1840
- return { courseId: c.id, title: c.title, sales: agg?.sales ?? 0, revenue: agg?.revenue ?? 0, currency: agg?.currency ?? c.currency };
1841
- }).sort((a, b) => b.revenue - a.revenue);
1842
- }
1843
- function CoursesGeneralSettings() {
1844
- const t = useTranslation();
1845
- const [currency, setCurrency] = React3.useState("BRL");
1846
- const [methods, setMethods] = React3.useState({ card: true, pix: true, boleto: true });
1847
- return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
1848
- /* @__PURE__ */ jsx(
1849
- SettingsGroup,
1850
- {
1851
- title: t("courses.settings.payments") || "Payments",
1852
- description: t("courses.settings.paymentsHelp") || "Connect Stripe in the Integrations tab; the platform fee applies automatically.",
1853
- children: /* @__PURE__ */ jsx(
1854
- SelectRow,
1855
- {
1856
- label: t("courses.settings.currency") || "Default currency",
1857
- value: currency,
1858
- onChange: setCurrency,
1859
- options: [
1860
- { value: "BRL", label: "BRL (R$)" },
1861
- { value: "USD", label: "USD ($)" },
1862
- { value: "EUR", label: "EUR (\u20AC)" }
1863
- ]
1864
- }
1865
- )
1866
- }
1867
- ),
1868
- /* @__PURE__ */ jsxs(SettingsGroup, { title: t("courses.settings.methods") || "Payment methods", children: [
1869
- /* @__PURE__ */ jsx(ToggleRow, { label: t("courses.editor.payCard") || "Credit card", checked: methods.card, onChange: (v) => setMethods((m) => ({ ...m, card: v })) }),
1870
- /* @__PURE__ */ jsx(ToggleRow, { label: t("courses.editor.payPix") || "Pix", checked: methods.pix, onChange: (v) => setMethods((m) => ({ ...m, pix: v })) }),
1871
- /* @__PURE__ */ jsx(ToggleRow, { label: t("courses.editor.payBoleto") || "Boleto", checked: methods.boleto, onChange: (v) => setMethods((m) => ({ ...m, boleto: v })) })
1872
- ] })
1873
- ] });
1874
- }
1875
- CoursesGeneralSettings.displayName = "CoursesGeneralSettings";
1876
- function CoursesSettingsTab() {
1877
- const t = useTranslation();
1878
- return /* @__PURE__ */ jsx(
1879
- PluginSettingsPanel,
1880
- {
1881
- title: t("courses.settings.title") || "Course settings",
1882
- generalSettings: /* @__PURE__ */ jsx(CoursesGeneralSettings, {}),
1883
- hostPluginId: "courses",
1884
- routeBase: "/settings/courses"
1885
- }
1886
- );
1887
- }
1888
- CoursesSettingsTab.displayName = "CoursesSettingsTab";
1889
421
  function useSummary() {
1890
422
  const [summary, setSummary] = React3.useState(null);
1891
423
  React3.useEffect(() => {
@@ -2300,11 +832,19 @@ var kiwifyConnectorDef = {
2300
832
  };
2301
833
 
2302
834
  // src/index.ts
835
+ var CoursesListPage = React3.lazy(() => import('./CoursesListPage-WDG3YHF4.js').then((m) => ({ default: m.CoursesListPage })));
836
+ var CourseEditorPage = React3.lazy(() => import('./CourseEditorPage-GEBTVW7G.js').then((m) => ({ default: m.CourseEditorPage })));
837
+ var SalesPage = React3.lazy(() => import('./SalesPage-VNYJXPTU.js').then((m) => ({ default: m.SalesPage })));
838
+ var CourseOrderDetailView = React3.lazy(() => import('./CourseOrderDetailView-LWHDQNZ7.js').then((m) => ({ default: m.CourseOrderDetailView })));
839
+ var SubscriptionsPage = React3.lazy(() => import('./SubscriptionsPage-IGV723DM.js').then((m) => ({ default: m.SubscriptionsPage })));
840
+ var FinancialPage = React3.lazy(() => import('./FinancialPage-UXD25JYA.js').then((m) => ({ default: m.FinancialPage })));
841
+ var ReportsPage = React3.lazy(() => import('./ReportsPage-ZC3QUISB.js').then((m) => ({ default: m.ReportsPage })));
842
+ var CoursesSettingsTab = lazyComponent(() => import('./CoursesSettings-RLLYRP2R.js').then((m) => m.CoursesSettingsTab));
2303
843
  var asComp = (c) => c;
2304
844
  var read = (feature) => ({ feature, action: "read" });
2305
- function withProps(Page2, props) {
2306
- const Bound = (routeProps) => React3.createElement(Page2, { ...props, ...routeProps });
2307
- Bound.displayName = `${Page2.displayName ?? Page2.name}(bound)`;
845
+ function withProps(Page, props) {
846
+ const Bound = (routeProps) => React3.createElement(Page, { ...props, ...routeProps });
847
+ Bound.displayName = `${Page.displayName ?? Page.name}(bound)`;
2308
848
  return Bound;
2309
849
  }
2310
850
  function redirectTo(target) {