update on timeline on portfolio view + some corrections

This commit is contained in:
fahed
2026-02-10 13:20:49 +03:00
parent d15e54044e
commit 334727b232
37 changed files with 5119 additions and 1440 deletions

View File

@@ -2,7 +2,7 @@ import { useState, useEffect, useContext } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import {
ArrowLeft, Plus, Check, Trash2, Edit3, LayoutGrid, List,
GanttChart, Settings, Calendar, Clock
GanttChart, Settings, Calendar, Clock, MessageCircle, X
} from 'lucide-react'
import { format, differenceInDays, startOfDay, addDays, isAfter, isBefore } from 'date-fns'
import { AppContext } from '../App'
@@ -24,7 +24,6 @@ export default function ProjectDetail() {
const navigate = useNavigate()
const { teamMembers, brands } = useContext(AppContext)
const { permissions, canEditResource, canDeleteResource } = useAuth()
const canEditProject = canEditResource('project', project)
const canManageProject = permissions?.canEditProjects
const [project, setProject] = useState(null)
const [tasks, setTasks] = useState([])
@@ -37,12 +36,14 @@ export default function ProjectDetail() {
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [taskToDelete, setTaskToDelete] = useState(null)
const [taskForm, setTaskForm] = useState({
title: '', description: '', priority: 'medium', assigned_to: '', due_date: '', status: 'todo'
title: '', description: '', priority: 'medium', assigned_to: '', start_date: '', due_date: '', status: 'todo'
})
const [projectForm, setProjectForm] = useState({
name: '', description: '', brand_id: '', owner_id: '', status: 'active', due_date: ''
name: '', description: '', brand_id: '', owner_id: '', status: 'active', start_date: '', due_date: ''
})
const [showDiscussion, setShowDiscussion] = useState(false)
// Drag state for kanban
const [draggedTask, setDraggedTask] = useState(null)
const [dragOverCol, setDragOverCol] = useState(null)
@@ -72,6 +73,7 @@ export default function ProjectDetail() {
description: taskForm.description,
priority: taskForm.priority,
assigned_to: taskForm.assigned_to ? Number(taskForm.assigned_to) : null,
start_date: taskForm.start_date || null,
due_date: taskForm.due_date || null,
status: taskForm.status,
project_id: Number(id),
@@ -83,7 +85,7 @@ export default function ProjectDetail() {
}
setShowTaskModal(false)
setEditingTask(null)
setTaskForm({ title: '', description: '', priority: 'medium', assigned_to: '', due_date: '', status: 'todo' })
setTaskForm({ title: '', description: '', priority: 'medium', assigned_to: '', start_date: '', due_date: '', status: 'todo' })
loadProject()
} catch (err) {
console.error('Task save failed:', err)
@@ -122,6 +124,7 @@ export default function ProjectDetail() {
description: task.description || '',
priority: task.priority || 'medium',
assigned_to: task.assignedTo || task.assigned_to || '',
start_date: task.startDate || task.start_date ? new Date(task.startDate || task.start_date).toISOString().slice(0, 10) : '',
due_date: task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 10) : '',
status: task.status || 'todo',
})
@@ -130,7 +133,7 @@ export default function ProjectDetail() {
const openNewTask = () => {
setEditingTask(null)
setTaskForm({ title: '', description: '', priority: 'medium', assigned_to: '', due_date: '', status: 'todo' })
setTaskForm({ title: '', description: '', priority: 'medium', assigned_to: '', start_date: '', due_date: '', status: 'todo' })
setShowTaskModal(true)
}
@@ -142,6 +145,7 @@ export default function ProjectDetail() {
brand_id: project.brandId || project.brand_id || '',
owner_id: project.ownerId || project.owner_id || '',
status: project.status || 'active',
start_date: project.startDate || project.start_date ? new Date(project.startDate || project.start_date).toISOString().slice(0, 10) : '',
due_date: project.dueDate ? new Date(project.dueDate).toISOString().slice(0, 10) : '',
})
setShowProjectModal(true)
@@ -155,6 +159,7 @@ export default function ProjectDetail() {
brand_id: projectForm.brand_id ? Number(projectForm.brand_id) : null,
owner_id: projectForm.owner_id ? Number(projectForm.owner_id) : null,
status: projectForm.status,
start_date: projectForm.start_date || null,
due_date: projectForm.due_date || null,
})
setShowProjectModal(false)
@@ -212,13 +217,16 @@ export default function ProjectDetail() {
)
}
const canEditProject = canEditResource('project', project)
const completedTasks = tasks.filter(t => t.status === 'done').length
const progress = tasks.length > 0 ? Math.round((completedTasks / tasks.length) * 100) : 0
const ownerName = project.ownerName || project.owner_name
const brandName = project.brandName || project.brand_name
return (
<div className="space-y-6 animate-fade-in">
<div className="flex gap-6 animate-fade-in">
{/* Main content */}
<div className={`space-y-6 min-w-0 ${showDiscussion ? 'flex-1' : 'w-full'}`}>
{/* Back button */}
<button
onClick={() => navigate('/projects')}
@@ -251,15 +259,26 @@ export default function ProjectDetail() {
)}
</div>
</div>
{canEditProject && (
<div className="flex items-center gap-2">
<button
onClick={openEditProject}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-text-secondary hover:text-text-primary hover:bg-surface-tertiary rounded-lg transition-colors"
onClick={() => setShowDiscussion(prev => !prev)}
className={`flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
showDiscussion ? 'bg-brand-primary text-white' : 'text-text-secondary hover:text-text-primary hover:bg-surface-tertiary'
}`}
>
<Settings className="w-4 h-4" />
Edit
<MessageCircle className="w-4 h-4" />
Discussion
</button>
)}
{canEditProject && (
<button
onClick={openEditProject}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-text-secondary hover:text-text-primary hover:bg-surface-tertiary rounded-lg transition-colors"
>
<Settings className="w-4 h-4" />
Edit
</button>
)}
</div>
</div>
{project.description && (
@@ -282,11 +301,6 @@ export default function ProjectDetail() {
</div>
</div>
{/* Discussion */}
<div className="bg-white rounded-xl border border-border p-6">
<CommentsSection entityType="project" entityId={Number(id)} />
</div>
{/* View switcher + Add Task */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1 bg-surface-tertiary rounded-lg p-0.5">
@@ -432,6 +446,25 @@ export default function ProjectDetail() {
{/* ─── GANTT / TIMELINE VIEW ─── */}
{view === 'gantt' && <GanttView tasks={tasks} project={project} onEditTask={openEditTask} />}
</div>{/* end main content */}
{/* ─── DISCUSSION SIDEBAR ─── */}
{showDiscussion && (
<div className="w-[340px] shrink-0 bg-white rounded-xl border border-border flex flex-col self-start sticky top-4" style={{ maxHeight: 'calc(100vh - 6rem)' }}>
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<h3 className="text-sm font-semibold text-text-primary flex items-center gap-1.5">
<MessageCircle className="w-4 h-4" />
Discussion
</h3>
<button onClick={() => setShowDiscussion(false)} className="p-1 hover:bg-surface-tertiary rounded-lg text-text-tertiary">
<X className="w-4 h-4" />
</button>
</div>
<div className="flex-1 overflow-y-auto p-4">
<CommentsSection entityType="project" entityId={Number(id)} />
</div>
</div>
)}
{/* ─── TASK MODAL ─── */}
<Modal
@@ -482,14 +515,19 @@ export default function ProjectDetail() {
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Assign To</label>
<select value={taskForm.assigned_to} onChange={e => setTaskForm(f => ({ ...f, assigned_to: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary">
<option value="">Unassigned</option>
{assignableUsers.map(m => <option key={m._id || m.id} value={m._id || m.id}>{m.name}</option>)}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Assign To</label>
<select value={taskForm.assigned_to} onChange={e => setTaskForm(f => ({ ...f, assigned_to: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary">
<option value="">Unassigned</option>
{assignableUsers.map(m => <option key={m._id || m.team_member_id} value={m._id || m.team_member_id}>{m.name}</option>)}
</select>
<label className="block text-sm font-medium text-text-primary mb-1">Start Date</label>
<input type="date" value={taskForm.start_date} onChange={e => setTaskForm(f => ({ ...f, start_date: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary" />
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Due Date</label>
@@ -566,11 +604,16 @@ export default function ProjectDetail() {
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Due Date</label>
<input type="date" value={projectForm.due_date} onChange={e => setProjectForm(f => ({ ...f, due_date: e.target.value }))}
<label className="block text-sm font-medium text-text-primary mb-1">Start Date</label>
<input type="date" value={projectForm.start_date} onChange={e => setProjectForm(f => ({ ...f, start_date: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary" />
</div>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">Due Date</label>
<input type="date" value={projectForm.due_date} onChange={e => setProjectForm(f => ({ ...f, due_date: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary" />
</div>
<div className="flex items-center justify-end gap-3 pt-4 border-t border-border">
<button onClick={() => setShowProjectModal(false)}
className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-surface-tertiary rounded-lg">
@@ -698,7 +741,9 @@ function GanttView({ tasks, project, onEditTask }) {
const dayWidth = Math.max(36, Math.min(60, 800 / totalDays))
const getBarStyle = (task) => {
const start = task.createdAt ? startOfDay(new Date(task.createdAt)) : today
const start = task.startDate || task.start_date
? startOfDay(new Date(task.startDate || task.start_date))
: task.createdAt ? startOfDay(new Date(task.createdAt)) : today
const end = task.dueDate ? startOfDay(new Date(task.dueDate)) : addDays(start, 3)
const left = differenceInDays(start, earliest) * dayWidth
const width = Math.max(dayWidth, (differenceInDays(end, start) + 1) * dayWidth)
@@ -775,7 +820,7 @@ function GanttView({ tasks, project, onEditTask }) {
})}
</div>
</div>
</div>
)
}