feat: bulk delete, team dispatch, calendar views, timeline colors
Deploy / deploy (push) Successful in 11s

- Multi-select bulk delete in all 5 list views (Artefacts, Posts, Tasks,
  Issues, Assets) with cascade deletes and confirmation modals
- Team-based issue dispatch: team picker on public issue form, team filter
  on Issues page, copy public link from Team page and Issues header,
  team assignment in IssueDetailPanel
- Month/Week toggle on PostCalendar and TaskCalendarView
- Month/Week/Day zoom on project and campaign timelines (InteractiveTimeline)
  and ProjectDetail GanttView, with Month as default
- Custom timeline bar colors: clickable color dot with 12-color palette
  popover on project, campaign, and task timeline bars
- Artefacts default view changed to list
- BulkSelectBar reusable component
- i18n keys for all new features (en + ar)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
fahed
2026-03-01 14:55:36 +03:00
parent 20d76dea8b
commit 42a5f17d0b
40 changed files with 3050 additions and 1625 deletions
+74 -4
View File
@@ -6,6 +6,8 @@ import { useLanguage } from '../i18n/LanguageContext'
import { api, PRIORITY_CONFIG, getBrandColor } from '../utils/api'
import TaskCard from '../components/TaskCard'
import TaskDetailPanel from '../components/TaskDetailPanel'
import BulkSelectBar from '../components/BulkSelectBar'
import Modal from '../components/Modal'
import TaskCalendarView from '../components/TaskCalendarView'
import DatePresetPicker from '../components/DatePresetPicker'
import EmptyState from '../components/EmptyState'
@@ -45,6 +47,8 @@ export default function Tasks() {
const [filterOverdue, setFilterOverdue] = useState(false)
const [activePreset, setActivePreset] = useState('')
const [showFilters, setShowFilters] = useState(false)
const [selectedIds, setSelectedIds] = useState(new Set())
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false)
// Assignable users & team
const [assignableUsers, setAssignableUsers] = useState([])
@@ -54,17 +58,17 @@ export default function Tasks() {
useEffect(() => { loadTasks() }, [currentUser])
useEffect(() => {
api.get('/users/assignable').then(res => setAssignableUsers(res.data || res || [])).catch(() => {})
api.get('/projects').then(res => setProjects(res.data || res || [])).catch(() => {})
api.get('/users/assignable').then(res => setAssignableUsers(Array.isArray(res) ? res : [])).catch(() => {})
api.get('/projects').then(res => setProjects(Array.isArray(res) ? res : [])).catch(() => {})
if (isSuperadmin) {
api.get('/team').then(res => setUsers(res.data || res || [])).catch(() => {})
api.get('/team').then(res => setUsers(Array.isArray(res) ? res : [])).catch(() => {})
}
}, [isSuperadmin])
const loadTasks = async () => {
try {
const res = await api.get('/tasks')
setTasks(res.data || res || [])
setTasks(Array.isArray(res) ? res : [])
} catch (err) {
console.error('Failed to load tasks:', err)
} finally {
@@ -177,6 +181,33 @@ export default function Tasks() {
}
}
const handleBulkDelete = async () => {
try {
await api.post('/tasks/bulk-delete', { ids: [...selectedIds] })
toast.success(t('tasks.deleted'))
setSelectedIds(new Set())
setShowBulkDeleteConfirm(false)
loadTasks()
} catch (err) {
console.error('Bulk delete failed:', err)
toast.error(t('common.deleteFailed'))
}
}
const toggleSelect = (id) => {
setSelectedIds(prev => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
const toggleSelectAll = () => {
if (selectedIds.size === sortedListTasks.length) setSelectedIds(new Set())
else setSelectedIds(new Set(sortedListTasks.map(t => t._id || t.id)))
}
const handleMove = async (taskId, newStatus) => {
try {
await api.patch(`/tasks/${taskId}`, { status: newStatus })
@@ -594,10 +625,27 @@ export default function Tasks() {
{/* ─── List View ───────────────────────── */}
{viewMode === 'list' && (
<>
{selectedIds.size > 0 && (
<BulkSelectBar
selectedCount={selectedIds.size}
onClear={() => setSelectedIds(new Set())}
onDelete={() => setShowBulkDeleteConfirm(true)}
/>
)}
<div className="bg-white rounded-xl border border-border overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border bg-surface-secondary/50">
<th className="w-8 px-3 py-2.5">
<input
type="checkbox"
checked={sortedListTasks.length > 0 && selectedIds.size === sortedListTasks.length}
onChange={toggleSelectAll}
className="w-3.5 h-3.5 rounded border-border text-brand-primary cursor-pointer"
onClick={e => e.stopPropagation()}
/>
</th>
<th className="w-8 px-3 py-2.5"></th>
<th
className="text-left px-3 py-2.5 text-xs font-semibold text-text-tertiary uppercase cursor-pointer hover:text-text-primary"
@@ -645,6 +693,14 @@ export default function Tasks() {
onClick={() => openTask(task)}
className="border-b border-border-light hover:bg-surface-secondary/30 cursor-pointer transition-colors group"
>
<td className="px-3 py-2.5" onClick={e => e.stopPropagation()}>
<input
type="checkbox"
checked={selectedIds.has(task._id || task.id)}
onChange={() => toggleSelect(task._id || task.id)}
className="w-3.5 h-3.5 rounded border-border text-brand-primary cursor-pointer"
/>
</td>
<td className="px-3 py-2.5">
<div className={`w-2.5 h-2.5 rounded-full ${priority.color}`} title={priority.label} />
</td>
@@ -686,6 +742,7 @@ export default function Tasks() {
</tbody>
</table>
</div>
</>
)}
{/* ─── Calendar View ───────────────────── */}
@@ -695,6 +752,19 @@ export default function Tasks() {
</>
)}
{/* ─── Bulk Delete Confirmation Modal ─────── */}
<Modal
isOpen={showBulkDeleteConfirm}
onClose={() => setShowBulkDeleteConfirm(false)}
title={t('common.bulkDeleteConfirm').replace('{count}', selectedIds.size)}
isConfirm
danger
confirmText={t('common.deleteSelected')}
onConfirm={handleBulkDelete}
>
{t('common.bulkDeleteDesc')}
</Modal>
{/* ─── Task Detail Side Panel ──────────────── */}
{selectedTask && (
<TaskDetailPanel