@hed-hog/lms 0.0.305 → 0.0.306

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.
@@ -1,317 +1,317 @@
1
- 'use client';
2
-
3
- import { Page, PageHeader } from '@/components/entity-list';
4
- import { Badge } from '@/components/ui/badge';
5
- import { Button } from '@/components/ui/button';
6
- import { Card, CardContent } from '@/components/ui/card';
7
- import { KpiCardsGrid, type KpiCardItem } from '@/components/ui/kpi-cards-grid';
8
- import { Separator } from '@/components/ui/separator';
9
- import { Skeleton } from '@/components/ui/skeleton';
10
- import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
11
- import { formatDate } from '@/lib/format-date';
12
- import { useApp, useQuery } from '@hed-hog/next-app-provider';
13
- import {
14
- BookOpen,
15
- Building2,
16
- CalendarDays,
17
- ExternalLink,
18
- Link2,
19
- Pencil,
20
- UserCheck,
21
- Users,
22
- } from 'lucide-react';
23
- import { notFound, useParams, useRouter } from 'next/navigation';
24
- import { useState } from 'react';
25
- import { AdministratorsTab } from '../_components/enterprise-administrators-tab';
26
- import { ClassesTab } from '../_components/enterprise-classes-tab';
27
- import { CoursesTab } from '../_components/enterprise-courses-tab';
28
- import { EnterpriseSheet } from '../_components/enterprise-sheet';
29
- import { StudentsTab } from '../_components/enterprise-students-tab';
30
- import type {
31
- EnterpriseAccount,
32
- EnterpriseStatus,
33
- } from '../_components/enterprise-types';
34
-
35
- // ── Constants ─────────────────────────────────────────────────────────────────
36
-
37
- const STATUS_VARIANT: Record<
38
- EnterpriseStatus,
39
- 'default' | 'secondary' | 'outline' | 'destructive'
40
- > = {
41
- active: 'default',
42
- trial: 'secondary',
43
- inactive: 'outline',
44
- suspended: 'destructive',
45
- };
46
-
47
- const STATUS_LABEL: Record<EnterpriseStatus, string> = {
48
- active: 'Active',
49
- trial: 'Trial',
50
- inactive: 'Inactive',
51
- suspended: 'Suspended',
52
- };
53
-
54
- // ── CRM Tab ───────────────────────────────────────────────────────────────────
55
-
56
- function CrmTab({
57
- crmAccountId,
58
- crmAccountName,
59
- onLinkCrm,
60
- }: {
61
- crmAccountId: number | null;
62
- crmAccountName: string | null;
63
- onLinkCrm?: () => void;
64
- }) {
65
- const router = useRouter();
66
- if (!crmAccountId || !crmAccountName) {
67
- return (
68
- <div className="flex flex-col items-center justify-center py-20 text-center">
69
- <div className="mb-4 text-muted-foreground/30">
70
- <Link2 className="h-12 w-12" />
71
- </div>
72
- <p className="text-sm font-medium text-muted-foreground">
73
- No CRM account linked
74
- </p>
75
- <p className="mt-1 max-w-xs text-xs text-muted-foreground/60">
76
- This enterprise account is not yet associated with a CRM commercial
77
- account. Link one to keep both records in sync.
78
- </p>
79
- <Button
80
- variant="outline"
81
- size="sm"
82
- className="mt-6"
83
- onClick={onLinkCrm}
84
- >
85
- <Link2 className="mr-2 h-4 w-4" />
86
- Link CRM account
87
- </Button>
88
- </div>
89
- );
90
- }
91
-
92
- return (
93
- <Card className="border-border/60">
94
- <CardContent className="p-6">
95
- <div className="flex items-start justify-between gap-4">
96
- <div className="flex items-center gap-4">
97
- <div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl border bg-muted/40">
98
- <Building2 className="h-6 w-6 text-muted-foreground" />
99
- </div>
100
- <div>
101
- <p className="text-base font-semibold">{crmAccountName}</p>
102
- <p className="text-xs text-muted-foreground">
103
- CRM Account #{crmAccountId}
104
- </p>
105
- </div>
106
- </div>
107
- <Button
108
- variant="outline"
109
- size="sm"
110
- onClick={() => router.push('/contact/accounts')}
111
- className="shrink-0"
112
- >
113
- <ExternalLink className="mr-2 h-4 w-4" />
114
- Open in CRM
115
- </Button>
116
- </div>
117
-
118
- <Separator className="my-5" />
119
-
120
- <dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">
121
- <div>
122
- <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
123
- Account name
124
- </dt>
125
- <dd className="mt-1 text-sm">{crmAccountName}</dd>
126
- </div>
127
- <div>
128
- <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
129
- CRM ID
130
- </dt>
131
- <dd className="mt-1 font-mono text-sm">#{crmAccountId}</dd>
132
- </div>
133
- </dl>
134
-
135
- <p className="mt-5 text-xs text-muted-foreground/60">
136
- Data is read from the CRM. Use{' '}
137
- <span className="font-medium">Open in CRM</span> to manage the
138
- commercial account directly.
139
- </p>
140
- </CardContent>
141
- </Card>
142
- );
143
- }
144
-
145
- // ── Overview Tab ──────────────────────────────────────────────────────────────
146
-
147
- function OverviewTab({ account }: { account: EnterpriseAccount }) {
148
- const { currentLocaleCode, getSettingValue } = useApp();
149
-
150
- const kpiItems: KpiCardItem[] = [
151
- {
152
- key: 'users',
153
- title: 'Users',
154
- value: account.usersCount,
155
- description: 'Total linked users',
156
- icon: Users,
157
- },
158
- {
159
- key: 'students',
160
- title: 'Students',
161
- value: account.studentsCount,
162
- description: 'Active learners',
163
- icon: UserCheck,
164
- },
165
- {
166
- key: 'classes',
167
- title: 'Classes',
168
- value: account.classesCount,
169
- description: 'Contracted classes',
170
- icon: CalendarDays,
171
- },
172
- {
173
- key: 'courses',
174
- title: 'Courses',
175
- value: account.coursesCount,
176
- description: 'Available courses',
177
- icon: BookOpen,
178
- },
179
- ];
180
-
181
- return (
182
- <div className="space-y-4">
183
- <KpiCardsGrid items={kpiItems} />
184
- <div className="flex flex-wrap items-center gap-x-6 gap-y-1 border-t pt-3 text-xs text-muted-foreground">
185
- <span>
186
- Slug:{' '}
187
- <span className="font-mono text-foreground/70">{account.slug}</span>
188
- </span>
189
- <span>
190
- Created:{' '}
191
- {formatDate(account.createdAt, getSettingValue, currentLocaleCode)}
192
- </span>
193
- <span>
194
- Updated:{' '}
195
- {formatDate(account.updatedAt, getSettingValue, currentLocaleCode)}
196
- </span>
197
- {account.portalEnabled && (
198
- <span className="text-primary/70">Portal enabled</span>
199
- )}
200
- </div>
201
- </div>
202
- );
203
- }
204
-
205
- // ── Page ──────────────────────────────────────────────────────────────────────
206
-
207
- export default function EnterpriseDetailPage() {
208
- const { id } = useParams<{ id: string }>();
209
- const { request } = useApp();
210
- const [isSheetOpen, setIsSheetOpen] = useState(false);
211
-
212
- const {
213
- data: account,
214
- isLoading,
215
- isError,
216
- refetch,
217
- } = useQuery<EnterpriseAccount>({
218
- queryKey: ['lms-enterprise-detail', id],
219
- queryFn: async () => {
220
- const res = await request<EnterpriseAccount>({
221
- url: `/lms/enterprise/${id}`,
222
- method: 'GET',
223
- });
224
- return (res as any).data ?? res;
225
- },
226
- });
227
-
228
- if (!isLoading && (isError || !account)) {
229
- notFound();
230
- }
231
-
232
- return (
233
- <Page>
234
- <PageHeader
235
- breadcrumbs={[
236
- { label: 'Home', href: '/' },
237
- { label: 'LMS', href: '/lms' },
238
- { label: 'Enterprise', href: '/lms/enterprise' },
239
- {
240
- label: isLoading ? '…' : (account?.name ?? ''),
241
- },
242
- ]}
243
- extraContent={
244
- account ? (
245
- <Badge variant={STATUS_VARIANT[account.status]}>
246
- {STATUS_LABEL[account.status]}
247
- </Badge>
248
- ) : (
249
- <Skeleton className="h-5 w-16 rounded-full" />
250
- )
251
- }
252
- actions={[
253
- {
254
- label: 'Edit',
255
- onClick: () => setIsSheetOpen(true),
256
- icon: <Pencil className="h-4 w-4" />,
257
- variant: 'outline',
258
- disabled: isLoading || !account,
259
- },
260
- ]}
261
- />
262
-
263
- {isLoading || !account ? (
264
- <div className="space-y-3 pt-4">
265
- <Skeleton className="h-8 w-64" />
266
- <Skeleton className="h-32 w-full" />
267
- </div>
268
- ) : (
269
- <Tabs defaultValue="overview">
270
- <TabsList>
271
- <TabsTrigger value="overview">Overview</TabsTrigger>
272
- <TabsTrigger value="crm">CRM</TabsTrigger>
273
- <TabsTrigger value="classes">Classes</TabsTrigger>
274
- <TabsTrigger value="courses">Courses</TabsTrigger>
275
- <TabsTrigger value="students">Students</TabsTrigger>
276
- <TabsTrigger value="administrators">Administrators</TabsTrigger>
277
- </TabsList>
278
-
279
- <TabsContent value="overview" className="mt-6">
280
- <OverviewTab account={account} />
281
- </TabsContent>
282
-
283
- <TabsContent value="crm" className="mt-4">
284
- <CrmTab
285
- crmAccountId={account.crmAccountId}
286
- crmAccountName={account.crmAccountName}
287
- onLinkCrm={() => setIsSheetOpen(true)}
288
- />
289
- </TabsContent>
290
-
291
- <TabsContent value="classes" className="mt-4">
292
- <ClassesTab enterpriseId={account.id} />
293
- </TabsContent>
294
-
295
- <TabsContent value="courses" className="mt-4">
296
- <CoursesTab enterpriseId={account.id} />
297
- </TabsContent>
298
-
299
- <TabsContent value="students" className="mt-4">
300
- <StudentsTab enterpriseId={account.id} />
301
- </TabsContent>
302
-
303
- <TabsContent value="administrators" className="mt-4">
304
- <AdministratorsTab enterpriseId={account.id} />
305
- </TabsContent>
306
- </Tabs>
307
- )}
308
-
309
- <EnterpriseSheet
310
- open={isSheetOpen}
311
- onOpenChange={setIsSheetOpen}
312
- editingAccount={account ?? null}
313
- onSave={() => void refetch()}
314
- />
315
- </Page>
316
- );
317
- }
1
+ 'use client';
2
+
3
+ import { Page, PageHeader } from '@/components/entity-list';
4
+ import { Badge } from '@/components/ui/badge';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Card, CardContent } from '@/components/ui/card';
7
+ import { KpiCardsGrid, type KpiCardItem } from '@/components/ui/kpi-cards-grid';
8
+ import { Separator } from '@/components/ui/separator';
9
+ import { Skeleton } from '@/components/ui/skeleton';
10
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
11
+ import { formatDate } from '@/lib/format-date';
12
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
13
+ import {
14
+ BookOpen,
15
+ Building2,
16
+ CalendarDays,
17
+ ExternalLink,
18
+ Link2,
19
+ Pencil,
20
+ UserCheck,
21
+ Users,
22
+ } from 'lucide-react';
23
+ import { notFound, useParams, useRouter } from 'next/navigation';
24
+ import { useState } from 'react';
25
+ import { AdministratorsTab } from '../_components/enterprise-administrators-tab';
26
+ import { ClassesTab } from '../_components/enterprise-classes-tab';
27
+ import { CoursesTab } from '../_components/enterprise-courses-tab';
28
+ import { EnterpriseSheet } from '../_components/enterprise-sheet';
29
+ import { StudentsTab } from '../_components/enterprise-students-tab';
30
+ import type {
31
+ EnterpriseAccount,
32
+ EnterpriseStatus,
33
+ } from '../_components/enterprise-types';
34
+
35
+ // ── Constants ─────────────────────────────────────────────────────────────────
36
+
37
+ const STATUS_VARIANT: Record<
38
+ EnterpriseStatus,
39
+ 'default' | 'secondary' | 'outline' | 'destructive'
40
+ > = {
41
+ active: 'default',
42
+ trial: 'secondary',
43
+ inactive: 'outline',
44
+ suspended: 'destructive',
45
+ };
46
+
47
+ const STATUS_LABEL: Record<EnterpriseStatus, string> = {
48
+ active: 'Active',
49
+ trial: 'Trial',
50
+ inactive: 'Inactive',
51
+ suspended: 'Suspended',
52
+ };
53
+
54
+ // ── CRM Tab ───────────────────────────────────────────────────────────────────
55
+
56
+ function CrmTab({
57
+ crmAccountId,
58
+ crmAccountName,
59
+ onLinkCrm,
60
+ }: {
61
+ crmAccountId: number | null;
62
+ crmAccountName: string | null;
63
+ onLinkCrm?: () => void;
64
+ }) {
65
+ const router = useRouter();
66
+ if (!crmAccountId || !crmAccountName) {
67
+ return (
68
+ <div className="flex flex-col items-center justify-center py-20 text-center">
69
+ <div className="mb-4 text-muted-foreground/30">
70
+ <Link2 className="h-12 w-12" />
71
+ </div>
72
+ <p className="text-sm font-medium text-muted-foreground">
73
+ No CRM account linked
74
+ </p>
75
+ <p className="mt-1 max-w-xs text-xs text-muted-foreground/60">
76
+ This enterprise account is not yet associated with a CRM commercial
77
+ account. Link one to keep both records in sync.
78
+ </p>
79
+ <Button
80
+ variant="outline"
81
+ size="sm"
82
+ className="mt-6"
83
+ onClick={onLinkCrm}
84
+ >
85
+ <Link2 className="mr-2 h-4 w-4" />
86
+ Link CRM account
87
+ </Button>
88
+ </div>
89
+ );
90
+ }
91
+
92
+ return (
93
+ <Card className="border-border/60">
94
+ <CardContent className="p-6">
95
+ <div className="flex items-start justify-between gap-4">
96
+ <div className="flex items-center gap-4">
97
+ <div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl border bg-muted/40">
98
+ <Building2 className="h-6 w-6 text-muted-foreground" />
99
+ </div>
100
+ <div>
101
+ <p className="text-base font-semibold">{crmAccountName}</p>
102
+ <p className="text-xs text-muted-foreground">
103
+ CRM Account #{crmAccountId}
104
+ </p>
105
+ </div>
106
+ </div>
107
+ <Button
108
+ variant="outline"
109
+ size="sm"
110
+ onClick={() => router.push('/contact/accounts')}
111
+ className="shrink-0"
112
+ >
113
+ <ExternalLink className="mr-2 h-4 w-4" />
114
+ Open in CRM
115
+ </Button>
116
+ </div>
117
+
118
+ <Separator className="my-5" />
119
+
120
+ <dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">
121
+ <div>
122
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
123
+ Account name
124
+ </dt>
125
+ <dd className="mt-1 text-sm">{crmAccountName}</dd>
126
+ </div>
127
+ <div>
128
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
129
+ CRM ID
130
+ </dt>
131
+ <dd className="mt-1 font-mono text-sm">#{crmAccountId}</dd>
132
+ </div>
133
+ </dl>
134
+
135
+ <p className="mt-5 text-xs text-muted-foreground/60">
136
+ Data is read from the CRM. Use{' '}
137
+ <span className="font-medium">Open in CRM</span> to manage the
138
+ commercial account directly.
139
+ </p>
140
+ </CardContent>
141
+ </Card>
142
+ );
143
+ }
144
+
145
+ // ── Overview Tab ──────────────────────────────────────────────────────────────
146
+
147
+ function OverviewTab({ account }: { account: EnterpriseAccount }) {
148
+ const { currentLocaleCode, getSettingValue } = useApp();
149
+
150
+ const kpiItems: KpiCardItem[] = [
151
+ {
152
+ key: 'users',
153
+ title: 'Users',
154
+ value: account.usersCount,
155
+ description: 'Total linked users',
156
+ icon: Users,
157
+ },
158
+ {
159
+ key: 'students',
160
+ title: 'Students',
161
+ value: account.studentsCount,
162
+ description: 'Active learners',
163
+ icon: UserCheck,
164
+ },
165
+ {
166
+ key: 'classes',
167
+ title: 'Classes',
168
+ value: account.classesCount,
169
+ description: 'Contracted classes',
170
+ icon: CalendarDays,
171
+ },
172
+ {
173
+ key: 'courses',
174
+ title: 'Courses',
175
+ value: account.coursesCount,
176
+ description: 'Available courses',
177
+ icon: BookOpen,
178
+ },
179
+ ];
180
+
181
+ return (
182
+ <div className="space-y-4">
183
+ <KpiCardsGrid items={kpiItems} />
184
+ <div className="flex flex-wrap items-center gap-x-6 gap-y-1 border-t pt-3 text-xs text-muted-foreground">
185
+ <span>
186
+ Slug:{' '}
187
+ <span className="font-mono text-foreground/70">{account.slug}</span>
188
+ </span>
189
+ <span>
190
+ Created:{' '}
191
+ {formatDate(account.createdAt, getSettingValue, currentLocaleCode)}
192
+ </span>
193
+ <span>
194
+ Updated:{' '}
195
+ {formatDate(account.updatedAt, getSettingValue, currentLocaleCode)}
196
+ </span>
197
+ {account.portalEnabled && (
198
+ <span className="text-primary/70">Portal enabled</span>
199
+ )}
200
+ </div>
201
+ </div>
202
+ );
203
+ }
204
+
205
+ // ── Page ──────────────────────────────────────────────────────────────────────
206
+
207
+ export default function EnterpriseDetailPage() {
208
+ const { id } = useParams<{ id: string }>();
209
+ const { request } = useApp();
210
+ const [isSheetOpen, setIsSheetOpen] = useState(false);
211
+
212
+ const {
213
+ data: account,
214
+ isLoading,
215
+ isError,
216
+ refetch,
217
+ } = useQuery<EnterpriseAccount>({
218
+ queryKey: ['lms-enterprise-detail', id],
219
+ queryFn: async () => {
220
+ const res = await request<EnterpriseAccount>({
221
+ url: `/lms/enterprise/${id}`,
222
+ method: 'GET',
223
+ });
224
+ return (res as any).data ?? res;
225
+ },
226
+ });
227
+
228
+ if (!isLoading && (isError || !account)) {
229
+ notFound();
230
+ }
231
+
232
+ return (
233
+ <Page>
234
+ <PageHeader
235
+ breadcrumbs={[
236
+ { label: 'Home', href: '/' },
237
+ { label: 'LMS', href: '/lms' },
238
+ { label: 'Enterprise', href: '/lms/enterprise' },
239
+ {
240
+ label: isLoading ? '…' : (account?.name ?? ''),
241
+ },
242
+ ]}
243
+ extraContent={
244
+ account ? (
245
+ <Badge variant={STATUS_VARIANT[account.status]}>
246
+ {STATUS_LABEL[account.status]}
247
+ </Badge>
248
+ ) : (
249
+ <Skeleton className="h-5 w-16 rounded-full" />
250
+ )
251
+ }
252
+ actions={[
253
+ {
254
+ label: 'Edit',
255
+ onClick: () => setIsSheetOpen(true),
256
+ icon: <Pencil className="h-4 w-4" />,
257
+ variant: 'outline',
258
+ disabled: isLoading || !account,
259
+ },
260
+ ]}
261
+ />
262
+
263
+ {isLoading || !account ? (
264
+ <div className="space-y-3 pt-4">
265
+ <Skeleton className="h-8 w-64" />
266
+ <Skeleton className="h-32 w-full" />
267
+ </div>
268
+ ) : (
269
+ <Tabs defaultValue="overview">
270
+ <TabsList>
271
+ <TabsTrigger value="overview">Overview</TabsTrigger>
272
+ <TabsTrigger value="crm">CRM</TabsTrigger>
273
+ <TabsTrigger value="classes">Classes</TabsTrigger>
274
+ <TabsTrigger value="courses">Courses</TabsTrigger>
275
+ <TabsTrigger value="students">Students</TabsTrigger>
276
+ <TabsTrigger value="administrators">Administrators</TabsTrigger>
277
+ </TabsList>
278
+
279
+ <TabsContent value="overview" className="mt-6">
280
+ <OverviewTab account={account} />
281
+ </TabsContent>
282
+
283
+ <TabsContent value="crm" className="mt-4">
284
+ <CrmTab
285
+ crmAccountId={account.crmAccountId}
286
+ crmAccountName={account.crmAccountName}
287
+ onLinkCrm={() => setIsSheetOpen(true)}
288
+ />
289
+ </TabsContent>
290
+
291
+ <TabsContent value="classes" className="mt-4">
292
+ <ClassesTab enterpriseId={account.id} />
293
+ </TabsContent>
294
+
295
+ <TabsContent value="courses" className="mt-4">
296
+ <CoursesTab enterpriseId={account.id} />
297
+ </TabsContent>
298
+
299
+ <TabsContent value="students" className="mt-4">
300
+ <StudentsTab enterpriseId={account.id} />
301
+ </TabsContent>
302
+
303
+ <TabsContent value="administrators" className="mt-4">
304
+ <AdministratorsTab enterpriseId={account.id} />
305
+ </TabsContent>
306
+ </Tabs>
307
+ )}
308
+
309
+ <EnterpriseSheet
310
+ open={isSheetOpen}
311
+ onOpenChange={setIsSheetOpen}
312
+ editingAccount={account ?? null}
313
+ onSave={() => void refetch()}
314
+ />
315
+ </Page>
316
+ );
317
+ }
@@ -41,6 +41,7 @@ export function EnterpriseClassCreateSheet({
41
41
  startDate: cls.startDate,
42
42
  endDate: cls.endDate ?? null,
43
43
  capacity: cls.capacity,
44
+ enrolledCount: 0,
44
45
  status: cls.status,
45
46
  deliveryMode: cls.deliveryMode,
46
47
  });