@fayz-ai/plugin-courses 0.2.2 → 0.2.3

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 (44) hide show
  1. package/README.md +2 -0
  2. package/dist/components/CommerceUI.d.ts +39 -0
  3. package/dist/components/CommerceUI.d.ts.map +1 -0
  4. package/dist/connectors/stripe.d.ts +3 -0
  5. package/dist/connectors/stripe.d.ts.map +1 -0
  6. package/dist/dashboardWidgets.d.ts +3 -0
  7. package/dist/dashboardWidgets.d.ts.map +1 -0
  8. package/dist/index.cjs +735 -54
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +738 -57
  12. package/dist/index.js.map +1 -1
  13. package/dist/lib/format.d.ts +4 -0
  14. package/dist/lib/format.d.ts.map +1 -0
  15. package/dist/locales/en.d.ts.map +1 -1
  16. package/dist/locales/pt-BR.d.ts.map +1 -1
  17. package/dist/pages/CourseEditorPage.d.ts.map +1 -1
  18. package/dist/pages/FinancialPage.d.ts +6 -0
  19. package/dist/pages/FinancialPage.d.ts.map +1 -0
  20. package/dist/pages/MembersAreaPage.d.ts +6 -0
  21. package/dist/pages/MembersAreaPage.d.ts.map +1 -0
  22. package/dist/pages/ReportsPage.d.ts +6 -0
  23. package/dist/pages/ReportsPage.d.ts.map +1 -0
  24. package/dist/pages/SalesPage.d.ts +6 -0
  25. package/dist/pages/SalesPage.d.ts.map +1 -0
  26. package/dist/pages/SubscriptionsPage.d.ts +6 -0
  27. package/dist/pages/SubscriptionsPage.d.ts.map +1 -0
  28. package/dist/settings/CoursesSettings.d.ts +6 -0
  29. package/dist/settings/CoursesSettings.d.ts.map +1 -0
  30. package/package.json +7 -5
  31. package/src/components/CommerceUI.tsx +77 -0
  32. package/src/connectors/stripe.ts +34 -0
  33. package/src/dashboardWidgets.tsx +51 -0
  34. package/src/index.ts +51 -12
  35. package/src/lib/format.ts +22 -0
  36. package/src/locales/en.ts +109 -0
  37. package/src/locales/pt-BR.ts +109 -0
  38. package/src/pages/CourseEditorPage.tsx +133 -35
  39. package/src/pages/FinancialPage.tsx +76 -0
  40. package/src/pages/MembersAreaPage.tsx +60 -0
  41. package/src/pages/ReportsPage.tsx +59 -0
  42. package/src/pages/SalesPage.tsx +57 -0
  43. package/src/pages/SubscriptionsPage.tsx +56 -0
  44. package/src/settings/CoursesSettings.tsx +55 -0
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { hashRouterAdapter, registerPluginFactory, registerTranslations, useTranslation } from '@fayz-ai/core';
2
- import { getCoursesProviderOptional, setCoursesProvider, createMockCoursesProvider, getCoursesProvider } from '@fayz-ai/courses';
2
+ import { setCoursesTenantResolver, getCoursesProviderOptional, setCoursesProvider, createMockCoursesProvider, getCoursesProvider } from '@fayz-ai/courses';
3
+ import { useOrganizationStore, PluginSettingsPanel, SettingsGroup, SelectRow, ToggleRow } from '@fayz-ai/saas';
3
4
  import * as React2 from 'react';
4
- import { useModuleLayout, PageHeaderActions, Button, Badge, Input, toast } from '@fayz-ai/ui';
5
- import { jsxs, jsx } from 'react/jsx-runtime';
5
+ import { defineKpiWidget, KpiCard, useModuleLayout, PageHeaderActions, Button, Badge, Input, toast, Card, DataTable } from '@fayz-ai/ui';
6
+ import { jsx, jsxs } from 'react/jsx-runtime';
6
7
 
7
8
  // src/index.ts
8
9
 
@@ -38,7 +39,116 @@ var en = {
38
39
  "courses.editor.deleteLesson": "Remove",
39
40
  "courses.editor.untitledCourse": "Untitled course",
40
41
  "courses.editor.untitledModule": "New module",
41
- "courses.editor.untitledLesson": "New lesson"
42
+ "courses.editor.untitledLesson": "New lesson",
43
+ // Editor tabs
44
+ "courses.editor.tab.details": "General",
45
+ "courses.editor.tab.curriculum": "Curriculum",
46
+ "courses.editor.tab.offers": "Offers",
47
+ "courses.editor.tab.members": "Members area",
48
+ "courses.editor.tab.settings": "Payment",
49
+ "courses.editor.tab.checkout": "Checkout",
50
+ "courses.editor.tab.links": "Links",
51
+ "courses.editor.membersHelp": "Buyers of this course are added to its members area automatically. Access groups per offer:",
52
+ "courses.editor.defaultGroup": "Default group",
53
+ "courses.editor.settingsHelp": "Payment methods offered at checkout (Stripe).",
54
+ "courses.editor.payCard": "Credit card (up to 12x)",
55
+ "courses.editor.payPix": "Pix",
56
+ "courses.editor.payBoleto": "Boleto",
57
+ "courses.editor.settingsNote": "Method availability depends on Stripe in your region; wired in Stage 3.",
58
+ "courses.editor.checkoutSoon": "Custom checkout builder comes in a later milestone. A default checkout is generated per offer.",
59
+ "courses.editor.linksHelp": "Shareable checkout links, one per offer.",
60
+ // Offers
61
+ "courses.offers.help": "Price points a customer can buy. Mark one default; order-bumps show on other checkouts.",
62
+ "courses.offers.name": "Offer name",
63
+ "courses.offers.price": "Price",
64
+ "courses.offers.oneTime": "One-time",
65
+ "courses.offers.subscription": "Subscription",
66
+ "courses.offers.default": "Default",
67
+ "courses.offers.bump": "Bump",
68
+ "courses.offers.remove": "Remove",
69
+ "courses.offers.add": "Add offer",
70
+ "courses.offers.newName": "New offer",
71
+ // Sales
72
+ "courses.sales.title": "Sales",
73
+ "courses.sales.subtitle": "Every transaction across your courses.",
74
+ "courses.sales.date": "Date",
75
+ "courses.sales.customer": "Customer",
76
+ "courses.sales.method": "Method",
77
+ "courses.sales.status": "Status",
78
+ "courses.sales.fee": "Platform fee",
79
+ "courses.sales.amount": "Amount",
80
+ "courses.sales.count": "Sales",
81
+ "courses.sales.revenue": "Revenue",
82
+ "courses.sales.empty": "No sales yet.",
83
+ "courses.financialStatus.paid": "Paid",
84
+ "courses.financialStatus.pending": "Pending",
85
+ "courses.financialStatus.refunded": "Refunded",
86
+ "courses.financialStatus.chargeback": "Chargeback",
87
+ // Subscriptions
88
+ "courses.subs.title": "Subscriptions",
89
+ "courses.subs.subtitle": "Recurring plans and MRR.",
90
+ "courses.subs.start": "Start date",
91
+ "courses.subs.customer": "Customer",
92
+ "courses.subs.status": "Status",
93
+ "courses.subs.value": "Net value",
94
+ "courses.subs.active": "Active subscriptions",
95
+ "courses.subs.mrr": "Monthly recurring revenue",
96
+ "courses.subs.empty": "No subscriptions yet.",
97
+ "courses.subStatus.active": "Active",
98
+ "courses.subStatus.canceled": "Canceled",
99
+ "courses.subStatus.past_due": "Past due",
100
+ "courses.interval.month": "month",
101
+ "courses.interval.year": "year",
102
+ // Financial
103
+ "courses.financial.title": "Financial settings",
104
+ "courses.financial.subtitle": "Balance, payouts and your Stripe connection.",
105
+ "courses.financial.available": "Available balance",
106
+ "courses.financial.pending": "Pending balance",
107
+ "courses.financial.revenue": "Total revenue",
108
+ "courses.financial.feeCollected": "Platform fees",
109
+ "courses.financial.refundRate": "refund rate",
110
+ "courses.financial.payouts": "Payouts",
111
+ "courses.financial.noPayouts": "No payouts yet.",
112
+ "courses.financial.date": "Date",
113
+ "courses.financial.amount": "Amount",
114
+ "courses.financial.status": "Status",
115
+ "courses.financial.connect": "Connect Stripe",
116
+ "courses.financial.connected": "Stripe connected",
117
+ "courses.financial.notConnected": "Connect your Stripe account to receive payouts. The platform fee is applied automatically on each sale.",
118
+ "courses.financial.platformFee": "Platform fee",
119
+ "courses.financial.connectSoon": "Stripe Connect onboarding wires up in Stage 3.",
120
+ "courses.payoutStatus.paid": "Paid",
121
+ "courses.payoutStatus.pending": "Pending",
122
+ "courses.payoutStatus.failed": "Failed",
123
+ // Reports
124
+ "courses.reports.title": "Reports",
125
+ "courses.reports.subtitle": "Revenue by course.",
126
+ "courses.reports.byCourse": "Revenue by course",
127
+ "courses.reports.course": "Course",
128
+ "courses.reports.sales": "Sales",
129
+ "courses.reports.revenue": "Revenue",
130
+ "courses.reports.empty": "No revenue yet.",
131
+ // Members area
132
+ "courses.membersArea.title": "Members area",
133
+ "courses.membersArea.subtitle": "One area per course; students land here after purchase.",
134
+ "courses.membersArea.name": "Members area",
135
+ "courses.membersArea.status": "Status",
136
+ "courses.membersArea.students": "Students",
137
+ "courses.membersArea.empty": "No members areas yet.",
138
+ // Settings
139
+ "courses.settings.title": "Course settings",
140
+ "courses.settings.subtitle": "Payments, currency and checkout methods",
141
+ "courses.settings.payments": "Payments",
142
+ "courses.settings.paymentsHelp": "Connect Stripe in the Integrations tab; the platform fee applies automatically.",
143
+ "courses.settings.currency": "Default currency",
144
+ "courses.settings.methods": "Payment methods",
145
+ // Dashboard KPIs
146
+ "courses.kpi.revenue": "Total revenue",
147
+ "courses.kpi.sales": "Sales",
148
+ "courses.kpi.refunds": "refunds",
149
+ "courses.kpi.mrr": "Recurring revenue (MRR)",
150
+ "courses.kpi.activeSubs": "active",
151
+ "courses.kpi.fees": "Platform fees"
42
152
  };
