@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.
Files changed (79) hide show
  1. package/README.md +3 -3
  2. package/dist/components/CommerceUI.d.ts +19 -2
  3. package/dist/components/CommerceUI.d.ts.map +1 -1
  4. package/dist/components/dashboardWidgets.d.ts.map +1 -0
  5. package/dist/index.d.ts +26 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +773 -309
  8. package/dist/index.js.map +1 -1
  9. package/dist/integrations/kiwify/connectorDef.d.ts +34 -0
  10. package/dist/integrations/kiwify/connectorDef.d.ts.map +1 -0
  11. package/dist/integrations/kiwify/data.d.ts +58 -0
  12. package/dist/integrations/kiwify/data.d.ts.map +1 -0
  13. package/dist/lib/nav.d.ts.map +1 -0
  14. package/dist/locales/en.d.ts.map +1 -1
  15. package/dist/locales/pt-BR.d.ts.map +1 -1
  16. package/dist/public/createPublicCheckoutPlugin.d.ts +1 -1
  17. package/dist/public/createPublicCheckoutPlugin.d.ts.map +1 -1
  18. package/dist/public/{data.d.ts → data/index.d.ts} +1 -1
  19. package/dist/public/data/index.d.ts.map +1 -0
  20. package/dist/public/{data.supabase.d.ts → data/supabase.d.ts} +2 -2
  21. package/dist/public/data/supabase.d.ts.map +1 -0
  22. package/dist/public/index.d.ts +4 -4
  23. package/dist/public/index.d.ts.map +1 -1
  24. package/dist/public/index.js +353 -0
  25. package/dist/public/index.js.map +1 -0
  26. package/dist/public/{CheckoutScreen.d.ts → views/CheckoutScreen.d.ts} +1 -1
  27. package/dist/public/views/CheckoutScreen.d.ts.map +1 -0
  28. package/dist/views/CourseEditorPage.d.ts.map +1 -0
  29. package/dist/views/CourseOrderDetailView.d.ts +4 -3
  30. package/dist/views/CourseOrderDetailView.d.ts.map +1 -1
  31. package/dist/{pages → views}/CoursesListPage.d.ts.map +1 -1
  32. package/dist/views/FinancialPage.d.ts.map +1 -0
  33. package/dist/{pages → views}/ReportsPage.d.ts.map +1 -1
  34. package/dist/views/SalesPage.d.ts +19 -0
  35. package/dist/views/SalesPage.d.ts.map +1 -0
  36. package/dist/views/SubscriptionsPage.d.ts +7 -0
  37. package/dist/views/SubscriptionsPage.d.ts.map +1 -0
  38. package/package.json +12 -12
  39. package/dist/connectors/stripe.d.ts +0 -3
  40. package/dist/connectors/stripe.d.ts.map +0 -1
  41. package/dist/dashboardWidgets.d.ts.map +0 -1
  42. package/dist/nav.d.ts.map +0 -1
  43. package/dist/pages/CourseEditorPage.d.ts.map +0 -1
  44. package/dist/pages/FinancialPage.d.ts.map +0 -1
  45. package/dist/pages/SalesPage.d.ts +0 -6
  46. package/dist/pages/SalesPage.d.ts.map +0 -1
  47. package/dist/pages/SubscriptionsPage.d.ts +0 -6
  48. package/dist/pages/SubscriptionsPage.d.ts.map +0 -1
  49. package/dist/public/CheckoutScreen.d.ts.map +0 -1
  50. package/dist/public/data.d.ts.map +0 -1
  51. package/dist/public/data.supabase.d.ts.map +0 -1
  52. package/src/components/CommerceUI.tsx +0 -153
  53. package/src/connectors/stripe.ts +0 -34
  54. package/src/dashboardWidgets.tsx +0 -64
  55. package/src/index.ts +0 -150
  56. package/src/lib/format.ts +0 -22
  57. package/src/locales/en.ts +0 -203
  58. package/src/locales/index.ts +0 -7
  59. package/src/locales/pt-BR.ts +0 -203
  60. package/src/nav.ts +0 -9
  61. package/src/pages/CourseEditorPage.tsx +0 -1122
  62. package/src/pages/CoursesListPage.tsx +0 -137
  63. package/src/pages/FinancialPage.tsx +0 -81
  64. package/src/pages/ReportsPage.tsx +0 -59
  65. package/src/pages/SalesPage.tsx +0 -86
  66. package/src/pages/SubscriptionsPage.tsx +0 -56
  67. package/src/public/CheckoutScreen.tsx +0 -279
  68. package/src/public/createPublicCheckoutPlugin.tsx +0 -88
  69. package/src/public/data.supabase.ts +0 -63
  70. package/src/public/data.ts +0 -89
  71. package/src/public/index.ts +0 -26
  72. package/src/settings/CoursesSettings.tsx +0 -55
  73. package/src/views/CourseOrderDetailView.tsx +0 -132
  74. /package/dist/{dashboardWidgets.d.ts → components/dashboardWidgets.d.ts} +0 -0
  75. /package/dist/{nav.d.ts → lib/nav.d.ts} +0 -0
  76. /package/dist/{pages → views}/CourseEditorPage.d.ts +0 -0
  77. /package/dist/{pages → views}/CoursesListPage.d.ts +0 -0
  78. /package/dist/{pages → views}/FinancialPage.d.ts +0 -0
  79. /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 { defineKpiWidget, useModuleLayout, PageHeaderActions, Button, Badge, useSaveBar, toast, Tabs, TabsList, TabsTrigger, TabsContent, Input, SegmentedControl, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Checkbox, ConfirmDialog, KpiCard, Skeleton, Card, DataTable } from '@fayz-ai/ui';
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 { ArrowLeft, Users, ShoppingCart, Layers, Tag, ImagePlus, Loader2, UploadCloud, Info, Plus, Trash2, Clock, Lock, Check, GripVertical, ChevronDown, Video, FileText, PlayCircle } from 'lucide-react';
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", { className: "mx-auto max-w-6xl p-6 md:p-8", children: [
535
- /* @__PURE__ */ jsxs("div", { className: "mb-6 flex items-start justify-between gap-4", children: [
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
- if (loading || !course) return /* @__PURE__ */ jsx(EditorSkeleton, {});
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 /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-4xl p-6 md:p-8", children: [
848
- /* @__PURE__ */ jsxs(
849
- "button",
850
- {
851
- onClick: () => navigateTo("/courses"),
852
- className: "mb-4 inline-flex items-center gap-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground",
853
- children: [
854
- /* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" }),
855
- " ",
856
- t("courses.editor.back") || "Back to courses"
857
- ]
858
- }
859
- ),
860
- /* @__PURE__ */ jsxs("div", { className: "mb-6 flex flex-wrap items-center gap-x-3 gap-y-2", children: [
861
- /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold text-foreground", children: course.title || (t("courses.editor.untitledCourse") || "Untitled course") }),
862
- /* @__PURE__ */ jsx(Badge, { variant: course.status === "published" ? "success" : course.status === "archived" ? "secondary" : "outline", children: t(`courses.status.${course.status}`) || course.status }),
863
- /* @__PURE__ */ jsx(Badge, { variant: "outline", children: t(`courses.editor.delivery.${course.deliveryType}`) || course.deliveryType })
864
- ] }),
865
- /* @__PURE__ */ jsxs(Tabs, { value: tab, onValueChange: (v) => setTab(v), children: [
866
- /* @__PURE__ */ jsx("div", { className: "-mx-1 mb-6 overflow-x-auto px-1 pb-1", children: /* @__PURE__ */ jsx(TabsList, { className: "h-auto flex-nowrap", children: TABS.map((tk) => {
867
- const Icon = TAB_ICON[tk];
868
- return /* @__PURE__ */ jsxs(TabsTrigger, { value: tk, className: "gap-1.5 whitespace-nowrap py-1.5", children: [
869
- /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" }),
870
- t(`courses.editor.tab.${tk}`) || tk
871
- ] }, tk);
872
- }) }) }),
873
- /* @__PURE__ */ jsxs(TabsContent, { value: "details", className: "space-y-5", children: [
874
- /* @__PURE__ */ jsx(SectionCard, { title: t("courses.editor.sectionBasics") || "Basics", children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
875
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldTitle") || "Title", children: /* @__PURE__ */ jsx(Input, { value: course.title, onChange: (e) => patchCourse({ title: e.target.value }) }) }),
876
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldSubtitle") || "Subtitle", children: /* @__PURE__ */ jsx(Input, { value: course.subtitle ?? "", onChange: (e) => patchCourse({ subtitle: e.target.value }) }) }),
877
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldDescription") || "Description", children: /* @__PURE__ */ jsx(
878
- Textarea,
879
- {
880
- value: course.description ?? "",
881
- onChange: (e) => patchCourse({ description: e.target.value }),
882
- placeholder: t("courses.editor.descriptionPlaceholder") || "What will students learn?"
883
- }
884
- ) }),
885
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldDelivery") || "Format", hint: t("courses.editor.deliveryHint") || "How this course is delivered.", children: /* @__PURE__ */ jsx(
886
- SegmentedControl,
887
- {
888
- size: "md",
889
- options: deliveryOptions,
890
- value: course.deliveryType,
891
- onChange: (v) => patchCourse({ deliveryType: v }),
892
- "aria-label": t("courses.editor.fieldDelivery") || "Format"
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-1 space-y-3", children: [
905
- /* @__PURE__ */ jsx("input", { ref: fileRef, type: "file", accept: "image/*", className: "hidden", onChange: onPickThumbnail }),
906
- /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", disabled: uploading, onClick: () => fileRef.current?.click(), children: [
907
- uploading ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(UploadCloud, { className: "h-4 w-4" }),
908
- uploading ? t("courses.editor.uploading") || "Uploading\u2026" : t("courses.editor.thumbnailUpload") || "Upload image"
909
- ] }),
910
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.thumbnailUrl") || "Or paste an image URL", children: /* @__PURE__ */ jsx(
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
- value: course.thumbnailUrl ?? "",
914
- placeholder: "https://\u2026",
915
- onChange: (e) => patchCourse({ thumbnailUrl: e.target.value || null })
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
- /* @__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: [
921
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldPrice") || "Price", hint: course.currency, children: /* @__PURE__ */ jsx(
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
- /* @__PURE__ */ jsx(TabsContent, { value: "curriculum", className: "space-y-4", children: /* @__PURE__ */ jsx(
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
- icon: /* @__PURE__ */ jsx(Tag, { className: "h-5 w-5" }),
961
- title: t("courses.offers.empty") || "No offers yet",
962
- description: t("courses.offers.emptyHint") || "Create an offer to sell this course.",
963
- action: /* @__PURE__ */ jsxs(Button, { onClick: addOffer, children: [
964
- /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
965
- t("courses.offers.firstOffer") || "Create first offer"
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
- ) : /* @__PURE__ */ jsxs(Fragment, { children: [
969
- offers.map((offer) => /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-border bg-card p-4 shadow-sm md:p-5", children: [
970
- /* @__PURE__ */ jsxs("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
971
- /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "gap-1", children: offer.kind === "subscription" ? t("courses.offers.subscription") || "Subscription" : t("courses.offers.oneTime") || "One-time" }),
972
- /* @__PURE__ */ jsx(
973
- "button",
974
- {
975
- onClick: () => deleteOffer(offer.id),
976
- 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",
977
- "aria-label": t("courses.offers.remove") || "Remove",
978
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
979
- }
980
- )
981
- ] }),
982
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-[1fr_140px_160px]", children: [
983
- /* @__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 }) }) }),
984
- /* @__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 }) }) }),
985
- /* @__PURE__ */ jsx(Field, { label: t("courses.offers.kind") || "Type", children: /* @__PURE__ */ jsxs(
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.kind,
989
- onValueChange: (v) => patchOffer(offer.id, { kind: v, recurringInterval: v === "subscription" ? offer.recurringInterval ?? "month" : null }),
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: "one_time", children: t("courses.offers.oneTime") || "One-time" }),
994
- /* @__PURE__ */ jsx(SelectItem, { value: "subscription", children: t("courses.offers.subscription") || "Subscription" })
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
- 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(
1001
- Select,
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("label", { className: "flex cursor-pointer items-center gap-2 text-sm text-muted-foreground", children: [
1025
- /* @__PURE__ */ jsx(Checkbox, { checked: offer.isOrderBump, onChange: (v) => patchOffer(offer.id, { isOrderBump: v }) }),
1026
- t("courses.offers.bump") || "Order bump"
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
- offers.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1063
- /* @__PURE__ */ jsx("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: t("courses.editor.accessUnlockedBy") || "Access is unlocked by" }),
1064
- /* @__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: [
1065
- /* @__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" }) }),
1066
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate font-medium text-foreground", children: offer.name }),
1067
- /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm tabular-nums text-muted-foreground", children: formatMoney(offer.price, offer.currency) })
1068
- ] }, offer.id)) })
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
- /* @__PURE__ */ jsx(
1073
- ConfirmDialog,
1074
- {
1075
- open: pendingDelete !== null,
1076
- variant: "destructive",
1077
- title: pendingDelete?.kind === "module" ? t("courses.editor.deleteModuleConfirm") || "Delete this module?" : t("courses.editor.deleteLessonConfirm") || "Remove this lesson?",
1078
- 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.",
1079
- confirmLabel: t("courses.editor.confirmDelete") || "Delete",
1080
- cancelLabel: t("courses.editor.cancel") || "Cancel",
1081
- onConfirm: confirmDelete,
1082
- onCancel: () => setPendingDelete(null)
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, store]) => {
1543
+ ]).then(([core, store2]) => {
1521
1544
  if (!alive) return;
1522
1545
  const byId = /* @__PURE__ */ new Map();
1523
- for (const o of store) byId.set(o.id, o);
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(`/courses/sales/${o.id}`)
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("/courses/sales");
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(PageContainer, { title: t("courses.subs.title") || "Subscriptions", subtitle: t("courses.subs.subtitle") || "Recurring plans and MRR.", children: [
1706
- /* @__PURE__ */ jsxs(StatGrid, { children: [
1707
- /* @__PURE__ */ jsx(StatCard, { label: t("courses.subs.active") || "Active subscriptions", value: String(active.length) }),
1708
- /* @__PURE__ */ jsx(StatCard, { label: t("courses.subs.mrr") || "Monthly recurring revenue", value: formatMoney(mrr, currency) })
1709
- ] }),
1710
- /* @__PURE__ */ jsx(
1711
- SimpleTable,
1712
- {
1713
- columns,
1714
- rows: subs ?? [],
1715
- empty: t("courses.subs.empty") || "No subscriptions yet."
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, payouts and your Stripe connection.",
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
- function createStripeConnector() {
1904
- return {
1905
- id: "stripe-connect",
1906
- hostPluginId: "courses",
1907
- name: "Stripe",
1908
- description: "Receba os pagamentos dos alunos; a taxa da plataforma \xE9 aplicada automaticamente.",
1909
- icon: "CreditCard",
1910
- authKind: "oauth",
1911
- async getStatus() {
1912
- try {
1913
- const account = await getCoursesProvider().getCreatorAccount();
1914
- const fee = (account.platformFeeBps / 100).toFixed(2);
1915
- return {
1916
- connected: !!account.stripeAccountId && account.chargesEnabled,
1917
- detail: `Taxa da plataforma: ${fee}%`
1918
- };
1919
- } catch {
1920
- return { connected: false };
1921
- }
1922
- },
1923
- async startOAuth(_redirectTo) {
1924
- throw new Error("O onboarding do Stripe Connect \xE9 ligado na Etapa 3.");
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: "/courses/sales", icon: "TrendingUp", permission: read("courses.sales") }] : [],
1948
- ...modules.subscriptions ? [{ section, position: base + 3, label: labels.subscriptions ?? "Subscriptions", route: "/courses/subscriptions", icon: "Repeat", permission: read("courses.sales") }] : [],
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: "/courses/sales", component: asComp(SalesPage), permission: read("courses.sales") }] : [],
1957
- // Order detail — static `/courses/sales/:id` outscores `/courses/:id`.
1958
- ...modules.sales ? [{ path: "/courses/sales/:id", component: asComp(CourseOrderDetailView), permission: read("courses.sales") }] : [],
1959
- ...modules.subscriptions ? [{ path: "/courses/subscriptions", component: asComp(SubscriptionsPage), permission: read("courses.sales") }] : [],
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
- dependencies: [],
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
  }