@fayz-ai/plugin-courses 0.8.3 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/components/CommerceUI.d.ts +31 -1
  2. package/dist/components/CommerceUI.d.ts.map +1 -1
  3. package/dist/index.cjs +1237 -301
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1240 -304
  7. package/dist/index.js.map +1 -1
  8. package/dist/locales/en.d.ts.map +1 -1
  9. package/dist/locales/pt-BR.d.ts.map +1 -1
  10. package/dist/pages/CourseEditorPage.d.ts.map +1 -1
  11. package/dist/pages/CoursesListPage.d.ts.map +1 -1
  12. package/dist/pages/SalesPage.d.ts.map +1 -1
  13. package/dist/public/CheckoutScreen.d.ts +17 -0
  14. package/dist/public/CheckoutScreen.d.ts.map +1 -0
  15. package/dist/public/createPublicCheckoutPlugin.d.ts +27 -0
  16. package/dist/public/createPublicCheckoutPlugin.d.ts.map +1 -0
  17. package/dist/public/data.d.ts +28 -0
  18. package/dist/public/data.d.ts.map +1 -0
  19. package/dist/public/data.supabase.d.ts +7 -0
  20. package/dist/public/data.supabase.d.ts.map +1 -0
  21. package/dist/public/index.d.ts +9 -0
  22. package/dist/public/index.d.ts.map +1 -0
  23. package/dist/views/CourseOrderDetailView.d.ts +10 -0
  24. package/dist/views/CourseOrderDetailView.d.ts.map +1 -0
  25. package/package.json +14 -5
  26. package/src/components/CommerceUI.tsx +78 -2
  27. package/src/index.ts +6 -3
  28. package/src/locales/en.ts +73 -12
  29. package/src/locales/pt-BR.ts +73 -12
  30. package/src/pages/CourseEditorPage.tsx +990 -177
  31. package/src/pages/CoursesListPage.tsx +20 -1
  32. package/src/pages/SalesPage.tsx +49 -20
  33. package/src/public/CheckoutScreen.tsx +279 -0
  34. package/src/public/createPublicCheckoutPlugin.tsx +88 -0
  35. package/src/public/data.supabase.ts +63 -0
  36. package/src/public/data.ts +89 -0
  37. package/src/public/index.ts +26 -0
  38. package/src/views/CourseOrderDetailView.tsx +132 -0
  39. package/dist/pages/MembersAreaPage.d.ts +0 -6
  40. package/dist/pages/MembersAreaPage.d.ts.map +0 -1
  41. package/src/pages/MembersAreaPage.tsx +0 -60