43
153
 
44
154
  // src/locales/pt-BR.ts
@@ -73,7 +183,116 @@ var ptBR = {
73
183
  "courses.editor.deleteLesson": "Remover",
74
184
  "courses.editor.untitledCourse": "Curso sem t\xEDtulo",
75
185
  "courses.editor.untitledModule": "Novo m\xF3dulo",
76
- "courses.editor.untitledLesson": "Nova aula"
186
+ "courses.editor.untitledLesson": "Nova aula",
187
+ // Editor tabs
188
+ "courses.editor.tab.details": "Geral",
189
+ "courses.editor.tab.curriculum": "Conte\xFAdo",
190
+ "courses.editor.tab.offers": "Ofertas",
191
+ "courses.editor.tab.members": "\xC1rea de membros",
192
+ "courses.editor.tab.settings": "Pagamento",
193
+ "courses.editor.tab.checkout": "Checkout",
194
+ "courses.editor.tab.links": "Links",
195
+ "courses.editor.membersHelp": "Quem compra este curso entra automaticamente na \xE1rea de membros. Grupos de acesso por oferta:",
196
+ "courses.editor.defaultGroup": "Grupo padr\xE3o",
197
+ "courses.editor.settingsHelp": "M\xE9todos de pagamento no checkout (Stripe).",
198
+ "courses.editor.payCard": "Cart\xE3o de cr\xE9dito (at\xE9 12x)",
199
+ "courses.editor.payPix": "Pix",
200
+ "courses.editor.payBoleto": "Boleto",
201
+ "courses.editor.settingsNote": "A disponibilidade de cada m\xE9todo depende do Stripe na sua regi\xE3o; ligado na Etapa 3.",
202
+ "courses.editor.checkoutSoon": "O construtor de checkout chega numa etapa futura. Um checkout padr\xE3o \xE9 gerado por oferta.",
203
+ "courses.editor.linksHelp": "Links de checkout compartilh\xE1veis, um por oferta.",
204
+ // Offers
205
+ "courses.offers.help": "Pre\xE7os que o cliente pode comprar. Marque um padr\xE3o; order-bumps aparecem nos checkouts das outras.",
206
+ "courses.offers.name": "Nome da oferta",
207
+ "courses.offers.price": "Pre\xE7o",
208
+ "courses.offers.oneTime": "\xDAnico",
209
+ "courses.offers.subscription": "Assinatura",
210
+ "courses.offers.default": "Padr\xE3o",
211
+ "courses.offers.bump": "Bump",
212
+ "courses.offers.remove": "Remover",
213
+ "courses.offers.add": "Adicionar oferta",
214
+ "courses.offers.newName": "Nova oferta",
215
+ // Sales
216
+ "courses.sales.title": "Vendas",
217
+ "courses.sales.subtitle": "Todas as transa\xE7\xF5es dos seus cursos.",
218
+ "courses.sales.date": "Data",
219
+ "courses.sales.customer": "Cliente",
220
+ "courses.sales.method": "M\xE9todo",
221
+ "courses.sales.status": "Status",
222
+ "courses.sales.fee": "Taxa da plataforma",
223
+ "courses.sales.amount": "Valor",
224
+ "courses.sales.count": "Vendas",
225
+ "courses.sales.revenue": "Receita",
226
+ "courses.sales.empty": "Nenhuma venda ainda.",
227
+ "courses.financialStatus.paid": "Pago",
228
+ "courses.financialStatus.pending": "Pendente",
229
+ "courses.financialStatus.refunded": "Reembolsado",
230
+ "courses.financialStatus.chargeback": "Chargeback",
231
+ // Subscriptions
232
+ "courses.subs.title": "Assinaturas",
233
+ "courses.subs.subtitle": "Planos recorrentes e MRR.",
234
+ "courses.subs.start": "In\xEDcio",
235
+ "courses.subs.customer": "Cliente",
236
+ "courses.subs.status": "Status",
237
+ "courses.subs.value": "Valor l\xEDquido",
238
+ "courses.subs.active": "Assinaturas ativas",
239
+ "courses.subs.mrr": "Receita recorrente mensal",
240
+ "courses.subs.empty": "Nenhuma assinatura ainda.",
241
+ "courses.subStatus.active": "Ativa",
242
+ "courses.subStatus.canceled": "Cancelada",
243
+ "courses.subStatus.past_due": "Em atraso",
244
+ "courses.interval.month": "m\xEAs",
245
+ "courses.interval.year": "ano",
246
+ // Financial
247
+ "courses.financial.title": "Financeiro",
248
+ "courses.financial.subtitle": "Saldo, saques e sua conex\xE3o com o Stripe.",
249
+ "courses.financial.available": "Saldo dispon\xEDvel",
250
+ "courses.financial.pending": "Saldo pendente",
251
+ "courses.financial.revenue": "Receita total",
252
+ "courses.financial.feeCollected": "Taxas da plataforma",
253
+ "courses.financial.refundRate": "taxa de reembolso",
254
+ "courses.financial.payouts": "Saques",
255
+ "courses.financial.noPayouts": "Nenhum saque ainda.",
256
+ "courses.financial.date": "Data",
257
+ "courses.financial.amount": "Valor",
258
+ "courses.financial.status": "Status",
259
+ "courses.financial.connect": "Conectar Stripe",
260
+ "courses.financial.connected": "Stripe conectado",
261
+ "courses.financial.notConnected": "Conecte sua conta Stripe para receber os saques. A taxa da plataforma \xE9 aplicada automaticamente em cada venda.",
262
+ "courses.financial.platformFee": "Taxa da plataforma",
263
+ "courses.financial.connectSoon": "O onboarding do Stripe Connect \xE9 ligado na Etapa 3.",
264
+ "courses.payoutStatus.paid": "Conclu\xEDdo",
265
+ "courses.payoutStatus.pending": "Pendente",
266
+ "courses.payoutStatus.failed": "Falhou",
267
+ // Reports
268
+ "courses.reports.title": "Relat\xF3rios",
269
+ "courses.reports.subtitle": "Receita por curso.",
270
+ "courses.reports.byCourse": "Receita por curso",
271
+ "courses.reports.course": "Curso",
272
+ "courses.reports.sales": "Vendas",
273
+ "courses.reports.revenue": "Receita",
274
+ "courses.reports.empty": "Nenhuma receita ainda.",
275
+ // Members area
276
+ "courses.membersArea.title": "\xC1rea de membros",
277
+ "courses.membersArea.subtitle": "Uma \xE1rea por curso; o aluno entra ap\xF3s a compra.",
278
+ "courses.membersArea.name": "\xC1rea de membros",
279
+ "courses.membersArea.status": "Status",
280
+ "courses.membersArea.students": "Alunos",
281
+ "courses.membersArea.empty": "Nenhuma \xE1rea de membros ainda.",
282
+ // Settings
283
+ "courses.settings.title": "Configura\xE7\xF5es do curso",
284
+ "courses.settings.subtitle": "Pagamentos, moeda e m\xE9todos de checkout",
285
+ "courses.settings.payments": "Pagamentos",
286
+ "courses.settings.paymentsHelp": "Conecte o Stripe na aba Integra\xE7\xF5es; a taxa da plataforma \xE9 aplicada automaticamente.",
287
+ "courses.settings.currency": "Moeda padr\xE3o",
288
+ "courses.settings.methods": "M\xE9todos de pagamento",
289
+ // Dashboard KPIs
290
+ "courses.kpi.revenue": "Receita total",
291
+ "courses.kpi.sales": "Vendas",
292
+ "courses.kpi.refunds": "reembolsos",
293
+ "courses.kpi.mrr": "Receita recorrente (MRR)",
294
+ "courses.kpi.activeSubs": "ativas",
295
+ "courses.kpi.fees": "Taxas da plataforma"
77
296
  };
