@exxatdesignux/ui 0.0.6 → 0.0.7
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/bin/init.mjs +29 -0
- package/package.json +7 -2
- package/template/.nvmrc +1 -0
- package/template/.prettierignore +7 -0
- package/template/.prettierrc +11 -0
- package/template/AGENTS.md +485 -0
- package/template/Logo/Exxat_Prism.svg +39 -0
- package/template/Logo/Exxat_one.svg +36 -0
- package/template/README.md +58 -0
- package/template/app/(app)/compliance/page.tsx +10 -0
- package/template/app/(app)/dashboard/loading.tsx +18 -0
- package/template/app/(app)/dashboard/page.tsx +36 -0
- package/template/app/(app)/data-list/[id]/page.tsx +28 -0
- package/template/app/(app)/data-list/new/page.tsx +31 -0
- package/template/app/(app)/data-list/page.tsx +10 -0
- package/template/app/(app)/error.tsx +43 -0
- package/template/app/(app)/help/page.tsx +34 -0
- package/template/app/(app)/layout.tsx +54 -0
- package/template/app/(app)/loading.tsx +18 -0
- package/template/app/(app)/question-bank/page.tsx +10 -0
- package/template/app/(app)/rotations/page.tsx +15 -0
- package/template/app/(app)/settings/page.tsx +17 -0
- package/template/app/(app)/sites/all/page.tsx +13 -0
- package/template/app/(app)/team/page.tsx +10 -0
- package/template/app/favicon.ico +0 -0
- package/template/app/globals.css +1811 -0
- package/template/app/layout.tsx +95 -0
- package/template/app/page.tsx +9 -0
- package/template/components/.gitkeep +0 -0
- package/template/components/app-sidebar-dynamic.tsx +15 -0
- package/template/components/app-sidebar.tsx +901 -0
- package/template/components/ask-leo-composer.tsx +216 -0
- package/template/components/ask-leo-sidebar.tsx +509 -0
- package/template/components/chart-area-interactive.tsx +293 -0
- package/template/components/charts-overview.tsx +2321 -0
- package/template/components/command-menu-01.tsx +133 -0
- package/template/components/command-menu-02.tsx +386 -0
- package/template/components/command-menu.tsx +182 -0
- package/template/components/compliance-board-view.tsx +134 -0
- package/template/components/compliance-client.tsx +92 -0
- package/template/components/compliance-list-view.tsx +59 -0
- package/template/components/compliance-page-header.tsx +89 -0
- package/template/components/compliance-table.tsx +525 -0
- package/template/components/dashboard-onboarding-gallery.tsx +13 -0
- package/template/components/dashboard-onboarding.tsx +21 -0
- package/template/components/dashboard-promo-banner.tsx +67 -0
- package/template/components/dashboard-quota-progress-card.tsx +369 -0
- package/template/components/dashboard-report-charts.tsx +69 -0
- package/template/components/dashboard-section-heading.tsx +68 -0
- package/template/components/dashboard-tabs.tsx +598 -0
- package/template/components/data-list-client.tsx +239 -0
- package/template/components/data-list-table-cells.test.tsx +22 -0
- package/template/components/data-list-table-cells.tsx +173 -0
- package/template/components/data-list-table.tsx +879 -0
- package/template/components/data-table/filter-date-calendar.tsx +38 -0
- package/template/components/data-table/filter-text-value-input.tsx +77 -0
- package/template/components/data-table/index.tsx +1612 -0
- package/template/components/data-table/pagination.tsx +256 -0
- package/template/components/data-table/types.ts +91 -0
- package/template/components/data-table/use-table-state.ts +566 -0
- package/template/components/data-view-dashboard-charts-compliance.tsx +960 -0
- package/template/components/data-view-dashboard-charts-team.tsx +968 -0
- package/template/components/data-view-dashboard-charts.tsx +1668 -0
- package/template/components/data-views/board-card-primitives.tsx +93 -0
- package/template/components/data-views/index.ts +41 -0
- package/template/components/data-views/list-page-board-card.tsx +192 -0
- package/template/components/data-views/list-page-board-template.tsx +122 -0
- package/template/components/data-views/placement-board-card.tsx +262 -0
- package/template/components/export-drawer.tsx +375 -0
- package/template/components/exxat-product-logo.tsx +453 -0
- package/template/components/form-layout-01.tsx +131 -0
- package/template/components/getting-started.tsx +625 -0
- package/template/components/key-metrics.tsx +920 -0
- package/template/components/leo-insight-indicator.tsx +364 -0
- package/template/components/leo-typing-dots.tsx +121 -0
- package/template/components/list-hub-status-badge.tsx +51 -0
- package/template/components/list-page-dashboard-charts.tsx +18 -0
- package/template/components/nav-documents.tsx +89 -0
- package/template/components/nav-main.tsx +58 -0
- package/template/components/nav-secondary.tsx +64 -0
- package/template/components/nav-user.tsx +190 -0
- package/template/components/new-placement-back-btn.tsx +28 -0
- package/template/components/new-placement-form.tsx +1066 -0
- package/template/components/onboarding/index.ts +4 -0
- package/template/components/onboarding/onboarding-01.tsx +7 -0
- package/template/components/onboarding/onboarding-02.tsx +7 -0
- package/template/components/onboarding/onboarding-03.tsx +7 -0
- package/template/components/onboarding/onboarding-04.tsx +7 -0
- package/template/components/page-header.tsx +57 -0
- package/template/components/placement-detail.tsx +438 -0
- package/template/components/placements-board-view.tsx +404 -0
- package/template/components/placements-list-view.tsx +285 -0
- package/template/components/placements-page-header.tsx +160 -0
- package/template/components/placements-table-columns.tsx +639 -0
- package/template/components/product-switcher.tsx +116 -0
- package/template/components/question-bank-board-view.tsx +205 -0
- package/template/components/question-bank-client.tsx +77 -0
- package/template/components/question-bank-list-view.tsx +59 -0
- package/template/components/question-bank-page-header.tsx +89 -0
- package/template/components/question-bank-table.tsx +586 -0
- package/template/components/rotations-empty-state.tsx +47 -0
- package/template/components/rotations-panel-activator.tsx +8 -0
- package/template/components/secondary-nav.tsx +394 -0
- package/template/components/secondary-panel.tsx +239 -0
- package/template/components/section-cards.tsx +106 -0
- package/template/components/settings-appearance-card.tsx +424 -0
- package/template/components/settings-client.tsx +537 -0
- package/template/components/settings-form-row.tsx +42 -0
- package/template/components/sidebar-auto-collapse.tsx +23 -0
- package/template/components/sidebar-auto-open.tsx +18 -0
- package/template/components/sidebar-shell.tsx +37 -0
- package/template/components/site-header.tsx +93 -0
- package/template/components/sites-all-client.tsx +154 -0
- package/template/components/sites-board-view.tsx +67 -0
- package/template/components/sites-list-view.tsx +47 -0
- package/template/components/sites-table.tsx +312 -0
- package/template/components/system-banner-slot.tsx +66 -0
- package/template/components/table-properties/column-row.tsx +90 -0
- package/template/components/table-properties/draggable-list.ts +49 -0
- package/template/components/table-properties/drawer-button.tsx +231 -0
- package/template/components/table-properties/drawer.tsx +1102 -0
- package/template/components/table-properties/filter-card.tsx +251 -0
- package/template/components/table-properties/index.ts +22 -0
- package/template/components/table-properties/sort-card.tsx +59 -0
- package/template/components/table-properties/types.ts +124 -0
- package/template/components/task-list-panel.tsx +98 -0
- package/template/components/task-priority-badge.tsx +28 -0
- package/template/components/team-board-view.tsx +114 -0
- package/template/components/team-client.tsx +93 -0
- package/template/components/team-list-view.tsx +62 -0
- package/template/components/team-page-header.tsx +92 -0
- package/template/components/team-table.tsx +525 -0
- package/template/components/templates/list-page.tsx +576 -0
- package/template/components/templates/primary-page-template.tsx +56 -0
- package/template/components/theme-color-sync.tsx +32 -0
- package/template/components/theme-provider.tsx +71 -0
- package/template/components/tinted-icon-disc.tsx +53 -0
- package/template/components/ui/ai-thinking-surface.tsx +121 -0
- package/template/components/ui/avatar.tsx +1 -0
- package/template/components/ui/badge.tsx +1 -0
- package/template/components/ui/banner.tsx +1 -0
- package/template/components/ui/breadcrumb.tsx +1 -0
- package/template/components/ui/button.tsx +1 -0
- package/template/components/ui/calendar.tsx +1 -0
- package/template/components/ui/card.tsx +1 -0
- package/template/components/ui/chart.tsx +1 -0
- package/template/components/ui/checkbox.tsx +1 -0
- package/template/components/ui/coach-mark.tsx +1 -0
- package/template/components/ui/collapsible.tsx +1 -0
- package/template/components/ui/command.tsx +1 -0
- package/template/components/ui/date-picker-field.tsx +1 -0
- package/template/components/ui/dialog.tsx +1 -0
- package/template/components/ui/dot-pattern.tsx +159 -0
- package/template/components/ui/drag-handle-grip.tsx +1 -0
- package/template/components/ui/drawer.tsx +1 -0
- package/template/components/ui/dropdown-menu.tsx +1 -0
- package/template/components/ui/field.tsx +1 -0
- package/template/components/ui/form.tsx +1 -0
- package/template/components/ui/input-group.tsx +1 -0
- package/template/components/ui/input-mask.tsx +1 -0
- package/template/components/ui/input.tsx +1 -0
- package/template/components/ui/kbd.tsx +1 -0
- package/template/components/ui/label.tsx +1 -0
- package/template/components/ui/leo-icon.tsx +726 -0
- package/template/components/ui/payment-card-fields.tsx +1 -0
- package/template/components/ui/popover.tsx +1 -0
- package/template/components/ui/radio-group.tsx +1 -0
- package/template/components/ui/select.tsx +1 -0
- package/template/components/ui/selection-tile-grid.tsx +1 -0
- package/template/components/ui/separator.tsx +1 -0
- package/template/components/ui/sheet.tsx +1 -0
- package/template/components/ui/sidebar.tsx +1 -0
- package/template/components/ui/skeleton.tsx +1 -0
- package/template/components/ui/sonner.tsx +1 -0
- package/template/components/ui/status-badge.tsx +1 -0
- package/template/components/ui/table.tsx +1 -0
- package/template/components/ui/tabs.tsx +1 -0
- package/template/components/ui/textarea.tsx +1 -0
- package/template/components/ui/tip.tsx +1 -0
- package/template/components/ui/toggle-group.tsx +1 -0
- package/template/components/ui/toggle-switch.tsx +1 -0
- package/template/components/ui/toggle.tsx +1 -0
- package/template/components/ui/tooltip.tsx +1 -0
- package/template/components/ui/view-segmented-control.tsx +1 -0
- package/template/components.json +27 -0
- package/template/contexts/chart-variant-context.tsx +35 -0
- package/template/contexts/command-menu-context.tsx +28 -0
- package/template/contexts/dashboard-view-context.tsx +35 -0
- package/template/contexts/product-context.tsx +38 -0
- package/template/contexts/system-banner-context.tsx +127 -0
- package/template/docs/command-menu-pattern.md +45 -0
- package/template/docs/data-views-pattern.md +160 -0
- package/template/ecosystem.config.cjs +20 -0
- package/template/eslint.config.mjs +18 -0
- package/template/fontawesome-subset.manifest.json +190 -0
- package/template/hooks/.gitkeep +0 -0
- package/template/hooks/use-app-theme.ts +1 -0
- package/template/hooks/use-coach-mark.ts +1 -0
- package/template/hooks/use-mobile.ts +1 -0
- package/template/hooks/use-mod-key-label.ts +1 -0
- package/template/lib/.gitkeep +0 -0
- package/template/lib/ask-leo-route-context.ts +133 -0
- package/template/lib/chart-keyboard-selection.test.ts +20 -0
- package/template/lib/chart-keyboard-selection.ts +17 -0
- package/template/lib/chart-line-dash.ts +16 -0
- package/template/lib/coach-mark-registry.ts +68 -0
- package/template/lib/command-menu-config.ts +127 -0
- package/template/lib/command-menu-search-data.ts +44 -0
- package/template/lib/conditional-rule-match.ts +32 -0
- package/template/lib/dashboard-customize-coach-mark.ts +18 -0
- package/template/lib/dashboard-layout-merge.ts +63 -0
- package/template/lib/data-list-display-options.ts +35 -0
- package/template/lib/data-list-persistence.ts +280 -0
- package/template/lib/data-list-view-surface.ts +58 -0
- package/template/lib/data-list-view.ts +29 -0
- package/template/lib/data-view-dashboard-storage.ts +101 -0
- package/template/lib/date-filter.ts +8 -0
- package/template/lib/dev-log.test.ts +28 -0
- package/template/lib/dev-log.ts +8 -0
- package/template/lib/editable-target.ts +10 -0
- package/template/lib/floating-sheet-panel.ts +72 -0
- package/template/lib/initials-from-name.ts +7 -0
- package/template/lib/list-page-table-properties.ts +52 -0
- package/template/lib/list-status-badges.ts +168 -0
- package/template/lib/logo-dev.ts +12 -0
- package/template/lib/mock/compliance-kpi.ts +61 -0
- package/template/lib/mock/compliance.ts +146 -0
- package/template/lib/mock/dashboard.ts +105 -0
- package/template/lib/mock/navigation.tsx +231 -0
- package/template/lib/mock/placements-kpi.ts +134 -0
- package/template/lib/mock/placements.ts +183 -0
- package/template/lib/mock/question-bank-kpi.ts +61 -0
- package/template/lib/mock/question-bank.ts +142 -0
- package/template/lib/mock/sites-directory.ts +16 -0
- package/template/lib/mock/sites-kpi.ts +25 -0
- package/template/lib/mock/team-kpi.ts +60 -0
- package/template/lib/mock/team.ts +118 -0
- package/template/lib/motion-ui.ts +17 -0
- package/template/lib/placement-board-card-layout.ts +79 -0
- package/template/lib/placement-lifecycle.ts +5 -0
- package/template/lib/row-height.ts +10 -0
- package/template/lib/stock-portrait.ts +11 -0
- package/template/lib/utils.test.ts +13 -0
- package/template/lib/utils.ts +1 -0
- package/template/next.config.mjs +15 -0
- package/template/package.json +83 -0
- package/template/postcss.config.mjs +8 -0
- package/template/public/.gitkeep +0 -0
- package/template/public/Illustration/Rotation.svg +74 -0
- package/template/public/avatars/user.svg +11 -0
- package/template/public/favicon/favicon.ico +0 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/logos/exxat-one.svg +36 -0
- package/template/public/logos/exxat-prism.svg +39 -0
- package/template/public/mock-schools/emory.svg +4 -0
- package/template/public/mock-schools/rush.svg +4 -0
- package/template/scripts/fontawesome-subset-audit.mjs +190 -0
- package/template/scripts/pm2-startup-macos.sh +13 -0
- package/template/skills-lock.json +10 -0
- package/template/stores/app-store.ts +33 -0
- package/template/tests/setup.ts +1 -0
- package/template/tsconfig.json +35 -0
- package/template/types/react-payment-inputs.d.ts +19 -0
- package/template/vitest.config.ts +18 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PageHeader — Full-width content area header
|
|
3
|
+
*
|
|
4
|
+
* Sits at the top of a page's main content, BELOW the breadcrumb/topbar.
|
|
5
|
+
* Uses Ivy Presto (Adobe Fonts) for the title via font-heading CSS variable.
|
|
6
|
+
*
|
|
7
|
+
* WCAG 2.1 AA:
|
|
8
|
+
* ✓ <h1> landmark — one per page (WCAG 1.3.1)
|
|
9
|
+
* ✓ Sufficient colour contrast ≥ 4.5:1 on title + subtitle (SC 1.4.3)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as React from "react"
|
|
13
|
+
import { cn } from "@/lib/utils"
|
|
14
|
+
|
|
15
|
+
export interface PageHeaderProps {
|
|
16
|
+
/** Primary page title — rendered as <h1> in Ivy Presto serif */
|
|
17
|
+
title: string
|
|
18
|
+
/** Short descriptor or date shown below the title */
|
|
19
|
+
subtitle?: string
|
|
20
|
+
/** Optional slot for right-aligned actions (buttons, selectors, etc.) */
|
|
21
|
+
actions?: React.ReactNode
|
|
22
|
+
/** Extra className for the outer wrapper */
|
|
23
|
+
className?: string
|
|
24
|
+
/** When false, the title + subtitle are visually hidden (actions remain). */
|
|
25
|
+
showTitleBlock?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function PageHeader({ title, subtitle, actions, className, showTitleBlock = true }: PageHeaderProps) {
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
className={cn(
|
|
32
|
+
"flex flex-col gap-1 px-4 pt-2 pb-4 lg:px-6",
|
|
33
|
+
"sm:flex-row sm:items-end sm:gap-4",
|
|
34
|
+
showTitleBlock ? "sm:justify-between" : "sm:justify-end",
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
{/* Title block — hidden visually when showTitleBlock is false; keep h1 for a11y */}
|
|
39
|
+
<div className={cn("flex flex-col gap-0.5", !showTitleBlock && "sr-only")}>
|
|
40
|
+
<h1
|
|
41
|
+
className="text-2xl font-semibold tracking-tight leading-tight text-foreground"
|
|
42
|
+
style={{ fontFamily: "var(--font-heading)" }}
|
|
43
|
+
>
|
|
44
|
+
{title}
|
|
45
|
+
</h1>
|
|
46
|
+
{subtitle && (
|
|
47
|
+
<p className="text-sm text-muted-foreground leading-none">{subtitle}</p>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{/* Right-side actions — e.g. date picker, CTA buttons */}
|
|
52
|
+
{actions && (
|
|
53
|
+
<div className="flex items-center gap-2 shrink-0">{actions}</div>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { useState } from "react"
|
|
5
|
+
import { Badge } from "@/components/ui/badge"
|
|
6
|
+
import { Button } from "@/components/ui/button"
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
8
|
+
import { Tip } from "@/components/ui/tip"
|
|
9
|
+
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
|
10
|
+
import { Separator } from "@/components/ui/separator"
|
|
11
|
+
import {
|
|
12
|
+
DropdownMenu,
|
|
13
|
+
DropdownMenuContent,
|
|
14
|
+
DropdownMenuItem,
|
|
15
|
+
DropdownMenuSeparator,
|
|
16
|
+
DropdownMenuTrigger,
|
|
17
|
+
} from "@/components/ui/dropdown-menu"
|
|
18
|
+
import { cn } from "@/lib/utils"
|
|
19
|
+
import type { Placement } from "@/lib/mock/placements"
|
|
20
|
+
import { StatusBadge as PlacementStatusBadge } from "@/components/data-list-table-cells"
|
|
21
|
+
import { placementReadinessBadgeClass } from "@/lib/list-status-badges"
|
|
22
|
+
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
// Info row — <dl> pattern for structured data
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function InfoRow({
|
|
28
|
+
label,
|
|
29
|
+
value,
|
|
30
|
+
icon,
|
|
31
|
+
}: {
|
|
32
|
+
label: string
|
|
33
|
+
value: React.ReactNode
|
|
34
|
+
icon?: string
|
|
35
|
+
}) {
|
|
36
|
+
return (
|
|
37
|
+
<div className="flex items-start gap-3 py-2.5">
|
|
38
|
+
{icon && (
|
|
39
|
+
<i
|
|
40
|
+
className={cn(
|
|
41
|
+
"fa-light",
|
|
42
|
+
icon,
|
|
43
|
+
"text-muted-foreground text-[13px] mt-0.5 w-4 shrink-0"
|
|
44
|
+
)}
|
|
45
|
+
aria-hidden="true"
|
|
46
|
+
/>
|
|
47
|
+
)}
|
|
48
|
+
<dt className="text-sm text-muted-foreground w-32 shrink-0">{label}</dt>
|
|
49
|
+
<dd className="text-sm text-foreground">{value}</dd>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
// Tabs config
|
|
56
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
type TabId = "overview" | "schedule" | "compliance" | "activity"
|
|
59
|
+
|
|
60
|
+
const TABS: { id: TabId; label: string; icon: string }[] = [
|
|
61
|
+
{ id: "overview", label: "Overview", icon: "fa-circle-info" },
|
|
62
|
+
{ id: "schedule", label: "Schedule", icon: "fa-calendar-days" },
|
|
63
|
+
{ id: "compliance", label: "Compliance", icon: "fa-shield-check" },
|
|
64
|
+
{ id: "activity", label: "Activity", icon: "fa-clock-rotate-left" },
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
// Overview tab
|
|
69
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function OverviewTab({ placement }: { placement: Placement }) {
|
|
72
|
+
return (
|
|
73
|
+
<div className="grid gap-6 md:grid-cols-2" role="tabpanel" aria-label="Overview">
|
|
74
|
+
<Card>
|
|
75
|
+
<CardHeader>
|
|
76
|
+
<CardTitle className="text-base">Placement Info</CardTitle>
|
|
77
|
+
</CardHeader>
|
|
78
|
+
<CardContent>
|
|
79
|
+
<dl>
|
|
80
|
+
<InfoRow icon="fa-hospital" label="Site" value={placement.site} />
|
|
81
|
+
<InfoRow icon="fa-location-dot" label="Address" value={placement.siteAddress} />
|
|
82
|
+
<Separator />
|
|
83
|
+
<InfoRow icon="fa-briefcase" label="Internship" value={placement.internship} />
|
|
84
|
+
<InfoRow icon="fa-stethoscope" label="Specialization" value={placement.specialization} />
|
|
85
|
+
<InfoRow icon="fa-user-doctor" label="Supervisor" value={placement.supervisor} />
|
|
86
|
+
<InfoRow icon="fa-user-nurse" label="Preceptor" value={placement.supervisor} />
|
|
87
|
+
<Separator />
|
|
88
|
+
<InfoRow icon="fa-rotate" label="Rotation type" value="Clinical" />
|
|
89
|
+
<InfoRow icon="fa-graduation-cap" label="Credit hours" value="3" />
|
|
90
|
+
</dl>
|
|
91
|
+
</CardContent>
|
|
92
|
+
</Card>
|
|
93
|
+
|
|
94
|
+
<Card>
|
|
95
|
+
<CardHeader>
|
|
96
|
+
<CardTitle className="text-base">Supervisor & Notes</CardTitle>
|
|
97
|
+
</CardHeader>
|
|
98
|
+
<CardContent>
|
|
99
|
+
<dl>
|
|
100
|
+
<InfoRow
|
|
101
|
+
icon="fa-user"
|
|
102
|
+
label="Supervisor"
|
|
103
|
+
value={placement.supervisor}
|
|
104
|
+
/>
|
|
105
|
+
<InfoRow
|
|
106
|
+
icon="fa-envelope"
|
|
107
|
+
label="Email"
|
|
108
|
+
value={`${placement.supervisor.toLowerCase().replace(/\s|dr\.\s?/gi, ".")}@clinic.org`}
|
|
109
|
+
/>
|
|
110
|
+
<InfoRow icon="fa-phone" label="Phone" value="(312) 555-0147" />
|
|
111
|
+
<Separator />
|
|
112
|
+
<InfoRow
|
|
113
|
+
icon="fa-bullseye"
|
|
114
|
+
label="Learning objectives"
|
|
115
|
+
value="Develop clinical assessment skills and patient communication techniques in a supervised healthcare environment."
|
|
116
|
+
/>
|
|
117
|
+
<InfoRow
|
|
118
|
+
icon="fa-triangle-exclamation"
|
|
119
|
+
label="Special requirements"
|
|
120
|
+
value="Must complete CPR certification before start date. Scrubs required on-site."
|
|
121
|
+
/>
|
|
122
|
+
<InfoRow
|
|
123
|
+
icon="fa-note-sticky"
|
|
124
|
+
label="Notes"
|
|
125
|
+
value="Student has expressed interest in extending the placement if performance is satisfactory."
|
|
126
|
+
/>
|
|
127
|
+
</dl>
|
|
128
|
+
</CardContent>
|
|
129
|
+
</Card>
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
135
|
+
// Schedule tab
|
|
136
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
function ScheduleTab({ placement }: { placement: Placement }) {
|
|
139
|
+
const progressPct =
|
|
140
|
+
placement.progressWeeksTotal > 0
|
|
141
|
+
? Math.round(
|
|
142
|
+
(placement.progressWeeksDone / placement.progressWeeksTotal) * 100
|
|
143
|
+
)
|
|
144
|
+
: 0
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div role="tabpanel" aria-label="Schedule">
|
|
148
|
+
<Card>
|
|
149
|
+
<CardHeader>
|
|
150
|
+
<CardTitle className="text-base">Schedule</CardTitle>
|
|
151
|
+
</CardHeader>
|
|
152
|
+
<CardContent>
|
|
153
|
+
<dl className="grid gap-x-8 md:grid-cols-2">
|
|
154
|
+
<InfoRow icon="fa-calendar-days" label="Start date" value={placement.start} />
|
|
155
|
+
<InfoRow icon="fa-calendar-check" label="End date" value={placement.endDate} />
|
|
156
|
+
<InfoRow icon="fa-clock" label="Duration" value={placement.duration} />
|
|
157
|
+
<InfoRow icon="fa-hourglass-half" label="Hours/week" value="20" />
|
|
158
|
+
<InfoRow icon="fa-sun" label="Shift" value="Day" />
|
|
159
|
+
<InfoRow icon="fa-sigma" label="Total hours" value="240" />
|
|
160
|
+
<InfoRow icon="fa-building" label="Work arrangement" value="On-site" />
|
|
161
|
+
<InfoRow icon="fa-calendar-week" label="Weekends" value="No" />
|
|
162
|
+
</dl>
|
|
163
|
+
|
|
164
|
+
{placement.placementPhase === "ongoing" && (
|
|
165
|
+
<>
|
|
166
|
+
<Separator className="my-4" />
|
|
167
|
+
<div className="space-y-2">
|
|
168
|
+
<div className="flex items-center justify-between text-sm">
|
|
169
|
+
<span className="text-muted-foreground">Progress</span>
|
|
170
|
+
<span className="font-medium">
|
|
171
|
+
{placement.progressWeeksDone} / {placement.progressWeeksTotal} weeks ({progressPct}%)
|
|
172
|
+
</span>
|
|
173
|
+
</div>
|
|
174
|
+
<div
|
|
175
|
+
className="h-2 w-full rounded-full bg-muted overflow-hidden"
|
|
176
|
+
role="progressbar"
|
|
177
|
+
aria-valuenow={progressPct}
|
|
178
|
+
aria-valuemin={0}
|
|
179
|
+
aria-valuemax={100}
|
|
180
|
+
aria-label={`Placement progress: ${progressPct}%`}
|
|
181
|
+
>
|
|
182
|
+
<div
|
|
183
|
+
className="h-full rounded-full bg-primary transition-all"
|
|
184
|
+
style={{ width: `${progressPct}%` }}
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</>
|
|
189
|
+
)}
|
|
190
|
+
</CardContent>
|
|
191
|
+
</Card>
|
|
192
|
+
</div>
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
197
|
+
// Compliance tab
|
|
198
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
const COMPLIANCE_ITEMS = [
|
|
201
|
+
{ label: "Background check", passed: true },
|
|
202
|
+
{ label: "Immunizations", passed: true },
|
|
203
|
+
{ label: "HIPAA training", passed: true },
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
function ComplianceTab({ placement }: { placement: Placement }) {
|
|
207
|
+
return (
|
|
208
|
+
<div role="tabpanel" aria-label="Compliance">
|
|
209
|
+
<Card>
|
|
210
|
+
<CardHeader>
|
|
211
|
+
<CardTitle className="text-base">Compliance Checklist</CardTitle>
|
|
212
|
+
</CardHeader>
|
|
213
|
+
<CardContent className="space-y-4">
|
|
214
|
+
<ul className="space-y-3" aria-label="Compliance items">
|
|
215
|
+
{COMPLIANCE_ITEMS.map((item) => (
|
|
216
|
+
<li key={item.label} className="flex items-center gap-3 text-sm">
|
|
217
|
+
{item.passed ? (
|
|
218
|
+
<i
|
|
219
|
+
className="fa-solid fa-circle-check text-emerald-600 text-base"
|
|
220
|
+
aria-hidden="true"
|
|
221
|
+
/>
|
|
222
|
+
) : (
|
|
223
|
+
<i
|
|
224
|
+
className="fa-solid fa-circle-xmark text-red-500 text-base"
|
|
225
|
+
aria-hidden="true"
|
|
226
|
+
/>
|
|
227
|
+
)}
|
|
228
|
+
<span>{item.label}</span>
|
|
229
|
+
<span className="sr-only">
|
|
230
|
+
{item.passed ? "completed" : "incomplete"}
|
|
231
|
+
</span>
|
|
232
|
+
</li>
|
|
233
|
+
))}
|
|
234
|
+
</ul>
|
|
235
|
+
|
|
236
|
+
<Separator />
|
|
237
|
+
|
|
238
|
+
<dl>
|
|
239
|
+
<InfoRow
|
|
240
|
+
icon="fa-shield-check"
|
|
241
|
+
label="Readiness"
|
|
242
|
+
value={
|
|
243
|
+
<Badge
|
|
244
|
+
variant="outline"
|
|
245
|
+
className={cn(
|
|
246
|
+
"text-xs",
|
|
247
|
+
placementReadinessBadgeClass(placement.readiness),
|
|
248
|
+
)}
|
|
249
|
+
>
|
|
250
|
+
{placement.readiness}
|
|
251
|
+
</Badge>
|
|
252
|
+
}
|
|
253
|
+
/>
|
|
254
|
+
|
|
255
|
+
{placement.placementPhase === "upcoming" &&
|
|
256
|
+
placement.daysUntilStart > 0 && (
|
|
257
|
+
<InfoRow
|
|
258
|
+
icon="fa-calendar-days"
|
|
259
|
+
label="Days until start"
|
|
260
|
+
value={`${placement.daysUntilStart} days`}
|
|
261
|
+
/>
|
|
262
|
+
)}
|
|
263
|
+
|
|
264
|
+
<InfoRow
|
|
265
|
+
icon="fa-clipboard-check"
|
|
266
|
+
label="Compliance status"
|
|
267
|
+
value={placement.compliance}
|
|
268
|
+
/>
|
|
269
|
+
</dl>
|
|
270
|
+
</CardContent>
|
|
271
|
+
</Card>
|
|
272
|
+
</div>
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
277
|
+
// Activity tab
|
|
278
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
const MOCK_ACTIVITY = [
|
|
281
|
+
{
|
|
282
|
+
date: "03/23/2026",
|
|
283
|
+
description: "Supervisor evaluation submitted",
|
|
284
|
+
icon: "fa-file-check",
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
date: "03/22/2026",
|
|
288
|
+
description: "Weekly check-in completed",
|
|
289
|
+
icon: "fa-comments",
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
date: "03/18/2026",
|
|
293
|
+
description: "Hours log approved (20 hrs)",
|
|
294
|
+
icon: "fa-clock",
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
date: "03/15/2026",
|
|
298
|
+
description: "Placement started",
|
|
299
|
+
icon: "fa-play",
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
date: "03/10/2026",
|
|
303
|
+
description: "Compliance documents verified",
|
|
304
|
+
icon: "fa-shield-check",
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
date: "03/05/2026",
|
|
308
|
+
description: "Placement confirmed by coordinator",
|
|
309
|
+
icon: "fa-circle-check",
|
|
310
|
+
},
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
function ActivityTab() {
|
|
314
|
+
return (
|
|
315
|
+
<div role="tabpanel" aria-label="Activity">
|
|
316
|
+
<Card>
|
|
317
|
+
<CardHeader>
|
|
318
|
+
<CardTitle className="text-base">Activity Timeline</CardTitle>
|
|
319
|
+
</CardHeader>
|
|
320
|
+
<CardContent>
|
|
321
|
+
<ol className="relative border-s border-border ms-3" aria-label="Activity timeline">
|
|
322
|
+
{MOCK_ACTIVITY.map((item, idx) => (
|
|
323
|
+
<li key={idx} className="mb-6 ms-6 last:mb-0">
|
|
324
|
+
<span className="absolute -start-3 flex size-6 items-center justify-center rounded-full bg-muted ring-4 ring-background">
|
|
325
|
+
<i
|
|
326
|
+
className={cn("fa-light", item.icon, "text-xs text-muted-foreground")}
|
|
327
|
+
aria-hidden="true"
|
|
328
|
+
/>
|
|
329
|
+
</span>
|
|
330
|
+
<div className="flex flex-col gap-0.5">
|
|
331
|
+
<time className="text-xs text-muted-foreground">{item.date}</time>
|
|
332
|
+
<p className="text-sm text-foreground">{item.description}</p>
|
|
333
|
+
</div>
|
|
334
|
+
</li>
|
|
335
|
+
))}
|
|
336
|
+
</ol>
|
|
337
|
+
</CardContent>
|
|
338
|
+
</Card>
|
|
339
|
+
</div>
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
344
|
+
// Main component
|
|
345
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
export function PlacementDetail({ placement }: { placement: Placement }) {
|
|
348
|
+
const [activeTab, setActiveTab] = useState<TabId>("overview")
|
|
349
|
+
|
|
350
|
+
return (
|
|
351
|
+
<div className="space-y-6">
|
|
352
|
+
{/* Header */}
|
|
353
|
+
<div className="flex items-start gap-4">
|
|
354
|
+
<Avatar size="lg" className="size-12 shrink-0">
|
|
355
|
+
<AvatarFallback className="text-sm font-bold bg-primary/10 text-primary">
|
|
356
|
+
{placement.initials}
|
|
357
|
+
</AvatarFallback>
|
|
358
|
+
</Avatar>
|
|
359
|
+
<div className="flex-1 min-w-0">
|
|
360
|
+
<h1
|
|
361
|
+
className="text-xl font-semibold"
|
|
362
|
+
style={{ fontFamily: "var(--font-heading)" }}
|
|
363
|
+
>
|
|
364
|
+
{placement.student}
|
|
365
|
+
</h1>
|
|
366
|
+
<p className="text-sm text-muted-foreground">{placement.email}</p>
|
|
367
|
+
<div className="flex items-center gap-2 mt-2 flex-wrap">
|
|
368
|
+
<Badge variant="secondary">{placement.specialization}</Badge>
|
|
369
|
+
<PlacementStatusBadge status={placement.status} />
|
|
370
|
+
<Badge variant="outline">{placement.placementPhase}</Badge>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
374
|
+
<Tip label="Edit placement">
|
|
375
|
+
<Button size="sm" variant="outline">
|
|
376
|
+
<i className="fa-light fa-pen-to-square" aria-hidden="true" />{" "}
|
|
377
|
+
Edit
|
|
378
|
+
</Button>
|
|
379
|
+
</Tip>
|
|
380
|
+
<DropdownMenu>
|
|
381
|
+
<DropdownMenuTrigger asChild>
|
|
382
|
+
<Button size="sm" variant="outline" aria-label="More actions">
|
|
383
|
+
<i className="fa-light fa-ellipsis" aria-hidden="true" />
|
|
384
|
+
</Button>
|
|
385
|
+
</DropdownMenuTrigger>
|
|
386
|
+
<DropdownMenuContent align="end">
|
|
387
|
+
<DropdownMenuItem>
|
|
388
|
+
<i className="fa-light fa-download" aria-hidden="true" /> Export
|
|
389
|
+
</DropdownMenuItem>
|
|
390
|
+
<DropdownMenuItem>
|
|
391
|
+
<i className="fa-light fa-box-archive" aria-hidden="true" />{" "}
|
|
392
|
+
Archive
|
|
393
|
+
</DropdownMenuItem>
|
|
394
|
+
<DropdownMenuSeparator />
|
|
395
|
+
<DropdownMenuItem className="text-destructive">
|
|
396
|
+
<i className="fa-light fa-trash" aria-hidden="true" /> Delete
|
|
397
|
+
</DropdownMenuItem>
|
|
398
|
+
</DropdownMenuContent>
|
|
399
|
+
</DropdownMenu>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
|
|
403
|
+
{/* Tab bar */}
|
|
404
|
+
<div
|
|
405
|
+
role="tablist"
|
|
406
|
+
aria-label="Placement sections"
|
|
407
|
+
className="inline-flex items-center gap-0.5 rounded-lg bg-muted/60 p-[3px]"
|
|
408
|
+
>
|
|
409
|
+
{TABS.map((tab) => (
|
|
410
|
+
<button
|
|
411
|
+
key={tab.id}
|
|
412
|
+
role="tab"
|
|
413
|
+
aria-selected={activeTab === tab.id}
|
|
414
|
+
onClick={() => setActiveTab(tab.id)}
|
|
415
|
+
className={cn(
|
|
416
|
+
"px-3 py-1.5 text-xs rounded-md transition-all inline-flex items-center gap-1.5",
|
|
417
|
+
activeTab === tab.id
|
|
418
|
+
? "bg-background text-foreground font-medium shadow-sm"
|
|
419
|
+
: "text-muted-foreground hover:text-interactive-hover-foreground"
|
|
420
|
+
)}
|
|
421
|
+
>
|
|
422
|
+
<i
|
|
423
|
+
className={cn("fa-light", tab.icon, "text-xs")}
|
|
424
|
+
aria-hidden="true"
|
|
425
|
+
/>{" "}
|
|
426
|
+
{tab.label}
|
|
427
|
+
</button>
|
|
428
|
+
))}
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
{/* Tab content */}
|
|
432
|
+
{activeTab === "overview" && <OverviewTab placement={placement} />}
|
|
433
|
+
{activeTab === "schedule" && <ScheduleTab placement={placement} />}
|
|
434
|
+
{activeTab === "compliance" && <ComplianceTab placement={placement} />}
|
|
435
|
+
{activeTab === "activity" && <ActivityTab />}
|
|
436
|
+
</div>
|
|
437
|
+
)
|
|
438
|
+
}
|