@hed-hog/lms 0.0.314 → 0.0.315

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 (68) hide show
  1. package/dist/class-group/class-group.controller.d.ts +2 -2
  2. package/dist/class-group/class-group.service.d.ts +2 -2
  3. package/dist/enterprise/dto/enterprise-profile.dto.d.ts +13 -0
  4. package/dist/enterprise/dto/enterprise-profile.dto.d.ts.map +1 -0
  5. package/dist/enterprise/dto/enterprise-profile.dto.js +3 -0
  6. package/dist/enterprise/dto/enterprise-profile.dto.js.map +1 -0
  7. package/dist/enterprise/enterprise.controller.d.ts +3 -0
  8. package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
  9. package/dist/enterprise/enterprise.controller.js +14 -0
  10. package/dist/enterprise/enterprise.controller.js.map +1 -1
  11. package/dist/enterprise/enterprise.service.d.ts +3 -0
  12. package/dist/enterprise/enterprise.service.d.ts.map +1 -1
  13. package/dist/enterprise/enterprise.service.js +128 -1
  14. package/dist/enterprise/enterprise.service.js.map +1 -1
  15. package/dist/instructor/instructor.controller.d.ts +23 -0
  16. package/dist/instructor/instructor.controller.d.ts.map +1 -1
  17. package/dist/instructor/instructor.controller.js +41 -0
  18. package/dist/instructor/instructor.controller.js.map +1 -1
  19. package/dist/instructor/instructor.service.d.ts +25 -0
  20. package/dist/instructor/instructor.service.d.ts.map +1 -1
  21. package/dist/instructor/instructor.service.js +126 -8
  22. package/dist/instructor/instructor.service.js.map +1 -1
  23. package/hedhog/data/menu.yaml +23 -7
  24. package/hedhog/data/role.yaml +9 -1
  25. package/hedhog/data/route.yaml +54 -0
  26. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -1
  27. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +44 -44
  28. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -362
  29. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -111
  30. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -134
  31. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -113
  32. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -314
  33. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -62
  34. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +173 -173
  35. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -58
  36. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +51 -51
  37. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -276
  38. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -1216
  39. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1824 -1824
  40. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -443
  41. package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +40 -40
  42. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -185
  43. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -264
  44. package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +95 -95
  45. package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +73 -73
  46. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -136
  47. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -80
  48. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +949 -949
  49. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -525
  50. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +181 -181
  51. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +51 -51
  52. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -271
  53. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -167
  54. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -108
  55. package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -318
  56. package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -10
  57. package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +2 -1
  58. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +438 -0
  59. package/hedhog/frontend/app/instructors/_components/instructor-types.ts.ejs +40 -0
  60. package/hedhog/frontend/app/instructors/page.tsx.ejs +696 -0
  61. package/hedhog/frontend/app/training/page.tsx.ejs +2339 -0
  62. package/hedhog/table/enterprise_user.yaml +1 -1
  63. package/package.json +8 -8
  64. package/src/enterprise/dto/enterprise-profile.dto.ts +17 -0
  65. package/src/enterprise/enterprise.controller.ts +9 -1
  66. package/src/enterprise/enterprise.service.ts +147 -4
  67. package/src/instructor/instructor.controller.ts +36 -9
  68. package/src/instructor/instructor.service.ts +140 -10
