@fayz-ai/plugin-courses 0.9.0 → 0.9.1

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 (72) hide show
  1. package/README.md +3 -3
  2. package/dist/components/CommerceUI.d.ts +19 -2
  3. package/dist/components/CommerceUI.d.ts.map +1 -1
  4. package/dist/components/dashboardWidgets.d.ts.map +1 -0
  5. package/dist/index.d.ts +26 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +342 -253
  8. package/dist/index.js.map +1 -1
  9. package/dist/lib/nav.d.ts.map +1 -0
  10. package/dist/locales/en.d.ts.map +1 -1
  11. package/dist/locales/pt-BR.d.ts.map +1 -1
  12. package/dist/public/createPublicCheckoutPlugin.d.ts +1 -1
  13. package/dist/public/createPublicCheckoutPlugin.d.ts.map +1 -1
  14. package/dist/public/{data.d.ts → data/index.d.ts} +1 -1
  15. package/dist/public/data/index.d.ts.map +1 -0
  16. package/dist/public/{data.supabase.d.ts → data/supabase.d.ts} +2 -2
  17. package/dist/public/data/supabase.d.ts.map +1 -0
  18. package/dist/public/index.d.ts +4 -4
  19. package/dist/public/index.d.ts.map +1 -1
  20. package/dist/public/index.js +353 -0
  21. package/dist/public/index.js.map +1 -0
  22. package/dist/public/{CheckoutScreen.d.ts → views/CheckoutScreen.d.ts} +1 -1
  23. package/dist/public/views/CheckoutScreen.d.ts.map +1 -0
  24. package/dist/views/CourseEditorPage.d.ts.map +1 -0
  25. package/dist/views/CourseOrderDetailView.d.ts +4 -3
  26. package/dist/views/CourseOrderDetailView.d.ts.map +1 -1
  27. package/dist/{pages → views}/CoursesListPage.d.ts.map +1 -1
  28. package/dist/{pages → views}/FinancialPage.d.ts.map +1 -1
  29. package/dist/{pages → views}/ReportsPage.d.ts.map +1 -1
  30. package/dist/views/SalesPage.d.ts +19 -0
  31. package/dist/views/SalesPage.d.ts.map +1 -0
  32. package/dist/views/SubscriptionsPage.d.ts +7 -0
  33. package/dist/views/SubscriptionsPage.d.ts.map +1 -0
  34. package/package.json +6 -9
  35. package/dist/dashboardWidgets.d.ts.map +0 -1
  36. package/dist/nav.d.ts.map +0 -1
  37. package/dist/pages/CourseEditorPage.d.ts.map +0 -1
  38. package/dist/pages/SalesPage.d.ts +0 -6
  39. package/dist/pages/SalesPage.d.ts.map +0 -1
  40. package/dist/pages/SubscriptionsPage.d.ts +0 -6
  41. package/dist/pages/SubscriptionsPage.d.ts.map +0 -1
  42. package/dist/public/CheckoutScreen.d.ts.map +0 -1
  43. package/dist/public/data.d.ts.map +0 -1
  44. package/dist/public/data.supabase.d.ts.map +0 -1
  45. package/src/components/CommerceUI.tsx +0 -153
  46. package/src/connectors/stripe.ts +0 -34
  47. package/src/dashboardWidgets.tsx +0 -64
  48. package/src/index.ts +0 -150
  49. package/src/lib/format.ts +0 -22
  50. package/src/locales/en.ts +0 -203
  51. package/src/locales/index.ts +0 -7
  52. package/src/locales/pt-BR.ts +0 -203
  53. package/src/nav.ts +0 -9
  54. package/src/pages/CourseEditorPage.tsx +0 -1122
  55. package/src/pages/CoursesListPage.tsx +0 -137
  56. package/src/pages/FinancialPage.tsx +0 -81
  57. package/src/pages/ReportsPage.tsx +0 -59
  58. package/src/pages/SalesPage.tsx +0 -86
  59. package/src/pages/SubscriptionsPage.tsx +0 -56
  60. package/src/public/CheckoutScreen.tsx +0 -279
  61. package/src/public/createPublicCheckoutPlugin.tsx +0 -88
  62. package/src/public/data.supabase.ts +0 -63
  63. package/src/public/data.ts +0 -89
  64. package/src/public/index.ts +0 -26
  65. package/src/settings/CoursesSettings.tsx +0 -55
  66. package/src/views/CourseOrderDetailView.tsx +0 -132
  67. /package/dist/{dashboardWidgets.d.ts → components/dashboardWidgets.d.ts} +0 -0
  68. /package/dist/{nav.d.ts → lib/nav.d.ts} +0 -0
  69. /package/dist/{pages → views}/CourseEditorPage.d.ts +0 -0
  70. /package/dist/{pages → views}/CoursesListPage.d.ts +0 -0
  71. /package/dist/{pages → views}/FinancialPage.d.ts +0 -0
  72. /package/dist/{pages → views}/ReportsPage.d.ts +0 -0
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "fayz": {
4
4
  "status": "preview"
5
5
  },
