@exxatdesignux/ui 0.0.5 → 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.
Files changed (264) hide show
  1. package/bin/init.mjs +29 -0
  2. package/package.json +7 -2
  3. package/template/.nvmrc +1 -0
  4. package/template/.prettierignore +7 -0
  5. package/template/.prettierrc +11 -0
  6. package/template/AGENTS.md +485 -0
  7. package/template/Logo/Exxat_Prism.svg +39 -0
  8. package/template/Logo/Exxat_one.svg +36 -0
  9. package/template/README.md +58 -0
  10. package/template/app/(app)/compliance/page.tsx +10 -0
  11. package/template/app/(app)/dashboard/loading.tsx +18 -0
  12. package/template/app/(app)/dashboard/page.tsx +36 -0
  13. package/template/app/(app)/data-list/[id]/page.tsx +28 -0
  14. package/template/app/(app)/data-list/new/page.tsx +31 -0
  15. package/template/app/(app)/data-list/page.tsx +10 -0
  16. package/template/app/(app)/error.tsx +43 -0
  17. package/template/app/(app)/help/page.tsx +34 -0
  18. package/template/app/(app)/layout.tsx +54 -0
  19. package/template/app/(app)/loading.tsx +18 -0
  20. package/template/app/(app)/question-bank/page.tsx +10 -0
  21. package/template/app/(app)/rotations/page.tsx +15 -0
  22. package/template/app/(app)/settings/page.tsx +17 -0
  23. package/template/app/(app)/sites/all/page.tsx +13 -0
  24. package/template/app/(app)/team/page.tsx +10 -0
  25. package/template/app/favicon.ico +0 -0
  26. package/template/app/globals.css +1811 -0
  27. package/template/app/layout.tsx +95 -0
  28. package/template/app/page.tsx +9 -0
  29. package/template/components/.gitkeep +0 -0
  30. package/template/components/app-sidebar-dynamic.tsx +15 -0
  31. package/template/components/app-sidebar.tsx +901 -0
  32. package/template/components/ask-leo-composer.tsx +216 -0
  33. package/template/components/ask-leo-sidebar.tsx +509 -0
  34. package/template/components/chart-area-interactive.tsx +293 -0
  35. package/template/components/charts-overview.tsx +2321 -0
  36. package/template/components/command-menu-01.tsx +133 -0
  37. package/template/components/command-menu-02.tsx +386 -0
  38. package/template/components/command-menu.tsx +182 -0
  39. package/template/components/compliance-board-view.tsx +134 -0
  40. package/template/components/compliance-client.tsx +92 -0
  41. package/template/components/compliance-list-view.tsx +59 -0
  42. package/template/components/compliance-page-header.tsx +89 -0
  43. package/template/components/compliance-table.tsx +525 -0
  44. package/template/components/dashboard-onboarding-gallery.tsx +13 -0
  45. package/template/components/dashboard-onboarding.tsx +21 -0
  46. package/template/components/dashboard-promo-banner.tsx +67 -0
  47. package/template/components/dashboard-quota-progress-card.tsx +369 -0
  48. package/template/components/dashboard-report-charts.tsx +69 -0
  49. package/template/components/dashboard-section-heading.tsx +68 -0
  50. package/template/components/dashboard-tabs.tsx +598 -0
  51. package/template/components/data-list-client.tsx +239 -0
  52. package/template/components/data-list-table-cells.test.tsx +22 -0
  53. package/template/components/data-list-table-cells.tsx +173 -0
  54. package/template/components/data-list-table.tsx +879 -0
  55. package/template/components/data-table/filter-date-calendar.tsx +38 -0
  56. package/template/components/data-table/filter-text-value-input.tsx +77 -0
  57. package/template/components/data-table/index.tsx +1612 -0
  58. package/template/components/data-table/pagination.tsx +256 -0
  59. package/template/components/data-table/types.ts +91 -0
  60. package/template/components/data-table/use-table-state.ts +566 -0
  61. package/template/components/data-view-dashboard-charts-compliance.tsx +960 -0
  62. package/template/components/data-view-dashboard-charts-team.tsx +968 -0
  63. package/template/components/data-view-dashboard-charts.tsx +1668 -0
  64. package/template/components/data-views/board-card-primitives.tsx +93 -0
  65. package/template/components/data-views/index.ts +41 -0
  66. package/template/components/data-views/list-page-board-card.tsx +192 -0
  67. package/template/components/data-views/list-page-board-template.tsx +122 -0
  68. package/template/components/data-views/placement-board-card.tsx +262 -0
  69. package/template/components/export-drawer.tsx +375 -0
  70. package/template/components/exxat-product-logo.tsx +453 -0
  71. package/template/components/form-layout-01.tsx +131 -0
  72. package/template/components/getting-started.tsx +625 -0
  73. package/template/components/key-metrics.tsx +920 -0
  74. package/template/components/leo-insight-indicator.tsx +364 -0
  75. package/template/components/leo-typing-dots.tsx +121 -0
  76. package/template/components/list-hub-status-badge.tsx +51 -0
  77. package/template/components/list-page-dashboard-charts.tsx +18 -0
  78. package/template/components/nav-documents.tsx +89 -0
  79. package/template/components/nav-main.tsx +58 -0
  80. package/template/components/nav-secondary.tsx +64 -0
  81. package/template/components/nav-user.tsx +190 -0
  82. package/template/components/new-placement-back-btn.tsx +28 -0
  83. package/template/components/new-placement-form.tsx +1066 -0
  84. package/template/components/onboarding/index.ts +4 -0
  85. package/template/components/onboarding/onboarding-01.tsx +7 -0
  86. package/template/components/onboarding/onboarding-02.tsx +7 -0
  87. package/template/components/onboarding/onboarding-03.tsx +7 -0
  88. package/template/components/onboarding/onboarding-04.tsx +7 -0
  89. package/template/components/page-header.tsx +57 -0
  90. package/template/components/placement-detail.tsx +438 -0
  91. package/template/components/placements-board-view.tsx +404 -0
  92. package/template/components/placements-list-view.tsx +285 -0
  93. package/template/components/placements-page-header.tsx +160 -0
  94. package/template/components/placements-table-columns.tsx +639 -0
  95. package/template/components/product-switcher.tsx +116 -0
  96. package/template/components/question-bank-board-view.tsx +205 -0
  97. package/template/components/question-bank-client.tsx +77 -0
  98. package/template/components/question-bank-list-view.tsx +59 -0
  99. package/template/components/question-bank-page-header.tsx +89 -0
  100. package/template/components/question-bank-table.tsx +586 -0
  101. package/template/components/rotations-empty-state.tsx +47 -0
  102. package/template/components/rotations-panel-activator.tsx +8 -0
  103. package/template/components/secondary-nav.tsx +394 -0
  104. package/template/components/secondary-panel.tsx +239 -0
  105. package/template/components/section-cards.tsx +106 -0
  106. package/template/components/settings-appearance-card.tsx +424 -0
  107. package/template/components/settings-client.tsx +537 -0
  108. package/template/components/settings-form-row.tsx +42 -0
  109. package/template/components/sidebar-auto-collapse.tsx +23 -0
  110. package/template/components/sidebar-auto-open.tsx +18 -0
  111. package/template/components/sidebar-shell.tsx +37 -0
  112. package/template/components/site-header.tsx +93 -0
  113. package/template/components/sites-all-client.tsx +154 -0
  114. package/template/components/sites-board-view.tsx +67 -0
  115. package/template/components/sites-list-view.tsx +47 -0
  116. package/template/components/sites-table.tsx +312 -0
  117. package/template/components/system-banner-slot.tsx +66 -0
  118. package/template/components/table-properties/column-row.tsx +90 -0
  119. package/template/components/table-properties/draggable-list.ts +49 -0
  120. package/template/components/table-properties/drawer-button.tsx +231 -0
  121. package/template/components/table-properties/drawer.tsx +1102 -0
  122. package/template/components/table-properties/filter-card.tsx +251 -0
  123. package/template/components/table-properties/index.ts +22 -0
  124. package/template/components/table-properties/sort-card.tsx +59 -0
  125. package/template/components/table-properties/types.ts +124 -0
  126. package/template/components/task-list-panel.tsx +98 -0
  127. package/template/components/task-priority-badge.tsx +28 -0
  128. package/template/components/team-board-view.tsx +114 -0
  129. package/template/components/team-client.tsx +93 -0
  130. package/template/components/team-list-view.tsx +62 -0
  131. package/template/components/team-page-header.tsx +92 -0
  132. package/template/components/team-table.tsx +525 -0
  133. package/template/components/templates/list-page.tsx +576 -0
  134. package/template/components/templates/primary-page-template.tsx +56 -0
  135. package/template/components/theme-color-sync.tsx +32 -0
  136. package/template/components/theme-provider.tsx +71 -0
  137. package/template/components/tinted-icon-disc.tsx +53 -0
  138. package/template/components/ui/ai-thinking-surface.tsx +121 -0
  139. package/template/components/ui/avatar.tsx +1 -0
  140. package/template/components/ui/badge.tsx +1 -0
  141. package/template/components/ui/banner.tsx +1 -0
  142. package/template/components/ui/breadcrumb.tsx +1 -0
  143. package/template/components/ui/button.tsx +1 -0
  144. package/template/components/ui/calendar.tsx +1 -0
  145. package/template/components/ui/card.tsx +1 -0
  146. package/template/components/ui/chart.tsx +1 -0
  147. package/template/components/ui/checkbox.tsx +1 -0
  148. package/template/components/ui/coach-mark.tsx +1 -0
  149. package/template/components/ui/collapsible.tsx +1 -0
  150. package/template/components/ui/command.tsx +1 -0
  151. package/template/components/ui/date-picker-field.tsx +1 -0
  152. package/template/components/ui/dialog.tsx +1 -0
  153. package/template/components/ui/dot-pattern.tsx +159 -0
  154. package/template/components/ui/drag-handle-grip.tsx +1 -0
  155. package/template/components/ui/drawer.tsx +1 -0
  156. package/template/components/ui/dropdown-menu.tsx +1 -0
  157. package/template/components/ui/field.tsx +1 -0
  158. package/template/components/ui/form.tsx +1 -0
  159. package/template/components/ui/input-group.tsx +1 -0
  160. package/template/components/ui/input-mask.tsx +1 -0
  161. package/template/components/ui/input.tsx +1 -0
  162. package/template/components/ui/kbd.tsx +1 -0
  163. package/template/components/ui/label.tsx +1 -0
  164. package/template/components/ui/leo-icon.tsx +726 -0
  165. package/template/components/ui/payment-card-fields.tsx +1 -0
  166. package/template/components/ui/popover.tsx +1 -0
  167. package/template/components/ui/radio-group.tsx +1 -0
  168. package/template/components/ui/select.tsx +1 -0
  169. package/template/components/ui/selection-tile-grid.tsx +1 -0
  170. package/template/components/ui/separator.tsx +1 -0
  171. package/template/components/ui/sheet.tsx +1 -0
  172. package/template/components/ui/sidebar.tsx +1 -0
  173. package/template/components/ui/skeleton.tsx +1 -0
  174. package/template/components/ui/sonner.tsx +1 -0
  175. package/template/components/ui/status-badge.tsx +1 -0
  176. package/template/components/ui/table.tsx +1 -0
  177. package/template/components/ui/tabs.tsx +1 -0
  178. package/template/components/ui/textarea.tsx +1 -0
  179. package/template/components/ui/tip.tsx +1 -0
  180. package/template/components/ui/toggle-group.tsx +1 -0
  181. package/template/components/ui/toggle-switch.tsx +1 -0
  182. package/template/components/ui/toggle.tsx +1 -0
  183. package/template/components/ui/tooltip.tsx +1 -0
  184. package/template/components/ui/view-segmented-control.tsx +1 -0
  185. package/template/components.json +27 -0
  186. package/template/contexts/chart-variant-context.tsx +35 -0
  187. package/template/contexts/command-menu-context.tsx +28 -0
  188. package/template/contexts/dashboard-view-context.tsx +35 -0
  189. package/template/contexts/product-context.tsx +38 -0
  190. package/template/contexts/system-banner-context.tsx +127 -0
  191. package/template/docs/command-menu-pattern.md +45 -0
  192. package/template/docs/data-views-pattern.md +160 -0
  193. package/template/ecosystem.config.cjs +20 -0
  194. package/template/eslint.config.mjs +18 -0
  195. package/template/fontawesome-subset.manifest.json +190 -0
  196. package/template/hooks/.gitkeep +0 -0
  197. package/template/hooks/use-app-theme.ts +1 -0
  198. package/template/hooks/use-coach-mark.ts +1 -0
  199. package/template/hooks/use-mobile.ts +1 -0
  200. package/template/hooks/use-mod-key-label.ts +1 -0
  201. package/template/lib/.gitkeep +0 -0
  202. package/template/lib/ask-leo-route-context.ts +133 -0
  203. package/template/lib/chart-keyboard-selection.test.ts +20 -0
  204. package/template/lib/chart-keyboard-selection.ts +17 -0
  205. package/template/lib/chart-line-dash.ts +16 -0
  206. package/template/lib/coach-mark-registry.ts +68 -0
  207. package/template/lib/command-menu-config.ts +127 -0
  208. package/template/lib/command-menu-search-data.ts +44 -0
  209. package/template/lib/conditional-rule-match.ts +32 -0
  210. package/template/lib/dashboard-customize-coach-mark.ts +18 -0
  211. package/template/lib/dashboard-layout-merge.ts +63 -0
  212. package/template/lib/data-list-display-options.ts +35 -0
  213. package/template/lib/data-list-persistence.ts +280 -0
  214. package/template/lib/data-list-view-surface.ts +58 -0
  215. package/template/lib/data-list-view.ts +29 -0
  216. package/template/lib/data-view-dashboard-storage.ts +101 -0
  217. package/template/lib/date-filter.ts +8 -0
  218. package/template/lib/dev-log.test.ts +28 -0
  219. package/template/lib/dev-log.ts +8 -0
  220. package/template/lib/editable-target.ts +10 -0
  221. package/template/lib/floating-sheet-panel.ts +72 -0
  222. package/template/lib/initials-from-name.ts +7 -0
  223. package/template/lib/list-page-table-properties.ts +52 -0
  224. package/template/lib/list-status-badges.ts +168 -0
  225. package/template/lib/logo-dev.ts +12 -0
  226. package/template/lib/mock/compliance-kpi.ts +61 -0
  227. package/template/lib/mock/compliance.ts +146 -0
  228. package/template/lib/mock/dashboard.ts +105 -0
  229. package/template/lib/mock/navigation.tsx +231 -0
  230. package/template/lib/mock/placements-kpi.ts +134 -0
  231. package/template/lib/mock/placements.ts +183 -0
  232. package/template/lib/mock/question-bank-kpi.ts +61 -0
  233. package/template/lib/mock/question-bank.ts +142 -0
  234. package/template/lib/mock/sites-directory.ts +16 -0
  235. package/template/lib/mock/sites-kpi.ts +25 -0
  236. package/template/lib/mock/team-kpi.ts +60 -0
  237. package/template/lib/mock/team.ts +118 -0
  238. package/template/lib/motion-ui.ts +17 -0
  239. package/template/lib/placement-board-card-layout.ts +79 -0
  240. package/template/lib/placement-lifecycle.ts +5 -0
  241. package/template/lib/row-height.ts +10 -0
  242. package/template/lib/stock-portrait.ts +11 -0
  243. package/template/lib/utils.test.ts +13 -0
  244. package/template/lib/utils.ts +1 -0
  245. package/template/next.config.mjs +15 -0
  246. package/template/package.json +83 -0
  247. package/template/postcss.config.mjs +8 -0
  248. package/template/public/.gitkeep +0 -0
  249. package/template/public/Illustration/Rotation.svg +74 -0
  250. package/template/public/avatars/user.svg +11 -0
  251. package/template/public/favicon/favicon.ico +0 -0
  252. package/template/public/favicon.ico +0 -0
  253. package/template/public/logos/exxat-one.svg +36 -0
  254. package/template/public/logos/exxat-prism.svg +39 -0
  255. package/template/public/mock-schools/emory.svg +4 -0
  256. package/template/public/mock-schools/rush.svg +4 -0
  257. package/template/scripts/fontawesome-subset-audit.mjs +190 -0
  258. package/template/scripts/pm2-startup-macos.sh +13 -0
  259. package/template/skills-lock.json +10 -0
  260. package/template/stores/app-store.ts +33 -0
  261. package/template/tests/setup.ts +1 -0
  262. package/template/tsconfig.json +35 -0
  263. package/template/types/react-payment-inputs.d.ts +19 -0
  264. package/template/vitest.config.ts +18 -0
