@hed-hog/lms 0.0.331 → 0.0.347

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 (135) hide show
  1. package/dist/class-group/class-group.controller.d.ts +8 -8
  2. package/dist/class-group/class-group.service.d.ts +8 -8
  3. package/dist/course/course.controller.d.ts +6 -1
  4. package/dist/course/course.controller.d.ts.map +1 -1
  5. package/dist/course/course.controller.js +19 -2
  6. package/dist/course/course.controller.js.map +1 -1
  7. package/dist/course/course.service.d.ts +6 -0
  8. package/dist/course/course.service.d.ts.map +1 -1
  9. package/dist/course/course.service.js +63 -28
  10. package/dist/course/course.service.js.map +1 -1
  11. package/dist/course/dto/create-course.dto.d.ts +1 -0
  12. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  13. package/dist/course/dto/create-course.dto.js +5 -0
  14. package/dist/course/dto/create-course.dto.js.map +1 -1
  15. package/dist/enterprise/enterprise.controller.d.ts +84 -12
  16. package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
  17. package/dist/enterprise/enterprise.controller.js +10 -0
  18. package/dist/enterprise/enterprise.controller.js.map +1 -1
  19. package/dist/enterprise/enterprise.service.d.ts +90 -12
  20. package/dist/enterprise/enterprise.service.d.ts.map +1 -1
  21. package/dist/enterprise/enterprise.service.js +413 -40
  22. package/dist/enterprise/enterprise.service.js.map +1 -1
  23. package/dist/enterprise/training/training-admin.controller.d.ts +9 -6
  24. package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
  25. package/dist/enterprise/training/training-admin.controller.js +10 -6
  26. package/dist/enterprise/training/training-admin.controller.js.map +1 -1
  27. package/dist/enterprise/training/training-admin.service.d.ts +11 -5
  28. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
  29. package/dist/enterprise/training/training-admin.service.js +108 -52
  30. package/dist/enterprise/training/training-admin.service.js.map +1 -1
  31. package/dist/enterprise/training/training-viewer.controller.d.ts +3 -0
  32. package/dist/enterprise/training/training-viewer.controller.d.ts.map +1 -1
  33. package/dist/evaluation/evaluation.controller.d.ts +2 -2
  34. package/dist/evaluation/evaluation.service.d.ts +2 -2
  35. package/dist/instructor/dto/create-instructor-skill.dto.d.ts +0 -4
  36. package/dist/instructor/dto/create-instructor-skill.dto.d.ts.map +1 -1
  37. package/dist/instructor/dto/create-instructor-skill.dto.js +0 -21
  38. package/dist/instructor/dto/create-instructor-skill.dto.js.map +1 -1
  39. package/dist/instructor/dto/update-instructor-skill.dto.d.ts +0 -4
  40. package/dist/instructor/dto/update-instructor-skill.dto.d.ts.map +1 -1
  41. package/dist/instructor/dto/update-instructor-skill.dto.js +0 -22
  42. package/dist/instructor/dto/update-instructor-skill.dto.js.map +1 -1
  43. package/dist/instructor/instructor-skill.controller.d.ts +4 -4
  44. package/dist/instructor/instructor-skill.service.d.ts +4 -7
  45. package/dist/instructor/instructor-skill.service.d.ts.map +1 -1
  46. package/dist/instructor/instructor-skill.service.js +2 -89
  47. package/dist/instructor/instructor-skill.service.js.map +1 -1
  48. package/dist/instructor/instructor.controller.d.ts +21 -0
  49. package/dist/instructor/instructor.controller.d.ts.map +1 -1
  50. package/dist/instructor/instructor.controller.js +19 -0
  51. package/dist/instructor/instructor.controller.js.map +1 -1
  52. package/dist/instructor/instructor.service.d.ts +27 -0
  53. package/dist/instructor/instructor.service.d.ts.map +1 -1
  54. package/dist/instructor/instructor.service.js +79 -25
  55. package/dist/instructor/instructor.service.js.map +1 -1
  56. package/dist/lms.module.d.ts.map +1 -1
  57. package/dist/lms.module.js.map +1 -1
  58. package/dist/training/dto/create-training.dto.d.ts +1 -0
  59. package/dist/training/dto/create-training.dto.d.ts.map +1 -1
  60. package/dist/training/dto/create-training.dto.js +5 -0
  61. package/dist/training/dto/create-training.dto.js.map +1 -1
  62. package/dist/training/training.controller.d.ts +4 -0
  63. package/dist/training/training.controller.d.ts.map +1 -1
  64. package/dist/training/training.service.d.ts +8 -0
  65. package/dist/training/training.service.d.ts.map +1 -1
  66. package/dist/training/training.service.js +71 -6
  67. package/dist/training/training.service.js.map +1 -1
  68. package/hedhog/data/route.yaml +23 -1
  69. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +80 -33
  70. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +3 -3
  71. package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +591 -0
  72. package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +6 -1
  73. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +39 -7
  74. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +1 -3
  75. package/hedhog/frontend/app/classes/page.tsx.ejs +34 -7
  76. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +3 -33
  77. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +9 -9
  78. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +109 -0
  79. package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +40 -13
  80. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +76 -81
  81. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +60 -0
  82. package/hedhog/frontend/app/courses/[id]/structure/_components/course-scheduled-classes-tab.tsx.ejs +406 -0
  83. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
  84. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
  85. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
  86. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
  87. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +243 -34
  88. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -0
  89. package/hedhog/frontend/app/courses/page.tsx.ejs +6 -1
  90. package/hedhog/frontend/app/enterprise/_components/enterprise-activity-timeline.tsx.ejs +87 -0
  91. package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +4 -0
  92. package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +31 -5
  93. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +79 -20
  94. package/hedhog/frontend/app/enterprise/_components/enterprise-company-identity-card.tsx.ejs +11 -2
  95. package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +201 -0
  96. package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +55 -24
  97. package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +430 -296
  98. package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +277 -0
  99. package/hedhog/frontend/app/enterprise/_components/enterprise-overview-analytics.tsx.ejs +205 -0
  100. package/hedhog/frontend/app/enterprise/_components/enterprise-person-edit-sheet.tsx.ejs +97 -0
  101. package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +82 -57
  102. package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +4 -0
  103. package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +60 -22
  104. package/hedhog/frontend/app/enterprise/_components/enterprise-types.ts.ejs +54 -0
  105. package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +211 -0
  106. package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -7
  107. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +31 -19
  108. package/hedhog/frontend/app/exams/page.tsx.ejs +6 -1
  109. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +51 -104
  110. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +625 -366
  111. package/hedhog/frontend/app/instructors/page.tsx.ejs +6 -1
  112. package/hedhog/frontend/app/paths/page.tsx.ejs +76 -8
  113. package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +6 -1
  114. package/hedhog/frontend/app/training/page.tsx.ejs +78 -9
  115. package/hedhog/frontend/messages/en.json +101 -10
  116. package/hedhog/frontend/messages/pt.json +115 -11
  117. package/hedhog/table/enterprise_student_license_event.yaml +30 -0
  118. package/hedhog/table/instructor_skill.yaml +0 -11
  119. package/hedhog/table/learning_path.yaml +4 -0
  120. package/package.json +6 -6
  121. package/src/course/course.controller.ts +18 -0
  122. package/src/course/course.service.ts +85 -26
  123. package/src/course/dto/create-course.dto.ts +4 -0
  124. package/src/enterprise/enterprise.controller.ts +5 -0
  125. package/src/enterprise/enterprise.service.ts +507 -29
  126. package/src/enterprise/training/training-admin.controller.ts +4 -0
  127. package/src/enterprise/training/training-admin.service.ts +115 -51
  128. package/src/instructor/dto/create-instructor-skill.dto.ts +0 -17
  129. package/src/instructor/dto/update-instructor-skill.dto.ts +0 -18
  130. package/src/instructor/instructor-skill.service.ts +2 -97
  131. package/src/instructor/instructor.controller.ts +16 -0
  132. package/src/instructor/instructor.service.ts +87 -10
  133. package/src/lms.module.ts +1 -0
  134. package/src/training/dto/create-training.dto.ts +4 -0
  135. package/src/training/training.service.ts +104 -5