78
297
 
79
298
  // src/locales/index.ts
@@ -145,22 +364,48 @@ function CoursesListPage() {
145
364
  ] });
146
365
  }
147
366
  CoursesListPage.displayName = "CoursesListPage";
367
+
368
+ // src/lib/format.ts
369
+ function formatMoney(value, currency = "BRL") {
370
+ try {
371
+ return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value);
372
+ } catch {
373
+ return `${currency} ${value.toFixed(2)}`;
374
+ }
375
+ }
376
+ function formatDate(iso) {
377
+ try {
378
+ return new Intl.DateTimeFormat("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" }).format(new Date(iso));
379
+ } catch {
380
+ return iso.slice(0, 10);
381
+ }
382
+ }
383
+ function formatPercent(fraction, digits = 0) {
384
+ return `${(fraction * 100).toFixed(digits)}%`;
385
+ }
148
386
  var STATUSES = ["draft", "published", "archived"];
387
+ var TABS = ["details", "curriculum", "offers", "members", "settings", "checkout", "links"];
149
388
  function CourseEditorPage({ id }) {
150
389
  const t = useTranslation();
151
390
  const [tab, setTab] = React2.useState("details");
152
391
  const [course, setCourse] = React2.useState(null);
153
392
  const [modules, setModules] = React2.useState([]);
154
393
  const [lessons, setLessons] = React2.useState([]);
394
+ const [offers, setOffers] = React2.useState([]);
155
395
  const reload = React2.useCallback(async () => {
156
396
  if (!id) return;
157
397
  const provider = getCoursesProvider();
158
398
  const c = await provider.getCourse(id);
159
399
  setCourse(c);
160
400
  if (c) {
161
- const [m, l] = await Promise.all([provider.listModules(c.id), provider.listLessons(c.id)]);
401
+ const [m, l, o] = await Promise.all([
402
+ provider.listModules(c.id),
403
+ provider.listLessons(c.id),
404
+ provider.listOffers(c.id)
405
+ ]);
162
406
  setModules(m);
163
407
  setLessons(l);
408
+ setOffers(o);
164
409
  }
165
410
  }, [id]);
166
411
  React2.useEffect(() => {
@@ -170,8 +415,7 @@ function CourseEditorPage({ id }) {
170
415
  return /* @__PURE__ */ jsx("div", { className: "p-8 text-muted-foreground", children: "\u2026" });
171
416
  }
172
417
  async function saveDetails() {
173
- const provider = getCoursesProvider();
174
- await provider.updateCourse(course.id, {
418
+ await getCoursesProvider().updateCourse(course.id, {
175
419
  title: course.title,
176
420
  subtitle: course.subtitle ?? void 0,
177
421
  description: course.description ?? void 0,
@@ -184,10 +428,7 @@ function CourseEditorPage({ id }) {
184
428
  setCourse((c) => c ? { ...c, ...patch } : c);
185
429
  }
186
430
  async function addModule() {
187
- await getCoursesProvider().createModule({
188
- courseId: course.id,
189
- title: t("courses.editor.untitledModule") || "New module"
190
- });
431
+ await getCoursesProvider().createModule({ courseId: course.id, title: t("courses.editor.untitledModule") || "New module" });
191
432
  await reload();
192
433
  }
193
434
  async function renameModule(moduleId, title) {
@@ -218,18 +459,42 @@ function CourseEditorPage({ id }) {
218
459
  await getCoursesProvider().deleteLesson(lessonId);
219
460
  await reload();
220
461
  }
462
+ async function addOffer() {
463
+ await getCoursesProvider().createOffer({
464
+ courseId: course.id,
465
+ name: t("courses.offers.newName") || "New offer",
466
+ price: course.price,
467
+ kind: "one_time"
468
+ });
469
+ await reload();
470
+ }
471
+ async function patchOffer(offerId, patch) {
472
+ await getCoursesProvider().updateOffer(offerId, {
473
+ name: patch.name,
474
+ price: patch.price,
475
+ kind: patch.kind,
476
+ recurringInterval: patch.recurringInterval,
477
+ isOrderBump: patch.isOrderBump,
478
+ isDefault: patch.isDefault
479
+ });
480
+ await reload();
481
+ }
482
+ async function deleteOffer(offerId) {
483
+ await getCoursesProvider().deleteOffer(offerId);
484
+ await reload();
485
+ }
221
486
  return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-4xl p-6 md:p-8", children: [
222
487
  /* @__PURE__ */ jsxs("button", { onClick: () => navigateTo("/courses"), className: "mb-4 text-sm text-muted-foreground hover:text-foreground", children: [
223
488
  "\u2190 ",
224
489
  t("courses.editor.back") || "Back to courses"
225
490
  ] }),
226
491
  /* @__PURE__ */ jsx("h1", { className: "mb-1 text-2xl font-bold text-foreground", children: course.title }),
227
- /* @__PURE__ */ jsx("div", { className: "mb-6 mt-4 flex gap-2 border-b border-border", children: ["details", "curriculum"].map((tk) => /* @__PURE__ */ jsx(
492
+ /* @__PURE__ */ jsx("div", { className: "mb-6 mt-4 flex gap-1 overflow-x-auto border-b border-border", children: TABS.map((tk) => /* @__PURE__ */ jsx(
228
493
  "button",
229
494
  {
230
495
  onClick: () => setTab(tk),
231
- className: `-mb-px border-b-2 px-3 py-2 text-sm font-medium ${tab === tk ? "border-primary text-foreground" : "border-transparent text-muted-foreground"}`,
232
- children: tk === "details" ? t("courses.editor.details") || "Details" : t("courses.editor.curriculum") || "Curriculum"
496
+ className: `-mb-px whitespace-nowrap border-b-2 px-3 py-2 text-sm font-medium ${tab === tk ? "border-primary text-foreground" : "border-transparent text-muted-foreground hover:text-foreground"}`,
497
+ children: t(`courses.editor.tab.${tk}`) || tk
233
498
  },
234
499
  tk
235
500
  )) }),
@@ -245,14 +510,7 @@ function CourseEditorPage({ id }) {
245
510
  }
246
511
  ) }),
247
512
  /* @__PURE__ */ jsxs("div", { className: "flex gap-4", children: [
248
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldPrice") || "Price", children: /* @__PURE__ */ jsx(
249
- Input,
250
- {
251
- type: "number",
252
- value: String(course.price),
253
- onChange: (e) => patchCourse({ price: Number(e.target.value) || 0 })
254
- }
255
- ) }),
513
+ /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldPrice") || "Price", children: /* @__PURE__ */ jsx(Input, { type: "number", value: String(course.price), onChange: (e) => patchCourse({ price: Number(e.target.value) || 0 }) }) }),
256
514
  /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldStatus") || "Status", children: /* @__PURE__ */ jsx(
257
515
  "select",
258
516
  {
@@ -281,31 +539,9 @@ function CourseEditorPage({ id }) {
281
539
  ] }),
282
540
  /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
283
541
  lessons.filter((l) => l.moduleId === mod.id).map((lesson) => /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-2 rounded-md border border-border p-3 md:grid-cols-[1fr_1fr_90px_auto]", children: [
284
- /* @__PURE__ */ jsx(
285
- Input,
286
- {
287
- defaultValue: lesson.title,
288
- placeholder: t("courses.editor.lessonTitle") || "Lesson title",
289
- onBlur: (e) => patchLesson(lesson.id, { title: e.target.value })
290
- }
291
- ),
292
- /* @__PURE__ */ jsx(
293
- Input,
294
- {
295
- defaultValue: lesson.videoUrl,
296
- placeholder: t("courses.editor.lessonVideo") || "Video URL (embed)",
297
- onBlur: (e) => patchLesson(lesson.id, { videoUrl: e.target.value })
298
- }
299
- ),
300
- /* @__PURE__ */ jsx(
301
- Input,
302
- {
303
- type: "number",
304
- defaultValue: String(Math.round(lesson.durationSec / 60)),
305
- onBlur: (e) => patchLesson(lesson.id, { durationSec: (Number(e.target.value) || 0) * 60 }),
306
- "aria-label": t("courses.editor.lessonDuration") || "Duration (min)"
307
- }
308
- ),
542
+ /* @__PURE__ */ jsx(Input, { defaultValue: lesson.title, placeholder: t("courses.editor.lessonTitle") || "Lesson title", onBlur: (e) => patchLesson(lesson.id, { title: e.target.value }) }),
543
+ /* @__PURE__ */ jsx(Input, { defaultValue: lesson.videoUrl, placeholder: t("courses.editor.lessonVideo") || "Video URL (embed)", onBlur: (e) => patchLesson(lesson.id, { videoUrl: e.target.value }) }),
544
+ /* @__PURE__ */ jsx(Input, { type: "number", defaultValue: String(Math.round(lesson.durationSec / 60)), onBlur: (e) => patchLesson(lesson.id, { durationSec: (Number(e.target.value) || 0) * 60 }), "aria-label": t("courses.editor.lessonDuration") || "Duration (min)" }),
309
545
  /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => deleteLesson(lesson.id), children: t("courses.editor.deleteLesson") || "Remove" })
310
546
  ] }, lesson.id)),
311
547
  /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: () => addLesson(mod.id), children: [
@@ -315,6 +551,67 @@ function CourseEditorPage({ id }) {
315
551
  ] })
316
552
  ] }, mod.id)),
