@exxatdesignux/ui 0.3.0 → 0.4.1
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/CHANGELOG.md +701 -6
- package/README.md +138 -0
- package/bin/init.mjs +134 -31
- package/consumer-extras/cursor-rules/exxat-board-cards.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-centralized-list-dataset.mdc +2 -2
- package/consumer-extras/cursor-rules/exxat-collaboration-access.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-data-tables.mdc +2 -0
- package/consumer-extras/cursor-rules/exxat-dedicated-search-surfaces.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +3 -3
- package/consumer-extras/cursor-rules/exxat-library-hub-header.mdc +28 -0
- package/consumer-extras/cursor-rules/exxat-mono-ids.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-person-identity-display.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-primary-nav-secondary-panel.mdc +6 -6
- package/consumer-extras/cursor-rules/exxat-reuse-before-custom.mdc +1 -1
- package/consumer-extras/cursor-skills/exxat-board-cards/SKILL.md +2 -2
- package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -3
- package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +2 -2
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +7 -7
- package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-list-page-view-shells/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +4 -4
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +8 -8
- package/consumer-extras/cursor-skills/exxat-token-economy/SKILL.md +277 -0
- package/consumer-extras/handbook/HANDBOOK.md +2 -0
- package/consumer-extras/handbook/glossary.md +2 -1
- package/consumer-extras/handbook/reference-implementations.md +31 -4
- package/consumer-extras/patterns/collaboration-access-pattern.md +7 -7
- package/consumer-extras/patterns/data-views-pattern.md +18 -16
- package/consumer-extras/patterns/kpi-flat-band-pattern.md +2 -2
- package/dist/components/data-table/index.js +2 -2
- package/dist/components/data-table/index.js.map +1 -1
- package/dist/components/data-table/pagination.js +3 -3
- package/dist/components/data-table/pagination.js.map +1 -1
- package/dist/components/data-table/use-table-state.d.ts +1 -1
- package/dist/components/data-table/use-table-state.js.map +1 -1
- package/dist/components/data-views/data-row-list.js.map +1 -1
- package/dist/components/data-views/finder-panel-view.d.ts +1 -1
- package/dist/components/data-views/finder-panel-view.js.map +1 -1
- package/dist/components/data-views/hub-table.d.ts +9 -3
- package/dist/components/data-views/hub-table.js +262 -40
- package/dist/components/data-views/hub-table.js.map +1 -1
- package/dist/components/data-views/index.js +262 -40
- package/dist/components/data-views/index.js.map +1 -1
- package/dist/components/data-views/list-page-split-hub-tokens.d.ts +2 -2
- package/dist/components/data-views/list-page-split-hub-tokens.js.map +1 -1
- package/dist/components/data-views/list-page-tree-column-header.d.ts +1 -1
- package/dist/components/data-views/list-page-tree-column-header.js.map +1 -1
- package/dist/components/data-views/list-page-tree-panel-shell.js.map +1 -1
- package/dist/components/data-views/os-folder-glyph.d.ts +1 -1
- package/dist/components/data-views/os-folder-glyph.js.map +1 -1
- package/dist/components/ui/avatar.d.ts +1 -1
- package/dist/components/ui/key-metrics.js.map +1 -1
- package/dist/index.js +136 -39
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/components/data-table/index.tsx +2 -2
- package/src/components/data-table/pagination.tsx +5 -1
- package/src/components/data-table/use-table-state.ts +1 -1
- package/src/components/data-views/data-row-list.tsx +1 -1
- package/src/components/data-views/finder-panel-view.tsx +2 -2
- package/src/components/data-views/hub-table.tsx +149 -41
- package/src/components/data-views/list-page-split-hub-tokens.ts +2 -2
- package/src/components/data-views/list-page-tree-column-header.tsx +1 -1
- package/src/components/data-views/os-folder-glyph.tsx +1 -1
- package/src/components/ui/key-metrics.tsx +1 -1
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +8 -7
- package/template/.cursor/rules/exxat-accessibility.mdc +1 -1
- package/template/.cursor/rules/exxat-command-menu.mdc +1 -1
- package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +6 -6
- package/template/.cursor/rules/exxat-data-tables.mdc +3 -3
- package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +5 -5
- package/template/.cursor/rules/exxat-mono-ids.mdc +1 -1
- package/template/.cursor/rules/exxat-page-vs-drawer.mdc +1 -1
- package/template/.cursor/rules/exxat-table-properties-drawer.mdc +1 -1
- package/template/AGENTS.md +43 -37
- package/template/app/(app)/columns/page.tsx +11 -0
- package/template/app/(app)/library/all/page.tsx +11 -0
- package/template/app/(app)/library/find/page.tsx +12 -0
- package/template/app/(app)/{question-bank → library}/layout.tsx +16 -16
- package/template/app/(app)/library/list/page.tsx +12 -0
- package/template/app/(app)/{question-bank → library}/new/page.tsx +10 -10
- package/template/app/(app)/library/page.tsx +11 -0
- package/template/app/(app)/tokens-themes/page.tsx +11 -0
- package/template/components/ask-leo-composer.tsx +2 -2
- package/template/components/columns-client.tsx +158 -0
- package/template/components/columns-showcase.tsx +541 -0
- package/template/components/data-views/index.ts +32 -6
- package/template/components/data-views/{question-bank-folder-tree-branch.tsx → library-folder-tree-branch.tsx} +19 -19
- package/template/components/data-views/table-cells.tsx +673 -0
- package/template/components/folder-details-shell.tsx +11 -11
- package/template/components/hub-tree-panel-view.tsx +24 -24
- package/template/components/{question-bank-board-view.tsx → library-board-view.tsx} +44 -44
- package/template/components/{question-bank-client.tsx → library-client.tsx} +82 -82
- package/template/components/{question-bank-dashboard-charts.tsx → library-dashboard-charts.tsx} +14 -14
- package/template/components/{question-bank-favorite-button.tsx → library-favorite-button.tsx} +7 -7
- package/template/components/{question-bank-hub-client.tsx → library-hub-client.tsx} +43 -43
- package/template/components/{question-bank-new-folder-sheet.tsx → library-new-folder-sheet.tsx} +14 -14
- package/template/components/{question-bank-os-folder-view.tsx → library-os-folder-view.tsx} +31 -31
- package/template/components/{question-bank-page-header.tsx → library-page-header.tsx} +6 -6
- package/template/components/library-panel-activator.tsx +8 -0
- package/template/components/{question-bank-secondary-nav.tsx → library-secondary-nav.tsx} +60 -60
- package/template/components/{question-bank-table.tsx → library-table.tsx} +97 -97
- package/template/components/list-hub-status-badge.tsx +2 -2
- package/template/components/{new-question-composer.tsx → new-library-item-form.tsx} +37 -37
- package/template/components/sidebar/app-sidebar.tsx +61 -5
- package/template/components/sidebar/secondary-panel.tsx +109 -56
- package/template/components/sidebar/sidebar-auto-collapse.tsx +2 -2
- package/template/components/sidebar/sidebar-auto-open.tsx +2 -1
- package/template/components/table-properties/types.ts +1 -1
- package/template/components/templates/discovery-hub-template.tsx +1 -1
- package/template/components/templates/new-focus-template.tsx +2 -2
- package/template/components/templates/secondary-panel-hub-template.tsx +1 -1
- package/template/components/tokens-secondary-nav.tsx +192 -0
- package/template/components/tokens-themes-client.tsx +476 -0
- package/template/components/tokens-themes-section.tsx +386 -0
- package/template/docs/HANDBOOK.md +187 -0
- package/template/docs/blueprints/README.md +1 -1
- package/template/docs/blueprints/board-card.md +1 -1
- package/template/docs/blueprints/data-table.md +2 -2
- package/template/docs/blueprints/list-page-template.md +3 -3
- package/template/docs/blueprints/page-header.md +4 -4
- package/template/docs/collaboration-access-pattern.md +7 -7
- package/template/docs/component-selection-guide.md +1 -1
- package/template/docs/data-views-pattern.md +18 -16
- package/template/docs/glossary.md +58 -0
- package/template/docs/kpi-flat-band-pattern.md +3 -3
- package/template/docs/kpi-trend-pattern.md +18 -3
- package/template/docs/large-dataset-strategy.md +155 -0
- package/template/docs/library-hub-header-pattern.md +25 -0
- package/template/docs/migrations/_template.md +1 -1
- package/template/docs/reference-implementations.md +151 -0
- package/template/docs/token-taxonomy.md +1 -1
- package/template/docs/voice-and-tone.md +262 -0
- package/template/eslint.config.mjs +9 -39
- package/template/hooks/use-secondary-panel-hub-nav.ts +10 -10
- package/template/lib/ask-leo-route-context.ts +6 -18
- package/template/lib/coach-mark-registry.ts +0 -16
- package/template/lib/command-menu-config.ts +5 -12
- package/template/lib/command-menu-search-data.ts +8 -39
- package/template/lib/{question-bank-authoring.ts → library-authoring.ts} +89 -88
- package/template/lib/library-dedicated-search.ts +19 -0
- package/template/lib/library-hub-search.ts +90 -0
- package/template/lib/library-nav.ts +477 -0
- package/template/lib/library-recent-searches.ts +22 -0
- package/template/lib/{placements-supported-views.ts → library-supported-views.ts} +2 -2
- package/template/lib/list-status-badges.ts +16 -104
- package/template/lib/mock/dashboard.ts +1 -1
- package/template/lib/mock/{question-bank-folders.ts → library-folders.ts} +30 -30
- package/template/lib/mock/library-header-collaborators.ts +54 -0
- package/template/lib/mock/{question-bank-inspector.ts → library-inspector.ts} +29 -29
- package/template/lib/mock/{question-bank-kpi.ts → library-kpi.ts} +20 -20
- package/template/lib/mock/library.ts +249 -0
- package/template/lib/mock/navigation.tsx +32 -26
- package/template/lib/table-state-lifecycle.ts +1 -1
- package/template/next.config.mjs +7 -4
- package/template/package.json +0 -1
- package/tokens/hooks-index.json +2874 -0
- package/consumer-extras/cursor-rules/exxat-question-bank-hub-header.mdc +0 -28
- package/template/app/(app)/examples/page.tsx +0 -41
- package/template/app/(app)/question-bank/find/page.tsx +0 -12
- package/template/app/(app)/question-bank/library/page.tsx +0 -11
- package/template/app/(app)/question-bank/list/page.tsx +0 -12
- package/template/app/(app)/question-bank/page.tsx +0 -11
- package/template/components/compliance-board-view.tsx +0 -142
- package/template/components/compliance-client.tsx +0 -92
- package/template/components/compliance-page-header.tsx +0 -89
- package/template/components/compliance-table.tsx +0 -468
- package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
- package/template/components/data-view-dashboard-charts-team.tsx +0 -971
- package/template/components/data-view-dashboard-charts.tsx +0 -1503
- package/template/components/new-placement-back-btn.tsx +0 -28
- package/template/components/new-placement-form.tsx +0 -942
- package/template/components/placement-board-card.tsx +0 -250
- package/template/components/placement-detail.tsx +0 -438
- package/template/components/placements-board-view.tsx +0 -397
- package/template/components/placements-client.tsx +0 -220
- package/template/components/placements-list-view.tsx +0 -124
- package/template/components/placements-page-header.tsx +0 -166
- package/template/components/placements-table-cells.test.tsx +0 -22
- package/template/components/placements-table-cells.tsx +0 -173
- package/template/components/placements-table-columns.tsx +0 -210
- package/template/components/placements-table.tsx +0 -934
- package/template/components/question-bank-panel-activator.tsx +0 -8
- package/template/components/rotations-empty-state.tsx +0 -50
- package/template/components/rotations-panel-activator.tsx +0 -8
- package/template/components/sites-board-view.tsx +0 -67
- package/template/components/sites-client.tsx +0 -154
- package/template/components/sites-table.tsx +0 -249
- package/template/components/team-board-view.tsx +0 -122
- package/template/components/team-client.tsx +0 -100
- package/template/components/team-page-header.tsx +0 -92
- package/template/components/team-table.tsx +0 -553
- package/template/docs/question-bank-hub-header-pattern.md +0 -25
- package/template/lib/compliance-supported-views.ts +0 -10
- package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
- package/template/lib/mock/compliance-kpi.ts +0 -61
- package/template/lib/mock/compliance.ts +0 -146
- package/template/lib/mock/placements-kpi.ts +0 -134
- package/template/lib/mock/placements.ts +0 -176
- package/template/lib/mock/question-bank-header-collaborators.ts +0 -54
- package/template/lib/mock/question-bank.ts +0 -249
- package/template/lib/mock/sites-directory.ts +0 -16
- package/template/lib/mock/sites-kpi.ts +0 -25
- package/template/lib/mock/team-kpi.ts +0 -60
- package/template/lib/mock/team.ts +0 -118
- package/template/lib/placement-board-card-layout.ts +0 -79
- package/template/lib/question-bank-dedicated-search.ts +0 -19
- package/template/lib/question-bank-hub-search.ts +0 -90
- package/template/lib/question-bank-nav.ts +0 -477
- package/template/lib/question-bank-recent-searches.ts +0 -22
- package/template/lib/question-bank-supported-views.ts +0 -12
- package/template/lib/sites-supported-views.ts +0 -10
- package/template/lib/team-supported-views.ts +0 -10
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Placement-specific list row body — exported as `PlacementListRowContent`.
|
|
5
|
-
*
|
|
6
|
-
* Shell (DataRowList wrapping, empty state, virtualization) is now provided by
|
|
7
|
-
* `HubTable.renderListRow` (see `placements-table.tsx`) — this file owns only the
|
|
8
|
-
* placement-specific row body (avatar + title + status badge + schedule summary).
|
|
9
|
-
*
|
|
10
|
-
* Previously this file also exported `PlacementsListView` (which wrapped `DataRowList`);
|
|
11
|
-
* that wrapper was inlined into `placements-table.tsx` when `HubTable.renderListRow`
|
|
12
|
-
* landed, so all `BoardCardLifecycleTabId` / `scheduleKeysForTab` imports are gone.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { Placement } from "@/lib/mock/placements"
|
|
16
|
-
import { StatusBadge } from "@/components/placements-table-cells"
|
|
17
|
-
import { Badge } from "@/components/ui/badge"
|
|
18
|
-
import { ListPageBoardCard, ListPageBoardCardAvatar } from "@/components/data-views/list-page-board-card"
|
|
19
|
-
import {
|
|
20
|
-
isBoardFieldActive,
|
|
21
|
-
SCHEDULE_KEYS,
|
|
22
|
-
} from "@/lib/placement-board-card-layout"
|
|
23
|
-
import { getConditionalRowBackground } from "@/lib/conditional-rule-match"
|
|
24
|
-
import type { ConditionalRule } from "@/components/table-properties/types"
|
|
25
|
-
import type { ColumnDef } from "@/components/data-table/types"
|
|
26
|
-
|
|
27
|
-
/** Above this count, `DataRowList` virtualizes against the window scroll. */
|
|
28
|
-
export const PLACEMENT_LIST_VIRTUAL_ROWS_THRESHOLD = 80
|
|
29
|
-
/** Initial row height guess (px); `measureElement` refines for variable content. */
|
|
30
|
-
export const PLACEMENT_LIST_ESTIMATE_ROW_PX = 100
|
|
31
|
-
|
|
32
|
-
function scheduleSummary(row: Placement): string | null {
|
|
33
|
-
switch (row.placementPhase) {
|
|
34
|
-
case "upcoming":
|
|
35
|
-
return row.daysUntilStart > 0
|
|
36
|
-
? `${row.start} · Starts in ${row.daysUntilStart} days`
|
|
37
|
-
: row.start
|
|
38
|
-
case "ongoing":
|
|
39
|
-
return `${row.progressWeeksDone} / ${row.progressWeeksTotal} wks · Ends ${row.endDate}`
|
|
40
|
-
case "completed":
|
|
41
|
-
return [row.completionDate, row.finalStatus].filter(v => v && v !== "—").join(" · ") || null
|
|
42
|
-
default:
|
|
43
|
-
return [row.start, row.duration].filter(Boolean).join(" · ") || null
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function PlacementListRowContent({
|
|
48
|
-
row,
|
|
49
|
-
hiddenColKeys,
|
|
50
|
-
boardColumns,
|
|
51
|
-
conditionalRules,
|
|
52
|
-
onOpen,
|
|
53
|
-
}: {
|
|
54
|
-
row: Placement
|
|
55
|
-
hiddenColKeys: Set<string>
|
|
56
|
-
boardColumns: ColumnDef<Placement>[]
|
|
57
|
-
conditionalRules: ConditionalRule[] | undefined
|
|
58
|
-
onOpen: (id: number) => void
|
|
59
|
-
}) {
|
|
60
|
-
const ruleBg = getConditionalRowBackground(row, conditionalRules, boardColumns)
|
|
61
|
-
const showStudent = isBoardFieldActive("student", hiddenColKeys, boardColumns)
|
|
62
|
-
const showStatus = isBoardFieldActive("status", hiddenColKeys, boardColumns)
|
|
63
|
-
const showSite = isBoardFieldActive("site", hiddenColKeys, boardColumns)
|
|
64
|
-
const showSpec = isBoardFieldActive("specialization", hiddenColKeys, boardColumns)
|
|
65
|
-
const showInternship = isBoardFieldActive("internship", hiddenColKeys, boardColumns)
|
|
66
|
-
const showSchedule = SCHEDULE_KEYS.some(k => isBoardFieldActive(k, hiddenColKeys, boardColumns))
|
|
67
|
-
const schedule = showSchedule ? scheduleSummary(row) : null
|
|
68
|
-
|
|
69
|
-
const title = showStudent ? row.student : `Placement ${row.id}`
|
|
70
|
-
|
|
71
|
-
const leading = showStudent ? (
|
|
72
|
-
<ListPageBoardCardAvatar initials={row.initials} className="size-9" />
|
|
73
|
-
) : (
|
|
74
|
-
<span className="size-9 shrink-0 rounded-full bg-muted/80" aria-hidden />
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
const rowEnd = showStatus ? (
|
|
78
|
-
<div className="flex shrink-0 items-center gap-2 pt-0.5">
|
|
79
|
-
<StatusBadge status={row.status} surface="board" />
|
|
80
|
-
<i className="fa-light fa-chevron-right text-xs text-muted-foreground" aria-hidden />
|
|
81
|
-
</div>
|
|
82
|
-
) : (
|
|
83
|
-
<i className="fa-light fa-chevron-right mt-1 shrink-0 text-xs text-muted-foreground" aria-hidden />
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
return (
|
|
87
|
-
<ListPageBoardCard
|
|
88
|
-
layout="row"
|
|
89
|
-
leading={leading}
|
|
90
|
-
rowEnd={rowEnd}
|
|
91
|
-
isNew={row.isNew}
|
|
92
|
-
style={ruleBg ? { background: ruleBg } : undefined}
|
|
93
|
-
onClick={() => onOpen(row.id)}
|
|
94
|
-
>
|
|
95
|
-
<div className="space-y-1">
|
|
96
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
97
|
-
<span className="text-sm font-semibold text-foreground">{title}</span>
|
|
98
|
-
{row.isNew ? (
|
|
99
|
-
<Badge variant="secondary" className="h-5 px-1.5 text-xs font-medium">
|
|
100
|
-
New
|
|
101
|
-
</Badge>
|
|
102
|
-
) : null}
|
|
103
|
-
</div>
|
|
104
|
-
{showSite ? (
|
|
105
|
-
<p className="text-xs text-foreground/90">
|
|
106
|
-
<span className="font-medium">{row.site}</span>
|
|
107
|
-
{row.siteAddress ? (
|
|
108
|
-
<span className="text-muted-foreground"> · {row.siteAddress}</span>
|
|
109
|
-
) : null}
|
|
110
|
-
</p>
|
|
111
|
-
) : null}
|
|
112
|
-
{(showSpec || showInternship) ? (
|
|
113
|
-
<p className="text-xs text-muted-foreground">
|
|
114
|
-
{[showSpec ? row.specialization : null, showInternship ? row.internship : null]
|
|
115
|
-
.filter(Boolean)
|
|
116
|
-
.join(" · ")}
|
|
117
|
-
</p>
|
|
118
|
-
) : null}
|
|
119
|
-
{schedule ? <p className="text-xs text-muted-foreground tabular-nums">{schedule}</p> : null}
|
|
120
|
-
</div>
|
|
121
|
-
</ListPageBoardCard>
|
|
122
|
-
)
|
|
123
|
-
}
|
|
124
|
-
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import { Button } from "@/components/ui/button"
|
|
5
|
-
import { Kbd, KbdGroup } from "@/components/ui/kbd"
|
|
6
|
-
import { PageHeader } from "@/components/page-header"
|
|
7
|
-
import {
|
|
8
|
-
DropdownMenu,
|
|
9
|
-
DropdownMenuContent,
|
|
10
|
-
DropdownMenuItem,
|
|
11
|
-
DropdownMenuSeparator,
|
|
12
|
-
DropdownMenuTrigger,
|
|
13
|
-
Shortcut,
|
|
14
|
-
} from "@/components/ui/dropdown-menu"
|
|
15
|
-
import { Tip } from "@/components/ui/tip"
|
|
16
|
-
import { useAltKeyLabel, useModKeyLabel } from "@/hooks/use-mod-key-label"
|
|
17
|
-
import { isEditableTarget } from "@/lib/editable-target"
|
|
18
|
-
|
|
19
|
-
export interface PlacementsPageHeaderProps {
|
|
20
|
-
/** Main heading in the page header */
|
|
21
|
-
title?: string
|
|
22
|
-
/** Primary button label */
|
|
23
|
-
primaryCtaLabel?: string
|
|
24
|
-
/** Shown under the page title */
|
|
25
|
-
subtitle?: string
|
|
26
|
-
onNewPlacement: () => void
|
|
27
|
-
onExport: () => void
|
|
28
|
-
showMetrics: boolean
|
|
29
|
-
onToggleMetrics: () => void
|
|
30
|
-
/** When false, title + subtitle are hidden visually (Display options). */
|
|
31
|
-
showTitleBlock?: boolean
|
|
32
|
-
className?: string
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* List hub shell header — title, primary CTA, overflow menu (export, metrics).
|
|
37
|
-
* Reusable for any route that needs the same chrome.
|
|
38
|
-
*/
|
|
39
|
-
export function PlacementsPageHeader({
|
|
40
|
-
title = "Sample records",
|
|
41
|
-
primaryCtaLabel = "New row",
|
|
42
|
-
subtitle = "24 demo rows · Last updated now",
|
|
43
|
-
onNewPlacement,
|
|
44
|
-
onExport,
|
|
45
|
-
showMetrics,
|
|
46
|
-
onToggleMetrics,
|
|
47
|
-
showTitleBlock = true,
|
|
48
|
-
className,
|
|
49
|
-
}: PlacementsPageHeaderProps) {
|
|
50
|
-
const mod = useModKeyLabel()
|
|
51
|
-
const alt = useAltKeyLabel()
|
|
52
|
-
const [moreOpen, setMoreOpen] = React.useState(false)
|
|
53
|
-
|
|
54
|
-
/** ⌘⌥N / Ctrl+Alt+N — avoids ⌘⇧N / Ctrl+Shift+N (private/incognito windows). */
|
|
55
|
-
React.useEffect(() => {
|
|
56
|
-
function onKey(e: KeyboardEvent) {
|
|
57
|
-
if (!e.altKey || (!e.metaKey && !e.ctrlKey)) return
|
|
58
|
-
if (e.key.toLowerCase() !== "n") return
|
|
59
|
-
if (isEditableTarget(e.target)) return
|
|
60
|
-
e.preventDefault()
|
|
61
|
-
onNewPlacement()
|
|
62
|
-
}
|
|
63
|
-
document.addEventListener("keydown", onKey)
|
|
64
|
-
return () => document.removeEventListener("keydown", onKey)
|
|
65
|
-
}, [onNewPlacement])
|
|
66
|
-
|
|
67
|
-
/** ⌘⌥M / Ctrl+Alt+M — avoids ⌘⇧O / Ctrl+Shift+O (bookmark manager in Chromium). */
|
|
68
|
-
React.useEffect(() => {
|
|
69
|
-
function onKey(e: KeyboardEvent) {
|
|
70
|
-
if (!e.altKey || (!e.metaKey && !e.ctrlKey)) return
|
|
71
|
-
if (e.key.toLowerCase() !== "m") return
|
|
72
|
-
if (isEditableTarget(e.target)) return
|
|
73
|
-
e.preventDefault()
|
|
74
|
-
setMoreOpen(o => !o)
|
|
75
|
-
}
|
|
76
|
-
document.addEventListener("keydown", onKey)
|
|
77
|
-
return () => document.removeEventListener("keydown", onKey)
|
|
78
|
-
}, [])
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<>
|
|
82
|
-
<Shortcut keys="⌘⇧E" onInvoke={onExport} />
|
|
83
|
-
<Shortcut keys="⌘⌥H" onInvoke={onToggleMetrics} />
|
|
84
|
-
<PageHeader
|
|
85
|
-
title={title}
|
|
86
|
-
subtitle={subtitle}
|
|
87
|
-
className={className}
|
|
88
|
-
showTitleBlock={showTitleBlock}
|
|
89
|
-
actions={
|
|
90
|
-
<div className="flex items-center gap-2" role="group" aria-label="Primary list actions">
|
|
91
|
-
<Tip
|
|
92
|
-
side="bottom"
|
|
93
|
-
label={
|
|
94
|
-
<>
|
|
95
|
-
<span>{primaryCtaLabel}</span>
|
|
96
|
-
<KbdGroup>
|
|
97
|
-
<Kbd>{mod}</Kbd>
|
|
98
|
-
<Kbd>{alt}</Kbd>
|
|
99
|
-
<Kbd>N</Kbd>
|
|
100
|
-
</KbdGroup>
|
|
101
|
-
</>
|
|
102
|
-
}
|
|
103
|
-
>
|
|
104
|
-
<Button size="lg" onClick={onNewPlacement}>
|
|
105
|
-
<i className="fa-light fa-plus" aria-hidden="true" />
|
|
106
|
-
{primaryCtaLabel}
|
|
107
|
-
</Button>
|
|
108
|
-
</Tip>
|
|
109
|
-
<DropdownMenu open={moreOpen} onOpenChange={setMoreOpen}>
|
|
110
|
-
<Tip
|
|
111
|
-
side="bottom"
|
|
112
|
-
label={
|
|
113
|
-
<>
|
|
114
|
-
<span>More actions</span>
|
|
115
|
-
<KbdGroup>
|
|
116
|
-
<Kbd>{mod}</Kbd>
|
|
117
|
-
<Kbd>{alt}</Kbd>
|
|
118
|
-
<Kbd>M</Kbd>
|
|
119
|
-
</KbdGroup>
|
|
120
|
-
</>
|
|
121
|
-
}
|
|
122
|
-
>
|
|
123
|
-
<DropdownMenuTrigger asChild>
|
|
124
|
-
<Button
|
|
125
|
-
size="lg"
|
|
126
|
-
variant="outline"
|
|
127
|
-
className="aspect-square px-0"
|
|
128
|
-
aria-label="More actions"
|
|
129
|
-
>
|
|
130
|
-
<i className="fa-light fa-ellipsis text-base" aria-hidden="true" />
|
|
131
|
-
</Button>
|
|
132
|
-
</DropdownMenuTrigger>
|
|
133
|
-
</Tip>
|
|
134
|
-
<DropdownMenuContent align="end">
|
|
135
|
-
<DropdownMenuItem
|
|
136
|
-
shortcut="⌘⇧E"
|
|
137
|
-
onSelect={() => {
|
|
138
|
-
/* Defer past Radix menu close + focus restore so Export Sheet mounts reliably. */
|
|
139
|
-
window.setTimeout(() => onExport(), 0)
|
|
140
|
-
}}
|
|
141
|
-
>
|
|
142
|
-
<i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
|
|
143
|
-
Export
|
|
144
|
-
</DropdownMenuItem>
|
|
145
|
-
<DropdownMenuSeparator />
|
|
146
|
-
<DropdownMenuItem
|
|
147
|
-
shortcut="⌘⌥H"
|
|
148
|
-
onSelect={() => {
|
|
149
|
-
window.setTimeout(() => onToggleMetrics(), 0)
|
|
150
|
-
}}
|
|
151
|
-
>
|
|
152
|
-
<i
|
|
153
|
-
className={`fa-light ${showMetrics ? "fa-eye-slash" : "fa-eye"}`}
|
|
154
|
-
aria-hidden="true"
|
|
155
|
-
/>
|
|
156
|
-
{showMetrics ? "Hide metric section" : "Show metric section"}
|
|
157
|
-
</DropdownMenuItem>
|
|
158
|
-
</DropdownMenuContent>
|
|
159
|
-
</DropdownMenu>
|
|
160
|
-
</div>
|
|
161
|
-
}
|
|
162
|
-
/>
|
|
163
|
-
</>
|
|
164
|
-
)
|
|
165
|
-
}
|
|
166
|
-
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest"
|
|
2
|
-
import { render, screen } from "@testing-library/react"
|
|
3
|
-
|
|
4
|
-
import { HireBadge, ReadinessBadge, StatusBadge } from "./placements-table-cells"
|
|
5
|
-
|
|
6
|
-
describe("placements-table-cells", () => {
|
|
7
|
-
it("renders StatusBadge label for confirmed", () => {
|
|
8
|
-
render(<StatusBadge status="confirmed" />)
|
|
9
|
-
expect(screen.getByText("Confirmed")).toBeInTheDocument()
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
it("ReadinessBadge uses destructive variant for risk copy", () => {
|
|
13
|
-
const { container } = render(<ReadinessBadge value="At risk" />)
|
|
14
|
-
expect(container.querySelector("[data-slot='badge']")).toBeTruthy()
|
|
15
|
-
expect(screen.getByText("At risk")).toBeInTheDocument()
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it("HireBadge shows em dash for empty", () => {
|
|
19
|
-
render(<HireBadge value="" />)
|
|
20
|
-
expect(screen.getByText("—")).toBeInTheDocument()
|
|
21
|
-
})
|
|
22
|
-
})
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Placement table cell primitives — extracted from placements-table for reuse and easier testing.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import * as React from "react"
|
|
8
|
-
import { Badge } from "@/components/ui/badge"
|
|
9
|
-
import { Button } from "@/components/ui/button"
|
|
10
|
-
import { Tip } from "@/components/ui/tip"
|
|
11
|
-
import {
|
|
12
|
-
DropdownMenu,
|
|
13
|
-
DropdownMenuContent,
|
|
14
|
-
DropdownMenuItem,
|
|
15
|
-
DropdownMenuSeparator,
|
|
16
|
-
DropdownMenuTrigger,
|
|
17
|
-
} from "@/components/ui/dropdown-menu"
|
|
18
|
-
import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
|
|
19
|
-
import {
|
|
20
|
-
PLACEMENT_STATUS_BADGE_CLASS,
|
|
21
|
-
PLACEMENT_STATUS_ICON,
|
|
22
|
-
PLACEMENT_STATUS_LABEL,
|
|
23
|
-
} from "@/lib/list-status-badges"
|
|
24
|
-
import { AvatarInitials } from "@/components/ui/avatar"
|
|
25
|
-
import type { Placement, Status } from "@/lib/mock/placements"
|
|
26
|
-
|
|
27
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
-
// Placement status — same maps + shell as other list hubs (`list-status-badges`)
|
|
29
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
-
|
|
31
|
-
function isPlacementStatus(v: string): v is Status {
|
|
32
|
-
return v in PLACEMENT_STATUS_LABEL
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function StatusBadge({
|
|
36
|
-
status,
|
|
37
|
-
surface = "table",
|
|
38
|
-
}: {
|
|
39
|
-
status: Status | string
|
|
40
|
-
surface?: "table" | "board"
|
|
41
|
-
}) {
|
|
42
|
-
if (!isPlacementStatus(status)) {
|
|
43
|
-
return (
|
|
44
|
-
<Badge variant="outline" className="text-xs shrink-0">
|
|
45
|
-
{String(status)}
|
|
46
|
-
</Badge>
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
|
-
return (
|
|
50
|
-
<ListHubStatusBadge
|
|
51
|
-
surface={surface}
|
|
52
|
-
label={PLACEMENT_STATUS_LABEL[status]}
|
|
53
|
-
tintClassName={PLACEMENT_STATUS_BADGE_CLASS[status]}
|
|
54
|
-
icon={PLACEMENT_STATUS_ICON[status]}
|
|
55
|
-
/>
|
|
56
|
-
)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function AvatarCircle({ initials }: { initials: string }) {
|
|
60
|
-
return (
|
|
61
|
-
<AvatarInitials
|
|
62
|
-
initials={initials}
|
|
63
|
-
className="size-7 shrink-0 text-xs"
|
|
64
|
-
fallbackClassName="text-xs"
|
|
65
|
-
/>
|
|
66
|
-
)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function WeeksProgressCell({ row }: { row: Placement }) {
|
|
70
|
-
const { progressWeeksDone, progressWeeksTotal } = row
|
|
71
|
-
const total = Math.max(1, progressWeeksTotal)
|
|
72
|
-
const pct = Math.min(100, Math.round((progressWeeksDone / total) * 100))
|
|
73
|
-
return (
|
|
74
|
-
<div className="flex min-w-[128px] max-w-[200px] flex-col gap-1.5">
|
|
75
|
-
<div className="h-2 overflow-hidden rounded-full bg-muted">
|
|
76
|
-
<div
|
|
77
|
-
className="h-full rounded-full bg-primary transition-[width]"
|
|
78
|
-
style={{ width: `${pct}%` }}
|
|
79
|
-
/>
|
|
80
|
-
</div>
|
|
81
|
-
<span className="text-xs tabular-nums text-muted-foreground">
|
|
82
|
-
{progressWeeksDone} / {progressWeeksTotal} wks
|
|
83
|
-
</span>
|
|
84
|
-
</div>
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function ReadinessBadge({ value }: { value: string }) {
|
|
89
|
-
const lower = value.toLowerCase()
|
|
90
|
-
const variant =
|
|
91
|
-
lower.includes("risk") || lower.includes("blocked")
|
|
92
|
-
? "destructive"
|
|
93
|
-
: lower.includes("review")
|
|
94
|
-
? "secondary"
|
|
95
|
-
: "outline"
|
|
96
|
-
return (
|
|
97
|
-
<Badge variant={variant} className="h-6 px-2 py-1 text-xs font-medium leading-none">
|
|
98
|
-
{value}
|
|
99
|
-
</Badge>
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function HireBadge({ value }: { value: string }) {
|
|
104
|
-
if (value === "—" || !value) return <span className="text-sm text-muted-foreground">—</span>
|
|
105
|
-
const yes = value.toLowerCase() === "yes"
|
|
106
|
-
return (
|
|
107
|
-
<Badge
|
|
108
|
-
variant={yes ? "default" : "secondary"}
|
|
109
|
-
className="h-6 border-0 px-2 py-1 text-xs font-medium leading-none"
|
|
110
|
-
>
|
|
111
|
-
{value}
|
|
112
|
-
</Badge>
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
117
|
-
// Row actions
|
|
118
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
119
|
-
|
|
120
|
-
export interface RowActionDef {
|
|
121
|
-
label: string
|
|
122
|
-
icon: string
|
|
123
|
-
onClick: (row: Placement) => void
|
|
124
|
-
variant?: "destructive"
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export const PLACEMENT_ROW_ACTIONS: RowActionDef[] = [
|
|
128
|
-
{ label: "Edit", icon: "fa-pen-to-square", onClick: _row => {} },
|
|
129
|
-
{ label: "Open", icon: "fa-arrow-up-right", onClick: _row => {} },
|
|
130
|
-
{ label: "Delete", icon: "fa-trash", onClick: _row => {}, variant: "destructive" },
|
|
131
|
-
]
|
|
132
|
-
|
|
133
|
-
export function RowActions({ row, actions }: { row: Placement; actions: RowActionDef[] }) {
|
|
134
|
-
if (!actions.length) return null
|
|
135
|
-
|
|
136
|
-
if (actions.length === 1) {
|
|
137
|
-
const a = actions[0]
|
|
138
|
-
return (
|
|
139
|
-
<Tip label={a.label} side="top">
|
|
140
|
-
<Button size="icon-sm" variant="ghost" aria-label={`${a.label} ${row.student}`}
|
|
141
|
-
onClick={() => a.onClick(row)}>
|
|
142
|
-
<i className={`fa-light ${a.icon} text-sm`} aria-hidden="true" />
|
|
143
|
-
</Button>
|
|
144
|
-
</Tip>
|
|
145
|
-
)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return (
|
|
149
|
-
<DropdownMenu>
|
|
150
|
-
<Tip label={`More options for ${row.student}`} side="top">
|
|
151
|
-
<DropdownMenuTrigger asChild>
|
|
152
|
-
<Button size="icon-sm" variant="ghost" aria-label={`More options for ${row.student}`}>
|
|
153
|
-
<i className="fa-light fa-ellipsis text-sm" aria-hidden="true" />
|
|
154
|
-
</Button>
|
|
155
|
-
</DropdownMenuTrigger>
|
|
156
|
-
</Tip>
|
|
157
|
-
<DropdownMenuContent align="end">
|
|
158
|
-
{actions.map((a, i) => (
|
|
159
|
-
<React.Fragment key={a.label}>
|
|
160
|
-
{a.variant === "destructive" && i > 0 && <DropdownMenuSeparator />}
|
|
161
|
-
<DropdownMenuItem
|
|
162
|
-
onClick={() => a.onClick(row)}
|
|
163
|
-
className={a.variant === "destructive" ? "text-destructive focus:text-destructive" : ""}
|
|
164
|
-
>
|
|
165
|
-
<i className={`fa-light ${a.icon}`} aria-hidden="true" />
|
|
166
|
-
{a.label}
|
|
167
|
-
</DropdownMenuItem>
|
|
168
|
-
</React.Fragment>
|
|
169
|
-
))}
|
|
170
|
-
</DropdownMenuContent>
|
|
171
|
-
</DropdownMenu>
|
|
172
|
-
)
|
|
173
|
-
}
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Placements columns, empty state, and Properties drawer label.
|
|
5
|
-
* Owned by the placements feature (`PlacementsClient` / `/data-list`); consumed by `PlacementsTable`.
|
|
6
|
-
*
|
|
7
|
-
* NOTE: Lifecycle parameterisation (all/upcoming/ongoing/completed) was removed.
|
|
8
|
-
* One canonical column set is exposed via `getPlacementColumns()`.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { FilterFieldDef, FilterOperator } from "@/components/table-properties/types"
|
|
12
|
-
import type { ColumnDef } from "@/components/data-table/types"
|
|
13
|
-
import {
|
|
14
|
-
AvatarCircle,
|
|
15
|
-
PLACEMENT_ROW_ACTIONS,
|
|
16
|
-
RowActions,
|
|
17
|
-
StatusBadge,
|
|
18
|
-
} from "@/components/placements-table-cells"
|
|
19
|
-
import { uniquePlacementFieldOptions, type Placement } from "@/lib/mock/placements"
|
|
20
|
-
|
|
21
|
-
const COLUMN_SELECT: ColumnDef<Placement> = {
|
|
22
|
-
key: "select",
|
|
23
|
-
label: "",
|
|
24
|
-
width: 40,
|
|
25
|
-
minWidth: 40,
|
|
26
|
-
defaultPin: "left",
|
|
27
|
-
lockPin: true,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const COLUMN_ACTIONS: ColumnDef<Placement> = {
|
|
31
|
-
key: "actions",
|
|
32
|
-
label: "",
|
|
33
|
-
width: 48,
|
|
34
|
-
minWidth: 48,
|
|
35
|
-
defaultPin: "right",
|
|
36
|
-
lockPin: true,
|
|
37
|
-
cell: (row) => (
|
|
38
|
-
<div className="flex items-center justify-center">
|
|
39
|
-
<RowActions row={row} actions={PLACEMENT_ROW_ACTIONS} />
|
|
40
|
-
</div>
|
|
41
|
-
),
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const CELL_STUDENT: ColumnDef<Placement>["cell"] = (row) => (
|
|
45
|
-
<div className="flex items-center gap-2.5 min-w-0">
|
|
46
|
-
<AvatarCircle initials={row.initials} />
|
|
47
|
-
<div className="flex flex-col min-w-0">
|
|
48
|
-
<span className="font-medium text-foreground text-sm leading-tight truncate">
|
|
49
|
-
{row.student}
|
|
50
|
-
</span>
|
|
51
|
-
<span className="text-xs text-muted-foreground leading-tight mt-0.5 truncate">
|
|
52
|
-
{row.email}
|
|
53
|
-
</span>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
function placementColumnToFilterFieldDef(c: ColumnDef<Placement>): FilterFieldDef | null {
|
|
60
|
-
if (!c.filter) return null
|
|
61
|
-
const f = c.filter
|
|
62
|
-
const defaultOps =
|
|
63
|
-
f.type === "select" || f.type === "date"
|
|
64
|
-
? (["is", "is_not"] as FilterOperator[])
|
|
65
|
-
: (["contains", "not_contains"] as FilterOperator[])
|
|
66
|
-
return {
|
|
67
|
-
key: c.key,
|
|
68
|
-
label: c.label,
|
|
69
|
-
icon: f.icon ?? "fa-filter",
|
|
70
|
-
type: f.type,
|
|
71
|
-
operators: (f.operators ?? defaultOps) as FilterOperator[],
|
|
72
|
-
options:
|
|
73
|
-
f.type === "date"
|
|
74
|
-
? uniquePlacementFieldOptions(c.key as keyof Placement)
|
|
75
|
-
: f.options,
|
|
76
|
-
...(f.textMask ? { textMask: f.textMask } : {}),
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function columnsToFilterFields(cols: ColumnDef<Placement>[]): FilterFieldDef[] {
|
|
81
|
-
return cols.map(placementColumnToFilterFieldDef).filter((x): x is FilterFieldDef => x !== null)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** All columns — original placements overview */
|
|
85
|
-
const PLACEMENT_COLUMNS_ALL: ColumnDef<Placement>[] = [
|
|
86
|
-
COLUMN_SELECT,
|
|
87
|
-
{
|
|
88
|
-
key: "student",
|
|
89
|
-
label: "Student",
|
|
90
|
-
width: 210,
|
|
91
|
-
minWidth: 180,
|
|
92
|
-
sortable: true,
|
|
93
|
-
sortKey: "student",
|
|
94
|
-
defaultPin: "left",
|
|
95
|
-
filter: {
|
|
96
|
-
type: "select",
|
|
97
|
-
icon: "fa-user",
|
|
98
|
-
operators: ["is", "is_not"],
|
|
99
|
-
options: uniquePlacementFieldOptions("student"),
|
|
100
|
-
},
|
|
101
|
-
cell: CELL_STUDENT,
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
key: "specialization",
|
|
105
|
-
label: "Specialization",
|
|
106
|
-
width: 160,
|
|
107
|
-
minWidth: 100,
|
|
108
|
-
sortable: true,
|
|
109
|
-
sortKey: "specialization",
|
|
110
|
-
filter: {
|
|
111
|
-
type: "select",
|
|
112
|
-
icon: "fa-stethoscope",
|
|
113
|
-
operators: ["is", "is_not"],
|
|
114
|
-
options: uniquePlacementFieldOptions("specialization"),
|
|
115
|
-
},
|
|
116
|
-
cell: (row) => (
|
|
117
|
-
<span className="block truncate text-sm text-foreground/80">{row.specialization}</span>
|
|
118
|
-
),
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
key: "site",
|
|
122
|
-
label: "Site",
|
|
123
|
-
width: 180,
|
|
124
|
-
minWidth: 100,
|
|
125
|
-
sortable: true,
|
|
126
|
-
sortKey: "site",
|
|
127
|
-
filter: {
|
|
128
|
-
type: "select",
|
|
129
|
-
icon: "fa-hospital",
|
|
130
|
-
operators: ["is", "is_not"],
|
|
131
|
-
options: uniquePlacementFieldOptions("site"),
|
|
132
|
-
},
|
|
133
|
-
cell: (row) => (
|
|
134
|
-
<div className="min-w-0" title={`${row.site} · ${row.siteAddress}`}>
|
|
135
|
-
<span className="block truncate text-sm font-medium text-foreground leading-tight">{row.site}</span>
|
|
136
|
-
<span className="block truncate text-xs text-muted-foreground mt-0.5 leading-tight">{row.siteAddress}</span>
|
|
137
|
-
</div>
|
|
138
|
-
),
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
key: "status",
|
|
142
|
-
label: "Status",
|
|
143
|
-
width: 130,
|
|
144
|
-
minWidth: 110,
|
|
145
|
-
sortable: true,
|
|
146
|
-
sortKey: "status",
|
|
147
|
-
filter: {
|
|
148
|
-
type: "select",
|
|
149
|
-
icon: "fa-circle-dot",
|
|
150
|
-
operators: ["is", "is_not"],
|
|
151
|
-
options: [
|
|
152
|
-
{ value: "confirmed", label: "Confirmed" },
|
|
153
|
-
{ value: "pending", label: "Pending" },
|
|
154
|
-
{ value: "under-review", label: "Under Review" },
|
|
155
|
-
{ value: "rejected", label: "Rejected" },
|
|
156
|
-
{ value: "completed", label: "Completed" },
|
|
157
|
-
],
|
|
158
|
-
},
|
|
159
|
-
cell: (row) => <StatusBadge status={row.status} />,
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
key: "start",
|
|
163
|
-
label: "Start Date",
|
|
164
|
-
width: 130,
|
|
165
|
-
minWidth: 110,
|
|
166
|
-
sortable: true,
|
|
167
|
-
sortKey: "start",
|
|
168
|
-
filter: {
|
|
169
|
-
type: "date",
|
|
170
|
-
icon: "fa-calendar-days",
|
|
171
|
-
operators: ["is", "is_not"],
|
|
172
|
-
},
|
|
173
|
-
cell: (row) => (
|
|
174
|
-
<span className="text-sm text-foreground/80 tabular-nums whitespace-nowrap">{row.start}</span>
|
|
175
|
-
),
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
key: "duration",
|
|
179
|
-
label: "Duration",
|
|
180
|
-
width: 96,
|
|
181
|
-
minWidth: 80,
|
|
182
|
-
cell: (row) => (
|
|
183
|
-
<span className="text-sm text-foreground/80 whitespace-nowrap">{row.duration}</span>
|
|
184
|
-
),
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
key: "supervisor",
|
|
188
|
-
label: "Supervisor",
|
|
189
|
-
width: 152,
|
|
190
|
-
minWidth: 100,
|
|
191
|
-
filter: {
|
|
192
|
-
type: "select",
|
|
193
|
-
icon: "fa-user-tie",
|
|
194
|
-
operators: ["is", "is_not"],
|
|
195
|
-
options: uniquePlacementFieldOptions("supervisor"),
|
|
196
|
-
},
|
|
197
|
-
cell: (row) => (
|
|
198
|
-
<span className="block truncate text-sm text-foreground/80">{row.supervisor}</span>
|
|
199
|
-
),
|
|
200
|
-
},
|
|
201
|
-
COLUMN_ACTIONS,
|
|
202
|
-
]
|
|
203
|
-
|
|
204
|
-
export function getPlacementColumns(): ColumnDef<Placement>[] {
|
|
205
|
-
return PLACEMENT_COLUMNS_ALL
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export const PLACEMENT_EMPTY_COPY = "No rows match your filters."
|
|
209
|
-
export const PLACEMENT_DRAWER_LABEL = "Placements"
|
|
210
|
-
|