@fayz-ai/plugin-courses 0.2.2 → 0.2.4

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