317
553
  /* @__PURE__ */ jsx(Button, { onClick: addModule, children: t("courses.editor.addModule") || "Add module" })
554
+ ] }),
555
+ tab === "offers" && /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
556
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("courses.offers.help") || "Price points a customer can buy. Mark one default; order-bumps show on other checkouts." }),
557
+ offers.map((offer) => /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-2 rounded-xl border border-border bg-card p-3 md:grid-cols-[1fr_120px_130px_auto]", children: [
558
+ /* @__PURE__ */ jsx(Input, { defaultValue: offer.name, onBlur: (e) => patchOffer(offer.id, { name: e.target.value }), "aria-label": t("courses.offers.name") || "Offer name" }),
559
+ /* @__PURE__ */ jsx(Input, { type: "number", defaultValue: String(offer.price), onBlur: (e) => patchOffer(offer.id, { price: Number(e.target.value) || 0 }), "aria-label": t("courses.offers.price") || "Price" }),
560
+ /* @__PURE__ */ jsxs(
561
+ "select",
562
+ {
563
+ className: "h-10 rounded-md border border-input bg-background px-2 text-sm",
564
+ value: offer.kind,
565
+ onChange: (e) => patchOffer(offer.id, { kind: e.target.value, recurringInterval: e.target.value === "subscription" ? offer.recurringInterval ?? "month" : null }),
566
+ children: [
567
+ /* @__PURE__ */ jsx("option", { value: "one_time", children: t("courses.offers.oneTime") || "One-time" }),
568
+ /* @__PURE__ */ jsx("option", { value: "subscription", children: t("courses.offers.subscription") || "Subscription" })
569
+ ]
570
+ }
571
+ ),
572
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
573
+ offer.isDefault && /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: t("courses.offers.default") || "Default" }),
574
+ offer.isOrderBump && /* @__PURE__ */ jsx(Badge, { variant: "outline", children: t("courses.offers.bump") || "Bump" }),
575
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => deleteOffer(offer.id), children: t("courses.offers.remove") || "Remove" })
576
+ ] })
577
+ ] }, offer.id)),
578
+ /* @__PURE__ */ jsxs(Button, { onClick: addOffer, children: [
579
+ "+ ",
580
+ t("courses.offers.add") || "Add offer"
581
+ ] })
582
+ ] }),
583
+ tab === "members" && /* @__PURE__ */ jsxs("div", { className: "space-y-3 text-sm", children: [
584
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: t("courses.editor.membersHelp") || "Buyers of this course are added to its members area automatically. Access groups per offer:" }),
585
+ /* @__PURE__ */ jsx("div", { className: "rounded-xl border border-border bg-card divide-y divide-border", children: offers.map((offer) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [
586
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: offer.name }),
587
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatMoney(offer.price, offer.currency) }),
588
+ /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: t("courses.editor.defaultGroup") || "Default group" })
589
+ ] }, offer.id)) })
590
+ ] }),
591
+ tab === "settings" && /* @__PURE__ */ jsxs("div", { className: "max-w-xl space-y-4 text-sm", children: [
592
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: t("courses.editor.settingsHelp") || "Payment methods offered at checkout (Stripe)." }),
593
+ [
594
+ ["card", t("courses.editor.payCard") || "Credit card (up to 12x)"],
595
+ ["pix", t("courses.editor.payPix") || "Pix"],
596
+ ["boleto", t("courses.editor.payBoleto") || "Boleto"]
597
+ ].map(([k, label]) => /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-3 rounded-lg border border-border bg-card px-4 py-3", children: [
598
+ /* @__PURE__ */ jsx("input", { type: "checkbox", defaultChecked: true, className: "h-4 w-4" }),
599
+ /* @__PURE__ */ jsx("span", { className: "text-foreground", children: label })
600
+ ] }, k)),
601
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("courses.editor.settingsNote") || "Method availability depends on Stripe in your region; wired in Stage 3." })
602
+ ] }),
603
+ tab === "checkout" && /* @__PURE__ */ jsx("div", { className: "rounded-xl border border-dashed border-border p-10 text-center text-sm text-muted-foreground", children: t("courses.editor.checkoutSoon") || "Custom checkout builder comes in a later milestone. A default checkout is generated per offer." }),
604
+ tab === "links" && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
605
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("courses.editor.linksHelp") || "Shareable checkout links, one per offer." }),
606
+ /* @__PURE__ */ jsx("div", { className: "rounded-xl border border-border bg-card divide-y divide-border", children: offers.map((offer) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1 px-4 py-3 md:flex-row md:items-center md:justify-between", children: [
607
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: offer.name }),
608
+ /* @__PURE__ */ jsxs("code", { className: "rounded bg-muted px-2 py-1 text-xs text-muted-foreground", children: [
609
+ "/checkout/",
610
+ course.slug,
611
+ "?offer=",
612
+ offer.id
613
+ ] })
614
+ ] }, offer.id)) })
318
615
  ] })