@@ -1,34 +1,55 @@
1
1
  'use client';
2
2
 
3
+ import {
4
+ AlertDialog,
5
+ AlertDialogCancel,
6
+ AlertDialogContent,
7
+ AlertDialogDescription,
8
+ AlertDialogFooter,
9
+ AlertDialogHeader,
10
+ AlertDialogTitle,
11
+ } from '@/components/ui/alert-dialog';
12
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
3
13
  import { Badge } from '@/components/ui/badge';
4
14
  import { Button } from '@/components/ui/button';
5
15
  import { Card, CardContent } from '@/components/ui/card';
6
- import { EntityPicker } from '@/components/ui/entity-picker';
16
+ import { Input } from '@/components/ui/input';
7
17
  import { KpiCardsGrid, type KpiCardItem } from '@/components/ui/kpi-cards-grid';
8
- import { Separator } from '@/components/ui/separator';
9
18
  import {
10
19
  Sheet,
11
20
  SheetContent,
21
+ SheetDescription,
12
22
  SheetHeader,
13
23
  SheetTitle,
14
24
  } from '@/components/ui/sheet';
15
25
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
16
26
  import { useApp } from '@hed-hog/next-app-provider';
17
27
  import {
28
+ AlertTriangle,
18
29
  BookOpen,
19
- Building2,
20
30
  CalendarDays,
21
- ExternalLink,
22
- Globe,
23
- Link2,
31
+ LineChart as LineChartIcon,
24
32
  Pencil,
25
- RefreshCw,
33
+ Trash2,
26
34
  UserCheck,
35
+ UserRound,
27
36
  } from 'lucide-react';
28
37
  import { useTranslations } from 'next-intl';
29
- import { useRouter } from 'next/navigation';
30
- import { useEffect, useState } from 'react';
31
- import { ActivityPanel } from './enterprise-activity-panel';
38
+ import { useEffect, useRef, useState } from 'react';
39
+ import {
40
+ Bar,
41
+ BarChart,
42
+ CartesianGrid,
43
+ Cell,
44
+ Line,
45
+ LineChart,
46
+ Pie,
47
+ PieChart,
48
+ ResponsiveContainer,
49
+ Tooltip,
50
+ XAxis,
51
+ YAxis,
52
+ } from 'recharts';
32
53
  import { AdministratorsTab } from './enterprise-administrators-tab';
33
54
  import { ClassesTab } from './enterprise-classes-tab';
