update on timeline on portfolio view + some corrections
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user