6
- "version": "0.9.0",
6
+ "version": "0.9.1",
7
7
  "description": "[experimental] Fayz SDK — courses admin management plugin",
8
8
  "type": "module",
9
9
  "sideEffects": false,
@@ -12,19 +12,16 @@
12
12
  "types": "./dist/index.d.ts",
13
13
  "exports": {
14
14
  ".": {
15
- "source": "./src/index.ts",
16
15
  "types": "./dist/index.d.ts",
17
16
  "import": "./dist/index.js"
18
17
  },
19
18
  "./public": {
20
- "source": "./src/public/index.ts",
21
19
  "types": "./dist/public/index.d.ts",
22
20
  "import": "./dist/public/index.js"
23
21
  }
24
22
  },
25
23
  "files": [
26
- "dist",
27
- "src"
24
+ "dist"
28
25
  ],
29
26
  "peerDependencies": {
30
27
  "react": "^18.0.0 || ^19.0.0",
@@ -36,10 +33,10 @@
36
33
  "@dnd-kit/core": "^6.3.1",
37
34
  "@dnd-kit/sortable": "^10.0.0",
38
35
  "@dnd-kit/utilities": "^3.2.2",
39
- "@fayz-ai/core": "^0.9.0",
40
- "@fayz-ai/ui": "^0.9.0",
41
- "@fayz-ai/courses": "^0.9.0",
42
- "@fayz-ai/saas": "^0.9.0"
36
+ "@fayz-ai/core": "^0.10.0",
37
+ "@fayz-ai/ui": "^0.10.0",
38
+ "@fayz-ai/courses": "^0.10.0",
39
+ "@fayz-ai/admin": "^0.10.0"
43
40
  },
