@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.
- package/README.md +2 -0
- package/dist/components/CommerceUI.d.ts +39 -0
- package/dist/components/CommerceUI.d.ts.map +1 -0
- package/dist/connectors/stripe.d.ts +3 -0
- package/dist/connectors/stripe.d.ts.map +1 -0
- package/dist/dashboardWidgets.d.ts +3 -0
- package/dist/dashboardWidgets.d.ts.map +1 -0
- package/dist/index.cjs +735 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +738 -57
- package/dist/index.js.map +1 -1
- package/dist/lib/format.d.ts +4 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/pages/CourseEditorPage.d.ts.map +1 -1
- package/dist/pages/FinancialPage.d.ts +6 -0
- package/dist/pages/FinancialPage.d.ts.map +1 -0
- package/dist/pages/MembersAreaPage.d.ts +6 -0
- package/dist/pages/MembersAreaPage.d.ts.map +1 -0
- package/dist/pages/ReportsPage.d.ts +6 -0
- package/dist/pages/ReportsPage.d.ts.map +1 -0
- package/dist/pages/SalesPage.d.ts +6 -0
- package/dist/pages/SalesPage.d.ts.map +1 -0
- package/dist/pages/SubscriptionsPage.d.ts +6 -0
- package/dist/pages/SubscriptionsPage.d.ts.map +1 -0
- package/dist/settings/CoursesSettings.d.ts +6 -0
- package/dist/settings/CoursesSettings.d.ts.map +1 -0
- package/package.json +7 -5
- package/src/components/CommerceUI.tsx +77 -0
- package/src/connectors/stripe.ts +34 -0
- package/src/dashboardWidgets.tsx +51 -0
- package/src/index.ts +51 -12
- package/src/lib/format.ts +22 -0
- package/src/locales/en.ts +109 -0
- package/src/locales/pt-BR.ts +109 -0
- package/src/pages/CourseEditorPage.tsx +133 -35
- package/src/pages/FinancialPage.tsx +76 -0
- package/src/pages/MembersAreaPage.tsx +60 -0
- package/src/pages/ReportsPage.tsx +59 -0
- package/src/pages/SalesPage.tsx +57 -0
- package/src/pages/SubscriptionsPage.tsx +56 -0
- 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 {
|
|
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([
|
|
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
|
-
|
|
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-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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
|
}
|