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
+62 -13
View File
@@ -1,5 +1,5 @@
import { useState } from 'react'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, CalendarDays } from 'lucide-react'
import { PRIORITY_CONFIG } from '../utils/api'
import { useLanguage } from '../i18n/LanguageContext'
@@ -27,6 +27,18 @@ function getMonthData(year, month) {
return cells
}
function getWeekData(startDate) {
const cells = []
const start = new Date(startDate)
start.setDate(start.getDate() - start.getDay())
for (let i = 0; i < 7; i++) {
const d = new Date(start)
d.setDate(start.getDate() + i)
cells.push({ day: d.getDate(), current: true, date: d })
}
return cells
}
function dateKey(d) {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
@@ -36,8 +48,12 @@ export default function TaskCalendarView({ tasks, onTaskClick }) {
const today = new Date()
const [year, setYear] = useState(today.getFullYear())
const [month, setMonth] = useState(today.getMonth())
const [calView, setCalView] = useState('month')
const [weekStart, setWeekStart] = useState(() => {
const d = new Date(); d.setDate(d.getDate() - d.getDay()); return d
})
const cells = getMonthData(year, month)
const cells = calView === 'month' ? getMonthData(year, month) : getWeekData(weekStart)
const todayKey = dateKey(today)
// Group tasks by due_date
@@ -62,9 +78,22 @@ export default function TaskCalendarView({ tasks, onTaskClick }) {
if (month === 11) { setMonth(0); setYear(y => y + 1) }
else setMonth(m => m + 1)
}
const goToday = () => { setYear(today.getFullYear()); setMonth(today.getMonth()) }
const prevWeek = () => setWeekStart(d => { const n = new Date(d); n.setDate(n.getDate() - 7); return n })
const nextWeek = () => setWeekStart(d => { const n = new Date(d); n.setDate(n.getDate() + 7); return n })
const goToday = () => {
setYear(today.getFullYear()); setMonth(today.getMonth())
const d = new Date(); d.setDate(d.getDate() - d.getDay()); setWeekStart(d)
}
const monthLabel = new Date(year, month).toLocaleString('default', { month: 'long', year: 'numeric' })
const weekLabel = (() => {
const start = new Date(weekStart)
start.setDate(start.getDate() - start.getDay())
const end = new Date(start); end.setDate(start.getDate() + 6)
const fmt = (d) => d.toLocaleString('default', { month: 'short', day: 'numeric' })
return `${fmt(start)} ${fmt(end)}, ${end.getFullYear()}`
})()
const getPillColor = (task) => {
const p = task.priority || 'medium'
@@ -81,17 +110,37 @@ export default function TaskCalendarView({ tasks, onTaskClick }) {
{/* Nav */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<button onClick={prevMonth} className="p-1.5 rounded-lg hover:bg-surface-tertiary text-text-tertiary hover:text-text-primary transition-colors">
<button onClick={calView === 'month' ? prevMonth : prevWeek} className="p-1.5 rounded-lg hover:bg-surface-tertiary text-text-tertiary hover:text-text-primary transition-colors">
<ChevronLeft className="w-4 h-4" />
</button>
<h3 className="text-sm font-semibold text-text-primary min-w-[150px] text-center">{monthLabel}</h3>
<button onClick={nextMonth} className="p-1.5 rounded-lg hover:bg-surface-tertiary text-text-tertiary hover:text-text-primary transition-colors">
<h3 className="text-sm font-semibold text-text-primary min-w-[180px] text-center">
{calView === 'month' ? monthLabel : weekLabel}
</h3>
<button onClick={calView === 'month' ? nextMonth : nextWeek} className="p-1.5 rounded-lg hover:bg-surface-tertiary text-text-tertiary hover:text-text-primary transition-colors">
<ChevronRight className="w-4 h-4" />
</button>
</div>
<button onClick={goToday} className="px-3 py-1 text-xs font-medium text-brand-primary hover:bg-brand-primary/5 rounded-lg transition-colors">
{t('tasks.today')}
</button>
<div className="flex items-center gap-2">
<div className="flex bg-surface-tertiary rounded-lg p-0.5">
<button
onClick={() => setCalView('month')}
className={`px-2.5 py-1 text-[11px] font-medium rounded-md transition-colors flex items-center gap-1 ${calView === 'month' ? 'bg-white shadow-sm text-text-primary' : 'text-text-tertiary'}`}
>
<CalendarIcon className="w-3 h-3" />
Month
</button>
<button
onClick={() => setCalView('week')}
className={`px-2.5 py-1 text-[11px] font-medium rounded-md transition-colors flex items-center gap-1 ${calView === 'week' ? 'bg-white shadow-sm text-text-primary' : 'text-text-tertiary'}`}
>
<CalendarDays className="w-3 h-3" />
Week
</button>
</div>
<button onClick={goToday} className="px-3 py-1 text-xs font-medium text-brand-primary hover:bg-brand-primary/5 rounded-lg transition-colors">
{t('tasks.today')}
</button>
</div>
</div>
{/* Day headers */}
@@ -112,7 +161,7 @@ export default function TaskCalendarView({ tasks, onTaskClick }) {
return (
<div
key={i}
className={`border-r border-b border-border min-h-[90px] p-1 ${
className={`border-r border-b border-border ${calView === 'week' ? 'min-h-[300px]' : 'min-h-[90px]'} p-1 ${
cell.current ? 'bg-white' : 'bg-surface-secondary/50'
}`}
>
@@ -122,7 +171,7 @@ export default function TaskCalendarView({ tasks, onTaskClick }) {
{cell.day}
</div>
<div className="space-y-0.5">
{dayTasks.slice(0, 3).map(task => (
{dayTasks.slice(0, calView === 'week' ? 10 : 3).map(task => (
<button
key={task._id || task.id}
onClick={() => onTaskClick(task)}
@@ -134,9 +183,9 @@ export default function TaskCalendarView({ tasks, onTaskClick }) {
{task.title}
</button>
))}
{dayTasks.length > 3 && (
{dayTasks.length > (calView === 'week' ? 10 : 3) && (
<div className="text-[9px] text-text-tertiary text-center font-medium">
+{dayTasks.length - 3} more
+{dayTasks.length - (calView === 'week' ? 10 : 3)} more
</div>
)}
</div>