@fayz-ai/plugin-courses 0.8.1 → 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.
- package/dist/components/CommerceUI.d.ts +31 -1
- package/dist/components/CommerceUI.d.ts.map +1 -1
- package/dist/index.cjs +1237 -301
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1240 -304
- package/dist/index.js.map +1 -1
- 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/CoursesListPage.d.ts.map +1 -1
- package/dist/pages/SalesPage.d.ts.map +1 -1
- package/dist/public/CheckoutScreen.d.ts +17 -0
- package/dist/public/CheckoutScreen.d.ts.map +1 -0
- package/dist/public/createPublicCheckoutPlugin.d.ts +27 -0
- package/dist/public/createPublicCheckoutPlugin.d.ts.map +1 -0
- package/dist/public/data.d.ts +28 -0
- package/dist/public/data.d.ts.map +1 -0
- package/dist/public/data.supabase.d.ts +7 -0
- package/dist/public/data.supabase.d.ts.map +1 -0
- package/dist/public/index.d.ts +9 -0
- package/dist/public/index.d.ts.map +1 -0
- package/dist/views/CourseOrderDetailView.d.ts +10 -0
- package/dist/views/CourseOrderDetailView.d.ts.map +1 -0
- package/package.json +14 -5
- package/src/components/CommerceUI.tsx +78 -2
- package/src/index.ts +6 -3
- package/src/locales/en.ts +73 -12
- package/src/locales/pt-BR.ts +73 -12
- package/src/pages/CourseEditorPage.tsx +990 -177
- package/src/pages/CoursesListPage.tsx +20 -1
- package/src/pages/SalesPage.tsx +49 -20
- package/src/public/CheckoutScreen.tsx +279 -0
- package/src/public/createPublicCheckoutPlugin.tsx +88 -0
- package/src/public/data.supabase.ts +63 -0
- package/src/public/data.ts +89 -0
- package/src/public/index.ts +26 -0
- package/src/views/CourseOrderDetailView.tsx +132 -0
- package/dist/pages/MembersAreaPage.d.ts +0 -6
- package/dist/pages/MembersAreaPage.d.ts.map +0 -1
- 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 +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
|
-
}
|