319
616
  ] });
320
617
  }
@@ -325,13 +622,381 @@ function Field({ label, children }) {
325
622
  children
326
623
  ] });
327
624
  }
625
+ function PageContainer({ title, subtitle, actions, children }) {
626
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-6xl p-6 md:p-8", children: [
627
+ /* @__PURE__ */ jsxs("div", { className: "mb-6 flex items-start justify-between gap-4", children: [
628
+ /* @__PURE__ */ jsxs("div", { children: [
629
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold text-foreground", children: title }),
630
+ subtitle && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: subtitle })
631
+ ] }),
632
+ actions
633
+ ] }),
634
+ children
635
+ ] });
636
+ }
637
+ function StatCard({ label, value, hint }) {
638
+ return /* @__PURE__ */ jsxs(Card, { className: "p-5", children: [
639
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: label }),
640
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-2xl font-bold text-foreground", children: value }),
641
+ hint && /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: hint })
642
+ ] });
643
+ }
644
+ function StatGrid({ children }) {
645
+ return /* @__PURE__ */ jsx("div", { className: "mb-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children });
646
+ }
647
+ function SimpleTable({ columns, rows, empty, loading }) {
648
+ const cols = React2.useMemo(
649
+ () => columns.map((c) => ({
650
+ id: c.key,
651
+ header: () => /* @__PURE__ */ jsx("div", { className: c.align === "right" ? "text-right" : "", children: c.header }),
652
+ cell: ({ row }) => /* @__PURE__ */ jsx("div", { className: c.align === "right" ? "text-right" : "", children: c.render(row.original) })
653
+ })),
654
+ [columns]
655
+ );
656
+ return /* @__PURE__ */ jsx(DataTable, { columns: cols, data: rows, emptyMessage: empty, loading });
657
+ }
658
+ function statusVariant2(s) {
659
+ if (s === "paid") return "success";
660
+ if (s === "pending") return "secondary";
661
+ if (s === "refunded" || s === "chargeback") return "destructive";
662
+ return "outline";
663
+ }
664
+ function SalesPage() {
665
+ const t = useTranslation();
666
+ const [orders, setOrders] = React2.useState(null);
667
+ React2.useEffect(() => {
668
+ void getCoursesProvider().listOrders().then(setOrders);
669
+ }, []);
670
+ const paid = (orders ?? []).filter((o) => o.financialStatus === "paid");
671
+ const revenue = paid.reduce((s, o) => s + o.total, 0);
672
+ const currency = orders?.[0]?.currency ?? "BRL";
673
+ const columns = [
674
+ { key: "date", header: t("courses.sales.date") || "Date", render: (o) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatDate(o.createdAt) }) },
675
+ { key: "customer", header: t("courses.sales.customer") || "Customer", render: (o) => /* @__PURE__ */ jsxs("div", { children: [
676
+ /* @__PURE__ */ jsx("div", { className: "font-medium text-foreground", children: o.customerName ?? "\u2014" }),
677
+ /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: o.customerEmail })
678
+ ] }) },
679
+ { key: "method", header: t("courses.sales.method") || "Method", render: (o) => /* @__PURE__ */ jsx("span", { className: "capitalize", children: o.paymentMethod ?? "\u2014" }) },
680
+ { key: "status", header: t("courses.sales.status") || "Status", render: (o) => /* @__PURE__ */ jsx(Badge, { variant: statusVariant2(o.financialStatus), children: t(`courses.financialStatus.${o.financialStatus}`) || o.financialStatus }) },
681
+ { key: "fee", header: t("courses.sales.fee") || "Platform fee", align: "right", render: (o) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatMoney(o.platformFee, o.currency) }) },
682
+ { key: "total", header: t("courses.sales.amount") || "Amount", align: "right", render: (o) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: formatMoney(o.total, o.currency) }) }
683
+ ];
684
+ return /* @__PURE__ */ jsxs(PageContainer, { title: t("courses.sales.title") || "Sales", subtitle: t("courses.sales.subtitle") || "Every transaction across your courses.", children: [
685
+ /* @__PURE__ */ jsxs(StatGrid, { children: [
686
+ /* @__PURE__ */ jsx(StatCard, { label: t("courses.sales.count") || "Sales", value: String(paid.length) }),
687
+ /* @__PURE__ */ jsx(StatCard, { label: t("courses.sales.revenue") || "Revenue", value: formatMoney(revenue, currency) })
688
+ ] }),
689
+ /* @__PURE__ */ jsx(
690
+ SimpleTable,
691
+ {
692
+ columns,
693
+ rows: orders ?? [],
694
+ empty: t("courses.sales.empty") || "No sales yet."
695
+ }
696
+ )
697
+ ] });
698
+ }
699
+ SalesPage.displayName = "SalesPage";
700
+ function statusVariant3(s) {
701
+ if (s === "active") return "success";
702
+ if (s === "past_due") return "secondary";
703
+ return "destructive";
704
+ }
705
+ function SubscriptionsPage() {
706
+ const t = useTranslation();
707
+ const [subs, setSubs] = React2.useState(null);
708
+ React2.useEffect(() => {
709
+ void getCoursesProvider().listSubscriptions().then(setSubs);
710
+ }, []);
711
+ const active = (subs ?? []).filter((s) => s.status === "active");
712
+ const mrr = active.reduce((sum, s) => sum + (s.interval === "year" ? s.netValue / 12 : s.netValue), 0);
713
+ const currency = subs?.[0]?.currency ?? "BRL";
714
+ const columns = [
715
+ { key: "start", header: t("courses.subs.start") || "Start date", render: (s) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatDate(s.startedAt) }) },
716
+ { key: "customer", header: t("courses.subs.customer") || "Customer", render: (s) => /* @__PURE__ */ jsxs("div", { children: [
717
+ /* @__PURE__ */ jsx("div", { className: "font-medium text-foreground", children: s.customerName ?? "\u2014" }),
718
+ /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: s.customerEmail })
719
+ ] }) },
720
+ { key: "status", header: t("courses.subs.status") || "Status", render: (s) => /* @__PURE__ */ jsx(Badge, { variant: statusVariant3(s.status), children: t(`courses.subStatus.${s.status}`) || s.status }) },
721
+ { key: "value", header: t("courses.subs.value") || "Net value", align: "right", render: (s) => /* @__PURE__ */ jsxs("span", { className: "font-medium text-foreground", children: [
722
+ formatMoney(s.netValue, s.currency),
723
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
724
+ " / ",
725
+ t(`courses.interval.${s.interval}`) || s.interval
726
+ ] })
727
+ ] }) }
728
+ ];
729
+ return /* @__PURE__ */ jsxs(PageContainer, { title: t("courses.subs.title") || "Subscriptions", subtitle: t("courses.subs.subtitle") || "Recurring plans and MRR.", children: [
730
+ /* @__PURE__ */ jsxs(StatGrid, { children: [
731
+ /* @__PURE__ */ jsx(StatCard, { label: t("courses.subs.active") || "Active subscriptions", value: String(active.length) }),
732
+ /* @__PURE__ */ jsx(StatCard, { label: t("courses.subs.mrr") || "Monthly recurring revenue", value: formatMoney(mrr, currency) })
733
+ ] }),
734
+ /* @__PURE__ */ jsx(
735
+ SimpleTable,
736
+ {
737
+ columns,
738
+ rows: subs ?? [],
739
+ empty: t("courses.subs.empty") || "No subscriptions yet."
740
+ }
741
+ )
742
+ ] });
743
+ }
744
+ SubscriptionsPage.displayName = "SubscriptionsPage";
745
+ function FinancialPage() {
746
+ const t = useTranslation();
747
+ const [summary, setSummary] = React2.useState(null);
748
+ const [payouts, setPayouts] = React2.useState(null);
749
+ const [account, setAccount] = React2.useState(null);
750
+ React2.useEffect(() => {
751
+ const p = getCoursesProvider();
752
+ void p.getFinancialSummary().then(setSummary);
753
+ void p.listPayouts().then(setPayouts);
754
+ void p.getCreatorAccount().then(setAccount);
755
+ }, []);
756
+ const currency = summary?.currency ?? "BRL";
757
+ const connected = !!account?.stripeAccountId && account.chargesEnabled;
758
+ function connectStripe() {
759
+ toast.info(t("courses.financial.connectSoon") || "Stripe Connect onboarding wires up in Stage 3.");
760
+ }
761
+ const columns = [
762
+ { key: "date", header: t("courses.financial.date") || "Date", render: (p) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatDate(p.createdAt) }) },
763
+ { 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) }) },
764
+ { key: "status", header: t("courses.financial.status") || "Status", align: "right", render: (p) => /* @__PURE__ */ jsx(Badge, { variant: p.status === "paid" ? "success" : p.status === "pending" ? "secondary" : "destructive", children: t(`courses.payoutStatus.${p.status}`) || p.status }) }
765
+ ];
766
+ return /* @__PURE__ */ jsxs(
767
+ PageContainer,
768
+ {
769
+ title: t("courses.financial.title") || "Financial settings",
770
+ subtitle: t("courses.financial.subtitle") || "Balance, payouts and your Stripe connection.",
771
+ actions: /* @__PURE__ */ jsx(Button, { variant: connected ? "outline" : "default", onClick: connectStripe, children: connected ? t("courses.financial.connected") || "Stripe connected" : t("courses.financial.connect") || "Connect Stripe" }),
772
+ children: [
773
+ !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: [
774
+ t("courses.financial.notConnected") || "Connect your Stripe account to receive payouts. The platform fee is applied automatically on each sale.",
775
+ account && /* @__PURE__ */ jsxs("span", { className: "ml-1 text-muted-foreground", children: [
776
+ "(",
777
+ t("courses.financial.platformFee") || "Platform fee",
778
+ ": ",
779
+ (account.platformFeeBps / 100).toFixed(2),
780
+ "%)"
781
+ ] })
782
+ ] }),
783
+ /* @__PURE__ */ jsxs(StatGrid, { children: [
784
+ /* @__PURE__ */ jsx(StatCard, { label: t("courses.financial.available") || "Available balance", value: formatMoney(summary?.availableBalance ?? 0, currency) }),
785
+ /* @__PURE__ */ jsx(StatCard, { label: t("courses.financial.pending") || "Pending balance", value: formatMoney(summary?.pendingBalance ?? 0, currency) }),
786
+ /* @__PURE__ */ jsx(StatCard, { label: t("courses.financial.revenue") || "Total revenue", value: formatMoney(summary?.totalRevenue ?? 0, currency) }),
787
+ /* @__PURE__ */ jsx(StatCard, { label: t("courses.financial.feeCollected") || "Platform fees", value: formatMoney(summary?.platformFeeTotal ?? 0, currency), hint: summary ? formatPercent(summary.refundRate, 1) + " " + (t("courses.financial.refundRate") || "refund rate") : void 0 })
788
+ ] }),
789
+ /* @__PURE__ */ jsx("h2", { className: "mb-3 text-sm font-semibold text-foreground", children: t("courses.financial.payouts") || "Payouts" }),
790
+ /* @__PURE__ */ jsx(
791
+ SimpleTable,
792
+ {
793
+ columns,
794
+ rows: payouts ?? [],
795
+ empty: t("courses.financial.noPayouts") || "No payouts yet."
796
+ }
797
+ )
798
+ ]
799
+ }
800
+ );
801
+ }
802
+ FinancialPage.displayName = "FinancialPage";
803
+ function ReportsPage() {
804
+ const t = useTranslation();
805
+ const [rows, setRows] = React2.useState(null);
806
+ React2.useEffect(() => {
807
+ const p = getCoursesProvider();
808
+ void Promise.all([p.listCourses(), p.listOrders({ financialStatus: "paid" })]).then(([courses, orders]) => {
809
+ setRows(buildRevenueByCourse(courses, orders));
810
+ });
811
+ }, []);
812
+ const columns = [
813
+ { key: "title", header: t("courses.reports.course") || "Course", render: (r) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: r.title }) },
814
+ { key: "sales", header: t("courses.reports.sales") || "Sales", align: "right", render: (r) => /* @__PURE__ */ jsx("span", { children: r.sales }) },
815
+ { key: "revenue", header: t("courses.reports.revenue") || "Revenue", align: "right", render: (r) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: formatMoney(r.revenue, r.currency) }) }
816
+ ];
817
+ return /* @__PURE__ */ jsxs(PageContainer, { title: t("courses.reports.title") || "Reports", subtitle: t("courses.reports.subtitle") || "Revenue by course.", children: [
818
+ /* @__PURE__ */ jsx("h2", { className: "mb-3 text-sm font-semibold text-foreground", children: t("courses.reports.byCourse") || "Revenue by course" }),
819
+ /* @__PURE__ */ jsx(
820
+ SimpleTable,
821
+ {
822
+ columns,
823
+ rows: rows ?? [],
824
+ empty: t("courses.reports.empty") || "No revenue yet."
825
+ }
826
+ )
827
+ ] });
828
+ }
829
+ ReportsPage.displayName = "ReportsPage";
830
+ function buildRevenueByCourse(courses, orders) {
831
+ const byCourse = /* @__PURE__ */ new Map();
832
+ for (const o of orders) {
833
+ const agg = byCourse.get(o.courseId) ?? { sales: 0, revenue: 0, currency: o.currency };
834
+ agg.sales += 1;
835
+ agg.revenue = Math.round((agg.revenue + o.total) * 100) / 100;
836
+ byCourse.set(o.courseId, agg);
837
+ }
838
+ return courses.map((c) => {
839
+ const agg = byCourse.get(c.id);
840
+ return { courseId: c.id, title: c.title, sales: agg?.sales ?? 0, revenue: agg?.revenue ?? 0, currency: agg?.currency ?? c.currency };
841
+ }).sort((a, b) => b.revenue - a.revenue);
842
+ }
843
+ function MembersAreaPage() {
844
+ const t = useTranslation();
845
+ const [rows, setRows] = React2.useState(null);
846
+ React2.useEffect(() => {
847
+ const p = getCoursesProvider();
848
+ void Promise.all([p.listCourses(), p.listOrders({ financialStatus: "paid" })]).then(([courses, orders]) => {
849
+ setRows(buildMembersAreas(courses, orders));
850
+ });
851
+ }, []);
852
+ const columns = [
853
+ { key: "title", header: t("courses.membersArea.name") || "Members area", render: (r) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: r.title }) },
854
+ { key: "status", header: t("courses.membersArea.status") || "Status", render: (r) => /* @__PURE__ */ jsx(Badge, { variant: r.status === "published" ? "success" : r.status === "archived" ? "outline" : "secondary", children: t(`courses.status.${r.status}`) || r.status }) },
855
+ { key: "students", header: t("courses.membersArea.students") || "Students", align: "right", render: (r) => /* @__PURE__ */ jsx("span", { className: "text-foreground", children: r.students }) }
856
+ ];
857
+ return /* @__PURE__ */ jsx(PageContainer, { title: t("courses.membersArea.title") || "Members area", subtitle: t("courses.membersArea.subtitle") || "One area per course; students land here after purchase.", children: /* @__PURE__ */ jsx(
858
+ SimpleTable,
859
+ {
860
+ columns,
861
+ rows: rows ?? [],
862
+ empty: t("courses.membersArea.empty") || "No members areas yet."
863
+ }
864
+ ) });
865
+ }
866
+ MembersAreaPage.displayName = "MembersAreaPage";
867
+ function buildMembersAreas(courses, orders) {
868
+ const students = /* @__PURE__ */ new Map();
869
+ for (const o of orders) {
870
+ const set = students.get(o.courseId) ?? /* @__PURE__ */ new Set();
871
+ if (o.customerId) set.add(o.customerId);
872
+ students.set(o.courseId, set);
873
+ }
874
+ return courses.map((c) => ({
875
+ courseId: c.id,
876
+ title: c.title,
877
+ status: c.status,
878
+ students: students.get(c.id)?.size ?? 0
879
+ }));
880
+ }
881
+ function CoursesGeneralSettings() {
882
+ const t = useTranslation();
883
+ const [currency, setCurrency] = React2.useState("BRL");
884
+ const [methods, setMethods] = React2.useState({ card: true, pix: true, boleto: true });
885
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
886
+ /* @__PURE__ */ jsx(
887
+ SettingsGroup,
888
+ {
889
+ title: t("courses.settings.payments") || "Payments",
890
+ description: t("courses.settings.paymentsHelp") || "Connect Stripe in the Integrations tab; the platform fee applies automatically.",
891
+ children: /* @__PURE__ */ jsx(
892
+ SelectRow,
893
+ {
894
+ label: t("courses.settings.currency") || "Default currency",
895
+ value: currency,
896
+ onChange: setCurrency,
897
+ options: [
898
+ { value: "BRL", label: "BRL (R$)" },
899
+ { value: "USD", label: "USD ($)" },
900
+ { value: "EUR", label: "EUR (\u20AC)" }
901
+ ]
902
+ }
903
+ )
904
+ }
905
+ ),
906
+ /* @__PURE__ */ jsxs(SettingsGroup, { title: t("courses.settings.methods") || "Payment methods", children: [
907
+ /* @__PURE__ */ jsx(ToggleRow, { label: t("courses.editor.payCard") || "Credit card", checked: methods.card, onChange: (v) => setMethods((m) => ({ ...m, card: v })) }),
908
+ /* @__PURE__ */ jsx(ToggleRow, { label: t("courses.editor.payPix") || "Pix", checked: methods.pix, onChange: (v) => setMethods((m) => ({ ...m, pix: v })) }),
909
+ /* @__PURE__ */ jsx(ToggleRow, { label: t("courses.editor.payBoleto") || "Boleto", checked: methods.boleto, onChange: (v) => setMethods((m) => ({ ...m, boleto: v })) })
910
+ ] })
911
+ ] });
912
+ }
913
+ CoursesGeneralSettings.displayName = "CoursesGeneralSettings";
914
+ function CoursesSettingsTab() {
915
+ const t = useTranslation();
916
+ return /* @__PURE__ */ jsx(
917
+ PluginSettingsPanel,
918
+ {
919
+ title: t("courses.settings.title") || "Course settings",
920
+ subtitle: t("courses.settings.subtitle") || "Payments, currency and checkout methods",
921
+ generalSettings: /* @__PURE__ */ jsx(CoursesGeneralSettings, {}),
922
+ hostPluginId: "courses",
923
+ routeBase: "/settings/courses"
924
+ }
925
+ );
926
+ }
927
+ CoursesSettingsTab.displayName = "CoursesSettingsTab";
928
+ function useSummary() {
929
+ const [summary, setSummary] = React2.useState(null);
930
+ React2.useEffect(() => {
931
+ void getCoursesProvider().getFinancialSummary().then(setSummary);
932
+ }, []);
933
+ return summary;
934
+ }
935
+ function RevenueKpi() {
936
+ const t = useTranslation();
937
+ const s = useSummary();
938
+ return /* @__PURE__ */ jsx(KpiCard, { label: t("courses.kpi.revenue") || "Total revenue", icon: "DollarSign", loading: !s, value: s ? formatMoney(s.totalRevenue, s.currency) : void 0 });
939
+ }
940
+ function SalesKpi() {
941
+ const t = useTranslation();
942
+ const s = useSummary();
943
+ return /* @__PURE__ */ jsx(KpiCard, { label: t("courses.kpi.sales") || "Sales", icon: "TrendingUp", loading: !s, value: s ? String(s.salesCount) : void 0, sub: s ? formatPercent(s.refundRate, 1) + " " + (t("courses.kpi.refunds") || "refunds") : void 0 });
944
+ }
945
+ function MrrKpi() {
946
+ const t = useTranslation();
947
+ const s = useSummary();
948
+ return /* @__PURE__ */ jsx(KpiCard, { label: t("courses.kpi.mrr") || "MRR", icon: "Repeat", loading: !s, value: s ? formatMoney(s.mrr, s.currency) : void 0, sub: s ? `${s.activeSubscriptions} ${t("courses.kpi.activeSubs") || "active"}` : void 0 });
949
+ }
950
+ function FeesKpi() {
951
+ const t = useTranslation();
952
+ const s = useSummary();
953
+ return /* @__PURE__ */ jsx(KpiCard, { label: t("courses.kpi.fees") || "Platform fees", icon: "Percent", loading: !s, value: s ? formatMoney(s.platformFeeTotal, s.currency) : void 0 });
954
+ }
955
+ function createCoursesDashboardWidgets() {
956
+ return [
957
+ defineKpiWidget({ id: "courses.kpi.revenue", title: "courses.kpi.revenue", domain: "courses", defaultOrder: 0, component: RevenueKpi }),
958
+ defineKpiWidget({ id: "courses.kpi.sales", title: "courses.kpi.sales", domain: "courses", defaultOrder: 1, component: SalesKpi }),
959
+ defineKpiWidget({ id: "courses.kpi.mrr", title: "courses.kpi.mrr", domain: "courses", defaultOrder: 2, component: MrrKpi }),
960
+ defineKpiWidget({ id: "courses.kpi.fees", title: "courses.kpi.fees", domain: "courses", defaultOrder: 3, component: FeesKpi })
961
+ ];
962
+ }
963
+ function createStripeConnector() {
964
+ return {
965
+ id: "stripe-connect",
966
+ hostPluginId: "courses",
967
+ name: "Stripe",
968
+ description: "Receba os pagamentos dos alunos; a taxa da plataforma \xE9 aplicada automaticamente.",
969
+ icon: "CreditCard",
970
+ authKind: "oauth",
971
+ async getStatus() {
972
+ try {
973
+ const account = await getCoursesProvider().getCreatorAccount();
974
+ const fee = (account.platformFeeBps / 100).toFixed(2);
975
+ return {
976
+ connected: !!account.stripeAccountId && account.chargesEnabled,
977
+ detail: `Taxa da plataforma: ${fee}%`
978
+ };
979
+ } catch {
980
+ return { connected: false };
981
+ }
982
+ },
983
+ async startOAuth(_redirectTo) {
984
+ throw new Error("O onboarding do Stripe Connect \xE9 ligado na Etapa 3.");
985
+ }
986
+ };
987
+ }
328
988
 
