@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.
- package/dist/course/course.service.js +4 -4
- package/hedhog/data/dashboard_component.yaml +152 -152
- package/hedhog/data/dashboard_item.yaml +166 -166
- package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +317 -317
- package/hedhog/frontend/app/enterprise/_components/enterprise-class-create-sheet.tsx.ejs +1 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +12 -1
- package/hedhog/frontend/app/enterprise/_components/enterprise-related-tab.tsx.ejs +44 -7
- package/hedhog/frontend/app/enterprise/_components/enterprise-types.ts.ejs +96 -96
- package/hedhog/frontend/app/page.tsx.ejs +5 -5
- package/hedhog/table/course.yaml +15 -15
- package/package.json +7 -7
- package/src/class-group/class-group.service.ts +413 -413
- package/src/class-group/dto/create-class-group.dto.ts +77 -77
- package/src/course/course.service.ts +165 -165
- package/src/course/dto/create-course.dto.ts +15 -15
- package/dist/enterprise/dto/add-enterprise-lead.dto.d.ts +0 -4
- package/dist/enterprise/dto/add-enterprise-lead.dto.d.ts.map +0 -1
- package/dist/enterprise/dto/add-enterprise-lead.dto.js +0 -22
- package/dist/enterprise/dto/add-enterprise-lead.dto.js.map +0 -1
|
@@ -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
|
+
}
|