34
55
  import { CompanyIdentityCard } from './enterprise-company-identity-card';
@@ -36,8 +57,7 @@ import { CoursesTab } from './enterprise-courses-tab';
36
57
  import { STATUS_LABEL, STATUS_VARIANT } from './enterprise-detail-constants';
37
58
  import { EnterpriseSheet, type EnterpriseFormValues } from './enterprise-sheet';
38
59
  import { StudentsTab } from './enterprise-students-tab';
39
- import type { EnterpriseAccount } from './enterprise-types';
40
- import { UserDistributionChart } from './enterprise-user-distribution-chart';
60
+ import type { EnterpriseAccount, EnterpriseOverview } from './enterprise-types';
41
61
 
42
62
  // ── Props ─────────────────────────────────────────────────────────────────────
43
63
 
@@ -58,26 +78,25 @@ export function EnterpriseDetailSheet({
58
78
  onAccountUpdate,
59
79
  onDataChange,
60
80
  }: EnterpriseDetailSheetProps) {
61
- // Local mirror so saves refresh the sheet without closing it
62
81
  const [currentAccount, setCurrentAccount] =
63
82
  useState<EnterpriseAccount | null>(account);
83
+ const [overview, setOverview] = useState<EnterpriseOverview | null>(null);
64
84
  const [editSheetOpen, setEditSheetOpen] = useState(false);
65
- const [companyPickerOpen, setCompanyPickerOpen] = useState(false);
66
- const [companyPickerValue, setCompanyPickerValue] = useState<
67
- string | number | null
68
- >(null);
69
- const [savingCompany, setSavingCompany] = useState(false);
85
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
86
+ const [deleteConfirmText, setDeleteConfirmText] = useState('');
87
+ const [isDeleting, setIsDeleting] = useState(false);
88
+ const onAccountUpdateRef = useRef(onAccountUpdate);
70
89
  const { request } = useApp();
71
- const router = useRouter();
72
90
  const t = useTranslations('lms.EnterpriseDetailPage');
73
91
 
74
- // Sync whenever the parent switches the account (e.g. clicking a different row)
92
+ useEffect(() => {
93
+ onAccountUpdateRef.current = onAccountUpdate;
94
+ }, [onAccountUpdate]);
95
+
75
96
  useEffect(() => {
76
97
  setCurrentAccount(account);
77
98
  }, [account]);
78
99
 
79
- // Refetch full account when sheet opens so studentsCount / managersCount are populated
80
- // (the list endpoint omits these counts; only getById computes them)
81
100
  useEffect(() => {
82
101
  if (!open || !account?.id) return;
83
102
  request<EnterpriseAccount>({
@@ -85,23 +104,49 @@ export function EnterpriseDetailSheet({
85
104
  method: 'GET',
86
105
  })
87
106
  .then((res) => {
88
- const refreshed: EnterpriseAccount = (res as any).data ?? res;
107
+ const refreshed: EnterpriseAccount =
108
+ (res as unknown as { data?: EnterpriseAccount }).data ??
109
+ (res as unknown as EnterpriseAccount);
89
110
  setCurrentAccount(refreshed);
90
111
  })
91
112
  .catch(() => {
92
113
  // keep the stale list-row data as fallback
93
114
  });
94
- // eslint-disable-next-line react-hooks/exhaustive-deps
95
- }, [open, account?.id]);
115
+ }, [open, account?.id, request]);
116
+
117
+ useEffect(() => {
118
+ if (!open || !account?.id) return;
119
+ request<EnterpriseOverview>({
120
+ url: `/lms/enterprise/${account.id}/overview`,
121
+ method: 'GET',
122
+ })
123
+ .then((res) => {
124
+ const data: EnterpriseOverview =
125
+ (res as unknown as { data?: EnterpriseOverview }).data ??
126
+ (res as unknown as EnterpriseOverview);
127
+ setOverview(data);
128
+ if (data?.account) {
129
+ setCurrentAccount(data.account);
130
+ onAccountUpdateRef.current?.(data.account);
131
+ }
132
+ })
133
+ .catch(() => {
134
+ setOverview(null);
135
+ });
136
+ }, [open, account?.id, request]);
96
137
 
97
138
  async function handleInternalSave(_values: EnterpriseFormValues) {
98
139
  if (!currentAccount) return;
99
140
  try {
100
- const res = await request<EnterpriseAccount>({
101
- url: `/lms/enterprise/${currentAccount.id}`,
141
+ const res = await request<EnterpriseOverview>({
142
+ url: `/lms/enterprise/${currentAccount.id}/overview`,
102
143
  method: 'GET',
103
144
  });
104
- const refreshed: EnterpriseAccount = (res as any).data ?? res;
145
+ const payload: EnterpriseOverview =
146
+ (res as unknown as { data?: EnterpriseOverview }).data ??
147
+ (res as unknown as EnterpriseOverview);
148
+ const refreshed = payload.account;
149
+ setOverview(payload);
105
150
  setCurrentAccount(refreshed);
106
151
  onAccountUpdate?.(refreshed);
107
152
  } catch {
@@ -128,11 +173,15 @@ export function EnterpriseDetailSheet({
128
173
  onDataChange?.();
129
174
  if (!currentAccount?.id) return;
130
175
  try {
131
- const res = await request<EnterpriseAccount>({
132
- url: `/lms/enterprise/${currentAccount.id}`,
176
+ const res = await request<EnterpriseOverview>({
177
+ url: `/lms/enterprise/${currentAccount.id}/overview`,
133
178
  method: 'GET',
134
179
  });
135
- const refreshed: EnterpriseAccount = (res as any).data ?? res;
180
+ const payload: EnterpriseOverview =
181
+ (res as unknown as { data?: EnterpriseOverview }).data ??
182
+ (res as unknown as EnterpriseOverview);
183
+ const refreshed = payload.account;
184
+ setOverview(payload);
136
185
  setCurrentAccount(refreshed);
137
186
  onAccountUpdate?.(refreshed);
138
187
  } catch {
@@ -140,31 +189,36 @@ export function EnterpriseDetailSheet({
140
189
  }
141
190
  }
142
191
 
143
- async function handleCompanySave(newPersonId: number) {
192
+ async function handleDeleteAccount() {
144
193
  if (!currentAccount?.id) return;
145
- setSavingCompany(true);
194
+ setIsDeleting(true);
146
195
  try {
147
196
  await request({
148
197
  url: `/lms/enterprise/${currentAccount.id}`,
149
- method: 'PATCH',
150
- data: { crm_person_id: newPersonId },
198
+ method: 'DELETE',
151
199
  });
152
- setCompanyPickerOpen(false);
153
- setCompanyPickerValue(null);
154
- await handleTabMutate();
155
- } catch {
156
- // toast handled by caller
200
+ setDeleteDialogOpen(false);
201
+ onOpenChange(false);
202
+ onDataChange?.();
157
203
  } finally {
158
- setSavingCompany(false);
204
+ setIsDeleting(false);
159
205
  }
160
206
  }
161
207
 
208
+ function getPersonAvatarUrl(avatarId?: number | null) {
209
+ return typeof avatarId === 'number' && avatarId > 0
210
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
211
+ : undefined;
212
+ }
213
+
214
+ const overviewKpis = overview?.kpis;
215
+
162
216
  const kpiItems: KpiCardItem[] = currentAccount
163
217
  ? [
164
218
  {
165
219
  key: 'students',
166
220
  title: t('kpis.students.label'),
167
- value: currentAccount.studentsCount,
221
+ value: overviewKpis?.students ?? currentAccount.studentsCount,
168
222
  description: t('kpis.students.description'),
169
223
  icon: UserCheck,
170
224
  accentClassName:
@@ -174,7 +228,7 @@ export function EnterpriseDetailSheet({
174
228
  {
175
229
  key: 'classes',
176
230
  title: t('kpis.contractedClasses.label'),
177
- value: currentAccount.classesCount,
231
+ value: overviewKpis?.classes ?? currentAccount.classesCount,
178
232
  description: t('kpis.contractedClasses.description'),
179
233
  icon: CalendarDays,
180
234
  accentClassName:
@@ -184,33 +238,94 @@ export function EnterpriseDetailSheet({
184
238
  {
185
239
  key: 'courses',
186
240
  title: t('kpis.courses.label'),
187
- value: currentAccount.coursesCount,
241
+ value: overviewKpis?.courses ?? currentAccount.coursesCount,
188
242
  description: t('kpis.courses.description'),
189
243
  icon: BookOpen,
190
244
  accentClassName: 'from-blue-500/20 via-cyan-500/10 to-transparent',
191
245
  iconContainerClassName: 'bg-blue-50 text-blue-600',
192
246
  },
247
+ {
248
+ key: 'administrators',
249
+ title: t('kpis.users.label'),
250
+ value:
251
+ overviewKpis?.administrators ??
252
+ currentAccount.adminsCount + currentAccount.managersCount,
253
+ description: t('kpis.users.description'),
254
+ icon: UserRound,
255
+ accentClassName: 'from-amber-500/20 via-orange-500/10 to-transparent',
256
+ iconContainerClassName: 'bg-amber-50 text-amber-600',
257
+ },
193
258
  ]
194
259
  : [];
195
260
 
261
+ const licenseUsageChart = [
262
+ {
263
+ name: 'Usadas',
264
+ value: overview?.licenseUsage.used ?? 0,
265
+ color: 'hsl(221 83% 53%)',
266
+ },
267
+ {
268
+ name: 'Disponiveis',
269
+ value: overview?.licenseUsage.available ?? 0,
270
+ color: 'hsl(160 84% 39%)',
271
+ },
272
+ ];
273
+
196
274
  return (
197
275
  <>
198
- {/* ── Detail Sheet ── */}
199
276
  <Sheet open={open} onOpenChange={onOpenChange}>
200
277
  <SheetContent
201
278
  side="right"
202
- className="flex w-full flex-col gap-0 overflow-y-auto p-0 sm:max-w-5xl"
279
+ className="flex w-full flex-col gap-0 overflow-y-auto p-0 sm:max-w-[95vw] lg:max-w-6xl"
203
280
  >
204
281
  {currentAccount && (
205
282
  <>
206
283
  <SheetHeader className="flex flex-row items-center justify-between gap-4 border-b px-6 py-4">
207
- <div className="flex items-center gap-3">
208
- <SheetTitle className="text-base">
209
- {currentAccount.name}
210
- </SheetTitle>
211
- <Badge variant={STATUS_VARIANT[currentAccount.status]}>
212
- {STATUS_LABEL[currentAccount.status]}
213
- </Badge>
284
+ <div className="flex min-w-0 items-center gap-3">
285
+ <Avatar className="h-10 w-10 border">
286
+ <AvatarImage
287
+ src={getPersonAvatarUrl(
288
+ currentAccount.crmAccount?.avatarId ?? null
289
+ )}
290
+ />
291
+ <AvatarFallback>
292
+ {currentAccount.name
293
+ .split(' ')
294
+ .filter(Boolean)
295
+ .slice(0, 2)
296
+ .map((part) => part[0]?.toUpperCase() ?? '')
297
+ .join('')}
298
+ </AvatarFallback>
299
+ </Avatar>
300
+ <div className="min-w-0">
301
+ <SheetTitle className="truncate text-base font-semibold">
302
+ {currentAccount.name}
303
+ </SheetTitle>
304
+ <div className="mt-0.5 flex items-center gap-2 text-xs text-muted-foreground">
305
+ <span className="font-mono">{currentAccount.slug}</span>
306
+ <Badge
307
+ variant={STATUS_VARIANT[currentAccount.status]}
308
+ className="text-[10px]"
309
+ >
310
+ {STATUS_LABEL[currentAccount.status]}
311
+ </Badge>
312
+ </div>
313
+ </div>
314
+ </div>
315
+ <SheetDescription className="sr-only">
316
+ {currentAccount.name} overview, metrics, classes, courses,
317
+ administrators, and students.
318
+ </SheetDescription>
319
+ <div className="flex items-center gap-2">
320
+ <Button
321
+ variant="outline"
322
+ size="sm"
323
+ onClick={() => setEditSheetOpen(true)}
324
+ className="shrink-0"
325
+ >
326
+ <Pencil className="mr-2 h-4 w-4" />
327
+ {t('actions.edit')}
328
+ </Button>
214
329
  </div>
215
330
  </SheetHeader>
216
331
 
@@ -221,9 +336,6 @@ export function EnterpriseDetailSheet({
221
336
  <TabsTrigger value="overview">
222
337
  {t('tabs.overview')}
223
338
  </TabsTrigger>
224
- <TabsTrigger value="empresa">
225
- {t('tabs.empresa')}
226
- </TabsTrigger>
227
339
  <TabsTrigger value="classes">
228
340
  {t('tabs.classes')}
229
341
  </TabsTrigger>
@@ -237,269 +349,236 @@ export function EnterpriseDetailSheet({
237
349
  {t('tabs.administrators')}
238
350
  </TabsTrigger>
239
351
  </TabsList>
240
- <Button
241
- variant="outline"
242
- size="sm"
243
- onClick={() => setEditSheetOpen(true)}
244
- className="shrink-0"
245
- >
246
- <Pencil className="mr-2 h-4 w-4" />
247
- {t('actions.edit')}
248
- </Button>
249
352
  </div>
250
353
 
251
- {/* ── Overview tab ── */}
252
354
  <TabsContent value="overview" className="mt-5 space-y-5">
253
- {/* Company identity card — click opens edit sheet */}
254
355
  <CompanyIdentityCard
255
356
  account={currentAccount}
256
357
  onEditClick={() => setEditSheetOpen(true)}
257
358
  />
258
359
 
259
- {/* KPI row */}
260
360
  <KpiCardsGrid items={kpiItems} />
261
361
 
262
- {/* Charts row */}
263
- <div className="grid grid-cols-1 gap-5 lg:grid-cols-2">
264
- <UserDistributionChart account={currentAccount} />
265
- <ActivityPanel enterpriseId={currentAccount.id} />
266
- </div>
267
- </TabsContent>
362
+ <div className="grid grid-cols-1 gap-5 lg:grid-cols-3">
363
+ <Card className="border-border/60 lg:col-span-2">
364
+ <CardContent className="p-4">
365
+ <div className="mb-2 flex items-center gap-2 text-sm font-medium">
366
+ <LineChartIcon className="h-4 w-4 text-muted-foreground" />
367
+ Uso de licencas no tempo
368
+ </div>
369
+ <div className="h-64 w-full">
370
+ <ResponsiveContainer width="100%" height="100%">
371
+ <LineChart data={overview?.licenseTimeline ?? []}>
372
+ <CartesianGrid
373
+ strokeDasharray="3 3"
374
+ vertical={false}
375
+ />
376
+ <XAxis
377
+ dataKey="label"
378
+ tickLine={false}
379
+ axisLine={false}
380
+ />
381
+ <YAxis
382
+ tickLine={false}
383
+ axisLine={false}
384
+ allowDecimals={false}
385
+ />
386
+ <Tooltip />
387
+ <Bar
388
+ dataKey="assigned"
389
+ fill="hsl(160 84% 39%)"
390
+ radius={[4, 4, 0, 0]}
391
+ />
392
+ <Bar
393
+ dataKey="revoked"
394
+ fill="hsl(0 84% 60%)"
395
+ radius={[4, 4, 0, 0]}
396
+ />
397
+ <Line
398
+ type="monotone"
399
+ dataKey="used"
400
+ stroke="hsl(221 83% 53%)"
401
+ strokeWidth={2.5}
402
+ dot={{ r: 3 }}
403
+ />
404
+ </LineChart>
405
+ </ResponsiveContainer>
406
+ </div>
407
+ </CardContent>
408
+ </Card>
268
409
 
269
- {/* ── Empresa tab ── */}
270
- <TabsContent value="empresa" className="mt-4 space-y-4">
271
- {/* ── Linked company card ── */}
272
- {currentAccount.crmAccountId &&
273
- currentAccount.crmAccountName ? (
274
410
  <Card className="border-border/60">
275
- <CardContent className="p-5">
276
- <div className="flex items-start justify-between gap-4">
277
- <div className="flex items-center gap-4">
278
- <div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl border bg-muted/40">
279
- <Building2 className="h-6 w-6 text-muted-foreground" />
280
- </div>
281
- <div>
282
- <p className="text-base font-semibold leading-tight">
283
- {currentAccount.crmAccount?.tradeName ||
284
- currentAccount.crmAccountName}
285
- </p>
286
- {currentAccount.crmAccount?.tradeName &&
287
- currentAccount.crmAccount.tradeName !==
288
- currentAccount.crmAccountName && (
289
- <p className="text-xs text-muted-foreground">
290
- {currentAccount.crmAccountName}
291
- </p>
292
- )}
293
- {currentAccount.crmAccount?.lifecycleStage && (
294
- <Badge
295
- variant="outline"
296
- className="mt-1 text-[10px]"
297
- >
298
- {currentAccount.crmAccount.lifecycleStage}
299
- </Badge>
300
- )}
301
- </div>
302
- </div>
303
- <Button
304
- variant="outline"
305
- size="sm"
306
- onClick={() =>
307
- router.push(
308
- `/contact/accounts?id=${currentAccount.crmAccountId}`
309
- )
310
- }
311
- className="shrink-0"
312
- >
313
- <ExternalLink className="mr-2 h-4 w-4" />
314
- {t('empresa.openRecord')}
315
- </Button>
411
+ <CardContent className="space-y-4 p-4">
412
+ <div className="text-sm font-medium">
413
+ Licencas atuais
316
414
  </div>
415
+ <div className="mx-auto h-40 w-40">
416
+ <ResponsiveContainer width="100%" height="100%">
417
+ <PieChart>
418
+ <Pie
419
+ data={licenseUsageChart}
420
+ dataKey="value"
421
+ innerRadius={42}
422
+ outerRadius={64}
423
+ strokeWidth={2}
424
+ >
425
+ {licenseUsageChart.map((slice) => (
426
+ <Cell key={slice.name} fill={slice.color} />
427
+ ))}
428
+ </Pie>
429
+ <Tooltip />
430
+ </PieChart>
431
+ </ResponsiveContainer>
432
+ </div>
433
+ <div className="text-xs text-muted-foreground">
434
+ {overview?.licenseUsage.used ?? 0} usadas
435
+ {typeof overview?.licenseUsage.limit === 'number'
436
+ ? ` de ${overview.licenseUsage.limit} limite`
437
+ : ' (ilimitado)'}
438
+ </div>
439
+ </CardContent>
440
+ </Card>
441
+ </div>
317
442
 
318
- <Separator className="my-4" />
319
-
320
- <dl className="grid grid-cols-1 gap-3 sm:grid-cols-2">
321
- {currentAccount.crmAccount?.industry && (
322
- <div>
323
- <dt className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
324
- {t('empresa.industry')}
325
- </dt>
326
- <dd className="mt-0.5 text-sm">
327
- {currentAccount.crmAccount.industry}
328
- </dd>
329
- </div>
330
- )}
331
- {currentAccount.crmAccount?.website && (
332
- <div>
333
- <dt className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
334
- {t('empresa.website')}
335
- </dt>
336
- <dd className="mt-0.5 text-sm">
337
- <a
338
- href={currentAccount.crmAccount.website}
339
- target="_blank"
340
- rel="noopener noreferrer"
341
- className="flex items-center gap-1 text-primary hover:underline"
342
- >
343
- <Globe className="h-3 w-3" />
344
- {currentAccount.crmAccount.website.replace(
345
- /^https?:\/\//,
346
- ''
347
- )}
348
- </a>
349
- </dd>
350
- </div>
351
- )}
352
- <div>
353
- <dt className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
354
- {t('empresa.recordId')}
355
- </dt>
356
- <dd className="mt-0.5 font-mono text-sm">
357
- #{currentAccount.crmAccountId}
358
- </dd>
359
- </div>
360
- </dl>
361
-
362
- <div className="mt-4 flex justify-end">
363
- <Button
364
- variant="ghost"
365
- size="sm"
366
- className="text-muted-foreground"
367
- onClick={() => setCompanyPickerOpen((v) => !v)}
368
- >
369
- <RefreshCw className="mr-2 h-3.5 w-3.5" />
370
- {t('empresa.changeCompany')}
371
- </Button>
443
+ <div className="grid grid-cols-1 gap-5 lg:grid-cols-2">
444
+ <Card className="border-border/60">
445
+ <CardContent className="space-y-3 p-4">
446
+ <div className="flex items-center justify-between">
447
+ <p className="text-sm font-medium">
448
+ Vagas em turmas agendadas
449
+ </p>
450
+ <Badge variant="outline">
451
+ {overview?.scheduledSeats.capacity ?? 0} total
452
+ </Badge>
453
+ </div>
454
+ <div className="h-56 w-full">
455
+ <ResponsiveContainer width="100%" height="100%">
456
+ <BarChart
457
+ data={[
458
+ {
459
+ name: 'Vagas',
460
+ usadas: overview?.scheduledSeats.used ?? 0,
461
+ abertas: overview?.scheduledSeats.open ?? 0,
462
+ },
463
+ ]}
464
+ >
465
+ <CartesianGrid
466
+ strokeDasharray="3 3"
467
+ vertical={false}
468
+ />
469
+ <XAxis
470
+ dataKey="name"
471
+ tickLine={false}
472
+ axisLine={false}
473
+ />
474
+ <YAxis
475
+ tickLine={false}
476
+ axisLine={false}
477
+ allowDecimals={false}
478
+ />
479
+ <Tooltip />
480
+ <Bar
481
+ dataKey="usadas"
482
+ stackId="seats"
483
+ fill="hsl(221 83% 53%)"
484
+ />
485
+ <Bar
486
+ dataKey="abertas"
487
+ stackId="seats"
488
+ fill="hsl(160 84% 39%)"
489
+ />
490
+ </BarChart>
491
+ </ResponsiveContainer>
372
492
  </div>
373
493
  </CardContent>
374
494
  </Card>
375
- ) : (
376
- /* ── Empty state ── */
377
- <div className="flex flex-col items-center justify-center py-10 text-center">
378
- <div className="mb-4 text-muted-foreground/30">
379
- <Link2 className="h-12 w-12" />
380
- </div>
381
- <p className="text-sm font-medium text-muted-foreground">
382
- {t('empresa.emptyTitle')}
383
- </p>
384
- <p className="mt-1 max-w-xs text-xs text-muted-foreground/60">
385
- {t('empresa.emptyDescription')}
386
- </p>
387
- <Button
388
- variant="outline"
389
- size="sm"
390
- className="mt-6"
391
- onClick={() => setCompanyPickerOpen(true)}
392
- >
393
- <Link2 className="mr-2 h-4 w-4" />
394
- {t('empresa.linkCompany')}
395
- </Button>
396
- </div>
397
- )}
398
495
 
399
- {/* ── Inline company picker (replace / create) ── */}
400
- {companyPickerOpen && (
401
- <Card className="border-dashed border-border/80">
402
- <CardContent className="p-5">
403
- <p className="mb-3 text-sm font-medium">
404
- {currentAccount.crmAccountId
405
- ? t('empresa.replaceCompany')
406
- : t('empresa.selectCompany')}
407
- </p>
408
- <EntityPicker
409
- name="companyPicker"
410
- value={companyPickerValue}
411
- onChange={(val) => setCompanyPickerValue(val)}
412
- label=""
413
- placeholder={t('empresa.pickerPlaceholder')}
414
- searchPlaceholder={t(
415
- 'empresa.pickerSearchPlaceholder'
416
- )}
417
- emptyLabel={t('empresa.pickerEmptyLabel')}
418
- entityLabel={t('empresa.pickerEntityLabel')}
419
- clearable={false}
420
- valueType="number"
421
- loadOptions={async ({ page, pageSize, search }) => {
422
- const params = new URLSearchParams({
423
- page: String(page),
424
- pageSize: String(pageSize),
425
- });
426
- if (search.trim())
427
- params.set('search', search.trim());
428
- const res = await request<{
429
- data: Array<{ id: number; name: string }>;
430
- total: number;
431
- lastPage?: number;
432
- }>({
433
- url: `/person/accounts?${params}`,
434
- method: 'GET',
435
- });
436
- return {
437
- items: res.data.data ?? [],
438
- hasMore: page < (res.data.lastPage ?? 1),
439
- };
440
- }}
441
- getOptionValue={(opt) => (opt as any).id}
442
- getOptionLabel={(opt) => (opt as any).name ?? ''}
443
- createFields={[
444
- {
445
- name: 'name',
446
- label: t('empresa.createFieldLabel'),
447
- placeholder: t(
448
- 'empresa.createFieldPlaceholder'
449
- ),
450
- required: true,
451
- },
452
- ]}
453
- mapSearchToCreateValues={(search) => ({
454
- name: search,
455
- })}
456
- onCreate={async (values) => {
457
- const res = await request<{
458
- id: number;
459
- name: string;
460
- }>({
461
- url: '/person/accounts',
462
- method: 'POST',
463
- data: {
464
- name: (values.name ?? '').trim(),
465
- status: 'active',
466
- },
467
- });
468
- const created = res.data as unknown as {
469
- id: number;
470
- name: string;
471
- };
472
- // immediately save the new company link
473
- await handleCompanySave(created.id);
474
- return created;
475
- }}
476
- />
477
- <div className="mt-4 flex justify-end gap-2">
478
- <Button
479
- variant="outline"
480
- size="sm"
481
- onClick={() => {
482
- setCompanyPickerOpen(false);
483
- setCompanyPickerValue(null);
484
- }}
485
- >
486
- {t('actions.cancel')}
487
- </Button>
488
- <Button
489
- size="sm"
490
- disabled={!companyPickerValue || savingCompany}
491
- onClick={() =>
492
- handleCompanySave(companyPickerValue as number)
496
+ <Card className="border-border/60">
497
+ <CardContent className="space-y-3 p-4">
498
+ <div className="flex items-center justify-between">
499
+ <p className="text-sm font-medium">
500
+ Atividades recentes
501
+ </p>
502
+ <Badge
503
+ variant={
504
+ overview?.kpis.portalEnabled
505
+ ? 'default'
506
+ : 'outline'
493
507
  }
494
508
  >
495
- {savingCompany
496
- ? t('actions.saving')
497
- : t('actions.save')}
498
- </Button>
509
+ {overview?.kpis.portalEnabled
510
+ ? 'Portal Hcode Training ativo'
511
+ : 'Sem acesso ao portal'}
512
+ </Badge>
513
+ </div>
514
+ <div className="space-y-3">
515
+ {(overview?.activities ?? [])
516
+ .slice(0, 8)
517
+ .map((activity) => (
518
+ <div
519
+ key={activity.id}
520
+ className="rounded-md border border-border/70 px-3 py-2"
521
+ >
522
+ <div className="flex items-center justify-between gap-2">
523
+ <p className="text-sm font-medium">
524
+ {activity.title}
525
+ </p>
526
+ <span className="text-[11px] text-muted-foreground">
527
+ {new Date(
528
+ activity.createdAt
529
+ ).toLocaleString('pt-BR')}
530
+ </span>
531
+ </div>
532
+ <p className="mt-0.5 text-xs text-muted-foreground">
533
+ {activity.description}
534
+ </p>
535
+ </div>
536
+ ))}
537
+ {(overview?.activities ?? []).length === 0 && (
538
+ <p className="text-sm text-muted-foreground">
539
+ Sem registros recentes.
540
+ </p>
541
+ )}
499
542
  </div>
500
543
  </CardContent>
501
544
  </Card>
502
- )}
545
+ </div>
546
+
547
+ {/* ── Danger Zone ──────────────────────────────────────── */}
548
+ <Card className="border-destructive/40">
549
+ <CardContent className="p-4">
550
+ <div className="mb-3 flex items-center gap-2">
551
+ <AlertTriangle className="h-4 w-4 text-destructive" />
552
+ <span className="text-sm font-semibold text-destructive">
553
+ Zona de perigo
554
+ </span>
555
+ </div>
556
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
557
+ <div className="space-y-1">
558
+ <p className="text-sm font-medium">
559
+ Excluir conta enterprise
560
+ </p>
561
+ <p className="text-xs text-muted-foreground">
562
+ Esta ação é irreversível. Todos os dados desta
563
+ conta — turmas, cursos, alunos e histórico — serão
564
+ permanentemente removidos.
565
+ </p>
566
+ </div>
567
+ <Button
568
+ variant="destructive"
569
+ size="sm"
570
+ className="shrink-0"
571
+ onClick={() => {
572
+ setDeleteConfirmText('');
573
+ setDeleteDialogOpen(true);
574
+ }}
575
+ >
576
+ <Trash2 className="mr-2 h-4 w-4" />
577
+ Excluir conta
578
+ </Button>
579
+ </div>
580
+ </CardContent>
581
+ </Card>
503
582
  </TabsContent>
504
583
 
505
584
  <TabsContent value="classes" className="mt-4">
@@ -536,6 +615,61 @@ export function EnterpriseDetailSheet({
536
615
  </SheetContent>
537
616
  </Sheet>
538
617
 
618
+ {/* ── Delete Confirmation Dialog ── */}
619
+ <AlertDialog
620
+ open={deleteDialogOpen}
621
+ onOpenChange={(v) => {
622
+ if (!v) setDeleteConfirmText('');
623
+ setDeleteDialogOpen(v);
624
+ }}
625
+ >
626
+ <AlertDialogContent>
627
+ <AlertDialogHeader>
628
+ <AlertDialogTitle className="flex items-center gap-2 text-destructive">
629
+ <AlertTriangle className="h-5 w-5" />
630
+ Excluir conta enterprise
631
+ </AlertDialogTitle>
632
+ <AlertDialogDescription asChild>
633
+ <div className="space-y-3 text-sm text-muted-foreground">
634
+ <p>
635
+ Esta ação é <strong>irreversível</strong>. Todos os dados
636
+ desta conta — turmas, cursos, alunos e histórico — serão
637
+ permanentemente removidos.
638
+ </p>
639
+ <p>
640
+ Para confirmar, digite o nome da conta:{' '}
641
+ <strong className="text-foreground">
642
+ {currentAccount?.name}
643
+ </strong>
644
+ </p>
645
+ <Input
646
+ value={deleteConfirmText}
647
+ onChange={(e) => setDeleteConfirmText(e.target.value)}
648
+ placeholder={currentAccount?.name ?? ''}
649
+ autoComplete="off"
650
+ autoCorrect="off"
651
+ spellCheck={false}
652
+ />
653
+ </div>
654
+ </AlertDialogDescription>
655
+ </AlertDialogHeader>
656
+ <AlertDialogFooter>
657
+ <AlertDialogCancel disabled={isDeleting}>
658
+ Cancelar
659
+ </AlertDialogCancel>
660
+ <Button
661
+ variant="destructive"
662
+ disabled={
663
+ deleteConfirmText !== (currentAccount?.name ?? '') || isDeleting
664
+ }
665
+ onClick={() => void handleDeleteAccount()}
666
+ >
667
+ {isDeleting ? 'Excluindo…' : 'Excluir definitivamente'}
668
+ </Button>
669
+ </AlertDialogFooter>
670
+ </AlertDialogContent>
671
+ </AlertDialog>
672
+
539
673
  {/* ── Edit Sheet (slides over the detail sheet) ── */}
540
674
  <EnterpriseSheet
541
675
  open={editSheetOpen}