@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.
- 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 +6 -4
- 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.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([
|
|
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
|
-
|
|
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-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
377
|
-
|
|
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
|
}
|