44
41
  "devDependencies": {
45
42
  "@types/react": "^18.3.0",
@@ -1 +0,0 @@
1
- {"version":3,"file":"dashboardWidgets.d.ts","sourceRoot":"","sources":["../src/dashboardWidgets.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AA0CvD;;2EAE2E;AAC3E,wBAAgB,6BAA6B,CAC3C,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAO,GAC9E,kBAAkB,EAAE,CAetB"}
package/dist/nav.d.ts.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"nav.d.ts","sourceRoot":"","sources":["../src/nav.ts"],"names":[],"mappings":"AAMA,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAE3C"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"CourseEditorPage.d.ts","sourceRoot":"","sources":["../../src/pages/CourseEditorPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAqF9B,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,qBAyiBvD;yBAziBe,gBAAgB"}
@@ -1,6 +0,0 @@
1
- import * as React from 'react';
2
- export declare function SalesPage(): React.JSX.Element;
3
- export declare namespace SalesPage {
4
- var displayName: string;
5
- }
6
- //# sourceMappingURL=SalesPage.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SalesPage.d.ts","sourceRoot":"","sources":["../../src/pages/SalesPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAmB9B,wBAAgB,SAAS,sBAiExB;yBAjEe,SAAS"}
@@ -1,6 +0,0 @@
1
- import * as React from 'react';
2
- export declare function SubscriptionsPage(): React.JSX.Element;
3
- export declare namespace SubscriptionsPage {
4
- var displayName: string;
5
- }
6
- //# sourceMappingURL=SubscriptionsPage.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SubscriptionsPage.d.ts","sourceRoot":"","sources":["../../src/pages/SubscriptionsPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAa9B,wBAAgB,iBAAiB,sBAyChC;yBAzCe,iBAAiB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"CheckoutScreen.d.ts","sourceRoot":"","sources":["../../src/public/CheckoutScreen.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAgC,KAAK,EAAE,EAAkB,MAAM,OAAO,CAAA;AAG7E,OAAO,KAAK,EAAyB,0BAA0B,EAAE,MAAM,QAAQ,CAAA;AAE/E,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,0BAA0B,CAAA;IACpC,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0EAA0E;IAC1E,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CAC/D;AAUD,eAAO,MAAM,cAAc,EAAE,EAAE,CAAC,mBAAmB,CAoPlD,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../../src/public/data.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAErD,MAAM,MAAM,qBAAqB,GAAG,KAAK,GAAG,MAAM,CAAA;AAElD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,aAAa,CAAC,EAAE,qBAAqB,CAAA;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,6EAA6E;IAC7E,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACrD,0EAA0E;IAC1E,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAAA;IACxD,4EAA4E;IAC5E,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAA;CAC/E;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;CACvB;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,GAAE,mBAAwB,GAAG,0BAA0B,CA8CrG"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"data.supabase.d.ts","sourceRoot":"","sources":["../../src/public/data.supabase.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,0BAA0B,EAAmB,MAAM,QAAQ,CAAA;AAEzE,MAAM,WAAW,uBAAuB;IACtC,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAA;CACjB;AAYD,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,uBAAuB,GAAG,0BAA0B,CA8B3G"}
@@ -1,153 +0,0 @@
1
- import * as React from 'react'
2
- import type { ColumnDef } from '@tanstack/react-table'
3
- import { Card, DataTable } from '@fayz-ai/ui'
4
-
5
- /** Standard padded container for the commerce admin pages. */
6
- export function PageContainer({ title, subtitle, actions, children }: {
7
- title: string
8
- subtitle?: string
9
- actions?: React.ReactNode
10
- children: React.ReactNode
11
- }) {
12
- return (
13
- <div className="mx-auto max-w-6xl p-6 md:p-8">
14
- <div className="mb-6 flex items-start justify-between gap-4">
15
- <div>
16
- <h1 className="text-2xl font-bold text-foreground">{title}</h1>
17
- {subtitle && <p className="mt-1 text-sm text-muted-foreground">{subtitle}</p>}
18
- </div>
19
- {actions}
20
- </div>
21
- {children}
22
- </div>
23
- )
24
- }
25
-
26
- /** A single KPI tile (revenue, sales, MRR…). */
27
- export function StatCard({ label, value, hint }: { label: string; value: string; hint?: string }) {
28
- return (
29
- <Card className="p-5">
30
- <p className="text-sm text-muted-foreground">{label}</p>
31
- <p className="mt-1 text-2xl font-bold text-foreground">{value}</p>
32
- {hint && <p className="mt-1 text-xs text-muted-foreground">{hint}</p>}
33
- </Card>
34
- )
35
- }
36
-
37
- export function StatGrid({ children }: { children: React.ReactNode }) {
38
- return <div className="mb-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">{children}</div>
39
- }
40
-
41
- /** Empty/loading state for the tables. */
42
- export function TableEmpty({ children }: { children: React.ReactNode }) {
43
- return (
44
- <div className="rounded-xl border border-dashed border-border p-12 text-center text-sm text-muted-foreground">
45
- {children}
46
- </div>
47
- )
48
- }
49
-
50
- /**
51
- * Standard empty state — an icon chip, a title, an optional description and a
52
- * call-to-action. Use anywhere a list/section can be legitimately empty so the
53
- * screen never shows a bare dashed box.
54
- */
55
- export function EmptyState({ icon, title, description, action }: {
56
- icon?: React.ReactNode
57
- title: string
58
- description?: string
59
- action?: React.ReactNode
60
- }) {
61
- return (
62
- <div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-border bg-card/40 px-6 py-12 text-center">
63
- {icon && (
64
- <div className="mb-3 flex h-11 w-11 items-center justify-center rounded-full bg-muted text-muted-foreground">
65
- {icon}
66
- </div>
67
- )}
68
- <p className="text-sm font-medium text-foreground">{title}</p>
69
- {description && <p className="mt-1 max-w-sm text-sm text-muted-foreground">{description}</p>}
70
- {action && <div className="mt-4">{action}</div>}
71
- </div>
72
- )
73
- }
74
-
75
- /**
76
- * A muted, low-emphasis informational callout. Use to set honest expectations
77
- * (e.g. "this applies to a feature that isn't shipped yet") without dressing it
78
- * up as an interactive control. Wrap key phrases in <strong> for emphasis.
79
- */
80
- export function InfoNote({ icon, children, className }: {
81
- icon?: React.ReactNode
82
- children: React.ReactNode
83
- className?: string
84
- }) {
85
- return (
86
- <div className={'flex items-start gap-2.5 rounded-lg border border-border bg-muted/40 px-3.5 py-3 text-sm text-muted-foreground ' + (className ?? '')}>
87
- {icon && <span className="mt-0.5 shrink-0 text-muted-foreground/80">{icon}</span>}
88
- <div className="min-w-0 [&_strong]:font-medium [&_strong]:text-foreground">{children}</div>
89
- </div>
90
- )
91
- }
92
-
93
- /** A titled card section — the house pattern for grouping form fields. */
94
- export function SectionCard({ title, description, actions, children, className }: {
95
- title?: string
96
- description?: string
97
- actions?: React.ReactNode
98
- children: React.ReactNode
99
- className?: string
100
- }) {
101
- return (
102
- <Card className={'p-5 md:p-6 ' + (className ?? '')}>
103
- {(title || actions) && (
104
- <div className="mb-4 flex items-start justify-between gap-4">
105
- <div>
106
- {title && <h2 className="text-sm font-semibold text-foreground">{title}</h2>}
107
- {description && <p className="mt-0.5 text-sm text-muted-foreground">{description}</p>}
108
- </div>
109
- {actions}
110
- </div>
111
- )}
112
- {children}
113
- </Card>
114
- )
115
- }
116
-
117
- export interface SimpleColumn<T> {
118
- key: string
119
- header: string
120
- render: (row: T) => React.ReactNode
121
- align?: 'left' | 'right'
122
- }
123
-
124
- /**
125
- * Thin adapter over the design-system DataTable (@fayz-ai/ui): the commerce
126
- * pages describe columns declaratively; this maps them to tanstack ColumnDef so
127
- * we reuse DataTable's sorting/skeleton/empty chrome instead of a bespoke table.
128
- */
129
- export function SimpleTable<T>({ columns, rows, empty, loading, onRowClick }: {
130
- columns: SimpleColumn<T>[]
131
- rows: T[]
132
- empty: string
133
- loading?: boolean
134
- onRowClick?: (row: T) => void
135
- }) {
136
- const cols = React.useMemo<ColumnDef<T>[]>(
137
- () => columns.map((c) => ({
138
- id: c.key,
139
- header: () => <div className={c.align === 'right' ? 'text-right' : ''}>{c.header}</div>,
140
- cell: ({ row }) => <div className={c.align === 'right' ? 'text-right' : ''}>{c.render(row.original)}</div>,
141
- })),
142
- [columns],
143
- )
144
- return (
145
- <DataTable
146
- columns={cols}
147
- data={rows}
148
- emptyMessage={empty}
149
- loading={loading}
150
- onRowClick={onRowClick ? (row: T) => onRowClick(row) : undefined}
151
- />
152
- )
153
- }
@@ -1,34 +0,0 @@
1
- import type { ConnectorDefinition } from '@fayz-ai/core'
2
- import { getCoursesProvider } from '@fayz-ai/courses'
3
-
4
- // Stripe Connect modeled as a connector so it surfaces in the plugin's unified
5
- // "Integrations" settings tab (same pattern as Financial Settings → Integrações),
6
- // instead of a bespoke block in the General tab. The host plugin is 'courses'.
7
- export function createStripeConnector(): ConnectorDefinition {
8
- return {
9
- id: 'stripe-connect',
10
- hostPluginId: 'courses',
11
- name: 'Stripe',
12
- description: 'Receba os pagamentos dos alunos; a taxa da plataforma é aplicada automaticamente.',
13
- icon: 'CreditCard',
14
- authKind: 'oauth',
15
- async getStatus() {
16
- try {
17
- const account = await getCoursesProvider().getCreatorAccount()
18
- const fee = (account.platformFeeBps / 100).toFixed(2)
19
- return {
20
- connected: !!account.stripeAccountId && account.chargesEnabled,
21
- detail: `Taxa da plataforma: ${fee}%`,
22
- }
23
- } catch {
24
- return { connected: false }
25
- }
26
- },
27
- async startOAuth(_redirectTo?: string): Promise<string> {
28
- // Stage 3 wires this to the `courses-connect-onboard` edge function and
29
- // returns the Stripe account-link URL to redirect to. Until then, surface
30
- // a clear message (ConnectorsHub toasts a thrown error's message).
31
- throw new Error('O onboarding do Stripe Connect é ligado na Etapa 3.')
32
- },
33
- }
34
- }
@@ -1,64 +0,0 @@
1
- import * as React from 'react'
2
- import type { DashboardWidgetDef } from '@fayz-ai/core'
3
- import { useTranslation } from '@fayz-ai/core'
4
- import { KpiCard, defineKpiWidget } from '@fayz-ai/ui'
5
- import { getCoursesProvider, type FinancialSummary } from '@fayz-ai/courses'
6
- import { formatMoney, formatPercent } from './lib/format'
7
-
8
- // Course KPIs contributed to the central admin Dashboard (rendered by
9
- // @fayz-ai/plugin-dashboard's DashboardCanvas, which auto-collects every
10
- // plugin's dashboardWidgets for surface 'home'). Mirrors Kiwify's dashboard.
11
-
12
- function useSummary(): FinancialSummary | null {
13
- const [summary, setSummary] = React.useState<FinancialSummary | null>(null)
14
- React.useEffect(() => {
15
- void getCoursesProvider().getFinancialSummary().then(setSummary)
16
- }, [])
17
- return summary
18
- }
19
-
20
- function RevenueKpi() {
21
- const t = useTranslation()
22
- const s = useSummary()
23
- return <KpiCard label={t('courses.kpi.revenue') || 'Total revenue'} icon="DollarSign" loading={!s} value={s ? formatMoney(s.totalRevenue, s.currency) : undefined} />
24
- }
25
-
26
- function SalesKpi() {
27
- const t = useTranslation()
28
- const s = useSummary()
29
- return <KpiCard label={t('courses.kpi.sales') || 'Sales'} icon="TrendingUp" loading={!s} value={s ? String(s.salesCount) : undefined} sub={s ? formatPercent(s.refundRate, 1) + ' ' + (t('courses.kpi.refunds') || 'refunds') : undefined} />
30
- }
31
-
32
- function MrrKpi() {
33
- const t = useTranslation()
34
- const s = useSummary()
35
- return <KpiCard label={t('courses.kpi.mrr') || 'MRR'} icon="Repeat" loading={!s} value={s ? formatMoney(s.mrr, s.currency) : undefined} sub={s ? `${s.activeSubscriptions} ${t('courses.kpi.activeSubs') || 'active'}` : undefined} />
36
- }
37
-
38
- function FeesKpi() {
39
- const t = useTranslation()
40
- const s = useSummary()
41
- return <KpiCard label={t('courses.kpi.fees') || 'Platform fees'} icon="Percent" loading={!s} value={s ? formatMoney(s.platformFeeTotal, s.currency) : undefined} />
42
- }
43
-
44
- /** Commerce KPIs are contributed only for the modules the host opted into —
45
- * a host embedding courses as a lightweight members feature must not get
46
- * revenue/MRR cards broadcast onto its home dashboard (FAY-1247 rule). */
47
- export function createCoursesDashboardWidgets(
48
- modules: { sales?: boolean; subscriptions?: boolean; financial?: boolean } = {},
49
- ): DashboardWidgetDef[] {
50
- return [
51
- ...(modules.sales
52
- ? [
53
- defineKpiWidget({ id: 'courses.kpi.revenue', title: 'courses.kpi.revenue', domain: 'courses', defaultOrder: 0, component: RevenueKpi }),
54
- defineKpiWidget({ id: 'courses.kpi.sales', title: 'courses.kpi.sales', domain: 'courses', defaultOrder: 1, component: SalesKpi }),
55
- ]
56
- : []),
57
- ...(modules.subscriptions
58
- ? [defineKpiWidget({ id: 'courses.kpi.mrr', title: 'courses.kpi.mrr', domain: 'courses', defaultOrder: 2, component: MrrKpi })]
59
- : []),
60
- ...(modules.financial
61
- ? [defineKpiWidget({ id: 'courses.kpi.fees', title: 'courses.kpi.fees', domain: 'courses', defaultOrder: 3, component: FeesKpi })]
62
- : []),
63
- ]
64
- }
package/src/index.ts DELETED
@@ -1,150 +0,0 @@
1
- import * as React from 'react'
2
- import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'
3
- import { registerTranslations, registerPluginFactory } from '@fayz-ai/core'
4
- import {
5
- getCoursesProviderOptional,
6
- setCoursesProvider,
7
- createMockCoursesProvider,
8
- setCoursesTenantResolver,
9
- } from '@fayz-ai/courses'
10
- import { useOrganizationStore } from '@fayz-ai/saas'
11
- import { coursesLocales } from './locales'
12
- import { CoursesListPage } from './pages/CoursesListPage'
13
- import { CourseEditorPage } from './pages/CourseEditorPage'
14
- import { SalesPage } from './pages/SalesPage'
15
- import { CourseOrderDetailView } from './views/CourseOrderDetailView'
16
- import { SubscriptionsPage } from './pages/SubscriptionsPage'
17
- import { FinancialPage } from './pages/FinancialPage'
18
- import { ReportsPage } from './pages/ReportsPage'
19
- import { CoursesSettingsTab } from './settings/CoursesSettings'
20
- import { createCoursesDashboardWidgets } from './dashboardWidgets'
21
- import { createStripeConnector } from './connectors/stripe'
22
-
23
- // ---------------------------------------------------------------------------
24
- // @fayz-ai/plugin-courses — admin management for the courses domain. This is a
25
- // PLUGIN, not an app: it only contributes nav + routes + settings + dashboard
26
- // widgets to the @fayz-ai/saas admin scaffold. Login, auth, permissions, the
27
- // sidebar/AppShell chrome and layout all come from @fayz-ai/saas — the plugin
28
- // never rebuilds them. Its screens render inside the saas AdminShell and use
29
- // @fayz-ai/ui primitives.
30
- // ---------------------------------------------------------------------------
31
-
32
- export interface CoursesPluginOptions {
33
- navPosition?: number
34
- navSection?: 'main' | 'secondary' | 'settings'
35
- navLabel?: string
36
- /** Nav labels for the module entries (default English). */
37
- labels?: {
38
- membersArea?: string
39
- sales?: string
40
- subscriptions?: string
41
- financial?: string
42
- reports?: string
43
- }
44
- scope?: PluginScope
45
- verticalId?: VerticalId
46
- /**
47
- * Commerce-vertical modules — ALL OFF by default (the one architectural
48
- * rule: shared-surface contributions are opt-in, never broadcast — FAY-1247).
49
- * course-admin (the vertical owner) enables everything via its PluginRef
50
- * config; a host embedding courses as a lightweight members feature (e.g.
51
- * agency-os's "Memberships") gets only the base nav entry + editor.
52
- */
53
- modules?: {
54
- membersArea?: boolean
55
- sales?: boolean
56
- subscriptions?: boolean
57
- financial?: boolean
58
- reports?: boolean
59
- }
60
- }
61
-
62
- const asComp = (c: unknown) => c as React.ComponentType<unknown>
63
- const read = (feature: string) => ({ feature, action: 'read' as const })
64
-
65
- export function createCoursesPlugin(options?: CoursesPluginOptions): PluginManifest {
66
- // Register locales globally so keys resolve even before the I18nProvider mounts.
67
- registerTranslations(coursesLocales)
68
-
69
- // Scope Supabase queries to the active organization (the creator/tenant). The
70
- // mock provider ignores this; the Supabase provider appends .eq('tenant_id').
71
- setCoursesTenantResolver(() => useOrganizationStore.getState().currentOrg?.id)
72
-
73
- // Defensive default: if the host app didn't set a provider, fall back to a
74
- // mock so the admin still renders. Apps that share admin↔member set their own.
75
- if (!getCoursesProviderOptional()) {
76
- setCoursesProvider(createMockCoursesProvider())
77
- }
78
-
79
- const base = options?.navPosition ?? 1
80
- const section = options?.navSection ?? 'main'
81
- const modules = options?.modules ?? {}
82
-
83
- // Opt-in nav/routes per module. Position offsets stay stable relative to the
84
- // enabled set so hosts control exactly where the block sits.
85
- const labels = options?.labels ?? {}
86
- const navigation = [
87
- { section, position: base, label: options?.navLabel ?? 'Courses', route: '/courses', icon: 'BookOpen', permission: read('courses') },
88
- // The members area is no longer a standalone nav entry — it lives inside each
89
- // course's record (CourseEditorPage → "Alunos" tab). modules.membersArea is
90
- // kept accepted-but-inert for backward compat with existing manifests.
91
- ...(modules.sales ? [{ section, position: base + 2, label: labels.sales ?? 'Sales', route: '/courses/sales', icon: 'TrendingUp', permission: read('courses.sales') }] : []),
92
- ...(modules.subscriptions ? [{ section, position: base + 3, label: labels.subscriptions ?? 'Subscriptions', route: '/courses/subscriptions', icon: 'Repeat', permission: read('courses.sales') }] : []),
93
- ...(modules.financial ? [{ section, position: base + 4, label: labels.financial ?? 'Financial', route: '/courses/financial', icon: 'CreditCard', permission: read('courses.finance') }] : []),
94
- ...(modules.reports ? [{ section, position: base + 5, label: labels.reports ?? 'Reports', route: '/courses/reports', icon: 'BarChart3', permission: read('courses.sales') }] : []),
95
- ]
96
-
97
- const routes = [
98
- // Static segments outscore the `:id` param in the saas router, so these
99
- // resolve before the editor route (AdminShell sorts by routeScore).
100
- { path: '/courses', component: asComp(CoursesListPage), permission: read('courses') },
101
- ...(modules.sales ? [{ path: '/courses/sales', component: asComp(SalesPage), permission: read('courses.sales') }] : []),
102
- // Order detail — static `/courses/sales/:id` outscores `/courses/:id`.
103
- ...(modules.sales ? [{ path: '/courses/sales/:id', component: asComp(CourseOrderDetailView), permission: read('courses.sales') }] : []),
104
- ...(modules.subscriptions ? [{ path: '/courses/subscriptions', component: asComp(SubscriptionsPage), permission: read('courses.sales') }] : []),
105
- ...(modules.financial ? [{ path: '/courses/financial', component: asComp(FinancialPage), permission: read('courses.finance') }] : []),
106
- ...(modules.reports ? [{ path: '/courses/reports', component: asComp(ReportsPage), permission: read('courses.sales') }] : []),
107
- { path: '/courses/:id', component: asComp(CourseEditorPage) },
108
- ]
109
-
110
- return {
111
- id: 'courses',
112
- name: options?.navLabel ?? 'Courses',
113
- icon: 'BookOpen',
114
- version: '1.0.0',
115
- scope: options?.scope ?? 'vertical',
116
- verticalId: options?.verticalId,
117
- // LMS is its own business model with opt-in commerce modules
118
- // (members/sales/subscriptions — see modules.* below) — targets the
119
- // saas/admin Panel world rather than the ecommerce storefront.
120
- scaffolds: ['saas'],
121
- defaultEnabled: true,
122
- dependencies: [],
123
- declaredFeatures: [
124
- { id: 'courses', label: 'Courses', group: 'Courses' },
125
- { id: 'courses.sales', label: 'Sales', group: 'Courses' },
126
- { id: 'courses.finance', label: 'Financial', group: 'Courses' },
127
- ],
128
- declaredLimits: [
129
- { key: 'courses', label: 'Courses', table: 'plg_courses_courses' },
130
- ],
131
- navigation,
132
- routes,
133
- widgets: [],
134
- // Commerce KPIs on the shared 'home' surface follow the same opt-in rule:
135
- // no module enabled → no widgets broadcast into a host's dashboard.
136
- dashboardWidgets: createCoursesDashboardWidgets(modules),
137
- connectors: [createStripeConnector()],
138
- settings: [
139
- { id: 'courses', label: 'Courses', icon: 'BookOpen', component: asComp(CoursesSettingsTab), order: 20, permission: read('courses') },
140
- ],
141
- locales: coursesLocales,
142
- }
143
- }
144
-
145
- // Self-register the factory so renderApp(manifest) with a PluginRef { id:
146
- // 'courses' } resolves without a separate plugins.generated.ts entry (apps may
147
- // still register explicitly; registration is idempotent on id).
148
- registerPluginFactory('courses', createCoursesPlugin as (config?: Record<string, unknown>) => PluginManifest)
149
-
150
- export { coursesLocales } from './locales'
package/src/lib/format.ts DELETED
@@ -1,22 +0,0 @@
1
- // Small display helpers shared across the courses admin pages. Kept local so the
2
- // plugin depends only on @fayz-ai/core + @fayz-ai/ui + @fayz-ai/courses.
3
-
4
- export function formatMoney(value: number, currency = 'BRL'): string {
5
- try {
6
- return new Intl.NumberFormat('pt-BR', { style: 'currency', currency }).format(value)
7
- } catch {
8
- return `${currency} ${value.toFixed(2)}`
9
- }
10
- }
11
-
12
- export function formatDate(iso: string): string {
13
- try {
14
- return new Intl.DateTimeFormat('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' }).format(new Date(iso))
15
- } catch {
16
- return iso.slice(0, 10)
17
- }
18
- }
19
-
20
- export function formatPercent(fraction: number, digits = 0): string {
21
- return `${(fraction * 100).toFixed(digits)}%`
22
- }