2026-05-15 12:25:08 +08:00
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
|
import { Card, Button, Progress, Tag, List, Spin, message, Empty, Space, Typography } from 'antd';
|
|
|
|
|
|
import { ArrowLeftOutlined, RightOutlined } from '@ant-design/icons';
|
|
|
|
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
|
|
import apiClient from '../../utils/request';
|
2026-06-13 12:44:48 +08:00
|
|
|
|
import { useLanguageStore } from '../../store/languageStore';
|
2026-05-15 12:25:08 +08:00
|
|
|
|
|
|
|
|
|
|
const { Title, Text } = Typography;
|
|
|
|
|
|
|
|
|
|
|
|
interface ProjectPhase {
|
|
|
|
|
|
id: number;
|
|
|
|
|
|
phase_name: string;
|
|
|
|
|
|
phase_order: number;
|
|
|
|
|
|
phase_type: string;
|
|
|
|
|
|
status: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface Project {
|
|
|
|
|
|
id: number;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
status: string;
|
|
|
|
|
|
current_phase: string;
|
|
|
|
|
|
phase_progress: number;
|
|
|
|
|
|
contract_amount: number;
|
|
|
|
|
|
phases: ProjectPhase[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ConstructionOverview: React.FC = () => {
|
|
|
|
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const navigate = useNavigate();
|
2026-06-13 12:44:48 +08:00
|
|
|
|
const { t, currentLanguage } = useLanguageStore();
|
2026-05-15 12:25:08 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => { fetchProjects(); }, []);
|
|
|
|
|
|
|
|
|
|
|
|
const fetchProjects = async () => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await apiClient.get('/construction/my-projects');
|
|
|
|
|
|
if (res.data.success) {
|
|
|
|
|
|
const projectsData = res.data.data || [];
|
|
|
|
|
|
const enriched = await Promise.all(projectsData.map(async (p: Project) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const phaseRes = await apiClient.get(`/projects/${p.id}/phases`);
|
|
|
|
|
|
return { ...p, phases: phaseRes.data.data || [] };
|
|
|
|
|
|
} catch { return { ...p, phases: [] }; }
|
|
|
|
|
|
}));
|
|
|
|
|
|
setProjects(enriched);
|
|
|
|
|
|
}
|
2026-06-13 12:44:48 +08:00
|
|
|
|
} catch (e) { message.error(t('construction.getListFailed')); }
|
2026-05-15 12:25:08 +08:00
|
|
|
|
finally { setLoading(false); }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getStatusColor = (status: string) => {
|
2026-05-17 01:24:37 +08:00
|
|
|
|
const map: Record<string, string> = { active: 'green', in_progress: 'processing', planning: 'blue', completed: 'default', suspended: 'orange' };
|
2026-05-15 12:25:08 +08:00
|
|
|
|
return map[status] || 'default';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getStatusText = (status: string) => {
|
2026-06-13 12:44:48 +08:00
|
|
|
|
const map: Record<string, string> = { active: t('construction.underConstruction'), in_progress: t('construction.underConstruction'), planning: t('construction.pendingStart'), completed: t('construction.completed'), suspended: t('construction.paused') };
|
2026-05-15 12:25:08 +08:00
|
|
|
|
return map[status] || status;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const activeProjects = projects.filter(p => p.status !== 'completed');
|
|
|
|
|
|
const completedProjects = projects.filter(p => p.status === 'completed');
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div style={{ padding: 24 }}>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Card title={<Title level={4} style={{ margin: 0 }}>{t('construction.overview')}</Title>}>
|
2026-05-15 12:25:08 +08:00
|
|
|
|
<Spin spinning={loading}>
|
|
|
|
|
|
{activeProjects.length === 0 && completedProjects.length === 0 ? (
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Empty description={t('construction.noProjects')} />
|
2026-05-15 12:25:08 +08:00
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{activeProjects.length > 0 && (
|
|
|
|
|
|
<>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Text strong style={{ fontSize: 16 }}>{t('construction.underConstruction')}{activeProjects.length}{t('common.unit')}</Text>
|
2026-05-15 12:25:08 +08:00
|
|
|
|
<List
|
|
|
|
|
|
style={{ marginTop: 16 }}
|
|
|
|
|
|
dataSource={activeProjects}
|
|
|
|
|
|
renderItem={(project) => (
|
|
|
|
|
|
<List.Item
|
2026-06-13 12:44:48 +08:00
|
|
|
|
actions={[<Button type="primary" icon={<RightOutlined />} onClick={() => navigate(`/construction/progress/${project.id}`)}>{t('construction.enter')}</Button>]}
|
2026-05-15 12:25:08 +08:00
|
|
|
|
>
|
|
|
|
|
|
<List.Item.Meta
|
|
|
|
|
|
title={<Space><Text strong>{project.name}</Text><Tag color={getStatusColor(project.status)}>{getStatusText(project.status)}</Tag></Space>}
|
|
|
|
|
|
description={
|
|
|
|
|
|
<div style={{ marginTop: 8 }}>
|
|
|
|
|
|
<div style={{ marginBottom: 4 }}>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Text type="secondary">{t('construction.currentPhase')}{project.current_phase || t('common.notSet')}</Text>
|
2026-05-15 12:25:08 +08:00
|
|
|
|
{project.phases.length > 0 && <Text type="secondary" style={{ marginLeft: 16 }}>({project.phases.filter(p => p.status === 'completed').length}/{project.phases.length})</Text>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Progress percent={project.phase_progress || 0} size="small" strokeColor="#1890ff" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</List.Item>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{completedProjects.length > 0 && (
|
|
|
|
|
|
<>
|
2026-06-13 12:44:48 +08:00
|
|
|
|
<Text strong style={{ fontSize: 16, marginTop: 24, display: 'block' }}>{t('construction.completedProjects')}{completedProjects.length}{t('common.unit')}</Text>
|
2026-05-15 12:25:08 +08:00
|
|
|
|
<List
|
|
|
|
|
|
style={{ marginTop: 16 }}
|
|
|
|
|
|
dataSource={completedProjects}
|
|
|
|
|
|
renderItem={(project) => (
|
|
|
|
|
|
<List.Item
|
2026-06-13 12:44:48 +08:00
|
|
|
|
actions={[<Button icon={<RightOutlined />} onClick={() => navigate(`/construction/progress/${project.id}`)}>{t('common.view')}</Button>]}
|
2026-05-15 12:25:08 +08:00
|
|
|
|
>
|
|
|
|
|
|
<List.Item.Meta
|
2026-06-13 12:44:48 +08:00
|
|
|
|
title={<Space><Text>{project.name}</Text><Tag>{t('construction.completed')}</Tag></Space>}
|
2026-05-15 12:25:08 +08:00
|
|
|
|
description={<Progress percent={100} size="small" />}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</List.Item>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Spin>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-13 12:44:48 +08:00
|
|
|
|
export default ConstructionOverview;
|