@@ -1,182 +1,182 @@
1
- import type { Course, FlatItem, Lesson, Session } from './types';
2
-
3
- // ─────────────────────────────────────────────────────────────────────────────
4
- // Text Highlight Helpers
5
- // ─────────────────────────────────────────────────────────────────────────────
6
-
7
- export interface TextSegment {
8
- text: string;
9
- highlight: boolean;
10
- }
11
-
12
- /**
13
- * Splits `text` into segments marking the first occurrence of `query`.
14
- * Returns a single non-highlighted segment when query is empty or not found.
15
- */
16
- export function getTextSegments(text: string, query: string): TextSegment[] {
17
- const q = query.trim();
18
- if (!q) return [{ text, highlight: false }];
19
-
20
- const idx = text.toLowerCase().indexOf(q.toLowerCase());
21
- if (idx === -1) return [{ text, highlight: false }];
22
-
23
- const segments: TextSegment[] = [];
24
- if (idx > 0) segments.push({ text: text.slice(0, idx), highlight: false });
25
- segments.push({ text: text.slice(idx, idx + q.length), highlight: true });
26
- const tail = text.slice(idx + q.length);
27
- if (tail.length > 0) segments.push({ text: tail, highlight: false });
28
- return segments;
29
- }
30
-
31
- // ─────────────────────────────────────────────────────────────────────────────
32
- // Flat Tree Builder
33
- // ─────────────────────────────────────────────────────────────────────────────
34
-
35
- export interface VisibleTreeResult {
36
- /** Ordered list of visible tree nodes. */
37
- items: FlatItem[];
38
- /** IDs of nodes that directly match the search query (for highlighting). */
39
- matchedIds: Set<string>;
40
- /** Session IDs that are effectively expanded (auto-expanded by search). */
41
- expandedBySearch: Set<string>;
42
- /** Number of directly matching items. */
43
- resultCount: number;
44
- }
45
-
46
- function lessonMatchesQuery(l: Lesson, q: string): boolean {
47
- return (
48
- l.title.toLowerCase().includes(q) ||
49
- l.code.toLowerCase().includes(q) ||
50
- l.type.toLowerCase().includes(q)
51
- );
52
- }
53
-
54
- function sessionMatchesQuery(s: Session, q: string): boolean {
55
- return (
56
- s.title.toLowerCase().includes(q) ||
57
- s.code.toLowerCase().includes(q)
58
- );
59
- }
60
-
61
- function courseMatchesQuery(c: Course, q: string): boolean {
62
- return (
63
- c.title.toLowerCase().includes(q) ||
64
- c.code.toLowerCase().includes(q)
65
- );
66
- }
67
-
68
- /**
69
- * Builds the ordered, visible flat list of tree nodes.
70
- *
71
- * - When `query` is empty: uses `expandedIds` for expand/collapse.
72
- * - When `query` is active: filters items, auto-expands sessions with results,
73
- * and returns `matchedIds` for highlight + `expandedBySearch` for chevron state.
74
- */
75
- export function buildVisibleItems(
76
- course: Course,
77
- sessions: Session[],
78
- lessons: Lesson[],
79
- expandedIds: Set<string>,
80
- query: string
81
- ): VisibleTreeResult {
82
- const sortedSessions = [...sessions].sort((a, b) => a.order - b.order);
83
- const q = query.trim().toLowerCase();
84
-
85
- // ── No active query: normal expand/collapse behaviour ─────────────────────
86
- if (!q) {
87
- const items: FlatItem[] = [];
88
- items.push({ id: course.id, type: 'course', depth: 0, data: course });
89
-
90
- for (const session of sortedSessions) {
91
- items.push({ id: session.id, type: 'session', depth: 1, data: session });
92
- if (expandedIds.has(session.id)) {
93
- const sessionLessons = lessons
94
- .filter((l) => l.sessionId === session.id)
95
- .sort((a, b) => a.order - b.order);
96
- for (const lesson of sessionLessons) {
97
- items.push({ id: lesson.id, type: 'lesson', depth: 2, data: lesson });
98
- }
99
- }
100
- }
101
-
102
- return {
103
- items,
104
- matchedIds: new Set(),
105
- expandedBySearch: new Set(),
106
- resultCount: 0,
107
- };
108
- }
109
-
110
- // ── Active query: filter + highlight ──────────────────────────────────────
111
- const matchedIds = new Set<string>();
112
- const expandedBySearch = new Set<string>();
113
-
114
- // Step 1 — mark all direct matches
115
- if (courseMatchesQuery(course, q)) matchedIds.add(course.id);
116
-
117
- for (const session of sortedSessions) {
118
- if (sessionMatchesQuery(session, q)) matchedIds.add(session.id);
119
-
120
- const sessionLessons = lessons.filter((l) => l.sessionId === session.id);
121
- for (const lesson of sessionLessons) {
122
- if (lessonMatchesQuery(lesson, q)) matchedIds.add(lesson.id);
123
- }
124
- }
125
-
126
- // Step 2 — compute which sessions need to be auto-expanded
127
- for (const session of sortedSessions) {
128
- const hasMatchingLesson = lessons.some(
129
- (l) => l.sessionId === session.id && matchedIds.has(l.id)
130
- );
131
- if (matchedIds.has(session.id) || hasMatchingLesson) {
132
- expandedBySearch.add(session.id);
133
- }
134
- }
135
-
136
- // Step 3 — build the visible list
137
- const items: FlatItem[] = [];
138
- const anyMatch = matchedIds.size > 0;
139
-
140
- // Course is always shown if anything matched (ancestor context)
141
- if (anyMatch) {
142
- items.push({ id: course.id, type: 'course', depth: 0, data: course });
143
- }
144
-
145
- for (const session of sortedSessions) {
146
- const sessionLessons = lessons
147
- .filter((l) => l.sessionId === session.id)
148
- .sort((a, b) => a.order - b.order);
149
-
150
- const sessionDirectMatch = matchedIds.has(session.id);
151
- const matchingLessons = sessionLessons.filter((l) => matchedIds.has(l.id));
152
-
153
- if (!sessionDirectMatch && matchingLessons.length === 0) continue;
154
-
155
- items.push({ id: session.id, type: 'session', depth: 1, data: session });
156
-
157
- // Show only matching lessons (session match shows all its lessons for context)
158
- const lessonsToShow = sessionDirectMatch ? sessionLessons : matchingLessons;
159
- for (const lesson of lessonsToShow) {
160
- items.push({ id: lesson.id, type: 'lesson', depth: 2, data: lesson });
161
- }
162
- }
163
-
164
- return {
165
- items,
166
- matchedIds,
167
- expandedBySearch,
168
- resultCount: matchedIds.size,
169
- };
170
- }
171
-
172
- /**
173
- * Returns the total lesson count for each session — keyed by session ID.
174
- * Computed once for the whole tree to avoid per-row array scans.
175
- */
176
- export function buildLessonCountMap(lessons: Lesson[]): Map<string, number> {
177
- const map = new Map<string, number>();
178
- for (const lesson of lessons) {
179
- map.set(lesson.sessionId, (map.get(lesson.sessionId) ?? 0) + 1);
180
- }
181
- return map;
1
+ import type { Course, FlatItem, Lesson, Session } from './types';
2
+
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ // Text Highlight Helpers
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+
7
+ export interface TextSegment {
8
+ text: string;
9
+ highlight: boolean;
10
+ }
11
+
12
+ /**
13
+ * Splits `text` into segments marking the first occurrence of `query`.
14
+ * Returns a single non-highlighted segment when query is empty or not found.
15
+ */
16
+ export function getTextSegments(text: string, query: string): TextSegment[] {
17
+ const q = query.trim();
18
+ if (!q) return [{ text, highlight: false }];
19
+
20
+ const idx = text.toLowerCase().indexOf(q.toLowerCase());
21
+ if (idx === -1) return [{ text, highlight: false }];
22
+
23
+ const segments: TextSegment[] = [];
24
+ if (idx > 0) segments.push({ text: text.slice(0, idx), highlight: false });
25
+ segments.push({ text: text.slice(idx, idx + q.length), highlight: true });
26
+ const tail = text.slice(idx + q.length);
27
+ if (tail.length > 0) segments.push({ text: tail, highlight: false });
28
+ return segments;
29
+ }
30
+
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+ // Flat Tree Builder
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+
35
+ export interface VisibleTreeResult {
36
+ /** Ordered list of visible tree nodes. */
37
+ items: FlatItem[];
38
+ /** IDs of nodes that directly match the search query (for highlighting). */
39
+ matchedIds: Set<string>;
40
+ /** Session IDs that are effectively expanded (auto-expanded by search). */
41
+ expandedBySearch: Set<string>;
42
+ /** Number of directly matching items. */
43
+ resultCount: number;
44
+ }
45
+
46
+ function lessonMatchesQuery(l: Lesson, q: string): boolean {
47
+ return (
48
+ l.title.toLowerCase().includes(q) ||
49
+ l.code.toLowerCase().includes(q) ||
50
+ l.type.toLowerCase().includes(q)
51
+ );
52
+ }
53
+
54
+ function sessionMatchesQuery(s: Session, q: string): boolean {
55
+ return (
56
+ s.title.toLowerCase().includes(q) ||
57
+ s.code.toLowerCase().includes(q)
58
+ );
59
+ }
60
+
61
+ function courseMatchesQuery(c: Course, q: string): boolean {
62
+ return (
63
+ c.title.toLowerCase().includes(q) ||
64
+ c.code.toLowerCase().includes(q)
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Builds the ordered, visible flat list of tree nodes.
70
+ *
71
+ * - When `query` is empty: uses `expandedIds` for expand/collapse.
72
+ * - When `query` is active: filters items, auto-expands sessions with results,
73
+ * and returns `matchedIds` for highlight + `expandedBySearch` for chevron state.
74
+ */
75
+ export function buildVisibleItems(
76
+ course: Course,
77
+ sessions: Session[],
78
+ lessons: Lesson[],
79
+ expandedIds: Set<string>,
80
+ query: string
81
+ ): VisibleTreeResult {
82
+ const sortedSessions = [...sessions].sort((a, b) => a.order - b.order);
83
+ const q = query.trim().toLowerCase();
84
+
85
+ // ── No active query: normal expand/collapse behaviour ─────────────────────
86
+ if (!q) {
87
+ const items: FlatItem[] = [];
88
+ items.push({ id: course.id, type: 'course', depth: 0, data: course });
89
+
90
+ for (const session of sortedSessions) {
91
+ items.push({ id: session.id, type: 'session', depth: 1, data: session });
92
+ if (expandedIds.has(session.id)) {
93
+ const sessionLessons = lessons
94
+ .filter((l) => l.sessionId === session.id)
95
+ .sort((a, b) => a.order - b.order);
96
+ for (const lesson of sessionLessons) {
97
+ items.push({ id: lesson.id, type: 'lesson', depth: 2, data: lesson });
98
+ }
99
+ }
100
+ }
101
+
102
+ return {
103
+ items,
104
+ matchedIds: new Set(),
105
+ expandedBySearch: new Set(),
106
+ resultCount: 0,
107
+ };
108
+ }
109
+
110
+ // ── Active query: filter + highlight ──────────────────────────────────────
111
+ const matchedIds = new Set<string>();
112
+ const expandedBySearch = new Set<string>();
113
+
114
+ // Step 1 — mark all direct matches
115
+ if (courseMatchesQuery(course, q)) matchedIds.add(course.id);
116
+
117
+ for (const session of sortedSessions) {
118
+ if (sessionMatchesQuery(session, q)) matchedIds.add(session.id);
119
+
120
+ const sessionLessons = lessons.filter((l) => l.sessionId === session.id);
121
+ for (const lesson of sessionLessons) {
122
+ if (lessonMatchesQuery(lesson, q)) matchedIds.add(lesson.id);
123
+ }
124
+ }
125
+
126
+ // Step 2 — compute which sessions need to be auto-expanded
127
+ for (const session of sortedSessions) {
128
+ const hasMatchingLesson = lessons.some(
129
+ (l) => l.sessionId === session.id && matchedIds.has(l.id)
130
+ );
131
+ if (matchedIds.has(session.id) || hasMatchingLesson) {
132
+ expandedBySearch.add(session.id);
133
+ }
134
+ }
135
+
136
+ // Step 3 — build the visible list
137
+ const items: FlatItem[] = [];
138
+ const anyMatch = matchedIds.size > 0;
139
+
140
+ // Course is always shown if anything matched (ancestor context)
141
+ if (anyMatch) {
142
+ items.push({ id: course.id, type: 'course', depth: 0, data: course });
143
+ }
144
+
145
+ for (const session of sortedSessions) {
146
+ const sessionLessons = lessons
147
+ .filter((l) => l.sessionId === session.id)
148
+ .sort((a, b) => a.order - b.order);
149
+
150
+ const sessionDirectMatch = matchedIds.has(session.id);
151
+ const matchingLessons = sessionLessons.filter((l) => matchedIds.has(l.id));
152
+
153
+ if (!sessionDirectMatch && matchingLessons.length === 0) continue;
154
+
155
+ items.push({ id: session.id, type: 'session', depth: 1, data: session });
156
+
157
+ // Show only matching lessons (session match shows all its lessons for context)
158
+ const lessonsToShow = sessionDirectMatch ? sessionLessons : matchingLessons;
159
+ for (const lesson of lessonsToShow) {
160
+ items.push({ id: lesson.id, type: 'lesson', depth: 2, data: lesson });
161
+ }
162
+ }
163
+
164
+ return {
165
+ items,
166
+ matchedIds,
167
+ expandedBySearch,
168
+ resultCount: matchedIds.size,
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Returns the total lesson count for each session — keyed by session ID.
174
+ * Computed once for the whole tree to avoid per-row array scans.
175
+ */
176
+ export function buildLessonCountMap(lessons: Lesson[]): Map<string, number> {
177
+ const map = new Map<string, number>();
178
+ for (const lesson of lessons) {
179
+ map.set(lesson.sessionId, (map.get(lesson.sessionId) ?? 0) + 1);
180
+ }
181
+ return map;
182
182
  }
@@ -1,52 +1,52 @@
1
- 'use client';
2
-
3
- import { cn } from '@/lib/utils';
4
- import { BookOpen } from 'lucide-react';
5
- import { HighlightedText } from './highlighted-text';
6
- import type { Course } from './types';
7
-
8
- interface TreeRowCourseProps {
9
- data: Course;
10
- isSelected: boolean;
11
- isActive: boolean;
12
- query: string;
13
- isMatched: boolean;
14
- onClick: (e: React.MouseEvent) => void;
15
- }
16
-
17
- export function TreeRowCourse({
18
- data,
19
- isSelected,
20
- isActive,
21
- query,
22
- onClick,
23
- }: TreeRowCourseProps) {
24
- return (
25
- <div
26
- onClick={onClick}
27
- role="treeitem"
28
- aria-selected={isActive}
29
- className={cn(
30
- 'flex items-center gap-2 px-3 py-1.5 rounded-md cursor-pointer select-none transition-colors text-sm font-semibold h-full',
31
- isActive && 'bg-accent text-accent-foreground ring-1 ring-inset ring-primary/20',
32
- isSelected && !isActive && 'bg-accent/60',
33
- !isActive && !isSelected && 'hover:bg-muted/60'
34
- )}
35
- >
36
- <BookOpen className="size-3.5 shrink-0 text-primary" aria-hidden />
37
- <span className="truncate flex-1">
38
- <HighlightedText text={data.title} query={query} />
39
- </span>
40
- <span
41
- className={cn(
42
- 'text-[0.6rem] px-1.5 py-0.5 rounded-full shrink-0 font-medium leading-none',
43
- data.published
44
- ? 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400'
45
- : 'bg-muted text-muted-foreground'
46
- )}
47
- >
48
- {data.published ? 'Publicado' : 'Rascunho'}
49
- </span>
50
- </div>
51
- );
1
+ 'use client';
2
+
3
+ import { cn } from '@/lib/utils';
4
+ import { BookOpen } from 'lucide-react';
5
+ import { HighlightedText } from './highlighted-text';
6
+ import type { Course } from './types';
7
+
8
+ interface TreeRowCourseProps {
9
+ data: Course;
10
+ isSelected: boolean;
11
+ isActive: boolean;
12
+ query: string;
13
+ isMatched: boolean;
14
+ onClick: (e: React.MouseEvent) => void;
15
+ }
16
+
17
+ export function TreeRowCourse({
18
+ data,
19
+ isSelected,
20
+ isActive,
21
+ query,
22
+ onClick,
23
+ }: TreeRowCourseProps) {
24
+ return (
25
+ <div
26
+ onClick={onClick}
27
+ role="treeitem"
28
+ aria-selected={isActive}
29
+ className={cn(
30
+ 'flex items-center gap-2 px-3 py-1.5 rounded-md cursor-pointer select-none transition-colors text-sm font-semibold h-full',
31
+ isActive && 'bg-accent text-accent-foreground ring-1 ring-inset ring-primary/20',
32
+ isSelected && !isActive && 'bg-accent/60',
33
+ !isActive && !isSelected && 'hover:bg-muted/60'
34
+ )}
35
+ >
36
+ <BookOpen className="size-3.5 shrink-0 text-primary" aria-hidden />
37
+ <span className="truncate flex-1">
38
+ <HighlightedText text={data.title} query={query} />
39
+ </span>
40
+ <span
41
+ className={cn(
42
+ 'text-[0.6rem] px-1.5 py-0.5 rounded-full shrink-0 font-medium leading-none',
43
+ data.published
44
+ ? 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400'
45
+ : 'bg-muted text-muted-foreground'
46
+ )}
47
+ >
48
+ {data.published ? 'Publicado' : 'Rascunho'}
49
+ </span>
50
+ </div>
51
+ );
52
52
  }