| @@ -0,0 +1,374 @@ | |||
| import { | |||
| CheckCircleFilled, | |||
| ClockCircleFilled, | |||
| CloseCircleFilled, | |||
| ExclamationCircleFilled, | |||
| InfoCircleFilled, | |||
| MenuFoldOutlined, | |||
| MenuUnfoldOutlined, | |||
| } from '@ant-design/icons' | |||
| import { | |||
| Alert, | |||
| Badge, | |||
| Button, | |||
| Layout, | |||
| Menu, | |||
| Space, | |||
| Table, | |||
| Tag, | |||
| Tooltip, | |||
| Typography, | |||
| theme, | |||
| type TableProps, | |||
| } from 'antd' | |||
| import { useEffect, useState, type CSSProperties } from 'react' | |||
| import { | |||
| isEditingAvailable, | |||
| isRightPanelCollapsible, | |||
| semanticStatusColors, | |||
| statusDefinitions, | |||
| workspaceThemeTokens, | |||
| type WorkspaceStatus, | |||
| } from './workspaceContracts' | |||
| import './WorkspaceShell.css' | |||
| const { Header, Sider, Content } = Layout | |||
| const { Text, Title } = Typography | |||
| type CycleRow = { | |||
| key: string | |||
| municipality: string | |||
| cycle: string | |||
| service: string | |||
| status: WorkspaceStatus | |||
| dueDate: string | |||
| owner: string | |||
| } | |||
| const cycleRows: CycleRow[] = [ | |||
| { | |||
| key: 'CAM-1042', | |||
| municipality: 'Fairview Borough', | |||
| cycle: 'Primary 2026', | |||
| service: 'Addressing', | |||
| status: 'onTrack', | |||
| dueDate: 'May 18', | |||
| owner: 'Client Services', | |||
| }, | |||
| { | |||
| key: 'CAM-1088', | |||
| municipality: 'Lake Township', | |||
| cycle: 'Primary 2026', | |||
| service: 'Transport', | |||
| status: 'atRisk', | |||
| dueDate: 'May 12', | |||
| owner: 'Production Lead', | |||
| }, | |||
| { | |||
| key: 'CAM-1120', | |||
| municipality: 'Northfield City', | |||
| cycle: 'Special 2026', | |||
| service: 'Sorting', | |||
| status: 'blocked', | |||
| dueDate: 'May 9', | |||
| owner: 'Data Quality', | |||
| }, | |||
| { | |||
| key: 'CAM-1137', | |||
| municipality: 'Pine County', | |||
| cycle: 'General 2026', | |||
| service: 'Office Copies', | |||
| status: 'inProgress', | |||
| dueDate: 'May 22', | |||
| owner: 'Operations', | |||
| }, | |||
| ] | |||
| const riskItems = [ | |||
| { | |||
| id: 'RQ-18', | |||
| title: 'Lake Township transport confirmation', | |||
| status: 'atRisk' as WorkspaceStatus, | |||
| owner: 'Production Lead', | |||
| }, | |||
| { | |||
| id: 'RQ-21', | |||
| title: 'Northfield missing sorting owner', | |||
| status: 'blocked' as WorkspaceStatus, | |||
| owner: 'Data Quality', | |||
| }, | |||
| { | |||
| id: 'RQ-24', | |||
| title: 'Pine County proof approval check', | |||
| status: 'inProgress' as WorkspaceStatus, | |||
| owner: 'Operations', | |||
| }, | |||
| ] | |||
| const statusIcons = { | |||
| onTrack: CheckCircleFilled, | |||
| atRisk: ExclamationCircleFilled, | |||
| blocked: CloseCircleFilled, | |||
| inProgress: InfoCircleFilled, | |||
| overdue: ClockCircleFilled, | |||
| } satisfies Record<WorkspaceStatus, typeof CheckCircleFilled> | |||
| function useViewportWidth() { | |||
| const [width, setWidth] = useState(() => | |||
| typeof window === 'undefined' ? 1600 : window.innerWidth, | |||
| ) | |||
| useEffect(() => { | |||
| const updateWidth = () => setWidth(window.innerWidth) | |||
| updateWidth() | |||
| window.addEventListener('resize', updateWidth) | |||
| return () => window.removeEventListener('resize', updateWidth) | |||
| }, []) | |||
| return width | |||
| } | |||
| function StatusIndicator({ status }: { status: WorkspaceStatus }) { | |||
| const definition = statusDefinitions[status] | |||
| const Icon = statusIcons[status] | |||
| return ( | |||
| <Tag | |||
| className="workspace-status" | |||
| style={{ | |||
| borderColor: definition.color, | |||
| color: definition.color, | |||
| }} | |||
| aria-label={`${definition.iconLabel}: ${definition.label}`} | |||
| > | |||
| <Icon aria-hidden="true" /> | |||
| <span>{definition.label}</span> | |||
| </Tag> | |||
| ) | |||
| } | |||
| function WorkspaceNavigation() { | |||
| return ( | |||
| <Sider width={248} className="workspace-nav" theme="light"> | |||
| <div className="workspace-brand"> | |||
| <Text className="workspace-brand__eyebrow">Campaign Tracker</Text> | |||
| <Title level={1}>Operations</Title> | |||
| </div> | |||
| <Menu | |||
| mode="inline" | |||
| defaultSelectedKeys={['cycles']} | |||
| items={[ | |||
| { key: 'cycles', label: 'Election Cycles' }, | |||
| { key: 'municipalities', label: 'Municipalities' }, | |||
| { key: 'milestones', label: 'Milestones' }, | |||
| { key: 'reports', label: 'Reports' }, | |||
| ]} | |||
| /> | |||
| </Sider> | |||
| ) | |||
| } | |||
| function RiskPanel({ | |||
| collapsed, | |||
| onToggle, | |||
| canCollapse, | |||
| }: { | |||
| collapsed: boolean | |||
| onToggle: () => void | |||
| canCollapse: boolean | |||
| }) { | |||
| if (collapsed) { | |||
| return ( | |||
| <Sider width={48} className="workspace-inspector workspace-inspector--rail"> | |||
| <Tooltip title="Open risk panel" placement="left"> | |||
| <Button | |||
| type="text" | |||
| icon={<MenuUnfoldOutlined />} | |||
| aria-label="Open risk panel" | |||
| onClick={onToggle} | |||
| /> | |||
| </Tooltip> | |||
| </Sider> | |||
| ) | |||
| } | |||
| return ( | |||
| <Sider width={336} className="workspace-inspector" theme="light"> | |||
| <div className="workspace-inspector__header"> | |||
| <div> | |||
| <Text className="workspace-kicker">Risk queue</Text> | |||
| <Title level={2}>Cutoff Watch</Title> | |||
| </div> | |||
| {canCollapse ? ( | |||
| <Tooltip title="Collapse risk panel" placement="left"> | |||
| <Button | |||
| type="text" | |||
| icon={<MenuFoldOutlined />} | |||
| aria-label="Collapse risk panel" | |||
| onClick={onToggle} | |||
| /> | |||
| </Tooltip> | |||
| ) : null} | |||
| </div> | |||
| <Space direction="vertical" size={8} className="workspace-risk-list"> | |||
| {riskItems.map((item) => ( | |||
| <article className="workspace-risk-item" key={item.id}> | |||
| <div> | |||
| <Text className="workspace-risk-item__id">{item.id}</Text> | |||
| <Text strong>{item.title}</Text> | |||
| </div> | |||
| <StatusIndicator status={item.status} /> | |||
| <Text type="secondary">{item.owner}</Text> | |||
| </article> | |||
| ))} | |||
| </Space> | |||
| </Sider> | |||
| ) | |||
| } | |||
| export function WorkspaceShell() { | |||
| const width = useViewportWidth() | |||
| const editingAvailable = isEditingAvailable(width) | |||
| const canCollapseRightPanel = isRightPanelCollapsible(width) | |||
| const [rightPanelCollapseRequested, setRightPanelCollapseRequested] = | |||
| useState(false) | |||
| const rightPanelCollapsed = | |||
| canCollapseRightPanel && rightPanelCollapseRequested | |||
| const { token } = theme.useToken() | |||
| const columns: TableProps<CycleRow>['columns'] = [ | |||
| { | |||
| title: 'Record', | |||
| dataIndex: 'key', | |||
| key: 'key', | |||
| render: (value: string) => <Text code>{value}</Text>, | |||
| }, | |||
| { | |||
| title: 'Municipality', | |||
| dataIndex: 'municipality', | |||
| key: 'municipality', | |||
| }, | |||
| { | |||
| title: 'Cycle', | |||
| dataIndex: 'cycle', | |||
| key: 'cycle', | |||
| }, | |||
| { | |||
| title: 'Service', | |||
| dataIndex: 'service', | |||
| key: 'service', | |||
| }, | |||
| { | |||
| title: 'Status', | |||
| dataIndex: 'status', | |||
| key: 'status', | |||
| render: (status: WorkspaceStatus) => <StatusIndicator status={status} />, | |||
| }, | |||
| { | |||
| title: 'Due', | |||
| dataIndex: 'dueDate', | |||
| key: 'dueDate', | |||
| }, | |||
| { | |||
| title: 'Owner', | |||
| dataIndex: 'owner', | |||
| key: 'owner', | |||
| }, | |||
| ] | |||
| return ( | |||
| <Layout | |||
| className="workspace-shell" | |||
| style={ | |||
| { | |||
| '--workspace-secondary': semanticStatusColors.secondary, | |||
| '--workspace-focus': workspaceThemeTokens.colorInfo, | |||
| '--workspace-border': workspaceThemeTokens.colorBorder, | |||
| '--workspace-surface': '#FFFFFF', | |||
| '--workspace-text-secondary': workspaceThemeTokens.colorTextSecondary, | |||
| } as CSSProperties | |||
| } | |||
| > | |||
| <WorkspaceNavigation /> | |||
| <Layout className="workspace-main"> | |||
| <Header className="workspace-header"> | |||
| <Space align="center" size={12}> | |||
| <Badge color={workspaceThemeTokens.colorPrimary} text="Primary workspace" /> | |||
| <Text type="secondary">Authenticated operations shell</Text> | |||
| </Space> | |||
| <Space> | |||
| <Button disabled={!editingAvailable}>Save View</Button> | |||
| <Tooltip | |||
| title={ | |||
| editingAvailable | |||
| ? 'Commit selected operational updates' | |||
| : 'Use a 1280px or wider desktop viewport for editing' | |||
| } | |||
| > | |||
| <Button type="primary" disabled={!editingAvailable}> | |||
| Commit Update | |||
| </Button> | |||
| </Tooltip> | |||
| </Space> | |||
| </Header> | |||
| <Content className="workspace-content"> | |||
| {!editingAvailable ? ( | |||
| <Alert | |||
| className="workspace-support-notice" | |||
| type="info" | |||
| showIcon | |||
| message="Reduced read mode" | |||
| 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." | |||
| /> | |||
| ) : null} | |||
| <section | |||
| className="workspace-board" | |||
| aria-label="Election cycle operations workspace" | |||
| > | |||
| <div className="workspace-board__header"> | |||
| <div> | |||
| <Text className="workspace-kicker">Election cycle setup</Text> | |||
| <Title level={2}>Municipality work queue</Title> | |||
| </div> | |||
| <Space size={8} wrap> | |||
| <StatusIndicator status="onTrack" /> | |||
| <StatusIndicator status="atRisk" /> | |||
| <StatusIndicator status="blocked" /> | |||
| </Space> | |||
| </div> | |||
| <Table | |||
| className="workspace-table" | |||
| columns={columns} | |||
| dataSource={cycleRows} | |||
| pagination={false} | |||
| size="small" | |||
| scroll={{ x: 960 }} | |||
| rowClassName={(row) => | |||
| row.status === 'blocked' ? 'workspace-row--blocked' : '' | |||
| } | |||
| /> | |||
| <div className="workspace-board__footer"> | |||
| <Text type="secondary"> | |||
| Last refreshed 12:48 PM. Legacy source context remains read-only; | |||
| new workflow updates route through extension records. | |||
| </Text> | |||
| <Button | |||
| style={{ borderColor: token.colorBorder }} | |||
| disabled={!editingAvailable} | |||
| > | |||
| Open Inspector | |||
| </Button> | |||
| </div> | |||
| </section> | |||
| </Content> | |||
| </Layout> | |||
| <RiskPanel | |||
| collapsed={rightPanelCollapsed} | |||
| canCollapse={canCollapseRightPanel} | |||
| onToggle={() => setRightPanelCollapseRequested((value) => !value)} | |||
| /> | |||
| </Layout> | |||
| ) | |||
| } | |||
Powered by TurnKey Linux.