- Added new components for an advanced Gantt chart, including AdvancedGanttChart, GanttGrid, DraggableTaskBar, and TimelineMarkers. - Implemented a demo page (GanttDemoPage) to showcase the functionality of the new Gantt chart components. - Enhanced project roadmap features with ProjectRoadmapGantt and related components for better project management visualization. - Introduced sample data for testing and demonstration purposes, improving the user experience in the Gantt chart interface. - Updated main routes to include the new Gantt demo page for easy access.
406 lines
14 KiB
TypeScript
406 lines
14 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
Modal,
|
|
Tabs,
|
|
Progress,
|
|
Tag,
|
|
List,
|
|
Avatar,
|
|
Badge,
|
|
Space,
|
|
Button,
|
|
Statistic,
|
|
Row,
|
|
Col,
|
|
Timeline,
|
|
Input,
|
|
Form,
|
|
DatePicker,
|
|
Select
|
|
} from 'antd';
|
|
import {
|
|
CalendarOutlined,
|
|
TeamOutlined,
|
|
CheckCircleOutlined,
|
|
ClockCircleOutlined,
|
|
FlagOutlined,
|
|
ExclamationCircleOutlined,
|
|
EditOutlined,
|
|
SaveOutlined,
|
|
CloseOutlined
|
|
} from '@ant-design/icons';
|
|
import { PhaseModalData, ProjectPhase, PhaseTask, PhaseMilestone } from '../../types/project-roadmap.types';
|
|
import { useAppSelector } from '../../hooks/useAppSelector';
|
|
import { themeWiseColor } from '../../utils/themeWiseColor';
|
|
import dayjs from 'dayjs';
|
|
|
|
const { TabPane } = Tabs;
|
|
const { TextArea } = Input;
|
|
|
|
interface PhaseModalProps {
|
|
visible: boolean;
|
|
phase: PhaseModalData | null;
|
|
onClose: () => void;
|
|
onUpdate?: (updates: Partial<ProjectPhase>) => void;
|
|
}
|
|
|
|
const PhaseModal: React.FC<PhaseModalProps> = ({
|
|
visible,
|
|
phase,
|
|
onClose,
|
|
onUpdate,
|
|
}) => {
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
const [form] = Form.useForm();
|
|
|
|
// Theme support
|
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
|
const isDarkMode = themeMode === 'dark';
|
|
|
|
if (!phase) return null;
|
|
|
|
const handleEdit = () => {
|
|
setIsEditing(true);
|
|
form.setFieldsValue({
|
|
name: phase.name,
|
|
description: phase.description,
|
|
startDate: dayjs(phase.startDate),
|
|
endDate: dayjs(phase.endDate),
|
|
status: phase.status,
|
|
});
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
try {
|
|
const values = await form.validateFields();
|
|
const updates: Partial<ProjectPhase> = {
|
|
name: values.name,
|
|
description: values.description,
|
|
startDate: values.startDate.toDate(),
|
|
endDate: values.endDate.toDate(),
|
|
status: values.status,
|
|
};
|
|
|
|
onUpdate?.(updates);
|
|
setIsEditing(false);
|
|
} catch (error) {
|
|
console.error('Validation failed:', error);
|
|
}
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
setIsEditing(false);
|
|
form.resetFields();
|
|
};
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case 'completed': return 'success';
|
|
case 'in-progress': return 'processing';
|
|
case 'on-hold': return 'warning';
|
|
default: return 'default';
|
|
}
|
|
};
|
|
|
|
const getPriorityColor = (priority: string) => {
|
|
switch (priority) {
|
|
case 'high': return 'red';
|
|
case 'medium': return 'orange';
|
|
case 'low': return 'green';
|
|
default: return 'default';
|
|
}
|
|
};
|
|
|
|
const getTaskStatusIcon = (status: string) => {
|
|
switch (status) {
|
|
case 'done': return <CheckCircleOutlined style={{ color: '#52c41a' }} />;
|
|
case 'in-progress': return <ClockCircleOutlined style={{ color: '#1890ff' }} />;
|
|
default: return <ExclamationCircleOutlined style={{ color: '#d9d9d9' }} />;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
title={
|
|
<div className="flex justify-between items-center">
|
|
<Space>
|
|
<Badge status={getStatusColor(phase.status)} />
|
|
{isEditing ? (
|
|
<Form.Item name="name" className="mb-0">
|
|
<Input className="dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100" />
|
|
</Form.Item>
|
|
) : (
|
|
<h4 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-0">
|
|
{phase.name}
|
|
</h4>
|
|
)}
|
|
</Space>
|
|
<Space>
|
|
{isEditing ? (
|
|
<>
|
|
<Button
|
|
type="primary"
|
|
icon={<SaveOutlined />}
|
|
onClick={handleSave}
|
|
size="small"
|
|
>
|
|
Save
|
|
</Button>
|
|
<Button
|
|
icon={<CloseOutlined />}
|
|
onClick={handleCancel}
|
|
size="small"
|
|
className="dark:border-gray-600 dark:text-gray-300"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<Button
|
|
type="text"
|
|
icon={<EditOutlined />}
|
|
onClick={handleEdit}
|
|
size="small"
|
|
className="dark:text-gray-300 dark:hover:bg-gray-700"
|
|
>
|
|
Edit
|
|
</Button>
|
|
)}
|
|
</Space>
|
|
</div>
|
|
}
|
|
open={visible}
|
|
onCancel={onClose}
|
|
width={800}
|
|
footer={null}
|
|
className="dark:bg-gray-800"
|
|
>
|
|
<Form form={form} layout="vertical">
|
|
<div className="mb-4">
|
|
{isEditing ? (
|
|
<Form.Item name="description" label={<span className="text-gray-700 dark:text-gray-300">Description</span>}>
|
|
<TextArea
|
|
rows={2}
|
|
className="dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"
|
|
/>
|
|
</Form.Item>
|
|
) : (
|
|
<p className="text-gray-600 dark:text-gray-400">{phase.description}</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Phase Statistics */}
|
|
<Row gutter={16} className="mb-6">
|
|
<Col span={6}>
|
|
<div className="bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-4">
|
|
<Statistic
|
|
title={<span className="text-gray-600 dark:text-gray-400">Progress</span>}
|
|
value={phase.progress}
|
|
suffix="%"
|
|
valueStyle={{ color: themeWiseColor('#1890ff', '#40a9ff', themeMode) }}
|
|
/>
|
|
<Progress
|
|
percent={phase.progress}
|
|
showInfo={false}
|
|
size="small"
|
|
strokeColor={themeWiseColor('#1890ff', '#40a9ff', themeMode)}
|
|
trailColor={themeWiseColor('#f0f0f0', '#4b5563', themeMode)}
|
|
/>
|
|
</div>
|
|
</Col>
|
|
<Col span={6}>
|
|
<div className="bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-4">
|
|
<Statistic
|
|
title={<span className="text-gray-600 dark:text-gray-400">Tasks</span>}
|
|
value={phase.completedTaskCount}
|
|
suffix={`/ ${phase.taskCount}`}
|
|
valueStyle={{ color: themeWiseColor('#52c41a', '#34d399', themeMode) }}
|
|
/>
|
|
</div>
|
|
</Col>
|
|
<Col span={6}>
|
|
<div className="bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-4">
|
|
<Statistic
|
|
title={<span className="text-gray-600 dark:text-gray-400">Milestones</span>}
|
|
value={phase.completedMilestoneCount}
|
|
suffix={`/ ${phase.milestoneCount}`}
|
|
valueStyle={{ color: themeWiseColor('#722ed1', '#9f7aea', themeMode) }}
|
|
/>
|
|
</div>
|
|
</Col>
|
|
<Col span={6}>
|
|
<div className="bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-4">
|
|
<Statistic
|
|
title={<span className="text-gray-600 dark:text-gray-400">Team</span>}
|
|
value={phase.teamMembers.length}
|
|
suffix="members"
|
|
valueStyle={{ color: themeWiseColor('#fa8c16', '#fbbf24', themeMode) }}
|
|
/>
|
|
</div>
|
|
</Col>
|
|
</Row>
|
|
|
|
{/* Timeline */}
|
|
<Row gutter={16} className="mb-6">
|
|
<Col span={12}>
|
|
{isEditing ? (
|
|
<Form.Item name="startDate" label={<span className="text-gray-700 dark:text-gray-300">Start Date</span>}>
|
|
<DatePicker className="w-full dark:bg-gray-700 dark:border-gray-600" />
|
|
</Form.Item>
|
|
) : (
|
|
<div className="bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-4">
|
|
<div className="flex items-center gap-2">
|
|
<CalendarOutlined className="text-gray-600 dark:text-gray-400" />
|
|
<span className="font-medium text-gray-900 dark:text-gray-100">Start:</span>
|
|
<span className="text-gray-600 dark:text-gray-400">{phase.startDate.toLocaleDateString()}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Col>
|
|
<Col span={12}>
|
|
{isEditing ? (
|
|
<Form.Item name="endDate" label={<span className="text-gray-700 dark:text-gray-300">End Date</span>}>
|
|
<DatePicker className="w-full dark:bg-gray-700 dark:border-gray-600" />
|
|
</Form.Item>
|
|
) : (
|
|
<div className="bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-4">
|
|
<div className="flex items-center gap-2">
|
|
<CalendarOutlined className="text-gray-600 dark:text-gray-400" />
|
|
<span className="font-medium text-gray-900 dark:text-gray-100">End:</span>
|
|
<span className="text-gray-600 dark:text-gray-400">{phase.endDate.toLocaleDateString()}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Col>
|
|
</Row>
|
|
|
|
{isEditing && (
|
|
<Row gutter={16} className="mb-6">
|
|
<Col span={12}>
|
|
<Form.Item name="status" label={<span className="text-gray-700 dark:text-gray-300">Status</span>}>
|
|
<Select className="dark:bg-gray-700 dark:border-gray-600">
|
|
<Select.Option value="not-started">Not Started</Select.Option>
|
|
<Select.Option value="in-progress">In Progress</Select.Option>
|
|
<Select.Option value="completed">Completed</Select.Option>
|
|
<Select.Option value="on-hold">On Hold</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
)}
|
|
|
|
<Tabs
|
|
defaultActiveKey="tasks"
|
|
className="dark:bg-gray-800"
|
|
tabBarStyle={{
|
|
borderBottom: `1px solid ${themeWiseColor('#f0f0f0', '#4b5563', themeMode)}`
|
|
}}
|
|
>
|
|
<TabPane tab={`Tasks (${phase.taskCount})`} key="tasks">
|
|
<List
|
|
dataSource={phase.tasks}
|
|
renderItem={(task: PhaseTask) => (
|
|
<List.Item>
|
|
<List.Item.Meta
|
|
avatar={getTaskStatusIcon(task.status)}
|
|
title={
|
|
<div className="flex justify-between items-center">
|
|
<Text strong>{task.name}</Text>
|
|
<Space>
|
|
<Tag color={getPriorityColor(task.priority)}>
|
|
{task.priority}
|
|
</Tag>
|
|
<Progress
|
|
percent={task.progress}
|
|
size="small"
|
|
style={{ width: 100 }}
|
|
/>
|
|
</Space>
|
|
</div>
|
|
}
|
|
description={
|
|
<div>
|
|
<Text type="secondary">{task.description}</Text>
|
|
<div className="mt-2 flex justify-between items-center">
|
|
<Space>
|
|
<CalendarOutlined />
|
|
<Text type="secondary">
|
|
{task.startDate.toLocaleDateString()} - {task.endDate.toLocaleDateString()}
|
|
</Text>
|
|
</Space>
|
|
{task.assigneeName && (
|
|
<Space>
|
|
<TeamOutlined />
|
|
<Text type="secondary">{task.assigneeName}</Text>
|
|
</Space>
|
|
)}
|
|
</div>
|
|
</div>
|
|
}
|
|
/>
|
|
</List.Item>
|
|
)}
|
|
/>
|
|
</TabPane>
|
|
|
|
<TabPane tab={`Milestones (${phase.milestoneCount})`} key="milestones">
|
|
<Timeline>
|
|
{phase.milestones.map((milestone: PhaseMilestone) => (
|
|
<Timeline.Item
|
|
key={milestone.id}
|
|
color={milestone.isCompleted ? 'green' : milestone.criticalPath ? 'red' : 'blue'}
|
|
dot={milestone.isCompleted ? <CheckCircleOutlined /> : <FlagOutlined />}
|
|
>
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<Text strong>{milestone.name}</Text>
|
|
{milestone.criticalPath && (
|
|
<Tag color="red" className="ml-2">Critical Path</Tag>
|
|
)}
|
|
{milestone.description && (
|
|
<div className="mt-1">
|
|
<Text type="secondary">{milestone.description}</Text>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="text-right">
|
|
<div>
|
|
<CalendarOutlined />
|
|
<span className="ml-1">{milestone.dueDate.toLocaleDateString()}</span>
|
|
</div>
|
|
<Badge
|
|
status={milestone.isCompleted ? 'success' : 'processing'}
|
|
text={milestone.isCompleted ? 'Completed' : 'Pending'}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Timeline.Item>
|
|
))}
|
|
</Timeline>
|
|
</TabPane>
|
|
|
|
<TabPane tab={`Team (${phase.teamMembers.length})`} key="team">
|
|
<List
|
|
dataSource={phase.teamMembers}
|
|
renderItem={(member: string) => (
|
|
<List.Item>
|
|
<List.Item.Meta
|
|
avatar={<Avatar>{member.charAt(0).toUpperCase()}</Avatar>}
|
|
title={member}
|
|
description={
|
|
<Text type="secondary">
|
|
{phase.tasks.filter(task => task.assigneeName === member).length} tasks assigned
|
|
</Text>
|
|
}
|
|
/>
|
|
</List.Item>
|
|
)}
|
|
/>
|
|
</TabPane>
|
|
</Tabs>
|
|
</Form>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default PhaseModal; |