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
+66 -59
View File
@@ -1,35 +1,38 @@
import { Routes, Route, Navigate } from 'react-router-dom'
import { useState, useEffect, createContext } from 'react'
import { useState, useEffect, createContext, lazy, Suspense } from 'react'
import { AuthProvider, useAuth } from './contexts/AuthContext'
import { LanguageProvider } from './i18n/LanguageContext'
import { ToastProvider } from './components/ToastContainer'
import ErrorBoundary from './components/ErrorBoundary'
import Layout from './components/Layout'
import Dashboard from './pages/Dashboard'
import PostProduction from './pages/PostProduction'
import Assets from './pages/Assets'
import Campaigns from './pages/Campaigns'
import CampaignDetail from './pages/CampaignDetail'
import Finance from './pages/Finance'
import Budgets from './pages/Budgets'
import Projects from './pages/Projects'
import ProjectDetail from './pages/ProjectDetail'
import Tasks from './pages/Tasks'
import Team from './pages/Team'
import Users from './pages/Users'
import Settings from './pages/Settings'
import Brands from './pages/Brands'
import Login from './pages/Login'
import Artefacts from './pages/Artefacts'
import PostCalendar from './pages/PostCalendar'
import PublicReview from './pages/PublicReview'
import Issues from './pages/Issues'
import PublicIssueSubmit from './pages/PublicIssueSubmit'
import PublicIssueTracker from './pages/PublicIssueTracker'
import Tutorial from './components/Tutorial'
import Modal from './components/Modal'
import { api } from './utils/api'
import { useLanguage } from './i18n/LanguageContext'
// Lazy-loaded page components
const Dashboard = lazy(() => import('./pages/Dashboard'))
const PostProduction = lazy(() => import('./pages/PostProduction'))
const Assets = lazy(() => import('./pages/Assets'))
const Campaigns = lazy(() => import('./pages/Campaigns'))
const CampaignDetail = lazy(() => import('./pages/CampaignDetail'))
const Finance = lazy(() => import('./pages/Finance'))
const Budgets = lazy(() => import('./pages/Budgets'))
const Projects = lazy(() => import('./pages/Projects'))
const ProjectDetail = lazy(() => import('./pages/ProjectDetail'))
const Tasks = lazy(() => import('./pages/Tasks'))
const Team = lazy(() => import('./pages/Team'))
const Users = lazy(() => import('./pages/Users'))
const Settings = lazy(() => import('./pages/Settings'))
const Brands = lazy(() => import('./pages/Brands'))
const Login = lazy(() => import('./pages/Login'))
const Artefacts = lazy(() => import('./pages/Artefacts'))
const PostCalendar = lazy(() => import('./pages/PostCalendar'))
const PublicReview = lazy(() => import('./pages/PublicReview'))
const Issues = lazy(() => import('./pages/Issues'))
const PublicIssueSubmit = lazy(() => import('./pages/PublicIssueSubmit'))
const PublicIssueTracker = lazy(() => import('./pages/PublicIssueTracker'))
const TEAM_ROLES = [
{ value: 'manager', label: 'Manager' },
{ value: 'approver', label: 'Approver' },
@@ -87,7 +90,7 @@ function AppContent() {
const loadTeam = async () => {
try {
const data = await api.get('/users/team')
const members = Array.isArray(data) ? data : (data.data || [])
const members = Array.isArray(data) ? data : []
setTeamMembers(members)
return members
} catch (err) {
@@ -99,7 +102,7 @@ function AppContent() {
const loadTeams = async () => {
try {
const data = await api.get('/teams')
setTeams(Array.isArray(data) ? data : (data.data || []))
setTeams(Array.isArray(data) ? data : [])
} catch (err) {
console.error('Failed to load teams:', err)
}
@@ -109,7 +112,7 @@ function AppContent() {
try {
const [, brandsData] = await Promise.all([
loadTeam(),
api.get('/brands').then(d => Array.isArray(d) ? d : (d.data || [])).catch(() => []),
api.get('/brands').then(d => Array.isArray(d) ? d : []).catch(() => []),
loadTeams(),
])
setBrands(brandsData)
@@ -270,40 +273,44 @@ function AppContent() {
{/* Tutorial overlay */}
{showTutorial && <Tutorial onComplete={handleTutorialComplete} />}
<Routes>
<Route path="/login" element={user ? <Navigate to="/" replace /> : <Login />} />
<Route path="/review/:token" element={<PublicReview />} />
<Route path="/submit-issue" element={<PublicIssueSubmit />} />
<Route path="/track/:token" element={<PublicIssueTracker />} />
<Route path="/" element={user ? <Layout /> : <Navigate to="/login" replace />}>
<Route index element={<Dashboard />} />
{hasModule('marketing') && <>
<Route path="posts" element={<PostProduction />} />
<Route path="calendar" element={<PostCalendar />} />
<Route path="artefacts" element={<Artefacts />} />
<Route path="assets" element={<Assets />} />
<Route path="campaigns" element={<Campaigns />} />
<Route path="campaigns/:id" element={<CampaignDetail />} />
<Route path="brands" element={<Brands />} />
</>}
{hasModule('finance') && (user?.role === 'superadmin' || user?.role === 'manager') && <>
<Route path="finance" element={<Finance />} />
<Route path="budgets" element={<Budgets />} />
</>}
{hasModule('projects') && <>
<Route path="projects" element={<Projects />} />
<Route path="projects/:id" element={<ProjectDetail />} />
<Route path="tasks" element={<Tasks />} />
</>}
{hasModule('issues') && <Route path="issues" element={<Issues />} />}
<Route path="team" element={<Team />} />
<Route path="settings" element={<Settings />} />
{user?.role === 'superadmin' && (
<Route path="users" element={<Users />} />
)}
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
<ErrorBoundary>
<Suspense fallback={<div className="min-h-screen bg-surface-secondary flex items-center justify-center"><div className="animate-pulse text-text-tertiary">Loading...</div></div>}>
<Routes>
<Route path="/login" element={user ? <Navigate to="/" replace /> : <Login />} />
<Route path="/review/:token" element={<PublicReview />} />
<Route path="/submit-issue" element={<PublicIssueSubmit />} />
<Route path="/track/:token" element={<PublicIssueTracker />} />
<Route path="/" element={user ? <Layout /> : <Navigate to="/login" replace />}>
<Route index element={<Dashboard />} />
{hasModule('marketing') && <>
<Route path="posts" element={<PostProduction />} />
<Route path="calendar" element={<PostCalendar />} />
<Route path="artefacts" element={<Artefacts />} />
<Route path="assets" element={<Assets />} />
<Route path="campaigns" element={<Campaigns />} />
<Route path="campaigns/:id" element={<CampaignDetail />} />
<Route path="brands" element={<Brands />} />
</>}
{hasModule('finance') && (user?.role === 'superadmin' || user?.role === 'manager') && <>
<Route path="finance" element={<Finance />} />
<Route path="budgets" element={<Budgets />} />
</>}
{hasModule('projects') && <>
<Route path="projects" element={<Projects />} />
<Route path="projects/:id" element={<ProjectDetail />} />
<Route path="tasks" element={<Tasks />} />
</>}
{hasModule('issues') && <Route path="issues" element={<Issues />} />}
<Route path="team" element={<Team />} />
<Route path="settings" element={<Settings />} />
{user?.role === 'superadmin' && (
<Route path="users" element={<Users />} />
)}
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Suspense>
</ErrorBoundary>
</AppContext.Provider>
)
}