@exxatdesignux/ui 0.2.9 → 0.2.11
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/consumer-extras/cursor-skills/exxat-card-vs-list-rows/SKILL.md +20 -0
- package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +33 -0
- package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +45 -0
- package/consumer-extras/cursor-skills/exxat-drawer-vs-dialog/SKILL.md +20 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +31 -5
- package/consumer-extras/cursor-skills/exxat-kpi-max-four/SKILL.md +19 -0
- package/consumer-extras/cursor-skills/exxat-kpi-trends/SKILL.md +27 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +1 -0
- package/consumer-extras/patterns/collaboration-access-pattern.md +114 -0
- package/consumer-extras/patterns/data-views-pattern.md +12 -4
- package/package.json +4 -1
- package/src/components/ui/banner.tsx +20 -7
- package/src/components/ui/date-picker-field.tsx +3 -3
- package/src/components/ui/dropdown-menu.tsx +17 -6
- package/src/components/ui/input-group.tsx +1 -1
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/select.tsx +1 -1
- package/src/components/ui/separator.tsx +2 -2
- package/src/components/ui/sidebar.tsx +31 -3
- package/src/components/ui/textarea.tsx +1 -1
- package/src/globals.css +0 -1
- package/src/index.ts +1 -0
- package/src/lib/date-filter.ts +13 -4
- package/src/lib/dropdown-menu-surface.ts +13 -0
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +27 -9
- package/template/.cursor/rules/exxat-data-tables.mdc +1 -0
- package/template/.nvmrc +1 -1
- package/template/AGENTS.md +82 -27
- package/template/app/(app)/examples/page.tsx +2 -1
- package/template/app/(app)/help/page.tsx +6 -0
- package/template/app/(app)/layout.tsx +7 -4
- package/template/app/(app)/question-bank/find/page.tsx +12 -0
- package/template/app/(app)/question-bank/layout.tsx +46 -0
- package/template/app/(app)/question-bank/library/page.tsx +11 -0
- package/template/app/(app)/question-bank/list/page.tsx +12 -0
- package/template/app/(app)/question-bank/page.tsx +4 -3
- package/template/app/globals.css +1 -2
- package/template/components/app-sidebar.tsx +51 -13
- package/template/components/ask-leo-composer.tsx +173 -45
- package/template/components/ask-leo-sidebar.tsx +9 -1
- package/template/components/chart-area-interactive.tsx +3 -13
- package/template/components/charts-overview.tsx +33 -6
- package/template/components/collaboration-access-flow.tsx +144 -0
- package/template/components/compliance-page-header.tsx +1 -1
- package/template/components/compliance-table.tsx +2 -2
- package/template/components/dashboard-tabs.tsx +4 -3
- package/template/components/data-list-table-cells.tsx +1 -1
- package/template/components/data-list-table.tsx +1 -1
- package/template/components/data-table/index.tsx +5 -5
- package/template/components/data-table/use-table-state.ts +18 -2
- package/template/components/data-view-dashboard-charts-compliance.tsx +8 -5
- package/template/components/data-view-dashboard-charts-team.tsx +8 -5
- package/template/components/data-view-dashboard-charts.tsx +62 -227
- package/template/components/dedicated-search-recents.tsx +96 -0
- package/template/components/dedicated-search-url-composer.tsx +112 -0
- package/template/components/getting-started.tsx +1 -1
- package/template/components/hub-tree-panel-view.tsx +10 -26
- package/template/components/invite-collaborators-drawer.tsx +453 -0
- package/template/components/key-metrics.tsx +54 -8
- package/template/components/nav-documents.tsx +1 -1
- package/template/components/new-placement-form.tsx +3 -3
- package/template/components/page-header.tsx +76 -59
- package/template/components/placements-board-view.tsx +3 -3
- package/template/components/placements-page-header.tsx +1 -1
- package/template/components/placements-table-columns.tsx +3 -2
- package/template/components/product-switcher.tsx +0 -1
- package/template/components/question-bank-board-view.tsx +35 -47
- package/template/components/question-bank-client.tsx +293 -81
- package/template/components/question-bank-dashboard-charts.tsx +174 -0
- package/template/components/question-bank-favorite-button.tsx +46 -0
- package/template/components/question-bank-hub-client.tsx +436 -0
- package/template/components/question-bank-list-view.tsx +26 -19
- package/template/components/question-bank-new-folder-sheet.tsx +56 -42
- package/template/components/question-bank-os-folder-view.tsx +3 -14
- package/template/components/question-bank-page-header.tsx +85 -53
- package/template/components/question-bank-panel-activator.tsx +3 -4
- package/template/components/question-bank-secondary-nav.tsx +523 -65
- package/template/components/question-bank-table.tsx +125 -343
- package/template/components/secondary-panel.tsx +130 -63
- package/template/components/settings-client.tsx +3 -1
- package/template/components/sidebar-shell.tsx +2 -0
- package/template/components/sites-all-client.tsx +1 -1
- package/template/components/sites-table.tsx +1 -1
- package/template/components/system-banner-slot.tsx +2 -1
- package/template/components/table-properties/drawer.tsx +3 -3
- package/template/components/table-properties/sort-card.tsx +1 -1
- package/template/components/team-page-header.tsx +1 -1
- package/template/components/team-table.tsx +8 -4
- package/template/components/templates/dedicated-search-landing-template.tsx +58 -0
- package/template/components/templates/dedicated-search-results-template.tsx +19 -0
- package/template/components/templates/discovery-hub-template.tsx +273 -0
- package/template/components/templates/list-page.tsx +11 -4
- package/template/components/templates/nested-secondary-panel-shell.tsx +57 -0
- package/template/components/templates/secondary-panel-hub-template.tsx +54 -0
- package/template/docs/card-vs-rows-pattern.md +36 -0
- package/template/docs/collaboration-access-pattern.md +114 -0
- package/template/docs/data-views-pattern.md +12 -4
- package/template/docs/drawer-vs-dialog-pattern.md +50 -0
- package/template/docs/kpi-strip-max-four-pattern.md +29 -0
- package/template/docs/kpi-trend-pattern.md +43 -0
- package/template/fontawesome-subset.manifest.json +2 -2
- package/template/hooks/use-location-hash.ts +14 -8
- package/template/hooks/use-secondary-panel-hub-nav.ts +98 -0
- package/template/lib/ask-leo-route-context.ts +24 -0
- package/template/lib/collaborator-access.ts +92 -0
- package/template/lib/command-menu-config.ts +8 -1
- package/template/lib/command-menu-search-data.ts +11 -8
- package/template/lib/data-list-display-options.ts +1 -1
- package/template/lib/data-view-dashboard-placements-layout.ts +215 -0
- package/template/lib/date-filter.ts +1 -0
- package/template/lib/dedicated-search-recents.ts +76 -0
- package/template/lib/dedicated-search-url.ts +23 -0
- package/template/lib/discovery-hub.ts +15 -0
- package/template/lib/list-status-badges.ts +1 -21
- package/template/lib/mock/navigation.tsx +4 -2
- package/template/lib/mock/placements.ts +9 -9
- package/template/lib/mock/question-bank-folders.ts +7 -0
- package/template/lib/mock/question-bank-header-collaborators.ts +45 -5
- package/template/lib/mock/question-bank-inspector.ts +1 -2
- package/template/lib/mock/question-bank-kpi.ts +38 -26
- package/template/lib/mock/question-bank.ts +43 -16
- package/template/lib/question-bank-dedicated-search.ts +19 -0
- package/template/lib/question-bank-hub-search.ts +90 -0
- package/template/lib/question-bank-nav.ts +322 -6
- package/template/lib/question-bank-recent-searches.ts +22 -0
- package/template/package.json +1 -2
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Namespaced recent-query storage for dedicated search routes (localStorage + sync event).
|
|
3
|
+
* Hub code creates one controller per surface (namespace) and passes it into
|
|
4
|
+
* {@link DedicatedSearchRecents} / {@link DedicatedSearchUrlComposer}.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const MAX_RECENTS = 12
|
|
8
|
+
|
|
9
|
+
function parseStored(raw: string | null): string[] {
|
|
10
|
+
if (!raw) return []
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(raw) as unknown
|
|
13
|
+
if (!Array.isArray(parsed)) return []
|
|
14
|
+
return parsed
|
|
15
|
+
.filter((x): x is string => typeof x === "string" && x.trim().length > 0)
|
|
16
|
+
.map(s => s.trim())
|
|
17
|
+
.slice(0, MAX_RECENTS)
|
|
18
|
+
} catch {
|
|
19
|
+
return []
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DedicatedSearchRecentsController {
|
|
24
|
+
/** Pass to `addEventListener` / `removeEventListener` (CustomEvent, no detail). */
|
|
25
|
+
readonly eventName: string
|
|
26
|
+
read: () => string[]
|
|
27
|
+
record: (query: string) => void
|
|
28
|
+
clear: () => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type DedicatedSearchRecentsLegacyKeys = {
|
|
32
|
+
storageKey: string
|
|
33
|
+
eventName: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param namespace — Stable id when not using `legacy` (storage key + event name derive from it).
|
|
38
|
+
* @param legacy — Optional stable keys for an existing shipped surface (avoid resetting users’ saved recents).
|
|
39
|
+
*/
|
|
40
|
+
export function createDedicatedSearchRecentsController(
|
|
41
|
+
namespace: string,
|
|
42
|
+
legacy?: DedicatedSearchRecentsLegacyKeys,
|
|
43
|
+
): DedicatedSearchRecentsController {
|
|
44
|
+
const storageKey = legacy?.storageKey ?? `exxat-ds.dedicated-search.recents.${namespace}.v1`
|
|
45
|
+
const eventName = legacy?.eventName ?? `exxat-dedicated-search-recents-${namespace}`
|
|
46
|
+
|
|
47
|
+
const read = (): string[] => {
|
|
48
|
+
if (typeof window === "undefined") return []
|
|
49
|
+
return parseStored(window.localStorage.getItem(storageKey))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const record = (query: string): void => {
|
|
53
|
+
const q = query.trim()
|
|
54
|
+
if (!q || typeof window === "undefined") return
|
|
55
|
+
const prev = read()
|
|
56
|
+
const deduped = [q, ...prev.filter(x => x.toLowerCase() !== q.toLowerCase())].slice(0, MAX_RECENTS)
|
|
57
|
+
try {
|
|
58
|
+
window.localStorage.setItem(storageKey, JSON.stringify(deduped))
|
|
59
|
+
} catch {
|
|
60
|
+
/* ignore quota / private mode */
|
|
61
|
+
}
|
|
62
|
+
window.dispatchEvent(new CustomEvent(eventName))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const clear = (): void => {
|
|
66
|
+
if (typeof window === "undefined") return
|
|
67
|
+
try {
|
|
68
|
+
window.localStorage.removeItem(storageKey)
|
|
69
|
+
} catch {
|
|
70
|
+
/* ignore */
|
|
71
|
+
}
|
|
72
|
+
window.dispatchEvent(new CustomEvent(eventName))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { eventName, read, record, clear }
|
|
76
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL helpers for dedicated search surfaces — hubs pass domain-specific patchers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type DedicatedSearchParamsPatch = (
|
|
6
|
+
/** Current query string snapshot (e.g. from `useSearchParams` serialization). */
|
|
7
|
+
searchParamsKey: string,
|
|
8
|
+
/** Trimmed query text; empty string means “clear primary search param”. */
|
|
9
|
+
submittedText: string,
|
|
10
|
+
) => URLSearchParams
|
|
11
|
+
|
|
12
|
+
/** Default: single `q` param, replaces or deletes only `q`. */
|
|
13
|
+
export function patchDedicatedSearchQueryParam(
|
|
14
|
+
searchParamsKey: string,
|
|
15
|
+
submittedText: string,
|
|
16
|
+
paramName = "q",
|
|
17
|
+
): URLSearchParams {
|
|
18
|
+
const next = new URLSearchParams(searchParamsKey)
|
|
19
|
+
const t = submittedText.trim()
|
|
20
|
+
if (t) next.set(paramName, t)
|
|
21
|
+
else next.delete(paramName)
|
|
22
|
+
return next
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type DiscoveryHubSearchItem = {
|
|
2
|
+
id: string
|
|
3
|
+
label: string
|
|
4
|
+
keywords?: string
|
|
5
|
+
icon?: string
|
|
6
|
+
href?: string
|
|
7
|
+
askLeoPrompt?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type DiscoveryHubSearchGroup = {
|
|
11
|
+
id: string
|
|
12
|
+
heading: string
|
|
13
|
+
items: DiscoveryHubSearchItem[]
|
|
14
|
+
searchOnly?: boolean
|
|
15
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared status chip labels, tint classes, and FA icon names for product list hubs
|
|
3
|
-
* (Placements, Team, Compliance
|
|
3
|
+
* (Placements, Team, Compliance — table, list, board), plus related chips
|
|
4
4
|
* (dashboard **task priority**, placement **readiness** on the detail drawer).
|
|
5
5
|
*
|
|
6
6
|
* Labels use **sentence / title case** (e.g. "Due soon", "Under Review"). Do **not** add **`uppercase`**.
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { ComplianceStatus } from "@/lib/mock/compliance"
|
|
17
|
-
import type { QuestionBankStatus } from "@/lib/mock/question-bank"
|
|
18
17
|
import type { Status as PlacementStatus } from "@/lib/mock/placements"
|
|
19
18
|
import type { TeamMember } from "@/lib/mock/team"
|
|
20
19
|
|
|
@@ -147,22 +146,3 @@ export const COMPLIANCE_STATUS_ICON: Record<ComplianceStatus, string> = {
|
|
|
147
146
|
pending: "fa-hourglass-half",
|
|
148
147
|
}
|
|
149
148
|
|
|
150
|
-
// ─── Question bank ────────────────────────────────────────────────────────
|
|
151
|
-
|
|
152
|
-
export const QUESTION_BANK_STATUS_LABEL: Record<QuestionBankStatus, string> = {
|
|
153
|
-
published: "Published",
|
|
154
|
-
draft: "Draft",
|
|
155
|
-
in_review: "In review",
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export const QUESTION_BANK_STATUS_BADGE_CLASS: Record<QuestionBankStatus, string> = {
|
|
159
|
-
published: LIST_HUB_STATUS_TINT_SUCCESS,
|
|
160
|
-
draft: LIST_HUB_STATUS_TINT_NEUTRAL,
|
|
161
|
-
in_review: LIST_HUB_STATUS_TINT_WARNING,
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export const QUESTION_BANK_STATUS_ICON: Record<QuestionBankStatus, string> = {
|
|
165
|
-
published: "fa-circle-check",
|
|
166
|
-
draft: "fa-pen-field",
|
|
167
|
-
in_review: "fa-user-magnifying-glass",
|
|
168
|
-
}
|
|
@@ -120,14 +120,16 @@ export const NAV_DOCUMENTS: NavLinkItem[] = [
|
|
|
120
120
|
{
|
|
121
121
|
key: "tokens",
|
|
122
122
|
title: "Tokens & themes",
|
|
123
|
-
|
|
123
|
+
/** Same page as Settings — disambiguate active state via `#appearance` (see `isNavActive`). */
|
|
124
|
+
url: "/settings#appearance",
|
|
124
125
|
icon: <i className="fa-light fa-palette" aria-hidden="true" />,
|
|
125
126
|
iconActive: <i className="fa-solid fa-palette" aria-hidden="true" />,
|
|
126
127
|
},
|
|
127
128
|
{
|
|
128
129
|
key: "more",
|
|
129
130
|
title: "More",
|
|
130
|
-
|
|
131
|
+
/** Same page as Get Help — disambiguate via `#more`. */
|
|
132
|
+
url: "/help#more",
|
|
131
133
|
icon: <i className="fa-light fa-ellipsis" aria-hidden="true" />,
|
|
132
134
|
iconActive: <i className="fa-solid fa-ellipsis" aria-hidden="true" />,
|
|
133
135
|
},
|
|
@@ -46,7 +46,7 @@ export const ALL_PLACEMENTS: Placement[] = [
|
|
|
46
46
|
id: 1, student: "Sarah Johnson", email: "s.johnson@college.edu", initials: "SJ", program: "Nursing", site: "City Medical Center",
|
|
47
47
|
siteAddress: "1400 N Lake Shore Dr, Chicago, IL", status: "confirmed", start: "03/15/2026", duration: "12 wks", supervisor: "Dr. Patel",
|
|
48
48
|
placementPhase: "ongoing", internship: "Med-Surg Clinical I", specialization: "Adult Health", compliance: "Complete", daysUntilStart: 0,
|
|
49
|
-
readiness: "Ready", progressWeeksDone: 5, progressWeeksTotal: 12, endDate: "06/07/2026", lastCheckin: "
|
|
49
|
+
readiness: "Ready", progressWeeksDone: 5, progressWeeksTotal: 12, endDate: "06/07/2026", lastCheckin: "03/22/2026",
|
|
50
50
|
completionDate: "—", finalStatus: "—", rating: 0, suggestedToHire: "—", isNew: true,
|
|
51
51
|
},
|
|
52
52
|
{
|
|
@@ -67,7 +67,7 @@ export const ALL_PLACEMENTS: Placement[] = [
|
|
|
67
67
|
id: 4, student: "James Williams", email: "j.williams@college.edu", initials: "JW", program: "Nursing", site: "Sunrise Hospital",
|
|
68
68
|
siteAddress: "5775 Wayzata Blvd, Minneapolis, MN", status: "confirmed", start: "04/07/2026", duration: "12 wks", supervisor: "Dr. Torres",
|
|
69
69
|
placementPhase: "ongoing", internship: "ICU Practicum", specialization: "Critical Care", compliance: "Complete", daysUntilStart: 0,
|
|
70
|
-
readiness: "Ready", progressWeeksDone: 2, progressWeeksTotal: 12, endDate: "06/30/2026", lastCheckin: "
|
|
70
|
+
readiness: "Ready", progressWeeksDone: 2, progressWeeksTotal: 12, endDate: "06/30/2026", lastCheckin: "03/21/2026",
|
|
71
71
|
completionDate: "—", finalStatus: "—", rating: 0, suggestedToHire: "—",
|
|
72
72
|
},
|
|
73
73
|
{
|
|
@@ -88,7 +88,7 @@ export const ALL_PLACEMENTS: Placement[] = [
|
|
|
88
88
|
id: 7, student: "Priya Sharma", email: "p.sharma@college.edu", initials: "PS", program: "Nursing", site: "Harbor Medical",
|
|
89
89
|
siteAddress: "1200 W Harrison St, Chicago, IL", status: "confirmed", start: "03/22/2026", duration: "12 wks", supervisor: "Dr. Patel",
|
|
90
90
|
placementPhase: "ongoing", internship: "Pediatric Nursing", specialization: "Pediatrics", compliance: "Complete", daysUntilStart: 0,
|
|
91
|
-
readiness: "Ready", progressWeeksDone: 7, progressWeeksTotal: 12, endDate: "06/14/2026", lastCheckin: "
|
|
91
|
+
readiness: "Ready", progressWeeksDone: 7, progressWeeksTotal: 12, endDate: "06/14/2026", lastCheckin: "03/23/2026",
|
|
92
92
|
completionDate: "—", finalStatus: "—", rating: 0, suggestedToHire: "—", isNew: true,
|
|
93
93
|
},
|
|
94
94
|
{
|
|
@@ -102,7 +102,7 @@ export const ALL_PLACEMENTS: Placement[] = [
|
|
|
102
102
|
id: 9, student: "Lena Fischer", email: "l.fischer@school.edu", initials: "LF", program: "Nursing", site: "Westside Clinic",
|
|
103
103
|
siteAddress: "2525 S Michigan Ave, Chicago, IL", status: "confirmed", start: "03/18/2026", duration: "12 wks", supervisor: "Dr. Santos",
|
|
104
104
|
placementPhase: "ongoing", internship: "Primary Care RN", specialization: "Family Practice", compliance: "Complete", daysUntilStart: 0,
|
|
105
|
-
readiness: "Ready", progressWeeksDone: 8, progressWeeksTotal: 12, endDate: "06/10/2026", lastCheckin: "
|
|
105
|
+
readiness: "Ready", progressWeeksDone: 8, progressWeeksTotal: 12, endDate: "06/10/2026", lastCheckin: "03/24/2026",
|
|
106
106
|
completionDate: "—", finalStatus: "—", rating: 0, suggestedToHire: "—",
|
|
107
107
|
},
|
|
108
108
|
{
|
|
@@ -116,7 +116,7 @@ export const ALL_PLACEMENTS: Placement[] = [
|
|
|
116
116
|
id: 11, student: "Nina Patel", email: "n.patel@university.edu", initials: "NP", program: "Social Work", site: "Hope Community Ctr",
|
|
117
117
|
siteAddress: "3517 W Arthington St, Chicago, IL", status: "confirmed", start: "03/25/2026", duration: "10 wks", supervisor: "Ms. Torres",
|
|
118
118
|
placementPhase: "ongoing", internship: "School Social Work", specialization: "Youth Services", compliance: "Complete", daysUntilStart: 0,
|
|
119
|
-
readiness: "Ready", progressWeeksDone: 4, progressWeeksTotal: 10, endDate: "06/03/2026", lastCheckin: "
|
|
119
|
+
readiness: "Ready", progressWeeksDone: 4, progressWeeksTotal: 10, endDate: "06/03/2026", lastCheckin: "03/19/2026",
|
|
120
120
|
completionDate: "—", finalStatus: "—", rating: 0, suggestedToHire: "—",
|
|
121
121
|
},
|
|
122
122
|
{
|
|
@@ -130,28 +130,28 @@ export const ALL_PLACEMENTS: Placement[] = [
|
|
|
130
130
|
id: 13, student: "Alex Morgan", email: "a.morgan@college.edu", initials: "AM", program: "Nursing", site: "City Medical Center",
|
|
131
131
|
siteAddress: "1400 N Lake Shore Dr, Chicago, IL", status: "completed", start: "01/06/2026", duration: "12 wks", supervisor: "Dr. Patel",
|
|
132
132
|
placementPhase: "completed", internship: "Med-Surg Clinical II", specialization: "Adult Health", compliance: "Complete", daysUntilStart: 0,
|
|
133
|
-
readiness: "Ready", progressWeeksDone: 12, progressWeeksTotal: 12, endDate: "03/30/2026", lastCheckin: "
|
|
133
|
+
readiness: "Ready", progressWeeksDone: 12, progressWeeksTotal: 12, endDate: "03/30/2026", lastCheckin: "03/28/2026",
|
|
134
134
|
completionDate: "03/30/2026", finalStatus: "Passed", rating: 4.8, suggestedToHire: "Yes",
|
|
135
135
|
},
|
|
136
136
|
{
|
|
137
137
|
id: 14, student: "Jordan Lee", email: "j.lee@university.edu", initials: "JL", program: "Physical Therapy", site: "Metro Rehab",
|
|
138
138
|
siteAddress: "250 E Superior St, Chicago, IL", status: "completed", start: "11/04/2025", duration: "8 wks", supervisor: "Dr. Kim",
|
|
139
139
|
placementPhase: "completed", internship: "Inpatient PT", specialization: "Acute Care", compliance: "Complete", daysUntilStart: 0,
|
|
140
|
-
readiness: "Ready", progressWeeksDone: 8, progressWeeksTotal: 8, endDate: "12/30/2025", lastCheckin: "
|
|
140
|
+
readiness: "Ready", progressWeeksDone: 8, progressWeeksTotal: 8, endDate: "12/30/2025", lastCheckin: "12/28/2025",
|
|
141
141
|
completionDate: "12/30/2025", finalStatus: "Passed", rating: 4.2, suggestedToHire: "Yes",
|
|
142
142
|
},
|
|
143
143
|
{
|
|
144
144
|
id: 15, student: "Sam Rivera", email: "s.rivera@school.edu", initials: "SR", program: "Occupational Therapy", site: "Bay Area Health",
|
|
145
145
|
siteAddress: "3100 Telegraph Ave, Oakland, CA", status: "completed", start: "10/01/2025", duration: "10 wks", supervisor: "Dr. Nguyen",
|
|
146
146
|
placementPhase: "completed", internship: "Hand Therapy OT", specialization: "Hand Therapy", compliance: "Complete", daysUntilStart: 0,
|
|
147
|
-
readiness: "Ready", progressWeeksDone: 10, progressWeeksTotal: 10, endDate: "12/10/2025", lastCheckin: "
|
|
147
|
+
readiness: "Ready", progressWeeksDone: 10, progressWeeksTotal: 10, endDate: "12/10/2025", lastCheckin: "12/08/2025",
|
|
148
148
|
completionDate: "12/10/2025", finalStatus: "Incomplete", rating: 3.5, suggestedToHire: "No",
|
|
149
149
|
},
|
|
150
150
|
{
|
|
151
151
|
id: 16, student: "Taylor Brooks", email: "t.brooks@college.edu", initials: "TB", program: "Nursing", site: "Sunrise Hospital",
|
|
152
152
|
siteAddress: "5775 Wayzata Blvd, Minneapolis, MN", status: "completed", start: "09/02/2025", duration: "12 wks", supervisor: "Dr. Torres",
|
|
153
153
|
placementPhase: "completed", internship: "Labor & Delivery", specialization: "Women's Health", compliance: "Complete", daysUntilStart: 0,
|
|
154
|
-
readiness: "Ready", progressWeeksDone: 12, progressWeeksTotal: 12, endDate: "11/25/2025", lastCheckin: "
|
|
154
|
+
readiness: "Ready", progressWeeksDone: 12, progressWeeksTotal: 12, endDate: "11/25/2025", lastCheckin: "11/22/2025",
|
|
155
155
|
completionDate: "11/25/2025", finalStatus: "Passed", rating: 5, suggestedToHire: "Yes",
|
|
156
156
|
},
|
|
157
157
|
]
|
|
@@ -102,6 +102,13 @@ export const QUESTION_BANK_FOLDER_ICON_OPTIONS: readonly string[] = [
|
|
|
102
102
|
] as const
|
|
103
103
|
|
|
104
104
|
export const DEFAULT_QUESTION_BANK_FOLDERS: QuestionBankFolder[] = [
|
|
105
|
+
{
|
|
106
|
+
id: "fld-favorites",
|
|
107
|
+
name: "Favorites",
|
|
108
|
+
parentId: null,
|
|
109
|
+
icon: "fa-star",
|
|
110
|
+
colorKey: "warning",
|
|
111
|
+
},
|
|
105
112
|
{
|
|
106
113
|
id: "fld-clinical",
|
|
107
114
|
name: "Clinical",
|
|
@@ -6,9 +6,49 @@ import { stockPortraitUrl } from "@/lib/stock-portrait"
|
|
|
6
6
|
import type { PageHeaderCollaborator } from "@/components/page-header"
|
|
7
7
|
|
|
8
8
|
export const QUESTION_BANK_HEADER_COLLABORATORS: PageHeaderCollaborator[] = [
|
|
9
|
-
{
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
{
|
|
10
|
+
id: "1",
|
|
11
|
+
name: "Alex Morgan",
|
|
12
|
+
email: "alex.morgan@example.com",
|
|
13
|
+
imageUrl: stockPortraitUrl("qb-collab-alex"),
|
|
14
|
+
initials: "AM",
|
|
15
|
+
access: "owner",
|
|
16
|
+
roles: ["Director", "Faculty"],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "2",
|
|
20
|
+
name: "Jordan Lee",
|
|
21
|
+
email: "jordan.lee@example.com",
|
|
22
|
+
imageUrl: stockPortraitUrl("qb-collab-jordan"),
|
|
23
|
+
initials: "JL",
|
|
24
|
+
access: "editor",
|
|
25
|
+
roles: ["Program coordinator"],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "3",
|
|
29
|
+
name: "Sam Rivera",
|
|
30
|
+
email: "sam.rivera@example.com",
|
|
31
|
+
imageUrl: stockPortraitUrl("qb-collab-sam"),
|
|
32
|
+
initials: "SR",
|
|
33
|
+
access: "editor",
|
|
34
|
+
roles: ["Faculty"],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "4",
|
|
38
|
+
name: "Taylor Kim",
|
|
39
|
+
email: "taylor.kim@example.com",
|
|
40
|
+
imageUrl: stockPortraitUrl("qb-collab-taylor"),
|
|
41
|
+
initials: "TK",
|
|
42
|
+
access: "commenter",
|
|
43
|
+
roles: ["Faculty"],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "5",
|
|
47
|
+
name: "Riley Patel",
|
|
48
|
+
email: "riley.patel@example.com",
|
|
49
|
+
imageUrl: stockPortraitUrl("qb-collab-riley"),
|
|
50
|
+
initials: "RP",
|
|
51
|
+
access: "viewer",
|
|
52
|
+
roles: ["Program coordinator"],
|
|
53
|
+
},
|
|
14
54
|
]
|
|
@@ -15,8 +15,7 @@ export const QUESTION_TYPE_ABBREV: Record<QuestionBankType, string> = {
|
|
|
15
15
|
export function deriveQuestionItemCode(q: QuestionBankItem): string {
|
|
16
16
|
const raw = q.itemCode?.trim()
|
|
17
17
|
if (raw) return raw
|
|
18
|
-
|
|
19
|
-
return `QB-${String(n).padStart(3, "0")}`
|
|
18
|
+
return q.questionId
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
export function deriveBloomLevel(q: QuestionBankItem): string {
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import type { MetricInsight, MetricItem } from "@/components/key-metrics"
|
|
2
|
-
import type { QuestionBankItem } from "@/lib/mock/question-bank"
|
|
2
|
+
import type { QuestionBankItem, QuestionBankType } from "@/lib/mock/question-bank"
|
|
3
|
+
|
|
4
|
+
/** Point-biserial below this mock threshold counts as a psychometric review flag. */
|
|
5
|
+
const PBI_REVIEW_THRESHOLD = 0.2
|
|
6
|
+
|
|
7
|
+
const TYPE_LABEL: Record<QuestionBankType, string> = {
|
|
8
|
+
multiple_choice: "Multiple choice",
|
|
9
|
+
true_false: "True / false",
|
|
10
|
+
short_answer: "Short answer",
|
|
11
|
+
}
|
|
3
12
|
|
|
4
13
|
export function questionBankKpiMetrics(rows: QuestionBankItem[]): MetricItem[] {
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
14
|
+
const mcq = rows.filter(r => r.type === "multiple_choice").length
|
|
15
|
+
const tf = rows.filter(r => r.type === "true_false").length
|
|
16
|
+
const sa = rows.filter(r => r.type === "short_answer").length
|
|
17
|
+
const writtenTypes = tf + sa
|
|
18
|
+
const lowPbiFlags = rows.filter(r => r.pbi != null && r.pbi < PBI_REVIEW_THRESHOLD).length
|
|
8
19
|
|
|
9
20
|
return [
|
|
10
21
|
{
|
|
@@ -17,45 +28,46 @@ export function questionBankKpiMetrics(rows: QuestionBankItem[]): MetricItem[] {
|
|
|
17
28
|
metricVariant: "hero",
|
|
18
29
|
},
|
|
19
30
|
{
|
|
20
|
-
id: "
|
|
21
|
-
label:
|
|
22
|
-
value:
|
|
31
|
+
id: "mcq",
|
|
32
|
+
label: TYPE_LABEL.multiple_choice,
|
|
33
|
+
value: mcq,
|
|
23
34
|
delta: "—",
|
|
24
35
|
trend: "neutral",
|
|
25
36
|
href: "#",
|
|
26
37
|
},
|
|
27
38
|
{
|
|
28
|
-
id: "
|
|
29
|
-
label: "
|
|
30
|
-
value:
|
|
31
|
-
delta:
|
|
32
|
-
trend:
|
|
39
|
+
id: "written",
|
|
40
|
+
label: "True / false & short answer",
|
|
41
|
+
value: writtenTypes,
|
|
42
|
+
delta: "—",
|
|
43
|
+
trend: "neutral",
|
|
33
44
|
href: "#",
|
|
34
45
|
},
|
|
35
46
|
{
|
|
36
|
-
id: "
|
|
37
|
-
label: "
|
|
38
|
-
value:
|
|
39
|
-
delta: "—",
|
|
40
|
-
trend: "neutral",
|
|
47
|
+
id: "pbi-flags",
|
|
48
|
+
label: "Low PBI (review)",
|
|
49
|
+
value: lowPbiFlags,
|
|
50
|
+
delta: lowPbiFlags >= 2 ? "+1" : "—",
|
|
51
|
+
trend: lowPbiFlags >= 2 ? "up" : "neutral",
|
|
52
|
+
trendPolarity: "lower_is_better",
|
|
41
53
|
href: "#",
|
|
42
54
|
},
|
|
43
55
|
]
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
export function questionBankKpiInsight(rows: QuestionBankItem[]): MetricInsight {
|
|
47
|
-
const
|
|
48
|
-
const
|
|
59
|
+
const hard = rows.filter(r => r.difficulty === "hard").length
|
|
60
|
+
const topics = new Set(rows.map(r => r.topic)).size
|
|
49
61
|
return {
|
|
50
62
|
title: "Folder library",
|
|
51
63
|
description:
|
|
52
|
-
|
|
53
|
-
?
|
|
54
|
-
:
|
|
55
|
-
? `${
|
|
56
|
-
: "
|
|
57
|
-
href: "/question-bank",
|
|
58
|
-
severity:
|
|
64
|
+
rows.length === 0
|
|
65
|
+
? "Add questions to populate metrics and charts."
|
|
66
|
+
: hard > 3
|
|
67
|
+
? `${hard} hard items in this view — balance with easier items where learners need quick wins.`
|
|
68
|
+
: `${rows.length} question${rows.length === 1 ? "" : "s"} across ${topics} topic${topics === 1 ? "" : "s"} in the filtered set.`,
|
|
69
|
+
href: "/question-bank/library",
|
|
70
|
+
severity: hard > 3 ? "warning" : "info",
|
|
59
71
|
actionLabel: "Ask Leo",
|
|
60
72
|
}
|
|
61
73
|
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Mock question bank items — replace with API in production.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export type QuestionBankStatus = "published" | "draft" | "in_review"
|
|
6
5
|
export type QuestionBankType = "multiple_choice" | "true_false" | "short_answer"
|
|
7
6
|
export type QuestionBankDifficulty = "easy" | "medium" | "hard"
|
|
8
7
|
|
|
@@ -17,13 +16,16 @@ export type QuestionBankBloomLevel =
|
|
|
17
16
|
|
|
18
17
|
export interface QuestionBankItem extends Record<string, unknown> {
|
|
19
18
|
id: string
|
|
19
|
+
/** Stable human-facing identifier (catalog, search, citations). */
|
|
20
|
+
questionId: string
|
|
20
21
|
/** Short preview / stem */
|
|
21
22
|
stem: string
|
|
22
23
|
topic: string
|
|
23
24
|
type: QuestionBankType
|
|
24
25
|
difficulty: QuestionBankDifficulty
|
|
25
|
-
status: QuestionBankStatus
|
|
26
26
|
author: string
|
|
27
|
+
/** Work email for the primary author (demo; optional when API omits). */
|
|
28
|
+
authorEmail?: string
|
|
27
29
|
updatedAt: string
|
|
28
30
|
/** Folder tree id (`lib/mock/question-bank-folders.ts`). */
|
|
29
31
|
folderId: string
|
|
@@ -54,19 +56,27 @@ export interface QuestionBankItem extends Record<string, unknown> {
|
|
|
54
56
|
avgScoreCorrectPct?: number
|
|
55
57
|
/** Where / when the item was last used on an exam. */
|
|
56
58
|
lastUsedLabel?: string
|
|
59
|
+
/** Starred outside the Favorites folder (list landing demo). */
|
|
60
|
+
isStarred?: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** New mock rows — assign a unique `questionId` when creating client-side. */
|
|
64
|
+
export function newQuestionBankQuestionId(): string {
|
|
65
|
+
return `QB-NEW-${Date.now().toString(36).toUpperCase()}`
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
export const QUESTION_BANK_ITEMS: QuestionBankItem[] = [
|
|
60
69
|
{
|
|
61
70
|
id: "q1",
|
|
71
|
+
questionId: "QB-ANA-001",
|
|
62
72
|
stem: "Which nerve roots contribute to the brachial plexus?",
|
|
63
73
|
topic: "Anatomy",
|
|
64
74
|
type: "multiple_choice",
|
|
65
75
|
difficulty: "medium",
|
|
66
|
-
status: "published",
|
|
67
76
|
author: "Dr. Chen",
|
|
77
|
+
authorEmail: "mei.chen@demo.exxat.io",
|
|
68
78
|
updatedAt: "2026-03-28",
|
|
69
|
-
folderId: "fld-
|
|
79
|
+
folderId: "fld-favorites",
|
|
70
80
|
itemCode: "QB-ANA-001",
|
|
71
81
|
bloomLevel: "Apply",
|
|
72
82
|
tags: ["Brachial plexus", "Peripheral nerves", "Spine"],
|
|
@@ -88,36 +98,45 @@ export const QUESTION_BANK_ITEMS: QuestionBankItem[] = [
|
|
|
88
98
|
},
|
|
89
99
|
{
|
|
90
100
|
id: "q2",
|
|
101
|
+
questionId: "QB-CNL-002",
|
|
91
102
|
stem: "Document baseline vitals before administering contrast.",
|
|
92
103
|
topic: "Clinical skills",
|
|
93
104
|
type: "true_false",
|
|
94
105
|
difficulty: "easy",
|
|
95
|
-
status: "published",
|
|
96
106
|
author: "Jordan Lee",
|
|
107
|
+
authorEmail: "jordan.lee@demo.exxat.io",
|
|
97
108
|
updatedAt: "2026-03-27",
|
|
98
109
|
folderId: "fld-skills-lab",
|
|
110
|
+
isStarred: true,
|
|
111
|
+
pbi: 0.55,
|
|
99
112
|
},
|
|
100
113
|
{
|
|
101
114
|
id: "q3",
|
|
115
|
+
questionId: "QB-NEU-003",
|
|
102
116
|
stem: "List three red flags for cauda equina syndrome.",
|
|
103
117
|
topic: "Neurology",
|
|
104
118
|
type: "short_answer",
|
|
105
119
|
difficulty: "hard",
|
|
106
|
-
status: "in_review",
|
|
107
120
|
author: "Alex Rivera",
|
|
121
|
+
authorEmail: "alex.rivera@demo.exxat.io",
|
|
108
122
|
updatedAt: "2026-03-26",
|
|
109
123
|
folderId: "fld-science",
|
|
124
|
+
isStarred: true,
|
|
125
|
+
pbi: 0.14,
|
|
110
126
|
},
|
|
111
127
|
{
|
|
112
128
|
id: "q4",
|
|
129
|
+
questionId: "QB-ETH-004",
|
|
113
130
|
stem: "HIPAA permits disclosure to family without consent when…",
|
|
114
131
|
topic: "Ethics & law",
|
|
115
132
|
type: "multiple_choice",
|
|
116
133
|
difficulty: "medium",
|
|
117
|
-
status: "draft",
|
|
118
134
|
author: "Sam Patel",
|
|
135
|
+
authorEmail: "sam.patel@demo.exxat.io",
|
|
119
136
|
updatedAt: "2026-03-25",
|
|
120
137
|
folderId: "fld-ethics",
|
|
138
|
+
isStarred: true,
|
|
139
|
+
pbi: 0.19,
|
|
121
140
|
options: [
|
|
122
141
|
{ text: "Patient is incapacitated and disclosure is in their best interest", isCorrect: true },
|
|
123
142
|
{ text: "Family member requests the information", isCorrect: false },
|
|
@@ -127,23 +146,25 @@ export const QUESTION_BANK_ITEMS: QuestionBankItem[] = [
|
|
|
127
146
|
},
|
|
128
147
|
{
|
|
129
148
|
id: "q5",
|
|
149
|
+
questionId: "QB-ASM-005",
|
|
130
150
|
stem: "Calculate BMI given height and weight (metric).",
|
|
131
151
|
topic: "Assessment",
|
|
132
152
|
type: "short_answer",
|
|
133
153
|
difficulty: "easy",
|
|
134
|
-
status: "published",
|
|
135
154
|
author: "Dr. Chen",
|
|
155
|
+
authorEmail: "mei.chen@demo.exxat.io",
|
|
136
156
|
updatedAt: "2026-03-24",
|
|
137
|
-
folderId: "fld-
|
|
157
|
+
folderId: "fld-favorites",
|
|
138
158
|
},
|
|
139
159
|
{
|
|
140
160
|
id: "q6",
|
|
161
|
+
questionId: "QB-ICO-006",
|
|
141
162
|
stem: "Sterile field must be prepared before which step?",
|
|
142
163
|
topic: "Infection control",
|
|
143
164
|
type: "multiple_choice",
|
|
144
165
|
difficulty: "medium",
|
|
145
|
-
status: "published",
|
|
146
166
|
author: "Morgan Lee",
|
|
167
|
+
authorEmail: "morgan.lee@demo.exxat.io",
|
|
147
168
|
updatedAt: "2026-03-23",
|
|
148
169
|
folderId: "fld-ops",
|
|
149
170
|
options: [
|
|
@@ -155,67 +176,73 @@ export const QUESTION_BANK_ITEMS: QuestionBankItem[] = [
|
|
|
155
176
|
},
|
|
156
177
|
{
|
|
157
178
|
id: "q7",
|
|
179
|
+
questionId: "QB-DOC-007",
|
|
158
180
|
stem: "SOAP note: subjective section documents patient-reported data only.",
|
|
159
181
|
topic: "Documentation",
|
|
160
182
|
type: "true_false",
|
|
161
183
|
difficulty: "easy",
|
|
162
|
-
status: "draft",
|
|
163
184
|
author: "Casey Nguyen",
|
|
185
|
+
authorEmail: "casey.nguyen@demo.exxat.io",
|
|
164
186
|
updatedAt: "2026-03-22",
|
|
165
187
|
folderId: "fld-clinical",
|
|
166
188
|
},
|
|
167
189
|
{
|
|
168
190
|
id: "q8",
|
|
191
|
+
questionId: "QB-RAD-008",
|
|
169
192
|
stem: "Contrast MRI safety screening includes renal function when…",
|
|
170
193
|
topic: "Radiology",
|
|
171
194
|
type: "multiple_choice",
|
|
172
195
|
difficulty: "hard",
|
|
173
|
-
status: "in_review",
|
|
174
196
|
author: "Riley Johnson",
|
|
197
|
+
authorEmail: "riley.johnson@demo.exxat.io",
|
|
175
198
|
updatedAt: "2026-03-21",
|
|
176
199
|
folderId: "fld-science",
|
|
177
200
|
},
|
|
178
201
|
{
|
|
179
202
|
id: "q9",
|
|
203
|
+
questionId: "QB-COM-009",
|
|
180
204
|
stem: "Therapeutic communication: reflect feelings before offering solutions.",
|
|
181
205
|
topic: "Communication",
|
|
182
206
|
type: "true_false",
|
|
183
207
|
difficulty: "medium",
|
|
184
|
-
status: "published",
|
|
185
208
|
author: "Quinn Martinez",
|
|
209
|
+
authorEmail: "quinn.martinez@demo.exxat.io",
|
|
186
210
|
updatedAt: "2026-03-20",
|
|
187
211
|
folderId: "fld-clinical",
|
|
188
212
|
},
|
|
189
213
|
{
|
|
190
214
|
id: "q10",
|
|
215
|
+
questionId: "QB-PHA-010",
|
|
191
216
|
stem: "Pediatric dose calculation uses body surface area when…",
|
|
192
217
|
topic: "Pharmacology",
|
|
193
218
|
type: "short_answer",
|
|
194
219
|
difficulty: "hard",
|
|
195
|
-
status: "published",
|
|
196
220
|
author: "Dr. Chen",
|
|
221
|
+
authorEmail: "mei.chen@demo.exxat.io",
|
|
197
222
|
updatedAt: "2026-03-19",
|
|
198
223
|
folderId: "fld-science",
|
|
199
224
|
},
|
|
200
225
|
{
|
|
201
226
|
id: "q11",
|
|
227
|
+
questionId: "QB-SAF-011",
|
|
202
228
|
stem: "Fall risk assessment should be repeated after medication changes.",
|
|
203
229
|
topic: "Safety",
|
|
204
230
|
type: "true_false",
|
|
205
231
|
difficulty: "easy",
|
|
206
|
-
status: "draft",
|
|
207
232
|
author: "Taylor Brooks",
|
|
233
|
+
authorEmail: "taylor.brooks@demo.exxat.io",
|
|
208
234
|
updatedAt: "2026-03-18",
|
|
209
235
|
folderId: "fld-ops",
|
|
210
236
|
},
|
|
211
237
|
{
|
|
212
238
|
id: "q12",
|
|
239
|
+
questionId: "QB-ICO-012",
|
|
213
240
|
stem: "Describe hand hygiene moments (WHO five moments).",
|
|
214
241
|
topic: "Infection control",
|
|
215
242
|
type: "short_answer",
|
|
216
243
|
difficulty: "medium",
|
|
217
|
-
status: "in_review",
|
|
218
244
|
author: "Jordan Lee",
|
|
245
|
+
authorEmail: "jordan.lee@demo.exxat.io",
|
|
219
246
|
updatedAt: "2026-03-17",
|
|
220
247
|
folderId: "fld-ops",
|
|
221
248
|
},
|