- }
- onClick={onCreatePhase}
- className="bg-blue-600 hover:bg-blue-700 border-blue-600"
- >
- Manage Phases
-
- }
- onClick={onCreateTask}
- className="hover:text-blue-600 dark:hover:text-blue-400 hover:border-blue-600"
- >
- Add Task
-
-
diff --git a/worklenz-frontend/src/pages/projects/projectView/gantt/components/phase-details-modal/PhaseDetailsModal.tsx b/worklenz-frontend/src/pages/projects/projectView/gantt/components/phase-details-modal/PhaseDetailsModal.tsx
index 5c0629c6..073df80d 100644
--- a/worklenz-frontend/src/pages/projects/projectView/gantt/components/phase-details-modal/PhaseDetailsModal.tsx
+++ b/worklenz-frontend/src/pages/projects/projectView/gantt/components/phase-details-modal/PhaseDetailsModal.tsx
@@ -1,10 +1,12 @@
import React, { useMemo, useState } from 'react';
-import { Modal, Typography, Divider, Space, Progress, Tag, Row, Col, Card, Statistic, theme, Tooltip, Input, DatePicker, Button, ColorPicker } from 'antd';
-import { CalendarOutlined, CheckCircleOutlined, ClockCircleOutlined, BgColorsOutlined, MinusOutlined, PauseOutlined, DoubleRightOutlined, UserOutlined, EditOutlined, SaveOutlined, CloseOutlined } from '@ant-design/icons';
+import { Modal, Typography, Divider, Progress, Tag, Row, Col, Card, Statistic, theme, Tooltip, Input, DatePicker, ColorPicker, message } from 'antd';
+import { CalendarOutlined, CheckCircleOutlined, ClockCircleOutlined, BgColorsOutlined, MinusOutlined, PauseOutlined, DoubleRightOutlined, UserOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { useTranslation } from 'react-i18next';
+import { useParams } from 'react-router-dom';
import AvatarGroup from '@/components/AvatarGroup';
import { GanttTask } from '../../types/gantt-types';
+import { useUpdatePhaseMutation } from '../../services/gantt-api.service';
const { Title, Text } = Typography;
@@ -16,24 +18,16 @@ interface PhaseDetailsModalProps {
}
const PhaseDetailsModal: React.FC
= ({ open, onClose, phase, onPhaseUpdate }) => {
+ const { projectId } = useParams<{ projectId: string }>();
const { t } = useTranslation('gantt/phase-details-modal');
const { token } = theme.useToken();
- // Editing state
- const [isEditing, setIsEditing] = useState(false);
- const [editedPhase, setEditedPhase] = useState>({});
+ // API mutation hook
+ const [updatePhase, { isLoading: isUpdating }] = useUpdatePhaseMutation();
- // Initialize edited phase when phase changes or editing starts
- React.useEffect(() => {
- if (phase && isEditing) {
- setEditedPhase({
- name: phase.name,
- start_date: phase.start_date,
- end_date: phase.end_date,
- color: phase.color,
- });
- }
- }, [phase, isEditing]);
+ // Inline editing state
+ const [editingField, setEditingField] = useState(null);
+ const [editedValues, setEditedValues] = useState>({});
// Calculate phase statistics
const phaseStats = useMemo(() => {
@@ -168,23 +162,72 @@ const PhaseDetailsModal: React.FC = ({ open, onClose, ph
}));
};
- const handleSavePhase = () => {
- if (phase && onPhaseUpdate && editedPhase) {
- onPhaseUpdate({
- id: phase.id,
- ...editedPhase,
- });
+ const handleFieldSave = async (field: string, value: any) => {
+ if (!phase || !projectId) {
+ message.error('Phase or project information is missing');
+ return;
+ }
+
+ // Get the actual phase_id from the phase object
+ const phaseId = phase.phase_id || (phase.id.startsWith('phase-') ? phase.id.replace('phase-', '') : phase.id);
+
+ if (!phaseId || phaseId === 'unmapped') {
+ message.error('Cannot edit unmapped phase');
+ return;
+ }
+
+ try {
+ // Prepare API request based on field
+ const updateData: any = {
+ phase_id: phaseId,
+ project_id: projectId,
+ };
+
+ // Map the field to API format
+ if (field === 'name') {
+ updateData.name = value;
+ } else if (field === 'color') {
+ updateData.color_code = value;
+ } else if (field === 'start_date') {
+ updateData.start_date = value ? new Date(value).toISOString() : null;
+ } else if (field === 'end_date') {
+ updateData.end_date = value ? new Date(value).toISOString() : null;
+ }
+
+ // Call the API
+ await updatePhase(updateData).unwrap();
+
+ // Show success message
+ message.success(`Phase ${field.replace('_', ' ')} updated successfully`);
+
+ // Call the parent handler to refresh data
+ if (onPhaseUpdate) {
+ onPhaseUpdate({
+ id: phase.id,
+ [field]: value,
+ });
+ }
+
+ // Clear editing state
+ setEditingField(null);
+ setEditedValues({});
+
+ } catch (error: any) {
+ console.error('Failed to update phase:', error);
+ message.error(error?.data?.message || `Failed to update phase ${field.replace('_', ' ')}`);
+
+ // Don't clear editing state on error so user can try again
}
- setIsEditing(false);
};
- const handleCancelEdit = () => {
- setIsEditing(false);
- setEditedPhase({});
+ const handleFieldCancel = () => {
+ setEditingField(null);
+ setEditedValues({});
};
- const handleStartEdit = () => {
- setIsEditing(true);
+ const startEditing = (field: string, currentValue: any) => {
+ setEditingField(field);
+ setEditedValues({ [field]: currentValue });
};
if (!phase) return null;
@@ -192,66 +235,36 @@ const PhaseDetailsModal: React.FC = ({ open, onClose, ph
return (
-
-
- {isEditing ? (
- <>
- }
- onClick={handleSavePhase}
- >
- Save
-
- }
- onClick={handleCancelEdit}
- >
- Cancel
-
- >
- ) : (
- }
- onClick={handleStartEdit}
- style={{ color: token.colorTextSecondary }}
- >
- Edit
-
- )}
-
+
+ handleFieldSave('color', color.toHexString())}
+ size="small"
+ showText={false}
+ trigger="click"
+ />
+ {editingField === 'name' ? (
+ setEditedValues(prev => ({ ...prev, name: e.target.value }))}
+ onPressEnter={() => handleFieldSave('name', editedValues.name)}
+ onBlur={() => handleFieldSave('name', editedValues.name)}
+ onKeyDown={(e) => e.key === 'Escape' && handleFieldCancel()}
+ className="font-semibold text-lg"
+ style={{ border: 'none', padding: 0, background: 'transparent' }}
+ autoFocus
+ />
+ ) : (
+ startEditing('name', phase.name)}
+ title="Click to edit"
+ >
+ {phase.name}
+
+ )}
}
open={open}
@@ -260,6 +273,7 @@ const PhaseDetailsModal: React.FC = ({ open, onClose, ph
width={1000}
centered
className="phase-details-modal"
+ confirmLoading={isUpdating}
>
{/* Left Side - Phase Overview and Stats */}
@@ -317,31 +331,61 @@ const PhaseDetailsModal: React.FC
= ({ open, onClose, ph
{t('timeline.startDate')}
- {isEditing ? (
+ {editingField === 'start_date' ? (
setEditedPhase(prev => ({ ...prev, start_date: date?.toDate() || null }))}
+ value={editedValues.start_date ? dayjs(editedValues.start_date) : (phase.start_date ? dayjs(phase.start_date) : null)}
+ onChange={(date) => {
+ const newDate = date?.toDate() || null;
+ setEditedValues(prev => ({ ...prev, start_date: newDate }));
+ handleFieldSave('start_date', newDate);
+ }}
size="small"
className="w-full"
placeholder="Select start date"
+ autoFocus
+ open={true}
+ onOpenChange={(open) => !open && handleFieldCancel()}
/>
) : (
- {formatDate(phase.start_date)}
+ startEditing('start_date', phase.start_date)}
+ title="Click to edit"
+ >
+ {formatDate(phase.start_date)}
+
)}
{t('timeline.endDate')}
- {isEditing ? (
+ {editingField === 'end_date' ? (
setEditedPhase(prev => ({ ...prev, end_date: date?.toDate() || null }))}
+ value={editedValues.end_date ? dayjs(editedValues.end_date) : (phase.end_date ? dayjs(phase.end_date) : null)}
+ onChange={(date) => {
+ const newDate = date?.toDate() || null;
+ setEditedValues(prev => ({ ...prev, end_date: newDate }));
+ handleFieldSave('end_date', newDate);
+ }}
size="small"
className="w-full"
placeholder="Select end date"
+ autoFocus
+ open={true}
+ onOpenChange={(open) => !open && handleFieldCancel()}
/>
) : (
- {formatDate(phase.end_date)}
+ startEditing('end_date', phase.end_date)}
+ title="Click to edit"
+ >
+ {formatDate(phase.end_date)}
+
)}