@@ -0,0 +1,879 @@
1
+ "use client"
2
+
3
+ /**
4
+ * DataListTable — Placements hub shell on top of the generic DataTable.
5
+ *
6
+ * Lifecycle tabs swap columns and filtered rows; column definitions and lifecycle copy
7
+ * are passed in from the page (`DataListClient` + `placements-table-columns.tsx`).
8
+ */
9
+
10
+ import * as React from "react"
11
+ import dynamic from "next/dynamic"
12
+ import { useRouter } from "next/navigation"
13
+ import { Button } from "@/components/ui/button"
14
+ import { Tip } from "@/components/ui/tip"
15
+ import { Skeleton } from "@/components/ui/skeleton"
16
+ import { KEY_METRICS_KPI_COUNT_DEFAULT } from "@/lib/dashboard-layout-merge"
17
+ import {
18
+ ALL_DASHBOARD_CARDS,
19
+ DEFAULT_VISIBLE_CARDS,
20
+ DEFAULT_SPANS,
21
+ DEFAULT_CHART_TYPES,
22
+ loadDashboardLayout,
23
+ mergeDashboardLayout,
24
+ saveDashboardLayout,
25
+ type ChartType,
26
+ type DashboardLayout,
27
+ } from "@/components/data-view-dashboard-charts"
28
+ import { CoachMark } from "@/components/ui/coach-mark"
29
+ import { useCoachMark } from "@/hooks/use-coach-mark"
30
+ import { DASHBOARD_CUSTOMIZE_COACH_STEPS } from "@/lib/dashboard-customize-coach-mark"
31
+ import { PlacementsBoardView, type PlacementsBoardColumnMenu } from "@/components/placements-board-view"
32
+ import { PlacementsListView } from "@/components/placements-list-view"
33
+ import { TablePropertiesDrawerButton } from "@/components/table-properties"
34
+ import type { FilterFieldDef } from "@/components/table-properties/types"
35
+ import type { DataListViewType } from "@/lib/data-list-view"
36
+ import {
37
+ DEFAULT_DATA_LIST_DISPLAY_OPTIONS,
38
+ type DataListDisplayOptions,
39
+ } from "@/lib/data-list-display-options"
40
+ import {
41
+ applyLifecyclePersisted,
42
+ loadLifecycleFromStorage,
43
+ scheduleLifecycleSave,
44
+ serializeLifecycle,
45
+ type TableStatePersistSlice,
46
+ } from "@/lib/data-list-persistence"
47
+ import type { OpenTablePropertiesHandle } from "@/lib/list-page-table-properties"
48
+ import { StatusBadge } from "@/components/data-list-table-cells"
49
+ import { columnsToFilterFields } from "@/components/placements-table-columns"
50
+ import { DataTable, DataTableToolbar } from "@/components/data-table"
51
+ import { CountSyncer, PaginationBar } from "@/components/data-table/pagination"
52
+ import type { DataTableExtendedProps } from "@/components/data-table"
53
+ import type { ColumnDef, ConditionalRule } from "@/components/data-table/types"
54
+ import { useTableState } from "@/components/data-table/use-table-state"
55
+ import { placementsForPhase, type Placement, type Status } from "@/lib/mock/placements"
56
+ import type { PlacementLifecycleTabId } from "@/lib/placement-lifecycle"
57
+ import { placementKpiInsightFromRows, placementKpiMetricsFromRows } from "@/lib/mock/placements-kpi"
58
+
59
+ const PlacementsDashboardChartsSection = dynamic(
60
+ () =>
61
+ import("@/components/data-view-dashboard-charts").then(mod => ({
62
+ default: mod.PlacementsDashboardChartsSection,
63
+ })),
64
+ {
65
+ ssr: false,
66
+ loading: () => (
67
+ <div className="mx-4 mb-8 mt-2 flex flex-col gap-3 border border-border rounded-xl p-6 lg:mx-6">
68
+ <Skeleton className="h-7 w-48 max-w-full" />
69
+ <Skeleton className="min-h-[200px] w-full rounded-lg" />
70
+ <Skeleton className="min-h-[200px] w-full rounded-lg" />
71
+ </div>
72
+ ),
73
+ },
74
+ )
75
+
76
+ function DataListBoardShell({
77
+ state,
78
+ openDrawerRef,
79
+ tableData,
80
+ columns,
81
+ lifecycleTabId,
82
+ view,
83
+ onViewChange,
84
+ pagination,
85
+ onPaginationChange,
86
+ conditionalRules,
87
+ onAddConditionalRule,
88
+ onRemoveConditionalRule,
89
+ onUpdateConditionalRule,
90
+ filterFields,
91
+ lifecycleDrawerLabel,
92
+ fieldDefinitionsForDrawer,
93
+ resolveColumnLabel,
94
+ renderFilterOptionValue,
95
+ displayOptions,
96
+ onDisplayOptionsChange,
97
+ }: {
98
+ state: ReturnType<typeof useTableState<Placement>>
99
+ openDrawerRef: React.MutableRefObject<() => void>
100
+ tableData: Placement[]
101
+ columns: ColumnDef<Placement>[]
102
+ lifecycleTabId: PlacementLifecycleTabId
103
+ view: DataListViewType
104
+ onViewChange?: (view: DataListViewType) => void
105
+ pagination: boolean
106
+ onPaginationChange: (v: boolean) => void
107
+ conditionalRules: ConditionalRule[]
108
+ onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
109
+ onRemoveConditionalRule: (id: string) => void
110
+ onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
111
+ filterFields: FilterFieldDef[]
112
+ lifecycleDrawerLabel: string
113
+ fieldDefinitionsForDrawer: { key: string; label: string; sortable?: boolean }[]
114
+ resolveColumnLabel: (key: string) => string
115
+ renderFilterOptionValue: (fieldKey: string, value: string) => React.ReactNode
116
+ displayOptions: DataListDisplayOptions
117
+ onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
118
+ }) {
119
+ React.useEffect(() => {
120
+ openDrawerRef.current = () => state.setSheetOpen(true)
121
+ }, [state.setSheetOpen])
122
+
123
+ const boardColumnMenu: PlacementsBoardColumnMenu = React.useMemo(
124
+ () => ({
125
+ filterableColumns: columns.filter(c => c.filter).map(c => ({ key: c.key, label: c.label })),
126
+ sortableColumns: columns.filter(c => c.sortable && c.sortKey).map(c => ({ key: c.key, label: c.label })),
127
+ groupableColumns: columns.filter(c => c.key !== "select" && c.key !== "actions").map(c => ({ key: c.key, label: c.label })),
128
+ groupBy: state.groupBy,
129
+ onAddFilter: state.addFilter,
130
+ onSortByField: (fieldKey, direction) => {
131
+ state.setSortRules(prev => {
132
+ const filtered = prev.filter(r => r.fieldKey !== fieldKey)
133
+ return [{ id: `sort-${Date.now()}`, fieldKey, direction }, ...filtered]
134
+ })
135
+ },
136
+ onToggleGroupBy: (fieldKey: string) => {
137
+ state.setGroupBy(prev => (prev === fieldKey ? null : fieldKey))
138
+ },
139
+ onOpenProperties: () => state.setSheetOpen(true),
140
+ }),
141
+ [columns, state],
142
+ )
143
+
144
+ return (
145
+ <>
146
+ <DataTableToolbar
147
+ state={state}
148
+ columns={columns}
149
+ searchable
150
+ renderFilterOptionValue={renderFilterOptionValue}
151
+ searchAriaLabel="Search placements"
152
+ toolbarSlot={(s) => (
153
+ <TablePropertiesDrawerButton
154
+ state={s}
155
+ totalRows={tableData.length}
156
+ pagination={pagination}
157
+ onPaginationChange={onPaginationChange}
158
+ conditionalRules={conditionalRules}
159
+ onAddConditionalRule={onAddConditionalRule}
160
+ onRemoveConditionalRule={onRemoveConditionalRule}
161
+ onUpdateConditionalRule={onUpdateConditionalRule}
162
+ filterFields={filterFields}
163
+ currentView={view}
164
+ onViewChange={onViewChange}
165
+ lifecycleTabLabel={lifecycleDrawerLabel}
166
+ fieldDefinitions={fieldDefinitionsForDrawer}
167
+ resolveColumnLabel={resolveColumnLabel}
168
+ displayOptions={displayOptions}
169
+ onDisplayOptionsChange={onDisplayOptionsChange}
170
+ renderFilterOptionValue={renderFilterOptionValue}
171
+ />
172
+ )}
173
+ />
174
+ <PlacementsBoardView
175
+ placements={state.rows as Placement[]}
176
+ lifecycleTabId={lifecycleTabId}
177
+ boardColumnMenu={boardColumnMenu}
178
+ boardDisplay={{
179
+ lineCount: displayOptions.boardLineCount,
180
+ showColumnLabels: displayOptions.showColumnLabels,
181
+ showColumnCounts: displayOptions.showBoardColumnCounts,
182
+ newCardAbove: displayOptions.boardNewCardAbove,
183
+ }}
184
+ hiddenColKeys={state.hiddenCols}
185
+ conditionalRules={conditionalRules}
186
+ boardColumns={state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")}
187
+ />
188
+ </>
189
+ )
190
+ }
191
+
192
+ /** List / row view: shared table state + toolbar + full-width rows */
193
+ function DataListListShell({
194
+ state,
195
+ openDrawerRef,
196
+ tableData,
197
+ columns,
198
+ lifecycleTabId,
199
+ view,
200
+ onViewChange,
201
+ pagination,
202
+ onPaginationChange,
203
+ conditionalRules,
204
+ onAddConditionalRule,
205
+ onRemoveConditionalRule,
206
+ onUpdateConditionalRule,
207
+ filterFields,
208
+ lifecycleDrawerLabel,
209
+ fieldDefinitionsForDrawer,
210
+ resolveColumnLabel,
211
+ renderFilterOptionValue,
212
+ displayOptions,
213
+ onDisplayOptionsChange,
214
+ listRows,
215
+ emptyTableCopy,
216
+ }: {
217
+ state: ReturnType<typeof useTableState<Placement>>
218
+ openDrawerRef: React.MutableRefObject<() => void>
219
+ tableData: Placement[]
220
+ columns: ColumnDef<Placement>[]
221
+ lifecycleTabId: PlacementLifecycleTabId
222
+ view: DataListViewType
223
+ onViewChange?: (view: DataListViewType) => void
224
+ pagination: boolean
225
+ onPaginationChange: (v: boolean) => void
226
+ conditionalRules: ConditionalRule[]
227
+ onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
228
+ onRemoveConditionalRule: (id: string) => void
229
+ onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
230
+ filterFields: FilterFieldDef[]
231
+ lifecycleDrawerLabel: string
232
+ fieldDefinitionsForDrawer: { key: string; label: string; sortable?: boolean }[]
233
+ resolveColumnLabel: (key: string) => string
234
+ renderFilterOptionValue: (fieldKey: string, value: string) => React.ReactNode
235
+ displayOptions: DataListDisplayOptions
236
+ onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
237
+ listRows: Placement[]
238
+ emptyTableCopy: string
239
+ }) {
240
+ React.useEffect(() => {
241
+ openDrawerRef.current = () => state.setSheetOpen(true)
242
+ }, [state.setSheetOpen])
243
+
244
+ return (
245
+ <>
246
+ <DataTableToolbar
247
+ state={state}
248
+ columns={columns}
249
+ searchable
250
+ renderFilterOptionValue={renderFilterOptionValue}
251
+ searchAriaLabel="Search placements"
252
+ toolbarSlot={s => (
253
+ <TablePropertiesDrawerButton
254
+ state={s}
255
+ totalRows={tableData.length}
256
+ pagination={pagination}
257
+ onPaginationChange={onPaginationChange}
258
+ conditionalRules={conditionalRules}
259
+ onAddConditionalRule={onAddConditionalRule}
260
+ onRemoveConditionalRule={onRemoveConditionalRule}
261
+ onUpdateConditionalRule={onUpdateConditionalRule}
262
+ filterFields={filterFields}
263
+ currentView={view}
264
+ onViewChange={onViewChange}
265
+ lifecycleTabLabel={lifecycleDrawerLabel}
266
+ fieldDefinitions={fieldDefinitionsForDrawer}
267
+ resolveColumnLabel={resolveColumnLabel}
268
+ displayOptions={displayOptions}
269
+ onDisplayOptionsChange={onDisplayOptionsChange}
270
+ renderFilterOptionValue={renderFilterOptionValue}
271
+ />
272
+ )}
273
+ />
274
+ <PlacementsListView
275
+ rows={listRows}
276
+ lifecycleTabId={lifecycleTabId}
277
+ hiddenColKeys={state.hiddenCols}
278
+ boardColumns={state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")}
279
+ conditionalRules={conditionalRules}
280
+ emptyCopy={emptyTableCopy}
281
+ />
282
+ </>
283
+ )
284
+ }
285
+
286
+ /** Dashboard view tab: same toolbar + properties as list/board; KPIs from filtered rows. */
287
+ function DataListDashboardShell({
288
+ state,
289
+ openDrawerRef,
290
+ tableData,
291
+ columns,
292
+ view,
293
+ onViewChange,
294
+ pagination,
295
+ onPaginationChange,
296
+ conditionalRules,
297
+ onAddConditionalRule,
298
+ onRemoveConditionalRule,
299
+ onUpdateConditionalRule,
300
+ filterFields,
301
+ lifecycleDrawerLabel,
302
+ fieldDefinitionsForDrawer,
303
+ resolveColumnLabel,
304
+ renderFilterOptionValue,
305
+ displayOptions,
306
+ onDisplayOptionsChange,
307
+ }: {
308
+ state: ReturnType<typeof useTableState<Placement>>
309
+ openDrawerRef: React.MutableRefObject<() => void>
310
+ tableData: Placement[]
311
+ columns: ColumnDef<Placement>[]
312
+ view: DataListViewType
313
+ onViewChange?: (view: DataListViewType) => void
314
+ pagination: boolean
315
+ onPaginationChange: (v: boolean) => void
316
+ conditionalRules: ConditionalRule[]
317
+ onAddConditionalRule: (rule: Omit<ConditionalRule, "id">) => void
318
+ onRemoveConditionalRule: (id: string) => void
319
+ onUpdateConditionalRule: (id: string, patch: Partial<ConditionalRule>) => void
320
+ filterFields: FilterFieldDef[]
321
+ lifecycleDrawerLabel: string
322
+ fieldDefinitionsForDrawer: { key: string; label: string; sortable?: boolean }[]
323
+ resolveColumnLabel: (key: string) => string
324
+ renderFilterOptionValue: (fieldKey: string, value: string) => React.ReactNode
325
+ displayOptions: DataListDisplayOptions
326
+ onDisplayOptionsChange: (patch: Partial<DataListDisplayOptions>) => void
327
+ }) {
328
+ React.useEffect(() => {
329
+ openDrawerRef.current = () => state.setSheetOpen(true)
330
+ }, [state.setSheetOpen])
331
+
332
+ const dashboardKpi = React.useMemo(
333
+ () => ({
334
+ metrics: placementKpiMetricsFromRows(state.rows as Placement[]),
335
+ insight: placementKpiInsightFromRows(state.rows as Placement[]),
336
+ }),
337
+ [state.rows],
338
+ )
339
+
340
+ /* Dashboard card layout — persisted to localStorage */
341
+ const [visibleCards, setVisibleCards] = React.useState<string[]>(DEFAULT_VISIBLE_CARDS)
342
+ const [cardOrder, setCardOrder] = React.useState<string[]>(ALL_DASHBOARD_CARDS.map(c => c.id))
343
+ const [cardSpans, setCardSpans] = React.useState<Record<string, 1 | 2>>(() => ({ ...DEFAULT_SPANS }))
344
+ const [cardChartTypes, setCardChartTypes] = React.useState<Record<string, ChartType>>(() => ({ ...DEFAULT_CHART_TYPES }))
345
+ const [keyMetricsKpiCount, setKeyMetricsKpiCount] = React.useState<number>(KEY_METRICS_KPI_COUNT_DEFAULT)
346
+ const [dashboardLayoutEdit, setDashboardLayoutEdit] = React.useState(false)
347
+ const dashboardLayoutHydrated = React.useRef(false)
348
+ const dashboardLayoutEditBaselineRef = React.useRef<DashboardLayout | null>(null)
349
+
350
+ React.useEffect(() => {
351
+ const saved = loadDashboardLayout()
352
+ const m = mergeDashboardLayout(saved)
353
+ setVisibleCards(m.visible)
354
+ setCardOrder(m.order)
355
+ setCardSpans(m.spans ?? { ...DEFAULT_SPANS })
356
+ setCardChartTypes(m.chartTypes ?? { ...DEFAULT_CHART_TYPES })
357
+ setKeyMetricsKpiCount(m.keyMetricsKpiCount ?? KEY_METRICS_KPI_COUNT_DEFAULT)
358
+ dashboardLayoutHydrated.current = true
359
+ }, [])
360
+
361
+ React.useEffect(() => {
362
+ if (!dashboardLayoutHydrated.current) return
363
+ saveDashboardLayout({
364
+ visible: visibleCards,
365
+ order: cardOrder,
366
+ spans: cardSpans,
367
+ chartTypes: cardChartTypes,
368
+ keyMetricsKpiCount,
369
+ })
370
+ }, [visibleCards, cardOrder, cardSpans, cardChartTypes, keyMetricsKpiCount])
371
+
372
+ const handleVisibleChange = React.useCallback((v: string[]) => {
373
+ setVisibleCards(v)
374
+ }, [])
375
+
376
+ const handleOrderChange = React.useCallback((o: string[]) => {
377
+ setCardOrder(o)
378
+ }, [])
379
+
380
+ const handleSpanChange = React.useCallback((id: string, span: 1 | 2) => {
381
+ setCardSpans(prev => ({ ...prev, [id]: span }))
382
+ }, [])
383
+
384
+ const handleChartTypeChange = React.useCallback((id: string, t: ChartType) => {
385
+ setCardChartTypes(prev => ({ ...prev, [id]: t }))
386
+ }, [])
387
+
388
+ const handleResetDashboardLayout = React.useCallback(() => {
389
+ setVisibleCards(ALL_DASHBOARD_CARDS.map(c => c.id))
390
+ setCardOrder(ALL_DASHBOARD_CARDS.map(c => c.id))
391
+ setCardSpans({ ...DEFAULT_SPANS })
392
+ setCardChartTypes({ ...DEFAULT_CHART_TYPES })
393
+ setKeyMetricsKpiCount(KEY_METRICS_KPI_COUNT_DEFAULT)
394
+ }, [])
395
+
396
+ const handleDashboardLayoutEditStart = React.useCallback(() => {
397
+ dashboardLayoutEditBaselineRef.current = {
398
+ visible: [...visibleCards],
399
+ order: [...cardOrder],
400
+ spans: { ...cardSpans },
401
+ chartTypes: { ...cardChartTypes },
402
+ keyMetricsKpiCount,
403
+ }
404
+ setDashboardLayoutEdit(true)
405
+ }, [visibleCards, cardOrder, cardSpans, cardChartTypes, keyMetricsKpiCount])
406
+
407
+ const handleDashboardLayoutEditDone = React.useCallback(() => {
408
+ setDashboardLayoutEdit(false)
409
+ }, [])
410
+
411
+ const handleDashboardLayoutEditCancel = React.useCallback(() => {
412
+ const b = dashboardLayoutEditBaselineRef.current
413
+ if (b) {
414
+ setVisibleCards(b.visible)
415
+ setCardOrder(b.order)
416
+ setCardSpans(b.spans ?? { ...DEFAULT_SPANS })
417
+ setCardChartTypes(b.chartTypes ?? { ...DEFAULT_CHART_TYPES })
418
+ setKeyMetricsKpiCount(b.keyMetricsKpiCount ?? KEY_METRICS_KPI_COUNT_DEFAULT)
419
+ }
420
+ setDashboardLayoutEdit(false)
421
+ }, [])
422
+
423
+ const dashboardCustomizeCoach = useCoachMark({
424
+ flowId: "placements-dashboard-customize",
425
+ steps: DASHBOARD_CUSTOMIZE_COACH_STEPS,
426
+ delay: 700,
427
+ dependsOnDismissedFlowId: "placements-views-tour",
428
+ })
429
+
430
+ return (
431
+ <>
432
+ <CoachMark state={dashboardCustomizeCoach} />
433
+ {!dashboardLayoutEdit ? (
434
+ <DataTableToolbar
435
+ state={state}
436
+ columns={columns}
437
+ searchable={displayOptions.showToolbarSearch}
438
+ renderFilterOptionValue={renderFilterOptionValue}
439
+ searchAriaLabel="Search placements"
440
+ toolbarSlot={s => (
441
+ <TablePropertiesDrawerButton
442
+ state={s}
443
+ totalRows={tableData.length}
444
+ pagination={pagination}
445
+ onPaginationChange={onPaginationChange}
446
+ conditionalRules={conditionalRules}
447
+ onAddConditionalRule={onAddConditionalRule}
448
+ onRemoveConditionalRule={onRemoveConditionalRule}
449
+ onUpdateConditionalRule={onUpdateConditionalRule}
450
+ filterFields={filterFields}
451
+ currentView={view}
452
+ onViewChange={onViewChange}
453
+ lifecycleTabLabel={lifecycleDrawerLabel}
454
+ fieldDefinitions={fieldDefinitionsForDrawer}
455
+ resolveColumnLabel={resolveColumnLabel}
456
+ displayOptions={displayOptions}
457
+ onDisplayOptionsChange={onDisplayOptionsChange}
458
+ renderFilterOptionValue={renderFilterOptionValue}
459
+ extraActions={
460
+ <Tip side="bottom" label="Edit dashboard layout on canvas">
461
+ <Button
462
+ type="button"
463
+ variant="ghost"
464
+ size="icon-sm"
465
+ aria-label="Edit dashboard layout"
466
+ onClick={handleDashboardLayoutEditStart}
467
+ className="text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover"
468
+ >
469
+ <i className="fa-light fa-pen-ruler text-[13px]" aria-hidden="true" />
470
+ </Button>
471
+ </Tip>
472
+ }
473
+ />
474
+ )}
475
+ />
476
+ ) : null}
477
+
478
+ {/* Contextual placement charts + KPI card (customise on canvas) */}
479
+ <PlacementsDashboardChartsSection
480
+ placements={state.rows as Placement[]}
481
+ keyMetrics={dashboardKpi}
482
+ visibleCards={visibleCards}
483
+ cardOrder={cardOrder}
484
+ cardSpans={cardSpans}
485
+ cardChartTypes={cardChartTypes}
486
+ keyMetricsKpiCount={keyMetricsKpiCount}
487
+ layoutEditMode={dashboardLayoutEdit}
488
+ onVisibleChange={handleVisibleChange}
489
+ onOrderChange={handleOrderChange}
490
+ onSpanChange={handleSpanChange}
491
+ onChartTypeChange={handleChartTypeChange}
492
+ onKeyMetricsKpiCountChange={setKeyMetricsKpiCount}
493
+ onResetLayout={handleResetDashboardLayout}
494
+ onLayoutEditDone={handleDashboardLayoutEditDone}
495
+ onLayoutEditCancel={handleDashboardLayoutEditCancel}
496
+ />
497
+ </>
498
+ )
499
+ }
500
+
501
+ // ─────────────────────────────────────────────────────────────────────────────
502
+ // Props
503
+ // ─────────────────────────────────────────────────────────────────────────────
504
+
505
+ export interface DataListTableProps {
506
+ view?: DataListViewType
507
+ onViewChange?: (view: DataListViewType) => void
508
+ /** Drives column set + row filter (ids: all | upcoming | ongoing | completed) */
509
+ lifecycleTabId?: PlacementLifecycleTabId
510
+ /** Shared display options (persist at page level — all view types). */
511
+ displayOptions?: DataListDisplayOptions
512
+ onDisplayOptionsChange?: (patch: Partial<DataListDisplayOptions>) => void
513
+ /** Lifecycle column set from the placements page (e.g. `getPlacementColumnsForLifecycle`). */
514
+ getColumnsForLifecycle: (tab: PlacementLifecycleTabId) => ColumnDef<Placement>[]
515
+ /** Empty-state copy for the active lifecycle tab — from the page. */
516
+ emptyTableCopy: string
517
+ /** Table Properties drawer lifecycle label — from the page. */
518
+ lifecycleDrawerLabel: string
519
+ }
520
+
521
+ /** Imperative handle — open Table Properties (table view only). */
522
+ export type DataListTableHandle = OpenTablePropertiesHandle
523
+
524
+ // ─────────────────────────────────────────────────────────────────────────────
525
+ // Main component
526
+ // ─────────────────────────────────────────────────────────────────────────────
527
+
528
+ export const DataListTable = React.forwardRef<DataListTableHandle, DataListTableProps>(function DataListTable({
529
+ view = "table",
530
+ onViewChange,
531
+ lifecycleTabId = "all",
532
+ displayOptions: displayOptionsProp,
533
+ onDisplayOptionsChange,
534
+ getColumnsForLifecycle,
535
+ emptyTableCopy,
536
+ lifecycleDrawerLabel,
537
+ }, ref) {
538
+ const displayOptions = React.useMemo(
539
+ () => ({ ...DEFAULT_DATA_LIST_DISPLAY_OPTIONS, ...displayOptionsProp }),
540
+ [displayOptionsProp],
541
+ )
542
+
543
+ const patchDisplayOptions = React.useCallback(
544
+ (patch: Partial<DataListDisplayOptions>) => {
545
+ onDisplayOptionsChange?.(patch)
546
+ },
547
+ [onDisplayOptionsChange],
548
+ )
549
+ const openDrawerRef = React.useRef<() => void>(() => {})
550
+
551
+ React.useImperativeHandle(ref, () => ({
552
+ openPropertiesDrawer: () => {
553
+ openDrawerRef.current()
554
+ },
555
+ }), [])
556
+
557
+ const router = useRouter()
558
+ const [pagination, setPagination] = React.useState(false)
559
+
560
+ const columns = React.useMemo(
561
+ () => getColumnsForLifecycle(lifecycleTabId),
562
+ [getColumnsForLifecycle, lifecycleTabId],
563
+ )
564
+
565
+ const tableData = React.useMemo(
566
+ () => placementsForPhase(lifecycleTabId),
567
+ [lifecycleTabId],
568
+ )
569
+
570
+ const filterFields = React.useMemo(() => columnsToFilterFields(columns), [columns])
571
+
572
+ const fieldDefinitionsForDrawer = React.useMemo(
573
+ () => columns
574
+ .filter(c => c.key !== "select" && c.key !== "actions")
575
+ .map(c => ({
576
+ key: c.key,
577
+ label: c.label,
578
+ sortable: !!(c.sortable && c.sortKey),
579
+ })),
580
+ [columns],
581
+ )
582
+
583
+ const resolveColumnLabel = React.useCallback(
584
+ (key: string) => columns.find(c => c.key === key)?.label ?? key,
585
+ [columns],
586
+ )
587
+
588
+ const [conditionalRules, setConditionalRules] = React.useState<ConditionalRule[]>([])
589
+
590
+ function addConditionalRule(rule: Omit<ConditionalRule, "id">) {
591
+ setConditionalRules(prev => [...prev, { ...rule, id: `cr-${Date.now()}` }])
592
+ }
593
+ function removeConditionalRule(id: string) {
594
+ setConditionalRules(prev => prev.filter(r => r.id !== id))
595
+ }
596
+ function updateConditionalRule(id: string, patch: Partial<ConditionalRule>) {
597
+ setConditionalRules(prev => prev.map(r => r.id === id ? { ...r, ...patch } : r))
598
+ }
599
+
600
+ const renderFilterOptionValue = React.useCallback(
601
+ (fieldKey: string, value: string): React.ReactNode => {
602
+ if (fieldKey === "status") return <StatusBadge status={value as Status} />
603
+ const col = columns.find(c => c.key === fieldKey)
604
+ const opt = col?.filter?.options?.find(o => o.value === value)
605
+ return <span className="text-foreground">{opt?.label ?? value}</span>
606
+ },
607
+ [columns],
608
+ )
609
+
610
+ const [paginationPage, setPaginationPage] = React.useState(1)
611
+ const [paginationPageSize, setPaginationPageSize] = React.useState(10)
612
+ const [filteredCount, setFilteredCount] = React.useState(tableData.length)
613
+
614
+ React.useEffect(() => {
615
+ setFilteredCount(tableData.length)
616
+ }, [tableData])
617
+
618
+ const totalPages = Math.max(1, Math.ceil(filteredCount / Math.max(1, paginationPageSize)))
619
+ const safePage = Math.min(paginationPage, totalPages)
620
+ const paginationOverride =
621
+ pagination && view !== "board" && view !== "dashboard"
622
+ ? { page: safePage, pageSize: paginationPageSize }
623
+ : undefined
624
+
625
+ const tableState = useTableState(tableData, columns, { key: "student", dir: "asc" }, paginationOverride)
626
+
627
+ const columnKeys = React.useMemo(() => new Set(columns.map(c => c.key)), [columns])
628
+
629
+ React.useLayoutEffect(() => {
630
+ const raw = loadLifecycleFromStorage(lifecycleTabId)
631
+ if (!raw) return
632
+ applyLifecyclePersisted(tableState as unknown as TableStatePersistSlice, raw, columnKeys)
633
+ setConditionalRules(raw.conditionalRules)
634
+ setPagination(raw.pagination)
635
+ setPaginationPage(raw.paginationPage)
636
+ setPaginationPageSize(raw.paginationPageSize)
637
+ }, [lifecycleTabId, columnKeys])
638
+
639
+ React.useEffect(() => {
640
+ openDrawerRef.current = () => tableState.setSheetOpen(true)
641
+ }, [tableState.setSheetOpen])
642
+
643
+ React.useEffect(() => {
644
+ const payload = serializeLifecycle(tableState as unknown as TableStatePersistSlice, {
645
+ conditionalRules,
646
+ pagination,
647
+ paginationPage: safePage,
648
+ paginationPageSize,
649
+ })
650
+ scheduleLifecycleSave(lifecycleTabId, payload)
651
+ }, [
652
+ lifecycleTabId,
653
+ tableState.sortRules,
654
+ tableState.search,
655
+ tableState.activeFilters,
656
+ tableState.filterConnectors,
657
+ tableState.groupBy,
658
+ tableState.colOrder,
659
+ tableState.hiddenCols,
660
+ tableState.colWidths,
661
+ tableState.colPins,
662
+ tableState.colWrap,
663
+ tableState.colMenuSearch,
664
+ tableState.rowHeight,
665
+ tableState.showGridlines,
666
+ tableState.filterBarVisible,
667
+ tableState.searchOpen,
668
+ conditionalRules,
669
+ pagination,
670
+ safePage,
671
+ paginationPageSize,
672
+ ])
673
+
674
+ function buildToolbarSlot(
675
+ s: ReturnType<typeof useTableState<Placement>>,
676
+ ): React.ReactNode {
677
+ return (
678
+ <TablePropertiesDrawerButton
679
+ state={s}
680
+ totalRows={tableData.length}
681
+ pagination={pagination}
682
+ onPaginationChange={setPagination}
683
+ conditionalRules={conditionalRules}
684
+ onAddConditionalRule={addConditionalRule}
685
+ onRemoveConditionalRule={removeConditionalRule}
686
+ onUpdateConditionalRule={updateConditionalRule}
687
+ filterFields={filterFields}
688
+ currentView={view}
689
+ onViewChange={onViewChange}
690
+ lifecycleTabLabel={lifecycleDrawerLabel}
691
+ fieldDefinitions={fieldDefinitionsForDrawer}
692
+ resolveColumnLabel={resolveColumnLabel}
693
+ displayOptions={displayOptions}
694
+ onDisplayOptionsChange={patchDisplayOptions}
695
+ renderFilterOptionValue={renderFilterOptionValue}
696
+ />
697
+ )
698
+ }
699
+
700
+ function bulkActionsSlot(selected: Set<string | number>, _rows: Placement[]): React.ReactNode {
701
+ const count = selected.size
702
+ const contextId = "bulk-selection-context"
703
+ return (
704
+ <>
705
+ <span id={contextId} className="sr-only">
706
+ {count} {count === 1 ? "row" : "rows"} selected
707
+ </span>
708
+ <Button size="sm" variant="default" aria-describedby={contextId}>
709
+ <i className="fa-light fa-circle-check" aria-hidden="true" /> Confirm
710
+ </Button>
711
+ <Button size="sm" variant="outline" aria-describedby={contextId}>
712
+ <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" /> Export
713
+ </Button>
714
+ <Button size="sm" variant="destructive" aria-describedby={contextId}>
715
+ <i className="fa-light fa-trash" aria-hidden="true" /> Delete
716
+ </Button>
717
+ </>
718
+ )
719
+ }
720
+
721
+ const tableProps: DataTableExtendedProps<Placement> = {
722
+ data: tableData,
723
+ columns,
724
+ getRowId: (row: Placement) => row.id,
725
+ getRowSelectionLabel: (row: Placement) => row.student,
726
+ selectable: true,
727
+ searchable: displayOptions.showToolbarSearch,
728
+ showColumnHeaders: displayOptions.showColumnLabels,
729
+ defaultSort: { key: "student" as const, dir: "asc" as const },
730
+ emptyState: emptyTableCopy,
731
+ toolbarSlot: buildToolbarSlot,
732
+ bulkActionsSlot,
733
+ renderFilterOptionValue,
734
+ conditionalRules,
735
+ onRowClick: (row: Placement) => router.push(`/data-list/${row.id}`),
736
+ state: tableState,
737
+ }
738
+
739
+ if (view === "board") {
740
+ return (
741
+ <DataListBoardShell
742
+ state={tableState}
743
+ openDrawerRef={openDrawerRef}
744
+ tableData={tableData}
745
+ columns={columns}
746
+ lifecycleTabId={lifecycleTabId}
747
+ view={view}
748
+ onViewChange={onViewChange}
749
+ pagination={pagination}
750
+ onPaginationChange={setPagination}
751
+ conditionalRules={conditionalRules}
752
+ onAddConditionalRule={addConditionalRule}
753
+ onRemoveConditionalRule={removeConditionalRule}
754
+ onUpdateConditionalRule={updateConditionalRule}
755
+ filterFields={filterFields}
756
+ lifecycleDrawerLabel={lifecycleDrawerLabel}
757
+ fieldDefinitionsForDrawer={fieldDefinitionsForDrawer}
758
+ resolveColumnLabel={resolveColumnLabel}
759
+ renderFilterOptionValue={renderFilterOptionValue}
760
+ displayOptions={displayOptions}
761
+ onDisplayOptionsChange={patchDisplayOptions}
762
+ />
763
+ )
764
+ }
765
+
766
+ if (view === "dashboard") {
767
+ return (
768
+ <DataListDashboardShell
769
+ state={tableState}
770
+ openDrawerRef={openDrawerRef}
771
+ tableData={tableData}
772
+ columns={columns}
773
+ view={view}
774
+ onViewChange={onViewChange}
775
+ pagination={pagination}
776
+ onPaginationChange={setPagination}
777
+ conditionalRules={conditionalRules}
778
+ onAddConditionalRule={addConditionalRule}
779
+ onRemoveConditionalRule={removeConditionalRule}
780
+ onUpdateConditionalRule={updateConditionalRule}
781
+ filterFields={filterFields}
782
+ lifecycleDrawerLabel={lifecycleDrawerLabel}
783
+ fieldDefinitionsForDrawer={fieldDefinitionsForDrawer}
784
+ resolveColumnLabel={resolveColumnLabel}
785
+ renderFilterOptionValue={renderFilterOptionValue}
786
+ displayOptions={displayOptions}
787
+ onDisplayOptionsChange={patchDisplayOptions}
788
+ />
789
+ )
790
+ }
791
+
792
+ if (view === "list") {
793
+ return (
794
+ <React.Fragment key={lifecycleTabId}>
795
+ {pagination ? (
796
+ <CountSyncer
797
+ count={tableState.rows.length}
798
+ onSync={setFilteredCount}
799
+ onReset={() => setPaginationPage(1)}
800
+ />
801
+ ) : null}
802
+ <DataListListShell
803
+ state={tableState}
804
+ openDrawerRef={openDrawerRef}
805
+ tableData={tableData}
806
+ columns={columns}
807
+ lifecycleTabId={lifecycleTabId}
808
+ view={view}
809
+ onViewChange={onViewChange}
810
+ pagination={pagination}
811
+ onPaginationChange={setPagination}
812
+ conditionalRules={conditionalRules}
813
+ onAddConditionalRule={addConditionalRule}
814
+ onRemoveConditionalRule={removeConditionalRule}
815
+ onUpdateConditionalRule={updateConditionalRule}
816
+ filterFields={filterFields}
817
+ lifecycleDrawerLabel={lifecycleDrawerLabel}
818
+ fieldDefinitionsForDrawer={fieldDefinitionsForDrawer}
819
+ resolveColumnLabel={resolveColumnLabel}
820
+ renderFilterOptionValue={renderFilterOptionValue}
821
+ displayOptions={displayOptions}
822
+ onDisplayOptionsChange={patchDisplayOptions}
823
+ listRows={pagination ? tableState.pagedRows : tableState.rows}
824
+ emptyTableCopy={emptyTableCopy}
825
+ />
826
+ {pagination ? (
827
+ <div className="mx-4 lg:mx-6 border-x border-b border-border rounded-b-lg overflow-hidden">
828
+ <PaginationBar
829
+ page={safePage}
830
+ pageSize={paginationPageSize}
831
+ total={filteredCount}
832
+ pageSizeOptions={[10, 25, 50, 100]}
833
+ onPageChange={setPaginationPage}
834
+ onPageSizeChange={n => {
835
+ setPaginationPageSize(n)
836
+ setPaginationPage(1)
837
+ }}
838
+ />
839
+ </div>
840
+ ) : null}
841
+ </React.Fragment>
842
+ )
843
+ }
844
+
845
+ if (pagination) {
846
+ return (
847
+ <React.Fragment key={lifecycleTabId}>
848
+ <CountSyncer
849
+ count={tableState.rows.length}
850
+ onSync={setFilteredCount}
851
+ onReset={() => setPaginationPage(1)}
852
+ />
853
+ <DataTable<Placement> {...tableProps} hasFooter />
854
+ <div className="mx-4 lg:mx-6 border-x border-b border-border rounded-b-lg overflow-hidden">
855
+ <PaginationBar
856
+ page={safePage}
857
+ pageSize={paginationPageSize}
858
+ total={filteredCount}
859
+ pageSizeOptions={[10, 25, 50, 100]}
860
+ onPageChange={setPaginationPage}
861
+ onPageSizeChange={n => {
862
+ setPaginationPageSize(n)
863
+ setPaginationPage(1)
864
+ }}
865
+ />
866
+ </div>
867
+ </React.Fragment>
868
+ )
869
+ }
870
+
871
+ return <DataTable<Placement> key={lifecycleTabId} {...tableProps} />
872
+ })
873
+
874
+ DataListTable.displayName = "DataListTable"
875
+
876
+
877
+ export type { DataListViewType } from "@/lib/data-list-view"
878
+ export { DATA_LIST_VIEW_TILES } from "@/lib/data-list-view"
879
+ export type { DataListDisplayOptions } from "@/lib/data-list-display-options"