329
989
  // src/index.ts
990
+ var asComp = (c) => c;
991
+ var read = (feature) => ({ feature, action: "read" });
330
992
  function createCoursesPlugin(options) {
331
993
  registerTranslations(coursesLocales);
994
+ setCoursesTenantResolver(() => useOrganizationStore.getState().currentOrg?.id);
332
995
  if (!getCoursesProviderOptional()) {
333
996
  setCoursesProvider(createMockCoursesProvider());
334
997
  }
998
+ const base = options?.navPosition ?? 1;
999
+ const section = options?.navSection ?? "main";
335
1000
  return {
336
1001
  id: "courses",
337
1002
  name: options?.navLabel ?? "Courses",
@@ -341,20 +1006,36 @@ function createCoursesPlugin(options) {
341
1006
  verticalId: options?.verticalId,
342
1007
  defaultEnabled: true,
343
1008
  dependencies: [],
1009
+ declaredFeatures: [
1010
+ { id: "courses", label: "Courses", group: "Courses" },
1011
+ { id: "courses.sales", label: "Sales", group: "Courses" },
1012
+ { id: "courses.finance", label: "Financial", group: "Courses" }
1013
+ ],
344
1014
  navigation: [
345
- {
346
- section: options?.navSection ?? "main",
347
- position: options?.navPosition ?? 1,
348
- label: options?.navLabel ?? "Courses",
349
- route: "/courses",
350
- icon: "BookOpen"
351
- }
1015
+ { section, position: base, label: options?.navLabel ?? "Courses", route: "/courses", icon: "BookOpen", permission: read("courses") },
1016
+ { section, position: base + 1, label: "Members area", route: "/courses/members", icon: "GraduationCap", permission: read("courses") },
1017
+ { section, position: base + 2, label: "Sales", route: "/courses/sales", icon: "TrendingUp", permission: read("courses.sales") },
1018
+ { section, position: base + 3, label: "Subscriptions", route: "/courses/subscriptions", icon: "Repeat", permission: read("courses.sales") },
1019
+ { section, position: base + 4, label: "Financial", route: "/courses/financial", icon: "CreditCard", permission: read("courses.finance") },
1020
+ { section, position: base + 5, label: "Reports", route: "/courses/reports", icon: "BarChart3", permission: read("courses.sales") }
352
1021
  ],
353
1022
  routes: [
354
- { path: "/courses", component: CoursesListPage },
355
- { path: "/courses/:id", component: CourseEditorPage }
1023
+ // Static segments outscore the `:id` param in the saas router, so these
1024
+ // resolve before the editor route (AdminShell sorts by routeScore).
1025
+ { path: "/courses", component: asComp(CoursesListPage), permission: read("courses") },
1026
+ { path: "/courses/members", component: asComp(MembersAreaPage), permission: read("courses") },
1027
+ { path: "/courses/sales", component: asComp(SalesPage), permission: read("courses.sales") },
1028
+ { path: "/courses/subscriptions", component: asComp(SubscriptionsPage), permission: read("courses.sales") },
1029
+ { path: "/courses/financial", component: asComp(FinancialPage), permission: read("courses.finance") },
1030
+ { path: "/courses/reports", component: asComp(ReportsPage), permission: read("courses.sales") },
1031
+ { path: "/courses/:id", component: asComp(CourseEditorPage) }
356
1032
  ],
357
1033
  widgets: [],
1034
+ dashboardWidgets: createCoursesDashboardWidgets(),
1035
+ connectors: [createStripeConnector()],
1036
+ settings: [
1037
+ { id: "courses", label: "Courses", icon: "BookOpen", component: asComp(CoursesSettingsTab), order: 20, permission: read("courses") }
1038
+ ],
358
1039
  locales: coursesLocales
359
1040
  };
360
1041
  }