@fayz-ai/plugin-courses 0.9.0 → 0.10.0
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/README.md +3 -3
- package/dist/components/CommerceUI.d.ts +19 -2
- package/dist/components/CommerceUI.d.ts.map +1 -1
- package/dist/components/dashboardWidgets.d.ts.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +773 -309
- package/dist/index.js.map +1 -1
- package/dist/integrations/kiwify/connectorDef.d.ts +34 -0
- package/dist/integrations/kiwify/connectorDef.d.ts.map +1 -0
- package/dist/integrations/kiwify/data.d.ts +58 -0
- package/dist/integrations/kiwify/data.d.ts.map +1 -0
- package/dist/lib/nav.d.ts.map +1 -0
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/public/createPublicCheckoutPlugin.d.ts +1 -1
- package/dist/public/createPublicCheckoutPlugin.d.ts.map +1 -1
- package/dist/public/{data.d.ts → data/index.d.ts} +1 -1
- package/dist/public/data/index.d.ts.map +1 -0
- package/dist/public/{data.supabase.d.ts → data/supabase.d.ts} +2 -2
- package/dist/public/data/supabase.d.ts.map +1 -0
- package/dist/public/index.d.ts +4 -4
- package/dist/public/index.d.ts.map +1 -1
- package/dist/public/index.js +353 -0
- package/dist/public/index.js.map +1 -0
- package/dist/public/{CheckoutScreen.d.ts → views/CheckoutScreen.d.ts} +1 -1
- package/dist/public/views/CheckoutScreen.d.ts.map +1 -0
- package/dist/views/CourseEditorPage.d.ts.map +1 -0
- package/dist/views/CourseOrderDetailView.d.ts +4 -3
- package/dist/views/CourseOrderDetailView.d.ts.map +1 -1
- package/dist/{pages → views}/CoursesListPage.d.ts.map +1 -1
- package/dist/views/FinancialPage.d.ts.map +1 -0
- package/dist/{pages → views}/ReportsPage.d.ts.map +1 -1
- package/dist/views/SalesPage.d.ts +19 -0
- package/dist/views/SalesPage.d.ts.map +1 -0
- package/dist/views/SubscriptionsPage.d.ts +7 -0
- package/dist/views/SubscriptionsPage.d.ts.map +1 -0
- package/package.json +12 -12
- package/dist/connectors/stripe.d.ts +0 -3
- package/dist/connectors/stripe.d.ts.map +0 -1
- package/dist/dashboardWidgets.d.ts.map +0 -1
- package/dist/nav.d.ts.map +0 -1
- package/dist/pages/CourseEditorPage.d.ts.map +0 -1
- package/dist/pages/FinancialPage.d.ts.map +0 -1
- package/dist/pages/SalesPage.d.ts +0 -6
- package/dist/pages/SalesPage.d.ts.map +0 -1
- package/dist/pages/SubscriptionsPage.d.ts +0 -6
- package/dist/pages/SubscriptionsPage.d.ts.map +0 -1
- package/dist/public/CheckoutScreen.d.ts.map +0 -1
- package/dist/public/data.d.ts.map +0 -1
- package/dist/public/data.supabase.d.ts.map +0 -1
- package/src/components/CommerceUI.tsx +0 -153
- package/src/connectors/stripe.ts +0 -34
- package/src/dashboardWidgets.tsx +0 -64
- package/src/index.ts +0 -150
- package/src/lib/format.ts +0 -22
- package/src/locales/en.ts +0 -203
- package/src/locales/index.ts +0 -7
- package/src/locales/pt-BR.ts +0 -203
- package/src/nav.ts +0 -9
- package/src/pages/CourseEditorPage.tsx +0 -1122
- package/src/pages/CoursesListPage.tsx +0 -137
- package/src/pages/FinancialPage.tsx +0 -81
- package/src/pages/ReportsPage.tsx +0 -59
- package/src/pages/SalesPage.tsx +0 -86
- package/src/pages/SubscriptionsPage.tsx +0 -56
- package/src/public/CheckoutScreen.tsx +0 -279
- package/src/public/createPublicCheckoutPlugin.tsx +0 -88
- package/src/public/data.supabase.ts +0 -63
- package/src/public/data.ts +0 -89
- package/src/public/index.ts +0 -26
- package/src/settings/CoursesSettings.tsx +0 -55
- package/src/views/CourseOrderDetailView.tsx +0 -132
- /package/dist/{dashboardWidgets.d.ts → components/dashboardWidgets.d.ts} +0 -0
- /package/dist/{nav.d.ts → lib/nav.d.ts} +0 -0
- /package/dist/{pages → views}/CourseEditorPage.d.ts +0 -0
- /package/dist/{pages → views}/CoursesListPage.d.ts +0 -0
- /package/dist/{pages → views}/FinancialPage.d.ts +0 -0
- /package/dist/{pages → views}/ReportsPage.d.ts +0 -0
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { hashRouterAdapter, registerPluginFactory, registerTranslations, useTranslation, getSupabaseClientOptional } from '@fayz-ai/core';
|
|
2
|
-
import { setCoursesTenantResolver, getCoursesProviderOptional, setCoursesProvider, createMockCoursesProvider, getCoursesProvider } from '@fayz-ai/courses';
|
|
3
|
-
import { useOrganizationStore, useLimitGuard, usePermissionOptional, PermissionGate, OrderListView, OrderDetailSkeleton, PersonLink, OrderDetailView, invalidateLimit, PluginSettingsPanel, SettingsGroup, SelectRow, ToggleRow } from '@fayz-ai/saas';
|
|
4
1
|
import * as React3 from 'react';
|
|
5
|
-
import {
|
|
2
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
3
|
+
import { hashRouterAdapter, createConnectionStore, connectionSettings, connectionStatus, settingsStatus, registerPluginFactory, registerTranslations, useTranslation, getSupabaseClientOptional, connectorRuntimeToken, CONNECTOR_RUNTIME_TOKEN_HEADER } from '@fayz-ai/core';
|
|
4
|
+
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, DatePicker, defineKpiWidget, toast, useModuleLayout, PageHeaderActions, Badge, useSaveBar, PageHeader, Separator, Tabs, TabsList, TabsTrigger, TabsContent, Input, SegmentedControl, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Checkbox, ConfirmDialog, KpiCard, Skeleton, HeaderTabs, Card, DataTable } from '@fayz-ai/ui';
|
|
6
7
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
7
|
-
import {
|
|
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';
|
|
8
9
|
import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCenter } from '@dnd-kit/core';
|
|
9
10
|
import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, arrayMove, useSortable } from '@dnd-kit/sortable';
|
|
10
11
|
import { CSS } from '@dnd-kit/utilities';
|
|
@@ -23,7 +24,6 @@ var en = {
|
|
|
23
24
|
"courses.status.draft": "Draft",
|
|
24
25
|
"courses.status.published": "Published",
|
|
25
26
|
"courses.status.archived": "Archived",
|
|
26
|
-
"courses.editor.back": "Back to courses",
|
|
27
27
|
"courses.editor.details": "Details",
|
|
28
28
|
"courses.editor.curriculum": "Curriculum",
|
|
29
29
|
"courses.editor.fieldTitle": "Title",
|
|
@@ -135,6 +135,9 @@ var en = {
|
|
|
135
135
|
"courses.offers.checkoutFlagsNote": "Apply once the public checkout ships (coming soon).",
|
|
136
136
|
// Sales
|
|
137
137
|
"courses.sales.title": "Sales",
|
|
138
|
+
"courses.order.title": "Order",
|
|
139
|
+
"courses.order.notFound": "Order not found",
|
|
140
|
+
"courses.order.noInstallments": "No installments.",
|
|
138
141
|
"courses.sales.subtitle": "Every transaction across your courses.",
|
|
139
142
|
"courses.sales.date": "Date",
|
|
140
143
|
"courses.sales.customer": "Customer",
|
|
@@ -177,11 +180,6 @@ var en = {
|
|
|
177
180
|
"courses.financial.date": "Date",
|
|
178
181
|
"courses.financial.amount": "Amount",
|
|
179
182
|
"courses.financial.status": "Status",
|
|
180
|
-
"courses.financial.connect": "Connect Stripe",
|
|
181
|
-
"courses.financial.connected": "Stripe connected",
|
|
182
|
-
"courses.financial.notConnected": "Connect your Stripe account to receive payouts. The platform fee is applied automatically on each sale.",
|
|
183
|
-
"courses.financial.platformFee": "Platform fee",
|
|
184
|
-
"courses.financial.connectSoon": "Stripe Connect onboarding wires up in Stage 3.",
|
|
185
183
|
"courses.payoutStatus.paid": "Paid",
|
|
186
184
|
"courses.payoutStatus.pending": "Pending",
|
|
187
185
|
"courses.payoutStatus.failed": "Failed",
|
|
@@ -228,7 +226,6 @@ var ptBR = {
|
|
|
228
226
|
"courses.status.draft": "Rascunho",
|
|
229
227
|
"courses.status.published": "Publicado",
|
|
230
228
|
"courses.status.archived": "Arquivado",
|
|
231
|
-
"courses.editor.back": "Voltar aos cursos",
|
|
232
229
|
"courses.editor.details": "Detalhes",
|
|
233
230
|
"courses.editor.curriculum": "Conte\xFAdo",
|
|
234
231
|
"courses.editor.fieldTitle": "T\xEDtulo",
|
|
@@ -340,6 +337,9 @@ var ptBR = {
|
|
|
340
337
|
"courses.offers.checkoutFlagsNote": "Aplicam-se quando o checkout p\xFAblico entrar no ar (em breve).",
|
|
341
338
|
// Sales
|
|
342
339
|
"courses.sales.title": "Vendas",
|
|
340
|
+
"courses.order.title": "Pedido",
|
|
341
|
+
"courses.order.notFound": "Pedido n\xE3o encontrado",
|
|
342
|
+
"courses.order.noInstallments": "Sem parcelas.",
|
|
343
343
|
"courses.sales.subtitle": "Todas as transa\xE7\xF5es dos seus cursos.",
|
|
344
344
|
"courses.sales.date": "Data",
|
|
345
345
|
"courses.sales.customer": "Cliente",
|
|
@@ -382,11 +382,6 @@ var ptBR = {
|
|
|
382
382
|
"courses.financial.date": "Data",
|
|
383
383
|
"courses.financial.amount": "Valor",
|
|
384
384
|
"courses.financial.status": "Status",
|
|
385
|
-
"courses.financial.connect": "Conectar Stripe",
|
|
386
|
-
"courses.financial.connected": "Stripe conectado",
|
|
387
|
-
"courses.financial.notConnected": "Conecte sua conta Stripe para receber os saques. A taxa da plataforma \xE9 aplicada automaticamente em cada venda.",
|
|
388
|
-
"courses.financial.platformFee": "Taxa da plataforma",
|
|
389
|
-
"courses.financial.connectSoon": "O onboarding do Stripe Connect \xE9 ligado na Etapa 3.",
|
|
390
385
|
"courses.payoutStatus.paid": "Conclu\xEDdo",
|
|
391
386
|
"courses.payoutStatus.pending": "Pendente",
|
|
392
387
|
"courses.payoutStatus.failed": "Falhou",
|
|
@@ -530,9 +525,24 @@ function formatDate(iso) {
|
|
|
530
525
|
function formatPercent(fraction, digits = 0) {
|
|
531
526
|
return `${(fraction * 100).toFixed(digits)}%`;
|
|
532
527
|
}
|
|
533
|
-
function PageContainer({ title, subtitle, actions, children }) {
|
|
534
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
535
|
-
|
|
528
|
+
function PageContainer({ title, subtitle, actions, navTabs, activeNavTab, onNavTabChange, children }) {
|
|
529
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
530
|
+
navTabs && navTabs.length > 0 ? (
|
|
531
|
+
// Mesma linha de CrudListView/OrderListView: abas à esquerda, ações à
|
|
532
|
+
// direita. Com abas o <h1> sai: a aba ativa já nomeia a tela, e a barra
|
|
533
|
+
// de topo nomeia o grupo.
|
|
534
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-6 flex flex-wrap items-center justify-between gap-3", children: [
|
|
535
|
+
/* @__PURE__ */ jsx(
|
|
536
|
+
HeaderTabs,
|
|
537
|
+
{
|
|
538
|
+
tabs: navTabs,
|
|
539
|
+
value: activeNavTab ?? navTabs[0].value,
|
|
540
|
+
onChange: (v) => onNavTabChange?.(v)
|
|
541
|
+
}
|
|
542
|
+
),
|
|
543
|
+
actions
|
|
544
|
+
] })
|
|
545
|
+
) : /* @__PURE__ */ jsxs("div", { className: "mb-6 flex items-start justify-between gap-4", children: [
|
|
536
546
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
537
547
|
/* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold text-foreground", children: title }),
|
|
538
548
|
subtitle && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: subtitle })
|
|
@@ -684,7 +694,18 @@ function CourseEditorPage({ id }) {
|
|
|
684
694
|
},
|
|
685
695
|
saveLabel: t("courses.editor.save") || "Salvar"
|
|
686
696
|
});
|
|
687
|
-
|
|
697
|
+
const header = /* @__PURE__ */ jsx(
|
|
698
|
+
PageHeader,
|
|
699
|
+
{
|
|
700
|
+
title: course?.title ?? "",
|
|
701
|
+
parentLabel: t("courses.title") || "Courses",
|
|
702
|
+
onBack: () => navigateTo("/courses")
|
|
703
|
+
}
|
|
704
|
+
);
|
|
705
|
+
if (loading || !course) return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
706
|
+
header,
|
|
707
|
+
/* @__PURE__ */ jsx(EditorSkeleton, {})
|
|
708
|
+
] });
|
|
688
709
|
function patchCourse(patch) {
|
|
689
710
|
setCourse((c) => c ? { ...c, ...patch } : c);
|
|
690
711
|
}
|
|
@@ -844,245 +865,246 @@ function CourseEditorPage({ id }) {
|
|
|
844
865
|
value: d,
|
|
845
866
|
label: t(`courses.editor.delivery.${d}`) || d
|
|
846
867
|
}));
|
|
847
|
-
return
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
] }) }),
|
|
896
|
-
/* @__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: [
|
|
897
|
-
/* @__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 ? (
|
|
898
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
899
|
-
/* @__PURE__ */ jsx("img", { src: course.thumbnailUrl, alt: "", className: "h-full w-full object-cover" })
|
|
900
|
-
) : /* @__PURE__ */ jsxs("div", { className: "flex h-full w-full flex-col items-center justify-center gap-1 text-muted-foreground", children: [
|
|
901
|
-
/* @__PURE__ */ jsx(ImagePlus, { className: "h-6 w-6" }),
|
|
902
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs", children: t("courses.editor.thumbnailEmpty") || "No image" })
|
|
868
|
+
return (
|
|
869
|
+
// Sem padding e sem max-width próprios: a shell já envolve toda página em
|
|
870
|
+
// `p-3 md:p-6` (AdminShell). Reaplicar `p-6 md:p-8` aqui empurrava o editor
|
|
871
|
+
// 32px à direita do título na barra de topo e de todas as outras telas, e o
|
|
872
|
+
// `mx-auto max-w-4xl` ainda o centralizava enquanto a barra ficava à esquerda.
|
|
873
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
874
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-5", children: [
|
|
875
|
+
/* @__PURE__ */ jsx("div", { className: "aspect-video w-36 shrink-0 overflow-hidden rounded-2xl border border-border bg-muted shadow-sm", children: course.thumbnailUrl ? (
|
|
876
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
877
|
+
/* @__PURE__ */ jsx("img", { src: course.thumbnailUrl, alt: "", className: "h-full w-full object-cover" })
|
|
878
|
+
) : /* @__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" }) }) }),
|
|
879
|
+
/* @__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: [
|
|
880
|
+
/* @__PURE__ */ jsx("h1", { className: "truncate text-2xl font-bold text-foreground", children: course.title || (t("courses.editor.untitledCourse") || "Untitled course") }),
|
|
881
|
+
/* @__PURE__ */ jsx(Badge, { variant: course.status === "published" ? "success" : course.status === "archived" ? "secondary" : "outline", children: t(`courses.status.${course.status}`) || course.status }),
|
|
882
|
+
/* @__PURE__ */ jsx(Badge, { variant: "outline", children: t(`courses.editor.delivery.${course.deliveryType}`) || course.deliveryType })
|
|
883
|
+
] }) })
|
|
884
|
+
] }),
|
|
885
|
+
/* @__PURE__ */ jsx(Separator, {}),
|
|
886
|
+
/* @__PURE__ */ jsxs(Tabs, { value: tab, onValueChange: (v) => setTab(v), children: [
|
|
887
|
+
/* @__PURE__ */ jsx(TabsList, { className: "max-w-full justify-start overflow-x-auto scrollbar-none", children: TABS.map((tk) => {
|
|
888
|
+
const Icon = TAB_ICON[tk];
|
|
889
|
+
return /* @__PURE__ */ jsxs(TabsTrigger, { value: tk, className: "gap-1.5", children: [
|
|
890
|
+
/* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" }),
|
|
891
|
+
t(`courses.editor.tab.${tk}`) || tk
|
|
892
|
+
] }, tk);
|
|
893
|
+
}) }),
|
|
894
|
+
/* @__PURE__ */ jsxs(TabsContent, { value: "details", className: "mt-4 space-y-5", children: [
|
|
895
|
+
/* @__PURE__ */ jsx(SectionCard, { title: t("courses.editor.sectionBasics") || "Basics", children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
896
|
+
/* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldTitle") || "Title", children: /* @__PURE__ */ jsx(Input, { value: course.title, onChange: (e) => patchCourse({ title: e.target.value }) }) }),
|
|
897
|
+
/* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldSubtitle") || "Subtitle", children: /* @__PURE__ */ jsx(Input, { value: course.subtitle ?? "", onChange: (e) => patchCourse({ subtitle: e.target.value }) }) }),
|
|
898
|
+
/* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldDescription") || "Description", children: /* @__PURE__ */ jsx(
|
|
899
|
+
Textarea,
|
|
900
|
+
{
|
|
901
|
+
value: course.description ?? "",
|
|
902
|
+
onChange: (e) => patchCourse({ description: e.target.value }),
|
|
903
|
+
placeholder: t("courses.editor.descriptionPlaceholder") || "What will students learn?"
|
|
904
|
+
}
|
|
905
|
+
) }),
|
|
906
|
+
/* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldDelivery") || "Format", hint: t("courses.editor.deliveryHint") || "How this course is delivered.", children: /* @__PURE__ */ jsx(
|
|
907
|
+
SegmentedControl,
|
|
908
|
+
{
|
|
909
|
+
size: "md",
|
|
910
|
+
options: deliveryOptions,
|
|
911
|
+
value: course.deliveryType,
|
|
912
|
+
onChange: (v) => patchCourse({ deliveryType: v }),
|
|
913
|
+
"aria-label": t("courses.editor.fieldDelivery") || "Format"
|
|
914
|
+
}
|
|
915
|
+
) })
|
|
903
916
|
] }) }),
|
|
904
|
-
/* @__PURE__ */ jsxs("div", { className: "flex-
|
|
905
|
-
/* @__PURE__ */ jsx("
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
917
|
+
/* @__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: [
|
|
918
|
+
/* @__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 ? (
|
|
919
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
920
|
+
/* @__PURE__ */ jsx("img", { src: course.thumbnailUrl, alt: "", className: "h-full w-full object-cover" })
|
|
921
|
+
) : /* @__PURE__ */ jsxs("div", { className: "flex h-full w-full flex-col items-center justify-center gap-1 text-muted-foreground", children: [
|
|
922
|
+
/* @__PURE__ */ jsx(ImagePlus, { className: "h-6 w-6" }),
|
|
923
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs", children: t("courses.editor.thumbnailEmpty") || "No image" })
|
|
924
|
+
] }) }),
|
|
925
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-3", children: [
|
|
926
|
+
/* @__PURE__ */ jsx("input", { ref: fileRef, type: "file", accept: "image/*", className: "hidden", onChange: onPickThumbnail }),
|
|
927
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", disabled: uploading, onClick: () => fileRef.current?.click(), children: [
|
|
928
|
+
uploading ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(UploadCloud, { className: "h-4 w-4" }),
|
|
929
|
+
uploading ? t("courses.editor.uploading") || "Uploading\u2026" : t("courses.editor.thumbnailUpload") || "Upload image"
|
|
930
|
+
] }),
|
|
931
|
+
/* @__PURE__ */ jsx(Field, { label: t("courses.editor.thumbnailUrl") || "Or paste an image URL", children: /* @__PURE__ */ jsx(
|
|
932
|
+
Input,
|
|
933
|
+
{
|
|
934
|
+
value: course.thumbnailUrl ?? "",
|
|
935
|
+
placeholder: "https://\u2026",
|
|
936
|
+
onChange: (e) => patchCourse({ thumbnailUrl: e.target.value || null })
|
|
937
|
+
}
|
|
938
|
+
) })
|
|
939
|
+
] })
|
|
940
|
+
] }) }),
|
|
941
|
+
/* @__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: [
|
|
942
|
+
/* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldPrice") || "Price", hint: course.currency, children: /* @__PURE__ */ jsx(
|
|
911
943
|
Input,
|
|
912
944
|
{
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
945
|
+
type: "number",
|
|
946
|
+
min: 0,
|
|
947
|
+
step: "0.01",
|
|
948
|
+
inputMode: "decimal",
|
|
949
|
+
value: String(course.price),
|
|
950
|
+
onChange: (e) => patchCourse({ price: Number(e.target.value) || 0 })
|
|
916
951
|
}
|
|
917
|
-
) })
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
Input,
|
|
923
|
-
{
|
|
924
|
-
type: "number",
|
|
925
|
-
min: 0,
|
|
926
|
-
step: "0.01",
|
|
927
|
-
inputMode: "decimal",
|
|
928
|
-
value: String(course.price),
|
|
929
|
-
onChange: (e) => patchCourse({ price: Number(e.target.value) || 0 })
|
|
930
|
-
}
|
|
931
|
-
) }),
|
|
932
|
-
/* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldStatus") || "Status", children: /* @__PURE__ */ jsxs(Select, { value: course.status, onValueChange: (v) => patchCourse({ status: v }), children: [
|
|
933
|
-
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
934
|
-
/* @__PURE__ */ jsx(SelectContent, { children: STATUSES.map((s) => /* @__PURE__ */ jsx(SelectItem, { value: s, children: t(`courses.status.${s}`) || s }, s)) })
|
|
952
|
+
) }),
|
|
953
|
+
/* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldStatus") || "Status", children: /* @__PURE__ */ jsxs(Select, { value: course.status, onValueChange: (v) => patchCourse({ status: v }), children: [
|
|
954
|
+
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
955
|
+
/* @__PURE__ */ jsx(SelectContent, { children: STATUSES.map((s) => /* @__PURE__ */ jsx(SelectItem, { value: s, children: t(`courses.status.${s}`) || s }, s)) })
|
|
956
|
+
] }) })
|
|
935
957
|
] }) })
|
|
936
|
-
] })
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
CurriculumBuilder,
|
|
940
|
-
{
|
|
941
|
-
t,
|
|
942
|
-
modules,
|
|
943
|
-
lessons,
|
|
944
|
-
autoFocusId,
|
|
945
|
-
onAutoFocused: () => setAutoFocusId(null),
|
|
946
|
-
onAddModule: addModule,
|
|
947
|
-
onRenameModule: renameModule,
|
|
948
|
-
onAddLesson: addLesson,
|
|
949
|
-
onPatchLesson: patchLesson,
|
|
950
|
-
onRequestDelete: (kind, delId) => setPendingDelete({ kind, id: delId }),
|
|
951
|
-
onReorderModules: reorderModules,
|
|
952
|
-
onReorderLessons: reorderLessons
|
|
953
|
-
}
|
|
954
|
-
) }),
|
|
955
|
-
/* @__PURE__ */ jsxs(TabsContent, { value: "offers", className: "space-y-4", children: [
|
|
956
|
-
/* @__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." }),
|
|
957
|
-
offers.length === 0 ? /* @__PURE__ */ jsx(
|
|
958
|
-
EmptyState,
|
|
958
|
+
] }),
|
|
959
|
+
/* @__PURE__ */ jsx(TabsContent, { value: "curriculum", className: "mt-4 space-y-4", children: /* @__PURE__ */ jsx(
|
|
960
|
+
CurriculumBuilder,
|
|
959
961
|
{
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
962
|
+
t,
|
|
963
|
+
modules,
|
|
964
|
+
lessons,
|
|
965
|
+
autoFocusId,
|
|
966
|
+
onAutoFocused: () => setAutoFocusId(null),
|
|
967
|
+
onAddModule: addModule,
|
|
968
|
+
onRenameModule: renameModule,
|
|
969
|
+
onAddLesson: addLesson,
|
|
970
|
+
onPatchLesson: patchLesson,
|
|
971
|
+
onRequestDelete: (kind, delId) => setPendingDelete({ kind, id: delId }),
|
|
972
|
+
onReorderModules: reorderModules,
|
|
973
|
+
onReorderLessons: reorderLessons
|
|
967
974
|
}
|
|
968
|
-
)
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
)
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
/* @__PURE__ */
|
|
985
|
-
|
|
975
|
+
) }),
|
|
976
|
+
/* @__PURE__ */ jsxs(TabsContent, { value: "offers", className: "mt-4 space-y-4", children: [
|
|
977
|
+
/* @__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." }),
|
|
978
|
+
offers.length === 0 ? /* @__PURE__ */ jsx(
|
|
979
|
+
EmptyState,
|
|
980
|
+
{
|
|
981
|
+
icon: /* @__PURE__ */ jsx(Tag, { className: "h-5 w-5" }),
|
|
982
|
+
title: t("courses.offers.empty") || "No offers yet",
|
|
983
|
+
description: t("courses.offers.emptyHint") || "Create an offer to sell this course.",
|
|
984
|
+
action: /* @__PURE__ */ jsxs(Button, { onClick: addOffer, children: [
|
|
985
|
+
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
|
|
986
|
+
t("courses.offers.firstOffer") || "Create first offer"
|
|
987
|
+
] })
|
|
988
|
+
}
|
|
989
|
+
) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
990
|
+
offers.map((offer) => /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-border bg-card p-4 shadow-sm md:p-5", children: [
|
|
991
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
|
|
992
|
+
/* @__PURE__ */ jsx(Badge, { variant: "outline", className: "gap-1", children: offer.kind === "subscription" ? t("courses.offers.subscription") || "Subscription" : t("courses.offers.oneTime") || "One-time" }),
|
|
993
|
+
/* @__PURE__ */ jsx(
|
|
994
|
+
"button",
|
|
995
|
+
{
|
|
996
|
+
onClick: () => deleteOffer(offer.id),
|
|
997
|
+
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",
|
|
998
|
+
"aria-label": t("courses.offers.remove") || "Remove",
|
|
999
|
+
children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
|
|
1000
|
+
}
|
|
1001
|
+
)
|
|
1002
|
+
] }),
|
|
1003
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-[1fr_140px_160px]", children: [
|
|
1004
|
+
/* @__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 }) }) }),
|
|
1005
|
+
/* @__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 }) }) }),
|
|
1006
|
+
/* @__PURE__ */ jsx(Field, { label: t("courses.offers.kind") || "Type", children: /* @__PURE__ */ jsxs(
|
|
1007
|
+
Select,
|
|
1008
|
+
{
|
|
1009
|
+
value: offer.kind,
|
|
1010
|
+
onValueChange: (v) => patchOffer(offer.id, { kind: v, recurringInterval: v === "subscription" ? offer.recurringInterval ?? "month" : null }),
|
|
1011
|
+
children: [
|
|
1012
|
+
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
1013
|
+
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
1014
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "one_time", children: t("courses.offers.oneTime") || "One-time" }),
|
|
1015
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "subscription", children: t("courses.offers.subscription") || "Subscription" })
|
|
1016
|
+
] })
|
|
1017
|
+
]
|
|
1018
|
+
}
|
|
1019
|
+
) })
|
|
1020
|
+
] }),
|
|
1021
|
+
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(
|
|
986
1022
|
Select,
|
|
987
1023
|
{
|
|
988
|
-
value: offer.
|
|
989
|
-
onValueChange: (v) => patchOffer(offer.id, {
|
|
1024
|
+
value: offer.recurringInterval ?? "month",
|
|
1025
|
+
onValueChange: (v) => patchOffer(offer.id, { recurringInterval: v }),
|
|
990
1026
|
children: [
|
|
991
1027
|
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
992
1028
|
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
993
|
-
/* @__PURE__ */ jsx(SelectItem, { value: "
|
|
994
|
-
/* @__PURE__ */ jsx(SelectItem, { value: "
|
|
1029
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "month", children: t("courses.interval.month") || "month" }),
|
|
1030
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "year", children: t("courses.interval.year") || "year" })
|
|
995
1031
|
] })
|
|
996
1032
|
]
|
|
997
1033
|
}
|
|
998
|
-
) })
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
value: offer.recurringInterval ?? "month",
|
|
1004
|
-
onValueChange: (v) => patchOffer(offer.id, { recurringInterval: v }),
|
|
1005
|
-
children: [
|
|
1006
|
-
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
1007
|
-
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
1008
|
-
/* @__PURE__ */ jsx(SelectItem, { value: "month", children: t("courses.interval.month") || "month" }),
|
|
1009
|
-
/* @__PURE__ */ jsx(SelectItem, { value: "year", children: t("courses.interval.year") || "year" })
|
|
1010
|
-
] })
|
|
1011
|
-
]
|
|
1012
|
-
}
|
|
1013
|
-
) }) }),
|
|
1014
|
-
/* @__PURE__ */ jsxs("div", { className: "mt-4 rounded-lg border border-dashed border-border bg-muted/30 p-3", children: [
|
|
1015
|
-
/* @__PURE__ */ jsxs("p", { className: "mb-2.5 flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
1016
|
-
/* @__PURE__ */ jsx(Clock, { className: "h-3.5 w-3.5" }),
|
|
1017
|
-
t("courses.offers.checkoutFlagsNote") || "Apply once the public checkout ships (coming soon)."
|
|
1018
|
-
] }),
|
|
1019
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-5 gap-y-2", children: [
|
|
1020
|
-
/* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 text-sm text-muted-foreground", children: [
|
|
1021
|
-
/* @__PURE__ */ jsx(Checkbox, { checked: offer.isDefault, onChange: (v) => patchOffer(offer.id, { isDefault: v }) }),
|
|
1022
|
-
t("courses.offers.default") || "Default"
|
|
1034
|
+
) }) }),
|
|
1035
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-4 rounded-lg border border-dashed border-border bg-muted/30 p-3", children: [
|
|
1036
|
+
/* @__PURE__ */ jsxs("p", { className: "mb-2.5 flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
1037
|
+
/* @__PURE__ */ jsx(Clock, { className: "h-3.5 w-3.5" }),
|
|
1038
|
+
t("courses.offers.checkoutFlagsNote") || "Apply once the public checkout ships (coming soon)."
|
|
1023
1039
|
] }),
|
|
1024
|
-
/* @__PURE__ */ jsxs("
|
|
1025
|
-
/* @__PURE__ */
|
|
1026
|
-
|
|
1040
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-5 gap-y-2", children: [
|
|
1041
|
+
/* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 text-sm text-muted-foreground", children: [
|
|
1042
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: offer.isDefault, onChange: (v) => patchOffer(offer.id, { isDefault: v }) }),
|
|
1043
|
+
t("courses.offers.default") || "Default"
|
|
1044
|
+
] }),
|
|
1045
|
+
/* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 text-sm text-muted-foreground", children: [
|
|
1046
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: offer.isOrderBump, onChange: (v) => patchOffer(offer.id, { isOrderBump: v }) }),
|
|
1047
|
+
t("courses.offers.bump") || "Order bump"
|
|
1048
|
+
] })
|
|
1027
1049
|
] })
|
|
1028
1050
|
] })
|
|
1051
|
+
] }, offer.id)),
|
|
1052
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: addOffer, className: "w-full border-dashed", children: [
|
|
1053
|
+
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
|
|
1054
|
+
t("courses.offers.add") || "Add offer"
|
|
1029
1055
|
] })
|
|
1030
|
-
] }, offer.id)),
|
|
1031
|
-
/* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: addOffer, className: "w-full border-dashed", children: [
|
|
1032
|
-
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
|
|
1033
|
-
t("courses.offers.add") || "Add offer"
|
|
1034
1056
|
] })
|
|
1035
|
-
] })
|
|
1036
|
-
] }),
|
|
1037
|
-
/* @__PURE__ */ jsxs(TabsContent, { value: "members", className: "space-y-4", children: [
|
|
1038
|
-
/* @__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." }),
|
|
1039
|
-
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
1040
|
-
/* @__PURE__ */ jsxs("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [
|
|
1041
|
-
t("courses.editor.enrolledStudents") || "Enrolled students",
|
|
1042
|
-
" (",
|
|
1043
|
-
students.length,
|
|
1044
|
-
")"
|
|
1045
|
-
] }),
|
|
1046
|
-
students.length === 0 ? /* @__PURE__ */ jsx(
|
|
1047
|
-
EmptyState,
|
|
1048
|
-
{
|
|
1049
|
-
icon: /* @__PURE__ */ jsx(Users, { className: "h-5 w-5" }),
|
|
1050
|
-
title: t("courses.editor.membersEmpty") || "No students yet",
|
|
1051
|
-
description: t("courses.editor.membersEmptyHint") || "Buyers are enrolled here automatically after a paid order."
|
|
1052
|
-
}
|
|
1053
|
-
) : /* @__PURE__ */ jsx("div", { className: "divide-y divide-border overflow-hidden rounded-xl border border-border bg-card", children: students.map((s) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
|
|
1054
|
-
/* @__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) }),
|
|
1055
|
-
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
|
|
1056
|
-
/* @__PURE__ */ jsx("span", { className: "truncate font-medium text-foreground", children: s.name }),
|
|
1057
|
-
s.email && /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-muted-foreground", children: s.email })
|
|
1058
|
-
] }),
|
|
1059
|
-
s.since && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: new Date(s.since).toLocaleDateString() })
|
|
1060
|
-
] }, s.key)) })
|
|
1061
1057
|
] }),
|
|
1062
|
-
|
|
1063
|
-
/* @__PURE__ */ jsx(
|
|
1064
|
-
/* @__PURE__ */
|
|
1065
|
-
/* @__PURE__ */
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1058
|
+
/* @__PURE__ */ jsxs(TabsContent, { value: "members", className: "mt-4 space-y-4", children: [
|
|
1059
|
+
/* @__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." }),
|
|
1060
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
1061
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [
|
|
1062
|
+
t("courses.editor.enrolledStudents") || "Enrolled students",
|
|
1063
|
+
" (",
|
|
1064
|
+
students.length,
|
|
1065
|
+
")"
|
|
1066
|
+
] }),
|
|
1067
|
+
students.length === 0 ? /* @__PURE__ */ jsx(
|
|
1068
|
+
EmptyState,
|
|
1069
|
+
{
|
|
1070
|
+
icon: /* @__PURE__ */ jsx(Users, { className: "h-5 w-5" }),
|
|
1071
|
+
title: t("courses.editor.membersEmpty") || "No students yet",
|
|
1072
|
+
description: t("courses.editor.membersEmptyHint") || "Buyers are enrolled here automatically after a paid order."
|
|
1073
|
+
}
|
|
1074
|
+
) : /* @__PURE__ */ jsx("div", { className: "divide-y divide-border overflow-hidden rounded-xl border border-border bg-card", children: students.map((s) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
|
|
1075
|
+
/* @__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) }),
|
|
1076
|
+
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
|
|
1077
|
+
/* @__PURE__ */ jsx("span", { className: "truncate font-medium text-foreground", children: s.name }),
|
|
1078
|
+
s.email && /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-muted-foreground", children: s.email })
|
|
1079
|
+
] }),
|
|
1080
|
+
s.since && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: new Date(s.since).toLocaleDateString() })
|
|
1081
|
+
] }, s.key)) })
|
|
1082
|
+
] }),
|
|
1083
|
+
offers.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
1084
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: t("courses.editor.accessUnlockedBy") || "Access is unlocked by" }),
|
|
1085
|
+
/* @__PURE__ */ jsx("div", { className: "divide-y divide-border overflow-hidden rounded-xl border border-border bg-card", children: offers.map((offer) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
|
|
1086
|
+
/* @__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" }) }),
|
|
1087
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate font-medium text-foreground", children: offer.name }),
|
|
1088
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm tabular-nums text-muted-foreground", children: formatMoney(offer.price, offer.currency) })
|
|
1089
|
+
] }, offer.id)) })
|
|
1090
|
+
] })
|
|
1069
1091
|
] })
|
|
1070
|
-
] })
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
)
|
|
1085
|
-
|
|
1092
|
+
] }),
|
|
1093
|
+
/* @__PURE__ */ jsx(
|
|
1094
|
+
ConfirmDialog,
|
|
1095
|
+
{
|
|
1096
|
+
open: pendingDelete !== null,
|
|
1097
|
+
variant: "destructive",
|
|
1098
|
+
title: pendingDelete?.kind === "module" ? t("courses.editor.deleteModuleConfirm") || "Delete this module?" : t("courses.editor.deleteLessonConfirm") || "Remove this lesson?",
|
|
1099
|
+
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.",
|
|
1100
|
+
confirmLabel: t("courses.editor.confirmDelete") || "Delete",
|
|
1101
|
+
cancelLabel: t("courses.editor.cancel") || "Cancel",
|
|
1102
|
+
onConfirm: confirmDelete,
|
|
1103
|
+
onCancel: () => setPendingDelete(null)
|
|
1104
|
+
}
|
|
1105
|
+
)
|
|
1106
|
+
] })
|
|
1107
|
+
);
|
|
1086
1108
|
}
|
|
1087
1109
|
CourseEditorPage.displayName = "CourseEditorPage";
|
|
1088
1110
|
function formatDuration(sec, t) {
|
|
@@ -1507,9 +1529,10 @@ function statusVariant2(s) {
|
|
|
1507
1529
|
return "outline";
|
|
1508
1530
|
}
|
|
1509
1531
|
var salesCache = null;
|
|
1510
|
-
function SalesPage() {
|
|
1532
|
+
function SalesPage({ navTabs, activeNavTab, ordersRoute = "/courses/sales" } = {}) {
|
|
1511
1533
|
const t = useTranslation();
|
|
1512
1534
|
const [orders, setOrders] = React3.useState(salesCache);
|
|
1535
|
+
const tabs = navTabs?.();
|
|
1513
1536
|
React3.useEffect(() => {
|
|
1514
1537
|
if (salesCache) return;
|
|
1515
1538
|
let alive = true;
|
|
@@ -1517,10 +1540,10 @@ function SalesPage() {
|
|
|
1517
1540
|
void Promise.all([
|
|
1518
1541
|
p.listCoreSales().catch(() => []),
|
|
1519
1542
|
p.listOrders().catch(() => [])
|
|
1520
|
-
]).then(([core,
|
|
1543
|
+
]).then(([core, store2]) => {
|
|
1521
1544
|
if (!alive) return;
|
|
1522
1545
|
const byId = /* @__PURE__ */ new Map();
|
|
1523
|
-
for (const o of
|
|
1546
|
+
for (const o of store2) byId.set(o.id, o);
|
|
1524
1547
|
for (const o of core) byId.set(o.id, o);
|
|
1525
1548
|
const list = [...byId.values()].sort((a, b) => a.createdAt < b.createdAt ? 1 : -1);
|
|
1526
1549
|
salesCache = list;
|
|
@@ -1552,6 +1575,12 @@ function SalesPage() {
|
|
|
1552
1575
|
{
|
|
1553
1576
|
title: t("courses.sales.title") || "Vendas",
|
|
1554
1577
|
subtitle: t("courses.sales.subtitle") || "Todas as transa\xE7\xF5es dos seus cursos.",
|
|
1578
|
+
navTabs: tabs?.map((tb) => ({ value: tb.value, label: tb.label })),
|
|
1579
|
+
activeNavTab,
|
|
1580
|
+
onNavTabChange: (v) => {
|
|
1581
|
+
const target = tabs?.find((tb) => tb.value === v);
|
|
1582
|
+
if (target) navigateTo(target.route);
|
|
1583
|
+
},
|
|
1555
1584
|
stats: [
|
|
1556
1585
|
{ label: t("courses.sales.count") || "Vendas", value: String(paid.length) },
|
|
1557
1586
|
{ label: t("courses.sales.revenue") || "Receita", value: formatMoney(revenue, currency) }
|
|
@@ -1560,7 +1589,7 @@ function SalesPage() {
|
|
|
1560
1589
|
rows: orders ?? [],
|
|
1561
1590
|
loading: orders === null,
|
|
1562
1591
|
empty: t("courses.sales.empty") || "Nenhuma venda ainda.",
|
|
1563
|
-
onRowClick: (o) => navigateTo(
|
|
1592
|
+
onRowClick: (o) => navigateTo(`${ordersRoute}/${o.id}`)
|
|
1564
1593
|
}
|
|
1565
1594
|
);
|
|
1566
1595
|
}
|
|
@@ -1578,7 +1607,7 @@ function instTone(s) {
|
|
|
1578
1607
|
if (s === "overdue") return "destructive";
|
|
1579
1608
|
return "warning";
|
|
1580
1609
|
}
|
|
1581
|
-
function CourseOrderDetailView({ id }) {
|
|
1610
|
+
function CourseOrderDetailView({ id, ordersRoute = "/courses/sales" }) {
|
|
1582
1611
|
const t = useTranslation();
|
|
1583
1612
|
const [order, setOrder] = React3.useState(() => id ? orderCache.get(id) ?? null : null);
|
|
1584
1613
|
const [loading, setLoading] = React3.useState(() => !(id && orderCache.has(id)));
|
|
@@ -1605,7 +1634,7 @@ function CourseOrderDetailView({ id }) {
|
|
|
1605
1634
|
alive = false;
|
|
1606
1635
|
};
|
|
1607
1636
|
}, [id]);
|
|
1608
|
-
const back = () => navigateTo(
|
|
1637
|
+
const back = () => navigateTo(ordersRoute);
|
|
1609
1638
|
if (loading) return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(OrderDetailSkeleton, {}) });
|
|
1610
1639
|
if (!order) {
|
|
1611
1640
|
return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(
|
|
@@ -1678,9 +1707,10 @@ function statusVariant3(s) {
|
|
|
1678
1707
|
if (s === "past_due") return "secondary";
|
|
1679
1708
|
return "destructive";
|
|
1680
1709
|
}
|
|
1681
|
-
function SubscriptionsPage() {
|
|
1710
|
+
function SubscriptionsPage({ navTabs, activeNavTab } = {}) {
|
|
1682
1711
|
const t = useTranslation();
|
|
1683
1712
|
const [subs, setSubs] = React3.useState(null);
|
|
1713
|
+
const tabs = navTabs?.();
|
|
1684
1714
|
React3.useEffect(() => {
|
|
1685
1715
|
void getCoursesProvider().listSubscriptions().then(setSubs);
|
|
1686
1716
|
}, []);
|
|
@@ -1702,38 +1732,45 @@ function SubscriptionsPage() {
|
|
|
1702
1732
|
] })
|
|
1703
1733
|
] }) }
|
|
1704
1734
|
];
|
|
1705
|
-
return /* @__PURE__ */ jsxs(
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
{
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1735
|
+
return /* @__PURE__ */ jsxs(
|
|
1736
|
+
PageContainer,
|
|
1737
|
+
{
|
|
1738
|
+
title: t("courses.subs.title") || "Subscriptions",
|
|
1739
|
+
subtitle: t("courses.subs.subtitle") || "Recurring plans and MRR.",
|
|
1740
|
+
navTabs: tabs?.map((tb) => ({ value: tb.value, label: tb.label })),
|
|
1741
|
+
activeNavTab,
|
|
1742
|
+
onNavTabChange: (v) => {
|
|
1743
|
+
const target = tabs?.find((tb) => tb.value === v);
|
|
1744
|
+
if (target) navigateTo(target.route);
|
|
1745
|
+
},
|
|
1746
|
+
children: [
|
|
1747
|
+
/* @__PURE__ */ jsxs(StatGrid, { children: [
|
|
1748
|
+
/* @__PURE__ */ jsx(StatCard, { label: t("courses.subs.active") || "Active subscriptions", value: String(active.length) }),
|
|
1749
|
+
/* @__PURE__ */ jsx(StatCard, { label: t("courses.subs.mrr") || "Monthly recurring revenue", value: formatMoney(mrr, currency) })
|
|
1750
|
+
] }),
|
|
1751
|
+
/* @__PURE__ */ jsx(
|
|
1752
|
+
SimpleTable,
|
|
1753
|
+
{
|
|
1754
|
+
columns,
|
|
1755
|
+
rows: subs ?? [],
|
|
1756
|
+
empty: t("courses.subs.empty") || "No subscriptions yet."
|
|
1757
|
+
}
|
|
1758
|
+
)
|
|
1759
|
+
]
|
|
1760
|
+
}
|
|
1761
|
+
);
|
|
1719
1762
|
}
|
|
1720
1763
|
SubscriptionsPage.displayName = "SubscriptionsPage";
|
|
1721
1764
|
function FinancialPage() {
|
|
1722
1765
|
const t = useTranslation();
|
|
1723
1766
|
const [summary, setSummary] = React3.useState(null);
|
|
1724
1767
|
const [payouts, setPayouts] = React3.useState(null);
|
|
1725
|
-
const [account, setAccount] = React3.useState(null);
|
|
1726
1768
|
React3.useEffect(() => {
|
|
1727
1769
|
const p = getCoursesProvider();
|
|
1728
1770
|
void p.getFinancialSummary().then(setSummary);
|
|
1729
1771
|
void p.listPayouts().then(setPayouts);
|
|
1730
|
-
void p.getCreatorAccount().then(setAccount);
|
|
1731
1772
|
}, []);
|
|
1732
1773
|
const currency = summary?.currency ?? "BRL";
|
|
1733
|
-
const connected = !!account?.stripeAccountId && account.chargesEnabled;
|
|
1734
|
-
function connectStripe() {
|
|
1735
|
-
toast.info(t("courses.financial.connectSoon") || "Stripe Connect onboarding wires up in Stage 3.");
|
|
1736
|
-
}
|
|
1737
1774
|
const columns = [
|
|
1738
1775
|
{ key: "date", header: t("courses.financial.date") || "Date", render: (p) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatDate(p.createdAt) }) },
|
|
1739
1776
|
{ key: "amount", header: t("courses.financial.amount") || "Amount", align: "right", render: (p) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: formatMoney(p.amount, p.currency) }) },
|
|
@@ -1743,19 +1780,8 @@ function FinancialPage() {
|
|
|
1743
1780
|
PageContainer,
|
|
1744
1781
|
{
|
|
1745
1782
|
title: t("courses.financial.title") || "Financial settings",
|
|
1746
|
-
subtitle: t("courses.financial.subtitle") || "Balance
|
|
1747
|
-
actions: /* @__PURE__ */ jsx(Button, { variant: connected ? "outline" : "default", onClick: connectStripe, children: connected ? t("courses.financial.connected") || "Stripe connected" : t("courses.financial.connect") || "Connect Stripe" }),
|
|
1783
|
+
subtitle: t("courses.financial.subtitle") || "Balance and payouts.",
|
|
1748
1784
|
children: [
|
|
1749
|
-
!connected && /* @__PURE__ */ jsxs("div", { className: "mb-6 rounded-xl border border-amber-500/40 bg-amber-500/10 p-4 text-sm text-foreground", children: [
|
|
1750
|
-
t("courses.financial.notConnected") || "Connect your Stripe account to receive payouts. The platform fee is applied automatically on each sale.",
|
|
1751
|
-
account && /* @__PURE__ */ jsxs("span", { className: "ml-1 text-muted-foreground", children: [
|
|
1752
|
-
"(",
|
|
1753
|
-
t("courses.financial.platformFee") || "Platform fee",
|
|
1754
|
-
": ",
|
|
1755
|
-
(account.platformFeeBps / 100).toFixed(2),
|
|
1756
|
-
"%)"
|
|
1757
|
-
] })
|
|
1758
|
-
] }),
|
|
1759
1785
|
/* @__PURE__ */ jsxs(StatGrid, { children: [
|
|
1760
1786
|
/* @__PURE__ */ jsx(StatCard, { label: t("courses.financial.available") || "Available balance", value: formatMoney(summary?.availableBalance ?? 0, currency) }),
|
|
1761
1787
|
/* @__PURE__ */ jsx(StatCard, { label: t("courses.financial.pending") || "Pending balance", value: formatMoney(summary?.pendingBalance ?? 0, currency) }),
|
|
@@ -1900,35 +1926,398 @@ function createCoursesDashboardWidgets(modules = {}) {
|
|
|
1900
1926
|
...modules.financial ? [defineKpiWidget({ id: "courses.kpi.fees", title: "courses.kpi.fees", domain: "courses", defaultOrder: 3, component: FeesKpi })] : []
|
|
1901
1927
|
];
|
|
1902
1928
|
}
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1929
|
+
var KIWIFY_FUNCTION = "kiwify-courses-sync";
|
|
1930
|
+
var KIWIFY_SOURCE = "kiwify";
|
|
1931
|
+
function client() {
|
|
1932
|
+
const supabase = getSupabaseClientOptional();
|
|
1933
|
+
if (!supabase) throw new Error("Sem conex\xE3o com o banco para falar com a Kiwify.");
|
|
1934
|
+
return supabase;
|
|
1935
|
+
}
|
|
1936
|
+
async function invoke(body) {
|
|
1937
|
+
const tenantId = getCoursesTenantId();
|
|
1938
|
+
const runtimeToken = await connectorRuntimeToken({ tenantId });
|
|
1939
|
+
const { data, error } = await client().functions.invoke(KIWIFY_FUNCTION, {
|
|
1940
|
+
body: { ...body, tenantId },
|
|
1941
|
+
headers: { [CONNECTOR_RUNTIME_TOKEN_HEADER]: runtimeToken }
|
|
1942
|
+
});
|
|
1943
|
+
if (error) throw new Error(error.message);
|
|
1944
|
+
return data;
|
|
1945
|
+
}
|
|
1946
|
+
function checkAccount() {
|
|
1947
|
+
return invoke({ action: "test" });
|
|
1948
|
+
}
|
|
1949
|
+
function listProducts() {
|
|
1950
|
+
return invoke({ action: "products" }).then((data) => data.products ?? []);
|
|
1951
|
+
}
|
|
1952
|
+
function importSales(input) {
|
|
1953
|
+
return invoke({ action: "sync_sales", from: input.from, to: input.to });
|
|
1954
|
+
}
|
|
1955
|
+
async function listMappings() {
|
|
1956
|
+
const tenantId = getCoursesTenantId();
|
|
1957
|
+
let query = client().from(COURSES_TABLES.courses).select("id,title,external_id,external_source").order("title");
|
|
1958
|
+
if (tenantId) query = query.eq("tenant_id", tenantId);
|
|
1959
|
+
const { data, error } = await query;
|
|
1960
|
+
if (error) throw new Error(error.message);
|
|
1961
|
+
return (data ?? []).map((row) => ({
|
|
1962
|
+
courseId: String(row.id),
|
|
1963
|
+
title: String(row.title ?? ""),
|
|
1964
|
+
// A course mapped to another provider is unmapped as far as Kiwify is
|
|
1965
|
+
// concerned — showing Hotmart's id in Kiwify's column would invite someone
|
|
1966
|
+
// to overwrite it.
|
|
1967
|
+
externalId: row.external_source === KIWIFY_SOURCE ? row.external_id ?? null : null
|
|
1968
|
+
}));
|
|
1969
|
+
}
|
|
1970
|
+
async function saveMapping(courseId, externalId) {
|
|
1971
|
+
const value = externalId?.trim() ? externalId.trim() : null;
|
|
1972
|
+
const { error } = await client().from(COURSES_TABLES.courses).update({
|
|
1973
|
+
external_id: value,
|
|
1974
|
+
// Cleared together: a source with no id is a row that claims an origin it
|
|
1975
|
+
// cannot name, and the partial unique index would stop enforcing anything.
|
|
1976
|
+
external_source: value ? KIWIFY_SOURCE : null
|
|
1977
|
+
}).eq("id", courseId);
|
|
1978
|
+
if (error) throw new Error(error.message);
|
|
1979
|
+
}
|
|
1980
|
+
var KIWIFY_CONNECTOR_ID = "kiwify";
|
|
1981
|
+
var KIWIFY_FIELDS = [
|
|
1982
|
+
{ key: "accountId", label: "Account ID", type: "text", placeholder: "Kiwify \u2192 Apps \u2192 API" },
|
|
1983
|
+
{ key: "clientId", label: "Client ID", type: "text", secret: true, placeholder: "Kiwify \u2192 Apps \u2192 API" },
|
|
1984
|
+
{ key: "clientSecret", label: "Client Secret", type: "password", placeholder: "Mostrado junto com o Client ID" }
|
|
1985
|
+
];
|
|
1986
|
+
var KIWIFY_MAX_WINDOW_DAYS = 90;
|
|
1987
|
+
var KIWIFY_SETTINGS = [
|
|
1988
|
+
{
|
|
1989
|
+
key: "importStatuses",
|
|
1990
|
+
label: "Quais vendas importar",
|
|
1991
|
+
description: "Kiwify tem onze situa\xE7\xF5es de venda; estas s\xE3o as que viram pedido aqui.",
|
|
1992
|
+
type: "multi-select",
|
|
1993
|
+
options: [
|
|
1994
|
+
{ value: "paid", label: "Pagas" },
|
|
1995
|
+
{ value: "pending", label: "Aguardando pagamento" },
|
|
1996
|
+
{ value: "refunded", label: "Reembolsadas" },
|
|
1997
|
+
{ value: "chargeback", label: "Chargeback" }
|
|
1998
|
+
],
|
|
1999
|
+
// Required to OPERATE and not to connect: a feed with nothing selected has
|
|
2000
|
+
// nothing to bring, so the connection is real and reads as "conectado,
|
|
2001
|
+
// falta configurar" (`plg_connections.status = 'pending'`).
|
|
2002
|
+
requiredToOperate: true,
|
|
2003
|
+
defaultValue: ["paid", "refunded", "chargeback"]
|
|
2004
|
+
},
|
|
2005
|
+
{
|
|
2006
|
+
// The difference between "a venda entrou" and "o aluno entrou" is a
|
|
2007
|
+
// decision. A school that reviews enrolments by hand needs the sale
|
|
2008
|
+
// recorded and the door still shut.
|
|
2009
|
+
key: "grantAccess",
|
|
2010
|
+
label: "Liberar o acesso automaticamente",
|
|
2011
|
+
description: "Venda paga vira matr\xEDcula na hora. Desligado, o pedido entra e a matr\xEDcula \xE9 sua.",
|
|
2012
|
+
type: "toggle",
|
|
2013
|
+
defaultValue: true
|
|
2014
|
+
},
|
|
2015
|
+
{
|
|
2016
|
+
key: "windowDays",
|
|
2017
|
+
label: "Per\xEDodo de cada sincroniza\xE7\xE3o",
|
|
2018
|
+
description: "Quantos dias para tr\xE1s buscar quando a sincroniza\xE7\xE3o roda.",
|
|
2019
|
+
type: "text",
|
|
2020
|
+
placeholder: "30",
|
|
2021
|
+
// 1–90. Kiwify refuses a wider window than 90 days, so a number that reads
|
|
2022
|
+
// fine here and fails at the provider is not expressible.
|
|
2023
|
+
pattern: "[1-9]|[1-8]\\d|90",
|
|
2024
|
+
patternHint: "De 1 a 90 dias \u2014 o limite \xE9 da pr\xF3pria Kiwify.",
|
|
2025
|
+
defaultValue: "30"
|
|
2026
|
+
}
|
|
2027
|
+
];
|
|
2028
|
+
var store = createConnectionStore(KIWIFY_CONNECTOR_ID, {
|
|
2029
|
+
fields: KIWIFY_FIELDS,
|
|
2030
|
+
settings: KIWIFY_SETTINGS
|
|
2031
|
+
});
|
|
2032
|
+
var STATUS_LABELS = {
|
|
2033
|
+
paid: "Pagas",
|
|
2034
|
+
pending: "Aguardando",
|
|
2035
|
+
refunded: "Reembolsadas",
|
|
2036
|
+
chargeback: "Chargeback"
|
|
2037
|
+
};
|
|
2038
|
+
function statusesDetail(value) {
|
|
2039
|
+
const list = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
|
|
2040
|
+
const labels = list.map((v) => STATUS_LABELS[String(v).trim()] ?? String(v).trim()).filter(Boolean);
|
|
2041
|
+
return labels.length > 0 ? labels.join(", ") : void 0;
|
|
2042
|
+
}
|
|
2043
|
+
function daysAgo(days) {
|
|
2044
|
+
const d = /* @__PURE__ */ new Date();
|
|
2045
|
+
d.setDate(d.getDate() - days);
|
|
2046
|
+
return d.toISOString().slice(0, 10);
|
|
2047
|
+
}
|
|
2048
|
+
function KiwifyExtraPanel() {
|
|
2049
|
+
const [connected, setConnected] = useState(false);
|
|
2050
|
+
const [loaded, setLoaded] = useState(false);
|
|
2051
|
+
const [products, setProducts] = useState(null);
|
|
2052
|
+
const [mappings, setMappings] = useState([]);
|
|
2053
|
+
const [from, setFrom] = useState(() => daysAgo(30));
|
|
2054
|
+
const [to, setTo] = useState(() => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
2055
|
+
const [importing, setImporting] = useState(false);
|
|
2056
|
+
const [checking, setChecking] = useState(false);
|
|
2057
|
+
const [runs, setRuns] = useState([]);
|
|
2058
|
+
useEffect(() => {
|
|
2059
|
+
let cancelled = false;
|
|
2060
|
+
void store.get().then((c) => {
|
|
2061
|
+
if (!cancelled) setConnected(!!c?.active);
|
|
2062
|
+
}).catch(() => {
|
|
2063
|
+
}).finally(() => {
|
|
2064
|
+
if (!cancelled) setLoaded(true);
|
|
2065
|
+
});
|
|
2066
|
+
return () => {
|
|
2067
|
+
cancelled = true;
|
|
2068
|
+
};
|
|
2069
|
+
}, []);
|
|
2070
|
+
useEffect(() => {
|
|
2071
|
+
if (!connected) return;
|
|
2072
|
+
void listMappings().then(setMappings).catch(() => {
|
|
2073
|
+
});
|
|
2074
|
+
void store.listRuns({ limit: 10 }).then(setRuns).catch(() => {
|
|
2075
|
+
});
|
|
2076
|
+
void listProducts().then(setProducts).catch(() => setProducts([]));
|
|
2077
|
+
}, [connected]);
|
|
2078
|
+
const mappedCount = useMemo(() => mappings.filter((m) => m.externalId).length, [mappings]);
|
|
2079
|
+
if (!loaded || !connected) return null;
|
|
2080
|
+
async function handleMap(courseId, externalId) {
|
|
2081
|
+
const previous = mappings;
|
|
2082
|
+
setMappings((rows) => rows.map((r) => r.courseId === courseId ? { ...r, externalId: externalId || null } : r));
|
|
2083
|
+
try {
|
|
2084
|
+
await saveMapping(courseId, externalId || null);
|
|
2085
|
+
} catch (e) {
|
|
2086
|
+
setMappings(previous);
|
|
2087
|
+
toast.error(e?.message ?? "N\xE3o deu para salvar o v\xEDnculo");
|
|
1925
2088
|
}
|
|
1926
|
-
}
|
|
2089
|
+
}
|
|
2090
|
+
async function handleImport() {
|
|
2091
|
+
setImporting(true);
|
|
2092
|
+
try {
|
|
2093
|
+
const result = await importSales({ from, to });
|
|
2094
|
+
const parts = [`${result.imported} novas`];
|
|
2095
|
+
if (result.updated) parts.push(`${result.updated} atualizadas`);
|
|
2096
|
+
if (result.enrolled) parts.push(`${result.enrolled} matr\xEDculas`);
|
|
2097
|
+
if (result.unmapped) parts.push(`${result.unmapped} sem curso vinculado`);
|
|
2098
|
+
toast.success(parts.join(" \xB7 "));
|
|
2099
|
+
void store.listRuns({ limit: 10 }).then(setRuns);
|
|
2100
|
+
} catch (e) {
|
|
2101
|
+
toast.error(e?.message ?? "Erro ao importar as vendas");
|
|
2102
|
+
} finally {
|
|
2103
|
+
setImporting(false);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
async function handleCheck() {
|
|
2107
|
+
setChecking(true);
|
|
2108
|
+
try {
|
|
2109
|
+
const result = await checkAccount();
|
|
2110
|
+
if (!result.ok) throw new Error(result.message ?? "A Kiwify n\xE3o reconheceu essa credencial");
|
|
2111
|
+
toast.success(result.message ?? "Credencial v\xE1lida");
|
|
2112
|
+
} catch (e) {
|
|
2113
|
+
toast.error(e?.message ?? "N\xE3o deu para falar com a Kiwify");
|
|
2114
|
+
} finally {
|
|
2115
|
+
setChecking(false);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-5 border-t pt-4", children: [
|
|
2119
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: handleCheck, disabled: checking, children: [
|
|
2120
|
+
checking ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(Plug, { className: "h-3.5 w-3.5" }),
|
|
2121
|
+
" ",
|
|
2122
|
+
"Testar credencial"
|
|
2123
|
+
] }),
|
|
2124
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
2125
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2126
|
+
/* @__PURE__ */ jsx(Link2, { className: "h-4 w-4 text-muted-foreground" }),
|
|
2127
|
+
/* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Qual produto \xE9 qual curso" })
|
|
2128
|
+
] }),
|
|
2129
|
+
/* @__PURE__ */ jsxs("p", { className: "mt-1 text-xs text-muted-foreground", children: [
|
|
2130
|
+
"Uma venda s\xF3 entra se o produto vendido na Kiwify apontar para um curso daqui.",
|
|
2131
|
+
" ",
|
|
2132
|
+
mappedCount,
|
|
2133
|
+
" de ",
|
|
2134
|
+
mappings.length,
|
|
2135
|
+
" vinculados."
|
|
2136
|
+
] }),
|
|
2137
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-3 max-h-72 divide-y overflow-auto rounded-md border", children: [
|
|
2138
|
+
mappings.length === 0 && /* @__PURE__ */ jsx("p", { className: "px-3 py-4 text-xs text-muted-foreground", children: "Nenhum curso cadastrado ainda." }),
|
|
2139
|
+
mappings.map((m) => /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-3 px-3 py-2 text-sm", children: [
|
|
2140
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: m.title }),
|
|
2141
|
+
/* @__PURE__ */ jsxs(
|
|
2142
|
+
"select",
|
|
2143
|
+
{
|
|
2144
|
+
className: "h-8 w-56 shrink-0 rounded-md border border-border bg-background px-2 text-xs",
|
|
2145
|
+
value: m.externalId ?? "",
|
|
2146
|
+
onChange: (e) => void handleMap(m.courseId, e.target.value),
|
|
2147
|
+
children: [
|
|
2148
|
+
/* @__PURE__ */ jsx("option", { value: "", children: "\u2014 sem v\xEDnculo \u2014" }),
|
|
2149
|
+
m.externalId && !(products ?? []).some((p) => p.id === m.externalId) && /* @__PURE__ */ jsx("option", { value: m.externalId, children: m.externalId }),
|
|
2150
|
+
(products ?? []).map((p) => /* @__PURE__ */ jsx("option", { value: p.id, children: p.name }, p.id))
|
|
2151
|
+
]
|
|
2152
|
+
}
|
|
2153
|
+
)
|
|
2154
|
+
] }, m.courseId))
|
|
2155
|
+
] }),
|
|
2156
|
+
products?.length === 0 && /* @__PURE__ */ jsx("p", { className: "mt-2 text-[11px] text-muted-foreground", children: "N\xE3o deu para ler o cat\xE1logo da Kiwify agora. Voc\xEA ainda pode colar o id do produto." })
|
|
2157
|
+
] }),
|
|
2158
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
2159
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2160
|
+
/* @__PURE__ */ jsx(Download, { className: "h-4 w-4 text-muted-foreground" }),
|
|
2161
|
+
/* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Importar vendas" })
|
|
2162
|
+
] }),
|
|
2163
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap items-end gap-3", children: [
|
|
2164
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
2165
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "De" }),
|
|
2166
|
+
/* @__PURE__ */ jsx(DatePicker, { value: from, onChange: setFrom, className: "mt-1" })
|
|
2167
|
+
] }),
|
|
2168
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
2169
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "At\xE9" }),
|
|
2170
|
+
/* @__PURE__ */ jsx(DatePicker, { value: to, onChange: setTo, className: "mt-1" })
|
|
2171
|
+
] }),
|
|
2172
|
+
/* @__PURE__ */ jsxs(Button, { size: "sm", onClick: handleImport, disabled: importing, children: [
|
|
2173
|
+
importing ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(Download, { className: "h-3.5 w-3.5" }),
|
|
2174
|
+
" ",
|
|
2175
|
+
"Importar"
|
|
2176
|
+
] })
|
|
2177
|
+
] }),
|
|
2178
|
+
/* @__PURE__ */ jsxs("p", { className: "mt-2 text-[11px] text-muted-foreground", children: [
|
|
2179
|
+
"A Kiwify responde no m\xE1ximo ",
|
|
2180
|
+
KIWIFY_MAX_WINDOW_DAYS,
|
|
2181
|
+
" dias por consulta. Reimportar o mesmo per\xEDodo n\xE3o duplica: uma venda j\xE1 importada \xE9 reconhecida pelo id da Kiwify e s\xF3 tem a situa\xE7\xE3o atualizada."
|
|
2182
|
+
] })
|
|
2183
|
+
] }),
|
|
2184
|
+
runs.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
2185
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2186
|
+
/* @__PURE__ */ jsx(History, { className: "h-4 w-4 text-muted-foreground" }),
|
|
2187
|
+
/* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Hist\xF3rico" })
|
|
2188
|
+
] }),
|
|
2189
|
+
/* @__PURE__ */ jsx("div", { className: "divide-y rounded-md border text-sm", children: runs.map((r) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-3 py-2", title: r.error ?? void 0, children: [
|
|
2190
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: r.startedAt ? new Date(r.startedAt).toLocaleString("pt-BR") : "\u2014" }),
|
|
2191
|
+
/* @__PURE__ */ jsxs("span", { className: "ml-auto text-xs", children: [
|
|
2192
|
+
r.written,
|
|
2193
|
+
"/",
|
|
2194
|
+
r.fetched,
|
|
2195
|
+
" importadas",
|
|
2196
|
+
typeof r.stats?.unmapped === "number" && r.stats.unmapped > 0 ? ` \xB7 ${r.stats.unmapped} sem v\xEDnculo` : ""
|
|
2197
|
+
] }),
|
|
2198
|
+
r.status === "success" ? /* @__PURE__ */ jsx(CheckCircle2, { className: "h-3.5 w-3.5 text-success" }) : /* @__PURE__ */ jsx(AlertCircle, { className: "h-3.5 w-3.5 text-warning" })
|
|
2199
|
+
] }, r.id)) })
|
|
2200
|
+
] }),
|
|
2201
|
+
/* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: "O segredo da API fica guardado fora deste aplicativo. Quem fala com a Kiwify \xE9 o servidor, que pede a credencial na hora da consulta." })
|
|
2202
|
+
] });
|
|
1927
2203
|
}
|
|
2204
|
+
var kiwifyConnectorDef = {
|
|
2205
|
+
id: KIWIFY_CONNECTOR_ID,
|
|
2206
|
+
hostPluginId: "courses",
|
|
2207
|
+
name: "Kiwify",
|
|
2208
|
+
description: "Traga as vendas feitas na Kiwify e libere o acesso do aluno na \xE1rea de membros.",
|
|
2209
|
+
icon: "GraduationCap",
|
|
2210
|
+
category: "courses",
|
|
2211
|
+
authKind: "api-key",
|
|
2212
|
+
// Inbound because WE reach Kiwify: it has never heard of Fayz, which is what
|
|
2213
|
+
// makes this a pull connector and why it ships an edge function at all.
|
|
2214
|
+
direction: "inbound",
|
|
2215
|
+
// Sync only. The sale has to exist INSIDE Fayz to enrol the student, appear in
|
|
2216
|
+
// Pedidos and be reported on — it is not a question the assistant can answer
|
|
2217
|
+
// by asking Kiwify at the moment it is asked.
|
|
2218
|
+
planes: ["sync"],
|
|
2219
|
+
docsUrl: "https://docs.kiwify.com.br/api-reference/general",
|
|
2220
|
+
// Kiwify documents 100 calls per minute for one account, and the budget is
|
|
2221
|
+
// expressed per SECOND — 1/s rather than 1.66/s because the scheduler rounds
|
|
2222
|
+
// its spacing up to whole milliseconds anyway and a pull of a few hundred
|
|
2223
|
+
// sales has no reason to run at the edge of a 429.
|
|
2224
|
+
rateLimit: { perSecond: 1 },
|
|
2225
|
+
// Grounded in what 0010's `courses_import_external_sales` actually does, and
|
|
2226
|
+
// padded with nothing. It reads courses to find which one was sold; it writes
|
|
2227
|
+
// the order, the student and the enrolment. It reads no order back, so there
|
|
2228
|
+
// is no `order: read` here.
|
|
2229
|
+
permissions: [
|
|
2230
|
+
{
|
|
2231
|
+
kind: "data",
|
|
2232
|
+
resource: "course",
|
|
2233
|
+
actions: ["read"],
|
|
2234
|
+
reason: "Descobrir qual curso daqui corresponde ao produto vendido na Kiwify. Nenhum curso \xE9 alterado."
|
|
2235
|
+
},
|
|
2236
|
+
{
|
|
2237
|
+
kind: "data",
|
|
2238
|
+
resource: "order",
|
|
2239
|
+
actions: ["write"],
|
|
2240
|
+
reason: "Registrar a venda: valor, forma de pagamento, situa\xE7\xE3o e os dados de quem comprou. Uma venda j\xE1 importada \xE9 atualizada, nunca duplicada.",
|
|
2241
|
+
sensitive: true
|
|
2242
|
+
},
|
|
2243
|
+
{
|
|
2244
|
+
kind: "data",
|
|
2245
|
+
resource: "customer",
|
|
2246
|
+
actions: ["write"],
|
|
2247
|
+
reason: "Cadastrar o aluno que comprou, com nome e e-mail, quando ele ainda n\xE3o existe aqui.",
|
|
2248
|
+
sensitive: true
|
|
2249
|
+
},
|
|
2250
|
+
{
|
|
2251
|
+
kind: "data",
|
|
2252
|
+
resource: "enrollment",
|
|
2253
|
+
actions: ["write"],
|
|
2254
|
+
reason: "Dar acesso ao curso quando a venda est\xE1 paga \u2014 e tirar o acesso quando ela \xE9 reembolsada ou vira chargeback."
|
|
2255
|
+
}
|
|
2256
|
+
],
|
|
2257
|
+
// Manual only, and that is the whole list today. A scheduled pull belongs to
|
|
2258
|
+
// the Fayz schedule worker (FAY-1347) and a webhook needs an endpoint Kiwify
|
|
2259
|
+
// can reach; declaring either here would promise a moment that never arrives.
|
|
2260
|
+
invokedAt: [
|
|
2261
|
+
{
|
|
2262
|
+
key: "courses.sales.import",
|
|
2263
|
+
label: "Voc\xEA escolhe um per\xEDodo aqui e as vendas da Kiwify s\xE3o importadas",
|
|
2264
|
+
plane: "sync",
|
|
2265
|
+
trigger: "manual",
|
|
2266
|
+
touches: ["course", "order", "customer", "enrollment"]
|
|
2267
|
+
}
|
|
2268
|
+
],
|
|
2269
|
+
fields: KIWIFY_FIELDS,
|
|
2270
|
+
settings: KIWIFY_SETTINGS,
|
|
2271
|
+
// Read from the row the list already batched, so a screen with N connectors
|
|
2272
|
+
// costs one plg_connections request rather than N.
|
|
2273
|
+
...connectionStatus(
|
|
2274
|
+
KIWIFY_CONNECTOR_ID,
|
|
2275
|
+
(connection) => ({
|
|
2276
|
+
connected: !!connection?.active,
|
|
2277
|
+
detail: statusesDetail(connection?.settings?.importStatuses),
|
|
2278
|
+
plane: "sync",
|
|
2279
|
+
...settingsStatus(KIWIFY_SETTINGS, connection)
|
|
2280
|
+
}),
|
|
2281
|
+
{ store: () => store }
|
|
2282
|
+
),
|
|
2283
|
+
...connectionSettings(store),
|
|
2284
|
+
// Reached only AFTER the credential sink accepted the pair — that ordering is
|
|
2285
|
+
// what makes `active = true` mean "a credential was placed for this tenant"
|
|
2286
|
+
// rather than "somebody pressed a button". Neither `clientId` nor
|
|
2287
|
+
// `clientSecret` arrives here; the hub split both out before calling this, so
|
|
2288
|
+
// the account id is the whole of what this function sees.
|
|
2289
|
+
saveConnection: async (values) => {
|
|
2290
|
+
await store.save({
|
|
2291
|
+
active: true,
|
|
2292
|
+
// The provider's own id for the account: what the row shows an operator,
|
|
2293
|
+
// and what the edge function spends the redeemed token against.
|
|
2294
|
+
providerAccountKey: values.accountId ?? null,
|
|
2295
|
+
settings: { accountId: values.accountId }
|
|
2296
|
+
});
|
|
2297
|
+
},
|
|
2298
|
+
disconnect: () => store.disconnect(),
|
|
2299
|
+
ExtraPanel: KiwifyExtraPanel
|
|
2300
|
+
};
|
|
1928
2301
|
|
|
1929
2302
|
// src/index.ts
|
|
1930
2303
|
var asComp = (c) => c;
|
|
1931
2304
|
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)`;
|
|
2308
|
+
return Bound;
|
|
2309
|
+
}
|
|
2310
|
+
function redirectTo(target) {
|
|
2311
|
+
const Redirect = (params) => {
|
|
2312
|
+
const to = typeof target === "function" ? target(params) : target;
|
|
2313
|
+
React3.useEffect(() => {
|
|
2314
|
+
navigateTo(to);
|
|
2315
|
+
}, [to]);
|
|
2316
|
+
return null;
|
|
2317
|
+
};
|
|
2318
|
+
Redirect.displayName = `Redirect(${typeof target === "string" ? target : "fn"})`;
|
|
2319
|
+
return Redirect;
|
|
2320
|
+
}
|
|
1932
2321
|
function createCoursesPlugin(options) {
|
|
1933
2322
|
registerTranslations(coursesLocales);
|
|
1934
2323
|
setCoursesTenantResolver(() => useOrganizationStore.getState().currentOrg?.id);
|
|
@@ -1939,27 +2328,48 @@ function createCoursesPlugin(options) {
|
|
|
1939
2328
|
const section = options?.navSection ?? "main";
|
|
1940
2329
|
const modules = options?.modules ?? {};
|
|
1941
2330
|
const labels = options?.labels ?? {};
|
|
2331
|
+
const ordersRoute = options?.routes?.orders ?? "/courses/sales";
|
|
2332
|
+
const subscriptionsRoute = options?.routes?.subscriptions ?? "/courses/subscriptions";
|
|
2333
|
+
const salesTabs = options?.salesTabs ?? false;
|
|
2334
|
+
const salesTabStrip = salesTabs ? () => [
|
|
2335
|
+
{ value: "orders", label: labels.sales ?? "Sales", route: ordersRoute },
|
|
2336
|
+
{ value: "subscriptions", label: labels.subscriptions ?? "Subscriptions", route: subscriptionsRoute }
|
|
2337
|
+
] : void 0;
|
|
1942
2338
|
const navigation = [
|
|
1943
2339
|
{ section, position: base, label: options?.navLabel ?? "Courses", route: "/courses", icon: "BookOpen", permission: read("courses") },
|
|
1944
2340
|
// The members area is no longer a standalone nav entry — it lives inside each
|
|
1945
2341
|
// course's record (CourseEditorPage → "Alunos" tab). modules.membersArea is
|
|
1946
2342
|
// kept accepted-but-inert for backward compat with existing manifests.
|
|
1947
|
-
...modules.sales ? [{ section, position: base + 2, label: labels.sales ?? "Sales", route:
|
|
1948
|
-
|
|
2343
|
+
...modules.sales ? [{ section, position: options?.ordersNavPosition ?? base + 2, label: labels.sales ?? "Sales", route: ordersRoute, icon: "TrendingUp", permission: read("courses.sales") }] : [],
|
|
2344
|
+
// Com `salesTabs`, Assinaturas é uma ABA de Pedidos — listá-la também no menu
|
|
2345
|
+
// seria a mesma navegação duas vezes, que é o que a faixa existe para evitar.
|
|
2346
|
+
...modules.subscriptions && !salesTabs ? [{ section, position: base + 3, label: labels.subscriptions ?? "Subscriptions", route: subscriptionsRoute, icon: "Repeat", permission: read("courses.sales") }] : [],
|
|
1949
2347
|
...modules.financial ? [{ section, position: base + 4, label: labels.financial ?? "Financial", route: "/courses/financial", icon: "CreditCard", permission: read("courses.finance") }] : [],
|
|
1950
2348
|
...modules.reports ? [{ section, position: base + 5, label: labels.reports ?? "Reports", route: "/courses/reports", icon: "BarChart3", permission: read("courses.sales") }] : []
|
|
1951
2349
|
];
|
|
2350
|
+
const salesProps = { navTabs: salesTabStrip, activeNavTab: "orders" };
|
|
2351
|
+
const subsProps = { navTabs: salesTabStrip, activeNavTab: "subscriptions" };
|
|
1952
2352
|
const routes = [
|
|
1953
2353
|
// Static segments outscore the `:id` param in the saas router, so these
|
|
1954
2354
|
// resolve before the editor route (AdminShell sorts by routeScore).
|
|
1955
2355
|
{ path: "/courses", component: asComp(CoursesListPage), permission: read("courses") },
|
|
1956
|
-
...modules.sales ? [{ path:
|
|
1957
|
-
// Order detail — static
|
|
1958
|
-
...modules.sales ? [{ path:
|
|
1959
|
-
...modules.subscriptions ? [{ path:
|
|
2356
|
+
...modules.sales ? [{ path: ordersRoute, component: asComp(withProps(SalesPage, salesProps)), permission: read("courses.sales") }] : [],
|
|
2357
|
+
// Order detail — the static `${ordersRoute}/:id` outscores `/courses/:id`.
|
|
2358
|
+
...modules.sales ? [{ path: `${ordersRoute}/:id`, component: asComp(withProps(CourseOrderDetailView, { ordersRoute })), permission: read("courses.sales") }] : [],
|
|
2359
|
+
...modules.subscriptions ? [{ path: subscriptionsRoute, component: asComp(withProps(SubscriptionsPage, subsProps)), permission: read("courses.sales") }] : [],
|
|
1960
2360
|
...modules.financial ? [{ path: "/courses/financial", component: asComp(FinancialPage), permission: read("courses.finance") }] : [],
|
|
1961
2361
|
...modules.reports ? [{ path: "/courses/reports", component: asComp(ReportsPage), permission: read("courses.sales") }] : [],
|
|
1962
|
-
{ path: "/courses/:id", component: asComp(CourseEditorPage) }
|
|
2362
|
+
{ path: "/courses/:id", component: asComp(CourseEditorPage) },
|
|
2363
|
+
// Onde as telas moravam quando o host as reposiciona. Redirecionam para não
|
|
2364
|
+
// deixar um link antigo renderizando tela em branco.
|
|
2365
|
+
...modules.sales && ordersRoute !== "/courses/sales" ? [
|
|
2366
|
+
// O detalhe vem PRIMEIRO e com o param: `/courses/sales/:id` (score 40)
|
|
2367
|
+
// ganha do curinga `/courses/sales/*` (29) que o shell registra junto
|
|
2368
|
+
// com o pai — sem ele, todo link antigo de pedido caía na lista.
|
|
2369
|
+
{ path: "/courses/sales/:id", component: asComp(redirectTo((p) => `${ordersRoute}/${p.id}`)) },
|
|
2370
|
+
{ path: "/courses/sales", component: asComp(redirectTo(ordersRoute)) }
|
|
2371
|
+
] : [],
|
|
2372
|
+
...modules.subscriptions && subscriptionsRoute !== "/courses/subscriptions" ? [{ path: "/courses/subscriptions", component: asComp(redirectTo(subscriptionsRoute)) }] : []
|
|
1963
2373
|
];
|
|
1964
2374
|
return {
|
|
1965
2375
|
id: "courses",
|
|
@@ -1973,7 +2383,11 @@ function createCoursesPlugin(options) {
|
|
|
1973
2383
|
// saas/admin Panel world rather than the ecommerce storefront.
|
|
1974
2384
|
scaffolds: ["saas"],
|
|
1975
2385
|
defaultEnabled: true,
|
|
1976
|
-
|
|
2386
|
+
// Cursos does not collect money. It produces the reason to charge — a
|
|
2387
|
+
// matrícula, a mensalidade — and Financeiro is what turns that into a
|
|
2388
|
+
// receivable and receives it. Stating the dependency is what keeps the two
|
|
2389
|
+
// from overlapping: the module that charges is one, and it is not this one.
|
|
2390
|
+
dependencies: ["financial"],
|
|
1977
2391
|
declaredFeatures: [
|
|
1978
2392
|
{ id: "courses", label: "Courses", group: "Courses" },
|
|
1979
2393
|
{ id: "courses.sales", label: "Sales", group: "Courses" },
|
|
@@ -1988,10 +2402,60 @@ function createCoursesPlugin(options) {
|
|
|
1988
2402
|
// Commerce KPIs on the shared 'home' surface follow the same opt-in rule:
|
|
1989
2403
|
// no module enabled → no widgets broadcast into a host's dashboard.
|
|
1990
2404
|
dashboardWidgets: createCoursesDashboardWidgets(modules),
|
|
1991
|
-
connectors: [createStripeConnector()],
|
|
1992
2405
|
settings: [
|
|
1993
2406
|
{ id: "courses", label: "Courses", icon: "BookOpen", component: asComp(CoursesSettingsTab), order: 20, permission: read("courses") }
|
|
1994
2407
|
],
|
|
2408
|
+
// No payment connector here, and no payment slug on the shelf either.
|
|
2409
|
+
// `stripe` and `mercadopago` used to sit in both lists, which put a second
|
|
2410
|
+
// Stripe row on the connectors screen of an app that has one Stripe account
|
|
2411
|
+
// and one place to charge from. Money-in is Financeiro's, whoever raised the
|
|
2412
|
+
// invoice — see `dependencies` above.
|
|
2413
|
+
//
|
|
2414
|
+
// Hotmart and Kiwify stay: the catalogue's `courses` shelf is literally
|
|
2415
|
+
// "courses sold by a third party", which is a different relationship — the
|
|
2416
|
+
// sale happens over there and we learn about it.
|
|
2417
|
+
integrations: ["mailchimp", "hotmart", "kiwify"],
|
|
2418
|
+
// Kiwify is that shelf's first BUILT one. It takes no payment: the student
|
|
2419
|
+
// paid on Kiwify's checkout into the creator's Kiwify account, and what is
|
|
2420
|
+
// imported is the fact of the sale, so the members area can let them in.
|
|
2421
|
+
// The slug stays on `integrations` above too — the catalogue row joins on
|
|
2422
|
+
// the union of the two lists.
|
|
2423
|
+
connectors: [kiwifyConnectorDef],
|
|
2424
|
+
functions: [
|
|
2425
|
+
{
|
|
2426
|
+
slug: KIWIFY_FUNCTION,
|
|
2427
|
+
// Invoked from the browser by an authenticated operator picking a
|
|
2428
|
+
// period, so Supabase must check the caller's JWT. Nothing outside
|
|
2429
|
+
// reaches it: Kiwify does not call us, and no Fayz ingress wakes a pull
|
|
2430
|
+
// connector.
|
|
2431
|
+
ingress: "none",
|
|
2432
|
+
verifyJwt: true,
|
|
2433
|
+
// NAMES only. The tenant's Kiwify pair is NOT here and could not be: a
|
|
2434
|
+
// Supabase secret is per-POOL and a pool serves N schools with N Kiwify
|
|
2435
|
+
// accounts. The pair is redeemed per call from the platform vault, which
|
|
2436
|
+
// is what the last three names are for.
|
|
2437
|
+
secrets: [
|
|
2438
|
+
"SUPABASE_URL",
|
|
2439
|
+
"SUPABASE_SERVICE_ROLE_KEY",
|
|
2440
|
+
"FAYZ_API_URL",
|
|
2441
|
+
"FAYZ_PROJECT_ID",
|
|
2442
|
+
"CONNECTOR_REDEMPTION_SECRET"
|
|
2443
|
+
],
|
|
2444
|
+
// What this connector's credential may touch. Direct table access is
|
|
2445
|
+
// not on the list on purpose (2026-08-11 ADR): the import goes through
|
|
2446
|
+
// the RPC, which takes the tenant from the connection row instead of
|
|
2447
|
+
// from the request body.
|
|
2448
|
+
grants: ["courses_import_external_sales"]
|
|
2449
|
+
}
|
|
2450
|
+
],
|
|
2451
|
+
declaredRpcs: [
|
|
2452
|
+
{
|
|
2453
|
+
name: "courses_import_external_sales",
|
|
2454
|
+
kind: "write",
|
|
2455
|
+
description: "Import third-party course sales as orders, students and enrolments. Idempotent on (tenant, external source, external id); the tenant comes from the connection row, never from the payload. A sale whose product maps to no course here is counted and skipped.",
|
|
2456
|
+
audits: false
|
|
2457
|
+
}
|
|
2458
|
+
],
|
|
1995
2459
|
locales: coursesLocales
|
|
1996
2460
|
};
|
|
1997
2461
|
}
|