@@ -0,0 +1,26 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @fayz-ai/plugin-courses/public — customer-facing course checkout surface.
3
+ //
4
+ // Separate entry from the admin plugin so a website/member host imports only the
5
+ // lean checkout screen + data seam (no @fayz-ai/saas / courses admin graph).
6
+ // Data: PublicCheckoutDataProvider — mock by default, Supabase RPC
7
+ // (course_place_order_simulated) when a tenantId + global client are configured.
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export { createPublicCheckoutPlugin } from './createPublicCheckoutPlugin'
11
+ export type { PublicCheckoutOptions, PublicCheckoutPlugin } from './createPublicCheckoutPlugin'
12
+
13
+ export { CheckoutScreen } from './CheckoutScreen'
14
+ export type { CheckoutScreenProps } from './CheckoutScreen'
15
+
16
+ // Data seam
17
+ export { createMockCheckoutProvider } from './data'
18
+ export { createSupabaseCheckoutProvider } from './data.supabase'
19
+ export type { SupabaseCheckoutOptions } from './data.supabase'
20
+ export type {
21
+ PublicCheckoutDataProvider,
22
+ PlaceOrderInput,
23
+ PlaceOrderResult,
24
+ CheckoutPaymentMethod,
25
+ MockCheckoutOptions,
26
+ } from './data'
@@ -0,0 +1,132 @@
1
+ import * as React from 'react'
2
+ import { useTranslation } from '@fayz-ai/core'
3
+ import {
4
+ OrderDetailView as SharedOrderDetailView, OrderDetailSkeleton, PersonLink,
5
+ type OrderDetailData, type OrderDetailInstallment, type OrderDetailTone,
6
+ } from '@fayz-ai/saas'
7
+ import { getCoursesProvider, type OrderDetail, type FinancialStatus } from '@fayz-ai/courses'
8
+ import { EmptyState } from '../components/CommerceUI'
9
+ import { formatMoney, formatDate } from '../lib/format'
10
+ import { navigateTo } from '../nav'
11
+
12
+ const Page = ({ children }: { children: React.ReactNode }) => (
13
+ <div className="mx-auto max-w-6xl p-6 md:p-8">{children}</div>
14
+ )
15
+
16
+ // Cache por id (fora do componente) — trocar de aba remonta a view; sem cache
17
+ // isso refaz o getOrder (skeleton + request) toda vez. Cache-first evita o refetch.
18
+ const orderCache = new Map<string, OrderDetail>()
19
+
20
+ function statusTone(s: FinancialStatus): OrderDetailTone {
21
+ if (s === 'paid') return 'success'
22
+ if (s === 'pending') return 'warning'
23
+ if (s === 'refunded' || s === 'chargeback') return 'destructive'
24
+ return 'muted'
25
+ }
26
+ function instTone(s: string): OrderDetailTone {
27
+ if (s === 'paid') return 'success'
28
+ if (s === 'overdue') return 'destructive'
29
+ return 'warning'
30
+ }
31
+
32
+ /** Order detail (`/courses/sales/:id`). Loads the canonical order and renders the
33
+ * SHARED archetype OrderDetailView (@fayz-ai/saas) — same component the shop uses. */
34
+ export function CourseOrderDetailView({ id }: { id?: string }) {
35
+ const t = useTranslation()
36
+ const [order, setOrder] = React.useState<OrderDetail | null>(() => (id ? orderCache.get(id) ?? null : null))
37
+ const [loading, setLoading] = React.useState(() => !(id && orderCache.has(id)))
38
+
39
+ React.useEffect(() => {
40
+ let alive = true
41
+ if (!id) { setLoading(false); return }
42
+ const cached = orderCache.get(id)
43
+ if (cached) { setOrder(cached); setLoading(false); return } // cache hit → sem request
44
+ setLoading(true)
45
+ void getCoursesProvider().getOrder(id).then((o) => {
46
+ if (!alive) return
47
+ if (o) orderCache.set(id, o)
48
+ setOrder(o); setLoading(false)
49
+ })
50
+ return () => { alive = false }
51
+ }, [id])
52
+
53
+ const back = () => navigateTo('/courses/sales')
54
+
55
+ if (loading) return <Page><OrderDetailSkeleton /></Page>
56
+ if (!order) {
57
+ return (
58
+ <Page>
59
+ <EmptyState
60
+ title={t('courses.order.notFound') || 'Pedido não encontrado'}
61
+ description=""
62
+ action={<button className="text-sm text-primary underline" onClick={back}>{t('common.back') || 'Voltar'}</button>}
63
+ />
64
+ </Page>
65
+ )
66
+ }
67
+
68
+ const cur = order.currency
69
+ const installments: OrderDetailInstallment[] = order.installments.map((m) => ({
70
+ number: m.installmentNumber,
71
+ amount: formatMoney(m.amount, cur),
72
+ status: m.status,
73
+ statusTone: instTone(m.status),
74
+ dueDate: m.dueDate ? formatDate(m.dueDate) : null,
75
+ paymentDate: m.paymentDate ? formatDate(m.paymentDate) : null,
76
+ note: m.cardBrand ? `${m.cardBrand}${m.cardInstallments ? ` ${m.cardInstallments}x` : ''}` : null,
77
+ }))
78
+
79
+ const data: OrderDetailData = {
80
+ title: `${t('courses.order.title') || 'Pedido'} #${order.id.slice(0, 8)}`,
81
+ subtitle: formatDate(order.createdAt),
82
+ badges: [
83
+ { label: t(`courses.financialStatus.${order.financialStatus}`) || order.financialStatus, tone: statusTone(order.financialStatus) },
84
+ ...(order.installmentCount && order.installmentCount > 1 ? [{ label: `${order.installmentCount}x`, tone: 'muted' as OrderDetailTone }] : []),
85
+ ],
86
+ customer: { name: order.customerName, email: order.customerEmail },
87
+ customerNode: (
88
+ <div className="space-y-1 text-sm">
89
+ {order.customerId ? (
90
+ <PersonLink
91
+ personId={order.customerId}
92
+ name={order.customerName ?? order.customerEmail ?? 'Cliente'}
93
+ className="font-medium"
94
+ />
95
+ ) : (
96
+ <p className="font-medium">{order.customerName ?? '—'}</p>
97
+ )}
98
+ {order.customerEmail && <p className="text-muted-foreground">{order.customerEmail}</p>}
99
+ </div>
100
+ ),
101
+ items: [{
102
+ name: order.courseTitle ?? '—',
103
+ description: order.offerName,
104
+ quantity: 1,
105
+ unitPrice: formatMoney(order.total, cur),
106
+ total: formatMoney(order.total, cur),
107
+ }],
108
+ totals: [
109
+ { label: 'Total', value: formatMoney(order.total, cur), strong: true },
110
+ ...(order.platformFee > 0 ? [{ label: 'Taxa', value: `− ${formatMoney(order.platformFee, cur)}`, muted: true }] : []),
111
+ { label: 'Líquido', value: formatMoney(order.netValue, cur) },
112
+ ],
113
+ paymentMethod: order.paymentMethod ?? undefined,
114
+ receivable: {
115
+ rows: [
116
+ { label: 'Recebido', value: formatMoney(order.paidAmount, cur) },
117
+ { label: 'Saldo', value: formatMoney(order.balance, cur), strong: order.balance > 0 },
118
+ ],
119
+ installments,
120
+ emptyLabel: t('courses.order.noInstallments') || 'Sem parcelas.',
121
+ },
122
+ onBack: back,
123
+ backLabel: t('courses.sales.title') || 'Vendas',
124
+ }
125
+
126
+ return (
127
+ <Page>
128
+ <SharedOrderDetailView {...data} />
129
+ </Page>
130
+ )
131
+ }
132
+ CourseOrderDetailView.displayName = 'CourseOrderDetailView'
@@ -1,6 +0,0 @@
1
- import * as React from 'react';
2
- export declare function MembersAreaPage(): React.JSX.Element;
3
- export declare namespace MembersAreaPage {
4
- var displayName: string;
5
- }
6
- //# sourceMappingURL=MembersAreaPage.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"MembersAreaPage.d.ts","sourceRoot":"","sources":["../../src/pages/MembersAreaPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAa9B,wBAAgB,eAAe,sBA8B9B;yBA9Be,eAAe"}
@@ -1,60 +0,0 @@
1
- import * as React from 'react'
2
- import { Badge } from '@fayz-ai/ui'
3
- import { useTranslation } from '@fayz-ai/core'
4
- import { getCoursesProvider, type Order, type Course } from '@fayz-ai/courses'
5
- import { PageContainer, SimpleTable, type SimpleColumn } from '../components/CommerceUI'
6
-
7
- interface Row {
8
- courseId: string
9
- title: string
10
- status: Course['status']
11
- students: number
12
- }
13
-
14
- export function MembersAreaPage() {
15
- const t = useTranslation()
16
- const [rows, setRows] = React.useState<Row[] | null>(null)
17
-
18
- React.useEffect(() => {
19
- const p = getCoursesProvider()
20
- void Promise.all([p.listCourses(), p.listOrders({ financialStatus: 'paid' })]).then(([courses, orders]) => {
21
- setRows(buildMembersAreas(courses, orders))
22
- })
23
- }, [])
24
-
25
- const columns: SimpleColumn<Row>[] = [
26
- { key: 'title', header: t('courses.membersArea.name') || 'Members area', render: (r) => <span className="font-medium text-foreground">{r.title}</span> },
27
- { key: 'status', header: t('courses.membersArea.status') || 'Status', render: (r) => (
28
- <Badge variant={r.status === 'published' ? 'success' : r.status === 'archived' ? 'outline' : 'secondary'}>
29
- {t(`courses.status.${r.status}`) || r.status}
30
- </Badge>
31
- ) },
32
- { key: 'students', header: t('courses.membersArea.students') || 'Students', align: 'right', render: (r) => <span className="text-foreground">{r.students}</span> },
33
- ]
34
-
35
- return (
36
- <PageContainer title={t('courses.membersArea.title') || 'Members area'} subtitle={t('courses.membersArea.subtitle') || 'One area per course; students land here after purchase.'}>
37
- <SimpleTable
38
- columns={columns}
39
- rows={rows ?? []}
40
- empty={t('courses.membersArea.empty') || 'No members areas yet.'}
41
- />
42
- </PageContainer>
43
- )
44
- }
45
- MembersAreaPage.displayName = 'MembersAreaPage'
46
-
47
- function buildMembersAreas(courses: Course[], orders: Order[]): Row[] {
48
- const students = new Map<string, Set<string>>()
49
- for (const o of orders) {
50
- const set = students.get(o.courseId) ?? new Set<string>()
51
- if (o.customerId) set.add(o.customerId)
52
- students.set(o.courseId, set)
53
- }
54
- return courses.map((c) => ({
55
- courseId: c.id,
56
- title: c.title,
57
- status: c.status,
58
- students: students.get(c.id)?.size ?? 0,
59
- }))
60
- }