浏览代码

Merge branch 1.2 into main (campaign-tracker-client/src/workspace/WorkspaceShell.tsx)

pull/15/head
nano 3 天前
父节点
当前提交
355e0294df
共有 1 个文件被更改,包括 374 次插入0 次删除
  1. +374
    -0
      campaign-tracker-client/src/workspace/WorkspaceShell.tsx

+ 374
- 0
campaign-tracker-client/src/workspace/WorkspaceShell.tsx 查看文件

@@ -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.