@fayz-ai/plugin-courses 0.2.3 → 0.8.0

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/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@fayz-ai/plugin-courses",
3
- "version": "0.2.3",
3
+ "fayz": {
4
+ "status": "preview"
5
+ },
6
+ "version": "0.8.0",
4
7
  "description": "[experimental] Fayz SDK — courses admin management plugin",
5
8
  "type": "module",
6
9
  "main": "./dist/index.cjs",
@@ -23,12 +26,12 @@
23
26
  "react-dom": "^18.0.0 || ^19.0.0"
24
27
  },
25
28
  "dependencies": {
26
- "@tanstack/react-table": "^8.20.0",
27
29
  "lucide-react": "^0.400.0",
28
- "@fayz-ai/core": "^0.7.0",
29
- "@fayz-ai/courses": "^0.7.0",
30
- "@fayz-ai/ui": "^0.7.0",
31
- "@fayz-ai/saas": "^0.7.0"
30
+ "@tanstack/react-table": "^8.20.0",
31
+ "@fayz-ai/core": "^0.8.0",
32
+ "@fayz-ai/ui": "^0.8.0",
33
+ "@fayz-ai/courses": "^0.8.0",
34
+ "@fayz-ai/saas": "^0.8.0"
32
35
  },
33
36
  "devDependencies": {
34
37
  "@types/react": "^18.3.0",
@@ -41,11 +41,24 @@ function FeesKpi() {
41
41
  return <KpiCard label={t('courses.kpi.fees') || 'Platform fees'} icon="Percent" loading={!s} value={s ? formatMoney(s.platformFeeTotal, s.currency) : undefined} />
42
42
  }
43
43
 
44
- export function createCoursesDashboardWidgets(): DashboardWidgetDef[] {
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[] {
45
50
  return [
46
- defineKpiWidget({ id: 'courses.kpi.revenue', title: 'courses.kpi.revenue', domain: 'courses', defaultOrder: 0, component: RevenueKpi }),
47
- defineKpiWidget({ id: 'courses.kpi.sales', title: 'courses.kpi.sales', domain: 'courses', defaultOrder: 1, component: SalesKpi }),
48
- defineKpiWidget({ id: 'courses.kpi.mrr', title: 'courses.kpi.mrr', domain: 'courses', defaultOrder: 2, component: MrrKpi }),
49
- defineKpiWidget({ id: 'courses.kpi.fees', title: 'courses.kpi.fees', domain: 'courses', defaultOrder: 3, component: FeesKpi }),
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
+ : []),
50
63
  ]
51
64
  }
package/src/index.ts CHANGED
@@ -35,6 +35,20 @@ export interface CoursesPluginOptions {
35
35
  navLabel?: string
36
36
  scope?: PluginScope
37
37
  verticalId?: VerticalId
38
+ /**
39
+ * Commerce-vertical modules — ALL OFF by default (the one architectural
40
+ * rule: shared-surface contributions are opt-in, never broadcast — FAY-1247).
41
+ * course-admin (the vertical owner) enables everything via its PluginRef
42
+ * config; a host embedding courses as a lightweight members feature (e.g.
43
+ * agency-os's "Memberships") gets only the base nav entry + editor.
44
+ */
45
+ modules?: {
46
+ membersArea?: boolean
47
+ sales?: boolean
48
+ subscriptions?: boolean
49
+ financial?: boolean
50
+ reports?: boolean
51
+ }
38
52
  }
39
53
 
40
54
  const asComp = (c: unknown) => c as React.ComponentType<unknown>
@@ -56,6 +70,30 @@ export function createCoursesPlugin(options?: CoursesPluginOptions): PluginManif
56
70
 
57
71
  const base = options?.navPosition ?? 1
58
72
  const section = options?.navSection ?? 'main'
73
+ const modules = options?.modules ?? {}
74
+
75
+ // Opt-in nav/routes per module. Position offsets stay stable relative to the
76
+ // enabled set so hosts control exactly where the block sits.
77
+ const navigation = [
78
+ { section, position: base, label: options?.navLabel ?? 'Courses', route: '/courses', icon: 'BookOpen', permission: read('courses') },
79
+ ...(modules.membersArea ? [{ section, position: base + 1, label: 'Members area', route: '/courses/members', icon: 'GraduationCap', permission: read('courses') }] : []),
80
+ ...(modules.sales ? [{ section, position: base + 2, label: 'Sales', route: '/courses/sales', icon: 'TrendingUp', permission: read('courses.sales') }] : []),
81
+ ...(modules.subscriptions ? [{ section, position: base + 3, label: 'Subscriptions', route: '/courses/subscriptions', icon: 'Repeat', permission: read('courses.sales') }] : []),
82
+ ...(modules.financial ? [{ section, position: base + 4, label: 'Financial', route: '/courses/financial', icon: 'CreditCard', permission: read('courses.finance') }] : []),
83
+ ...(modules.reports ? [{ section, position: base + 5, label: 'Reports', route: '/courses/reports', icon: 'BarChart3', permission: read('courses.sales') }] : []),
84
+ ]
85
+
86
+ const routes = [
87
+ // Static segments outscore the `:id` param in the saas router, so these
88
+ // resolve before the editor route (AdminShell sorts by routeScore).
89
+ { path: '/courses', component: asComp(CoursesListPage), permission: read('courses') },
90
+ ...(modules.membersArea ? [{ path: '/courses/members', component: asComp(MembersAreaPage), permission: read('courses') }] : []),
91
+ ...(modules.sales ? [{ path: '/courses/sales', component: asComp(SalesPage), permission: read('courses.sales') }] : []),
92
+ ...(modules.subscriptions ? [{ path: '/courses/subscriptions', component: asComp(SubscriptionsPage), permission: read('courses.sales') }] : []),
93
+ ...(modules.financial ? [{ path: '/courses/financial', component: asComp(FinancialPage), permission: read('courses.finance') }] : []),
94
+ ...(modules.reports ? [{ path: '/courses/reports', component: asComp(ReportsPage), permission: read('courses.sales') }] : []),
95
+ { path: '/courses/:id', component: asComp(CourseEditorPage) },
96
+ ]
59
97
 
60
98
  return {
61
99
  id: 'courses',
@@ -64,6 +102,10 @@ export function createCoursesPlugin(options?: CoursesPluginOptions): PluginManif
64
102
  version: '1.0.0',
65
103
  scope: options?.scope ?? 'vertical',
66
104
  verticalId: options?.verticalId,
105
+ // LMS is its own business model with opt-in commerce modules
106
+ // (members/sales/subscriptions — see modules.* below) — targets the
107
+ // saas/admin Panel world rather than the ecommerce storefront.
108
+ scaffolds: ['saas'],
67
109
  defaultEnabled: true,
68
110
  dependencies: [],
69
111
  declaredFeatures: [
@@ -71,27 +113,12 @@ export function createCoursesPlugin(options?: CoursesPluginOptions): PluginManif
71
113
  { id: 'courses.sales', label: 'Sales', group: 'Courses' },
72
114
  { id: 'courses.finance', label: 'Financial', group: 'Courses' },
73
115
  ],
74
- navigation: [
75
- { section, position: base, label: options?.navLabel ?? 'Courses', route: '/courses', icon: 'BookOpen', permission: read('courses') },
76
- { section, position: base + 1, label: 'Members area', route: '/courses/members', icon: 'GraduationCap', permission: read('courses') },
77
- { section, position: base + 2, label: 'Sales', route: '/courses/sales', icon: 'TrendingUp', permission: read('courses.sales') },
78
- { section, position: base + 3, label: 'Subscriptions', route: '/courses/subscriptions', icon: 'Repeat', permission: read('courses.sales') },
79
- { section, position: base + 4, label: 'Financial', route: '/courses/financial', icon: 'CreditCard', permission: read('courses.finance') },
80
- { section, position: base + 5, label: 'Reports', route: '/courses/reports', icon: 'BarChart3', permission: read('courses.sales') },
81
- ],
82
- routes: [
83
- // Static segments outscore the `:id` param in the saas router, so these
84
- // resolve before the editor route (AdminShell sorts by routeScore).
85
- { path: '/courses', component: asComp(CoursesListPage), permission: read('courses') },
86
- { path: '/courses/members', component: asComp(MembersAreaPage), permission: read('courses') },
87
- { path: '/courses/sales', component: asComp(SalesPage), permission: read('courses.sales') },
88
- { path: '/courses/subscriptions', component: asComp(SubscriptionsPage), permission: read('courses.sales') },
89
- { path: '/courses/financial', component: asComp(FinancialPage), permission: read('courses.finance') },
90
- { path: '/courses/reports', component: asComp(ReportsPage), permission: read('courses.sales') },
91
- { path: '/courses/:id', component: asComp(CourseEditorPage) },
92
- ],
116
+ navigation,
117
+ routes,
93
118
  widgets: [],
94
- dashboardWidgets: createCoursesDashboardWidgets(),
119
+ // Commerce KPIs on the shared 'home' surface follow the same opt-in rule:
120
+ // no module enabled → no widgets broadcast into a host's dashboard.
121
+ dashboardWidgets: createCoursesDashboardWidgets(modules),
95
122
  connectors: [createStripeConnector()],
96
123
  settings: [
97
124
  { id: 'courses', label: 'Courses', icon: 'BookOpen', component: asComp(CoursesSettingsTab), order: 20, permission: read('courses') },