25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

376 satır
10.0KB

  1. import {
  2. CheckCircleFilled,
  3. ClockCircleFilled,
  4. CloseCircleFilled,
  5. ExclamationCircleFilled,
  6. InfoCircleFilled,
  7. MenuFoldOutlined,
  8. MenuUnfoldOutlined,
  9. } from '@ant-design/icons'
  10. import {
  11. Alert,
  12. Badge,
  13. Button,
  14. Layout,
  15. Menu,
  16. Space,
  17. Table,
  18. Tag,
  19. Tooltip,
  20. Typography,
  21. theme,
  22. type TableProps,
  23. } from 'antd'
  24. import { useEffect, useState, type CSSProperties } from 'react'
  25. import {
  26. isEditingAvailable,
  27. isRightPanelCollapsible,
  28. semanticStatusColors,
  29. statusDefinitions,
  30. workspaceThemeTokens,
  31. type WorkspaceStatus,
  32. } from './workspaceContracts'
  33. import type { AuthenticatedUser } from '../auth/authContracts'
  34. import './WorkspaceShell.css'
  35. const { Header, Sider, Content } = Layout
  36. const { Text, Title } = Typography
  37. type CycleRow = {
  38. key: string
  39. municipality: string
  40. cycle: string
  41. service: string
  42. status: WorkspaceStatus
  43. dueDate: string
  44. owner: string
  45. }
  46. const cycleRows: CycleRow[] = [
  47. {
  48. key: 'CAM-1042',
  49. municipality: 'Fairview Borough',
  50. cycle: 'Primary 2026',
  51. service: 'Addressing',
  52. status: 'onTrack',
  53. dueDate: 'May 18',
  54. owner: 'Client Services',
  55. },
  56. {
  57. key: 'CAM-1088',
  58. municipality: 'Lake Township',
  59. cycle: 'Primary 2026',
  60. service: 'Transport',
  61. status: 'atRisk',
  62. dueDate: 'May 12',
  63. owner: 'Production Lead',
  64. },
  65. {
  66. key: 'CAM-1120',
  67. municipality: 'Northfield City',
  68. cycle: 'Special 2026',
  69. service: 'Sorting',
  70. status: 'blocked',
  71. dueDate: 'May 9',
  72. owner: 'Data Quality',
  73. },
  74. {
  75. key: 'CAM-1137',
  76. municipality: 'Pine County',
  77. cycle: 'General 2026',
  78. service: 'Office Copies',
  79. status: 'inProgress',
  80. dueDate: 'May 22',
  81. owner: 'Operations',
  82. },
  83. ]
  84. const riskItems = [
  85. {
  86. id: 'RQ-18',
  87. title: 'Lake Township transport confirmation',
  88. status: 'atRisk' as WorkspaceStatus,
  89. owner: 'Production Lead',
  90. },
  91. {
  92. id: 'RQ-21',
  93. title: 'Northfield missing sorting owner',
  94. status: 'blocked' as WorkspaceStatus,
  95. owner: 'Data Quality',
  96. },
  97. {
  98. id: 'RQ-24',
  99. title: 'Pine County proof approval check',
  100. status: 'inProgress' as WorkspaceStatus,
  101. owner: 'Operations',
  102. },
  103. ]
  104. const statusIcons = {
  105. onTrack: CheckCircleFilled,
  106. atRisk: ExclamationCircleFilled,
  107. blocked: CloseCircleFilled,
  108. inProgress: InfoCircleFilled,
  109. overdue: ClockCircleFilled,
  110. } satisfies Record<WorkspaceStatus, typeof CheckCircleFilled>
  111. function useViewportWidth() {
  112. const [width, setWidth] = useState(() =>
  113. typeof window === 'undefined' ? 1600 : window.innerWidth,
  114. )
  115. useEffect(() => {
  116. const updateWidth = () => setWidth(window.innerWidth)
  117. updateWidth()
  118. window.addEventListener('resize', updateWidth)
  119. return () => window.removeEventListener('resize', updateWidth)
  120. }, [])
  121. return width
  122. }
  123. function StatusIndicator({ status }: { status: WorkspaceStatus }) {
  124. const definition = statusDefinitions[status]
  125. const Icon = statusIcons[status]
  126. return (
  127. <Tag
  128. className="workspace-status"
  129. style={{
  130. borderColor: definition.color,
  131. color: definition.color,
  132. }}
  133. aria-label={`${definition.iconLabel}: ${definition.label}`}
  134. >
  135. <Icon aria-hidden="true" />
  136. <span>{definition.label}</span>
  137. </Tag>
  138. )
  139. }
  140. function WorkspaceNavigation() {
  141. return (
  142. <Sider width={248} className="workspace-nav" theme="light">
  143. <div className="workspace-brand">
  144. <Text className="workspace-brand__eyebrow">Campaign Tracker</Text>
  145. <Title level={1}>Operations</Title>
  146. </div>
  147. <Menu
  148. mode="inline"
  149. defaultSelectedKeys={['cycles']}
  150. items={[
  151. { key: 'cycles', label: 'Election Cycles' },
  152. { key: 'municipalities', label: 'Municipalities' },
  153. { key: 'milestones', label: 'Milestones' },
  154. { key: 'reports', label: 'Reports' },
  155. ]}
  156. />
  157. </Sider>
  158. )
  159. }
  160. function RiskPanel({
  161. collapsed,
  162. onToggle,
  163. canCollapse,
  164. }: {
  165. collapsed: boolean
  166. onToggle: () => void
  167. canCollapse: boolean
  168. }) {
  169. if (collapsed) {
  170. return (
  171. <Sider width={48} className="workspace-inspector workspace-inspector--rail">
  172. <Tooltip title="Open risk panel" placement="left">
  173. <Button
  174. type="text"
  175. icon={<MenuUnfoldOutlined />}
  176. aria-label="Open risk panel"
  177. onClick={onToggle}
  178. />
  179. </Tooltip>
  180. </Sider>
  181. )
  182. }
  183. return (
  184. <Sider width={336} className="workspace-inspector" theme="light">
  185. <div className="workspace-inspector__header">
  186. <div>
  187. <Text className="workspace-kicker">Risk queue</Text>
  188. <Title level={2}>Cutoff Watch</Title>
  189. </div>
  190. {canCollapse ? (
  191. <Tooltip title="Collapse risk panel" placement="left">
  192. <Button
  193. type="text"
  194. icon={<MenuFoldOutlined />}
  195. aria-label="Collapse risk panel"
  196. onClick={onToggle}
  197. />
  198. </Tooltip>
  199. ) : null}
  200. </div>
  201. <Space direction="vertical" size={8} className="workspace-risk-list">
  202. {riskItems.map((item) => (
  203. <article className="workspace-risk-item" key={item.id}>
  204. <div>
  205. <Text className="workspace-risk-item__id">{item.id}</Text>
  206. <Text strong>{item.title}</Text>
  207. </div>
  208. <StatusIndicator status={item.status} />
  209. <Text type="secondary">{item.owner}</Text>
  210. </article>
  211. ))}
  212. </Space>
  213. </Sider>
  214. )
  215. }
  216. export function WorkspaceShell({ user }: { user: AuthenticatedUser }) {
  217. const width = useViewportWidth()
  218. const editingAvailable = isEditingAvailable(width)
  219. const canCollapseRightPanel = isRightPanelCollapsible(width)
  220. const [rightPanelCollapseRequested, setRightPanelCollapseRequested] =
  221. useState(false)
  222. const rightPanelCollapsed =
  223. canCollapseRightPanel && rightPanelCollapseRequested
  224. const { token } = theme.useToken()
  225. const columns: TableProps<CycleRow>['columns'] = [
  226. {
  227. title: 'Record',
  228. dataIndex: 'key',
  229. key: 'key',
  230. render: (value: string) => <Text code>{value}</Text>,
  231. },
  232. {
  233. title: 'Municipality',
  234. dataIndex: 'municipality',
  235. key: 'municipality',
  236. },
  237. {
  238. title: 'Cycle',
  239. dataIndex: 'cycle',
  240. key: 'cycle',
  241. },
  242. {
  243. title: 'Service',
  244. dataIndex: 'service',
  245. key: 'service',
  246. },
  247. {
  248. title: 'Status',
  249. dataIndex: 'status',
  250. key: 'status',
  251. render: (status: WorkspaceStatus) => <StatusIndicator status={status} />,
  252. },
  253. {
  254. title: 'Due',
  255. dataIndex: 'dueDate',
  256. key: 'dueDate',
  257. },
  258. {
  259. title: 'Owner',
  260. dataIndex: 'owner',
  261. key: 'owner',
  262. },
  263. ]
  264. return (
  265. <Layout
  266. className="workspace-shell"
  267. style={
  268. {
  269. '--workspace-secondary': semanticStatusColors.secondary,
  270. '--workspace-focus': workspaceThemeTokens.colorInfo,
  271. '--workspace-border': workspaceThemeTokens.colorBorder,
  272. '--workspace-surface': '#FFFFFF',
  273. '--workspace-text-secondary': workspaceThemeTokens.colorTextSecondary,
  274. } as CSSProperties
  275. }
  276. >
  277. <WorkspaceNavigation />
  278. <Layout className="workspace-main">
  279. <Header className="workspace-header">
  280. <Space align="center" size={12}>
  281. <Badge color={workspaceThemeTokens.colorPrimary} text="Primary workspace" />
  282. <Text type="secondary">{user.userName}</Text>
  283. </Space>
  284. <Space>
  285. <Button disabled={!editingAvailable}>Save View</Button>
  286. <Tooltip
  287. title={
  288. editingAvailable
  289. ? 'Commit selected operational updates'
  290. : 'Use a 1280px or wider desktop viewport for editing'
  291. }
  292. >
  293. <Button type="primary" disabled={!editingAvailable}>
  294. Commit Update
  295. </Button>
  296. </Tooltip>
  297. </Space>
  298. </Header>
  299. <Content className="workspace-content">
  300. {!editingAvailable ? (
  301. <Alert
  302. className="workspace-support-notice"
  303. type="info"
  304. showIcon
  305. message="Reduced read mode"
  306. description="This viewport is below the 1280px operational minimum. Review is available, but editing and commit actions are disabled until the workspace is opened on a supported desktop width."
  307. />
  308. ) : null}
  309. <section
  310. className="workspace-board"
  311. aria-label="Election cycle operations workspace"
  312. >
  313. <div className="workspace-board__header">
  314. <div>
  315. <Text className="workspace-kicker">Election cycle setup</Text>
  316. <Title level={2}>Municipality work queue</Title>
  317. </div>
  318. <Space size={8} wrap>
  319. <StatusIndicator status="onTrack" />
  320. <StatusIndicator status="atRisk" />
  321. <StatusIndicator status="blocked" />
  322. </Space>
  323. </div>
  324. <Table
  325. className="workspace-table"
  326. columns={columns}
  327. dataSource={cycleRows}
  328. pagination={false}
  329. size="small"
  330. scroll={{ x: 960 }}
  331. rowClassName={(row) =>
  332. row.status === 'blocked' ? 'workspace-row--blocked' : ''
  333. }
  334. />
  335. <div className="workspace-board__footer">
  336. <Text type="secondary">
  337. Last refreshed 12:48 PM. Legacy source context remains read-only;
  338. new workflow updates route through extension records.
  339. </Text>
  340. <Button
  341. style={{ borderColor: token.colorBorder }}
  342. disabled={!editingAvailable}
  343. >
  344. Open Inspector
  345. </Button>
  346. </div>
  347. </section>
  348. </Content>
  349. </Layout>
  350. <RiskPanel
  351. collapsed={rightPanelCollapsed}
  352. canCollapse={canCollapseRightPanel}
  353. onToggle={() => setRightPanelCollapseRequested((value) => !value)}
  354. />
  355. </Layout>
  356. )
  357. }

Powered by TurnKey Linux.