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

@@ -7,13 +7,17 @@ import { api } from '../utils/api'
import TaskCard from '../components/TaskCard'
import Modal from '../components/Modal'
import CommentsSection from '../components/CommentsSection'
import EmptyState from '../components/EmptyState'
import { useToast } from '../components/ToastContainer'
export default function Tasks() {
const { t } = useLanguage()
const { currentUser } = useContext(AppContext)
const { user: authUser, canEditResource, canDeleteResource } = useAuth()
const toast = useToast()
const [tasks, setTasks] = useState([])
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [showModal, setShowModal] = useState(false)
const [editingTask, setEditingTask] = useState(null)
const [draggedTask, setDraggedTask] = useState(null)
@@ -24,7 +28,7 @@ export default function Tasks() {
const [users, setUsers] = useState([]) // for superadmin member filter
const [assignableUsers, setAssignableUsers] = useState([])
const [formData, setFormData] = useState({
title: '', description: '', priority: 'medium', due_date: '', status: 'todo', assigned_to: ''
title: '', description: '', priority: 'medium', start_date: '', due_date: '', status: 'todo', assigned_to: ''
})
const isSuperadmin = authUser?.role === 'superadmin'
@@ -55,29 +59,30 @@ export default function Tasks() {
if (filterView === 'all') return true
if (filterView === 'assigned_to_me') {
// Tasks where I'm the assignee (via team_member_id on my user record)
const myTeamMemberId = authUser?.team_member_id
return myTeamMemberId && task.assigned_to === myTeamMemberId
return task.assignedTo === authUser?.id || task.assigned_to === authUser?.id
}
if (filterView === 'created_by_me') {
return task.created_by_user_id === authUser?.id
return task.createdByUserId === authUser?.id || task.created_by_user_id === authUser?.id
}
// Superadmin filtering by specific team member (assigned_to = member id)
// Superadmin filtering by specific team member
if (isSuperadmin && !isNaN(Number(filterView))) {
return task.assigned_to === Number(filterView)
const memberId = Number(filterView)
return task.assignedTo === memberId || task.assigned_to === memberId
}
return true
})
const handleSave = async () => {
setSaving(true)
try {
const data = {
title: formData.title,
description: formData.description,
priority: formData.priority,
start_date: formData.start_date || null,
due_date: formData.due_date || null,
status: formData.status,
assigned_to: formData.assigned_to || null,
@@ -85,29 +90,38 @@ export default function Tasks() {
}
if (editingTask) {
await api.patch(`/tasks/${editingTask._id || editingTask.id}`, data)
toast.success(t('tasks.updated'))
} else {
await api.post('/tasks', data)
toast.success(t('tasks.created'))
}
setShowModal(false)
setEditingTask(null)
setFormData({ title: '', description: '', priority: 'medium', due_date: '', status: 'todo', assigned_to: '' })
setFormData({ title: '', description: '', priority: 'medium', start_date: '', due_date: '', status: 'todo', assigned_to: '' })
loadTasks()
} catch (err) {
console.error('Save failed:', err)
if (err.message?.includes('403') || err.message?.includes('modify your own')) {
alert('You can only edit your own tasks')
toast.error(t('tasks.canOnlyEditOwn'))
} else {
toast.error(t('common.saveFailed'))
}
} finally {
setSaving(false)
}
}
const handleMove = async (taskId, newStatus) => {
try {
await api.patch(`/tasks/${taskId}`, { status: newStatus })
toast.success(t('tasks.statusUpdated'))
loadTasks()
} catch (err) {
console.error('Move failed:', err)
if (err.message?.includes('403')) {
alert('You can only modify your own tasks')
toast.error(t('tasks.canOnlyEditOwn'))
} else {
toast.error(t('common.updateFailed'))
}
}
}
@@ -119,6 +133,7 @@ export default function Tasks() {
title: task.title || '',
description: task.description || '',
priority: task.priority || 'medium',
start_date: task.start_date || task.startDate || '',
due_date: task.due_date || task.dueDate || '',
status: task.status || 'todo',
assigned_to: task.assigned_to || '',
@@ -136,10 +151,12 @@ export default function Tasks() {
if (!taskToDelete) return
try {
await api.delete(`/tasks/${taskToDelete._id || taskToDelete.id}`)
toast.success(t('tasks.deleted'))
setTaskToDelete(null)
loadTasks()
} catch (err) {
console.error('Delete failed:', err)
toast.error(t('common.deleteFailed'))
}
}
@@ -232,7 +249,7 @@ export default function Tasks() {
</p>
</div>
<button
onClick={() => { setEditingTask(null); setFormData({ title: '', description: '', priority: 'medium', due_date: '', status: 'todo', assigned_to: '' }); setShowModal(true) }}
onClick={() => { setEditingTask(null); setFormData({ title: '', description: '', priority: 'medium', start_date: '', due_date: '', status: 'todo', assigned_to: '' }); setShowModal(true) }}
className="flex items-center gap-2 px-4 py-2 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light shadow-sm"
>
<Plus className="w-4 h-4" />
@@ -242,15 +259,19 @@ export default function Tasks() {
{/* Task columns */}
{filteredTasks.length === 0 ? (
<div className="py-20 text-center">
<CheckSquare className="w-12 h-12 text-text-tertiary mx-auto mb-3" />
<p className="text-text-secondary font-medium">
{tasks.length === 0 ? t('tasks.noTasks') : t('tasks.noMatch')}
</p>
<p className="text-sm text-text-tertiary mt-1">
{tasks.length === 0 ? t('tasks.createFirst') : t('tasks.tryFilter')}
</p>
</div>
<EmptyState
icon={CheckSquare}
title={tasks.length === 0 ? t('tasks.noTasks') : t('tasks.noMatch')}
description={tasks.length === 0 ? t('tasks.createFirst') : t('tasks.tryFilter')}
actionLabel={tasks.length === 0 ? t('tasks.createTask') : null}
onAction={tasks.length === 0 ? () => {
setEditingTask(null)
setFormData({ title: '', description: '', priority: 'medium', start_date: '', due_date: '', status: 'todo', assigned_to: '' })
setShowModal(true)
} : null}
secondaryActionLabel={tasks.length > 0 ? t('common.clearFilters') : null}
onSecondaryAction={() => setFilterView('all')}
/>
) : (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{columns.map(col => {
@@ -291,29 +312,18 @@ export default function Tasks() {
onDragEnd={handleDragEnd}
className={canEdit ? 'cursor-grab active:cursor-grabbing' : ''}
>
<div className="relative group">
<div className="relative group" onClick={() => canEdit && openEdit(task)}>
<TaskCard task={task} onMove={canEdit ? handleMove : undefined} showProject />
{/* Edit/Delete overlay */}
{(canEdit || canDelete) && (
{/* Delete overlay */}
{canDelete && (
<div className="absolute top-2 right-2 flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
{canEdit && (
<button
onClick={(e) => { e.stopPropagation(); openEdit(task) }}
className="p-1 hover:bg-surface-tertiary rounded text-text-tertiary hover:text-text-primary"
title={t('tasks.editTask')}
>
<Edit2 className="w-3.5 h-3.5" />
</button>
)}
{canDelete && (
<button
onClick={(e) => { e.stopPropagation(); handleDelete(task) }}
className="p-1 hover:bg-red-50 rounded text-text-tertiary hover:text-red-500"
title={t('common.delete')}
>
<Trash2 className="w-3.5 h-3.5" />
</button>
)}
<button
onClick={(e) => { e.stopPropagation(); handleDelete(task) }}
className="p-1 hover:bg-red-50 rounded text-text-tertiary hover:text-red-500"
title={t('common.delete')}
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
)}
</div>
@@ -382,16 +392,26 @@ export default function Tasks() {
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">{t('tasks.dueDate')}</label>
<label className="block text-sm font-medium text-text-primary mb-1">{t('timeline.startDate')}</label>
<input
type="date"
value={formData.due_date}
onChange={e => setFormData(f => ({ ...f, due_date: e.target.value }))}
value={formData.start_date}
onChange={e => setFormData(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">{t('tasks.dueDate')}</label>
<input
type="date"
value={formData.due_date}
onChange={e => setFormData(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>
{/* Comments (only for existing tasks) */}
{editingTask && (
<CommentsSection entityType="task" entityId={editingTask._id || editingTask.id} />
@@ -406,8 +426,8 @@ export default function Tasks() {
</button>
<button
onClick={handleSave}
disabled={!formData.title}
className="px-5 py-2 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
disabled={!formData.title || saving}
className={`px-5 py-2 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light disabled:opacity-50 disabled:cursor-not-allowed shadow-sm ${saving ? 'btn-loading' : ''}`}
>
{editingTask ? t('tasks.saveChanges') : t('tasks.createTask')}
</button>