Update detailed view for tasks
This commit is contained in:
parent
5694df4adf
commit
40cd82a2e6
4 changed files with 117 additions and 64 deletions
|
|
@ -9,7 +9,7 @@ import {
|
|||
CalendarIcon,
|
||||
ExclamationTriangleIcon,
|
||||
ArrowPathIcon,
|
||||
ClockIcon,
|
||||
ListBulletIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import ConfirmDialog from '../Shared/ConfirmDialog';
|
||||
import TaskModal from './TaskModal';
|
||||
|
|
@ -28,7 +28,7 @@ import { useToast } from '../Shared/ToastContext';
|
|||
import TaskPriorityIcon from './TaskPriorityIcon';
|
||||
import LoadingScreen from '../Shared/LoadingScreen';
|
||||
import MarkdownRenderer from '../Shared/MarkdownRenderer';
|
||||
import TimelinePanel from './TimelinePanel';
|
||||
import TaskTimeline from './TaskTimeline';
|
||||
|
||||
const TaskDetails: React.FC = () => {
|
||||
const { uuid } = useParams<{ uuid: string }>();
|
||||
|
|
@ -46,7 +46,6 @@ const TaskDetails: React.FC = () => {
|
|||
const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
|
||||
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
|
||||
const [taskToDelete, setTaskToDelete] = useState<Task | null>(null);
|
||||
const [isTimelineExpanded, setIsTimelineExpanded] = useState(false);
|
||||
|
||||
// Date and recurrence formatting functions (from TaskHeader)
|
||||
const formatDueDate = (dueDate: string) => {
|
||||
|
|
@ -254,7 +253,7 @@ const TaskDetails: React.FC = () => {
|
|||
onToggleCompletion={handleToggleCompletion}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<h2 className="text-2xl font-light text-gray-900 dark:text-gray-100">
|
||||
<h2 className="text-2xl font-normal text-gray-900 dark:text-gray-100">
|
||||
{task.name}
|
||||
</h2>
|
||||
{/* Project, tags, due date, and recurrence under title */}
|
||||
|
|
@ -337,29 +336,12 @@ const TaskDetails: React.FC = () => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
setIsTimelineExpanded(!isTimelineExpanded)
|
||||
}
|
||||
className={`p-2 rounded-full transition-colors duration-200 ${
|
||||
isTimelineExpanded
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400'
|
||||
}`}
|
||||
title={
|
||||
isTimelineExpanded
|
||||
? 'Hide Activity Timeline'
|
||||
: 'Show Activity Timeline'
|
||||
}
|
||||
>
|
||||
<ClockIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<div className="flex space-x-1">
|
||||
<button
|
||||
onClick={handleEdit}
|
||||
className="p-2 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 rounded-full transition-colors duration-200"
|
||||
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 rounded-full transition-colors duration-200"
|
||||
>
|
||||
<PencilSquareIcon className="h-5 w-5" />
|
||||
<PencilSquareIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
|
|
@ -367,38 +349,48 @@ const TaskDetails: React.FC = () => {
|
|||
e.stopPropagation();
|
||||
handleDeleteClick();
|
||||
}}
|
||||
className="p-2 text-gray-600 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 rounded-full transition-colors duration-200"
|
||||
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 rounded-full transition-colors duration-200"
|
||||
>
|
||||
<TrashIcon className="h-5 w-5" />
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content - Two column layout for notes and subtasks */}
|
||||
{(task.note || subtasks.length > 0) && (
|
||||
<div className="mb-8 mt-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Notes Column */}
|
||||
{task.note && (
|
||||
<div>
|
||||
<h4 className="text-base font-light text-gray-900 dark:text-gray-100 mb-4">
|
||||
{t('task.notes', 'Notes')}
|
||||
</h4>
|
||||
{/* Content - Two column layout */}
|
||||
<div className="mb-8 mt-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Left Column - Notes and Subtasks */}
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
{/* Notes Section - Always Visible */}
|
||||
<div>
|
||||
<h4 className="text-base font-light text-gray-900 dark:text-gray-100 mb-4">
|
||||
{t('task.notes', 'Notes')}
|
||||
</h4>
|
||||
{task.note ? (
|
||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6">
|
||||
<MarkdownRenderer
|
||||
content={task.note}
|
||||
className="prose dark:prose-invert max-w-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6">
|
||||
<div className="flex flex-col items-center justify-center py-8 text-gray-500 dark:text-gray-400">
|
||||
<PencilSquareIcon className="h-12 w-12 mb-3 opacity-50" />
|
||||
<span className="text-sm text-center">
|
||||
{t('task.noNotes', 'No notes added yet')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Subtasks Column */}
|
||||
{subtasks.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-light text-gray-900 dark:text-gray-100 mb-4">
|
||||
{t('task.subtasks', 'Subtasks')}
|
||||
</h4>
|
||||
{/* Subtasks Section - Always Visible */}
|
||||
<div>
|
||||
<h4 className="text-base font-light text-gray-900 dark:text-gray-100 mb-4">
|
||||
{t('task.subtasks', 'Subtasks')}
|
||||
</h4>
|
||||
{subtasks.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{subtasks.map((subtask) => (
|
||||
<div
|
||||
|
|
@ -479,24 +471,33 @@ const TaskDetails: React.FC = () => {
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6">
|
||||
<div className="flex flex-col items-center justify-center py-8 text-gray-500 dark:text-gray-400">
|
||||
<ListBulletIcon className="h-12 w-12 mb-3 opacity-50" />
|
||||
<span className="text-sm text-center">
|
||||
{t('task.noSubtasks', 'No subtasks yet')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Recent Activity */}
|
||||
<div>
|
||||
<h4 className="text-base font-light text-gray-900 dark:text-gray-100 mb-4">
|
||||
{t('task.recentActivity', 'Recent Activity')}
|
||||
</h4>
|
||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6">
|
||||
<TaskTimeline taskId={task.id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* End of main content sections */}
|
||||
|
||||
|
||||
{/* Activity Timeline */}
|
||||
{isTimelineExpanded && task?.id && (
|
||||
<div className="mb-8 mt-8">
|
||||
<TimelinePanel
|
||||
taskId={task.id}
|
||||
isExpanded={isTimelineExpanded}
|
||||
onToggle={() =>
|
||||
setIsTimelineExpanded(!isTimelineExpanded)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Task Modal for Editing */}
|
||||
<TaskModal
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { TaskEvent } from '../../entities/TaskEvent';
|
|||
import {
|
||||
getTaskTimeline,
|
||||
getEventTypeLabel,
|
||||
getStatusLabel,
|
||||
getPriorityLabel,
|
||||
} from '../../utils/taskEventService';
|
||||
import {
|
||||
|
|
@ -125,6 +124,27 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ taskId }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const getTranslatedStatusLabel = (status: number | string): string => {
|
||||
// Handle both numeric and string status values
|
||||
const statusMap: Record<string | number, string> = {
|
||||
// Numeric values
|
||||
0: t('status.notStarted'),
|
||||
1: t('status.inProgress'),
|
||||
2: t('status.completed'),
|
||||
3: t('status.archived'),
|
||||
4: t('status.waiting'),
|
||||
// String values
|
||||
'not_started': t('status.notStarted'),
|
||||
'in_progress': t('status.inProgress'),
|
||||
'done': t('status.completed'),
|
||||
'completed': t('status.completed'),
|
||||
'archived': t('status.archived'),
|
||||
'waiting': t('status.waiting'),
|
||||
};
|
||||
|
||||
return statusMap[status] || t('status.unknown', { status });
|
||||
};
|
||||
|
||||
const getEventDescription = (event: TaskEvent) => {
|
||||
const { event_type, old_value, new_value } = event;
|
||||
|
||||
|
|
@ -136,7 +156,7 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ taskId }) => {
|
|||
const oldStatus = old_value?.status;
|
||||
const newStatus = new_value?.status;
|
||||
if (oldStatus !== undefined && newStatus !== undefined) {
|
||||
return `${t('timeline.events.status')}: ${getStatusLabel(oldStatus)} → ${getStatusLabel(newStatus)}`;
|
||||
return `${t('timeline.events.status')}: ${getTranslatedStatusLabel(oldStatus)} → ${getTranslatedStatusLabel(newStatus)}`;
|
||||
}
|
||||
return t('timeline.events.statusChanged');
|
||||
}
|
||||
|
|
@ -152,7 +172,7 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ taskId }) => {
|
|||
const oldDate = old_value?.due_date;
|
||||
const newDate = new_value?.due_date;
|
||||
if (oldDate || newDate) {
|
||||
return `${t('timeline.events.dueDate')}: ${oldDate || t('timeline.events.none')} → ${newDate || t('timeline.events.none')}`;
|
||||
return `${t('timeline.events.dueDate')}: ${formatDate(oldDate)} → ${formatDate(newDate)}`;
|
||||
}
|
||||
return t('timeline.events.dueDateChanged');
|
||||
}
|
||||
|
|
@ -175,6 +195,30 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ taskId }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string | null) => {
|
||||
if (!dateString) return t('timeline.events.none');
|
||||
|
||||
// Handle ISO date strings (e.g., "2025-07-15T00:00:00.000Z")
|
||||
const date = new Date(dateString);
|
||||
|
||||
// Check if it's today, tomorrow, or yesterday
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||
const dateOnly = date.toISOString().split('T')[0];
|
||||
|
||||
if (dateOnly === today) return t('dateIndicators.today');
|
||||
if (dateOnly === tomorrow) return t('dateIndicators.tomorrow');
|
||||
if (dateOnly === yesterday) return t('dateIndicators.yesterday');
|
||||
|
||||
// Return formatted date (e.g., "Jul 15, 2025")
|
||||
return date.toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
const formatTimeAgo = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
|
|
|
|||
|
|
@ -186,8 +186,8 @@ export const getEventTypeLabel = (eventType: string): string => {
|
|||
export const getStatusLabel = (status: number): string => {
|
||||
const statusLabels: Record<number, string> = {
|
||||
0: 'Not Started',
|
||||
1: 'In Progress',
|
||||
2: 'Done',
|
||||
1: 'In Progress',
|
||||
2: 'Completed',
|
||||
3: 'Archived',
|
||||
4: 'Waiting',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -543,6 +543,14 @@
|
|||
"tomorrow": "TOMORROW",
|
||||
"yesterday": "YESTERDAY"
|
||||
},
|
||||
"status": {
|
||||
"notStarted": "Not Started",
|
||||
"inProgress": "In Progress",
|
||||
"completed": "Completed",
|
||||
"archived": "Archived",
|
||||
"waiting": "Waiting",
|
||||
"unknown": "Status {{status}}"
|
||||
},
|
||||
"taskViews": {
|
||||
"project": {
|
||||
"withName": "You are currently viewing all tasks associated with the \"{{projectName}}\" project. You can organize tasks within this project, set their priority, and track their completion. Use this space to focus on the tasks that belong specifically to this project.",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue