feat: bulk delete, team dispatch, calendar views, timeline colors
Deploy / deploy (push) Successful in 11s
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:
@@ -1,28 +1,17 @@
|
||||
import { useState, useEffect, useContext } from 'react'
|
||||
import { X, Copy, Eye, Lock, Send, Upload, FileText, Trash2, Check, Clock, CheckCircle2, XCircle } from 'lucide-react'
|
||||
import { api } from '../utils/api'
|
||||
import { api, STATUS_CONFIG, PRIORITY_CONFIG } from '../utils/api'
|
||||
import SlidePanel from './SlidePanel'
|
||||
import FormInput from './FormInput'
|
||||
import Modal from './Modal'
|
||||
import { useToast } from './ToastContainer'
|
||||
import { AppContext } from '../App'
|
||||
import { useLanguage } from '../i18n/LanguageContext'
|
||||
|
||||
const STATUS_CONFIG = {
|
||||
new: { label: 'New', bg: 'bg-purple-100', text: 'text-purple-700', dot: 'bg-purple-500' },
|
||||
acknowledged: { label: 'Acknowledged', bg: 'bg-blue-100', text: 'text-blue-700', dot: 'bg-blue-500' },
|
||||
in_progress: { label: 'In Progress', bg: 'bg-amber-100', text: 'text-amber-700', dot: 'bg-amber-500' },
|
||||
resolved: { label: 'Resolved', bg: 'bg-emerald-100', text: 'text-emerald-700', dot: 'bg-emerald-500' },
|
||||
declined: { label: 'Declined', bg: 'bg-surface-tertiary', text: 'text-text-secondary', dot: 'bg-text-tertiary' },
|
||||
}
|
||||
|
||||
const PRIORITY_CONFIG = {
|
||||
low: { label: 'Low', bg: 'bg-surface-tertiary', text: 'text-text-secondary' },
|
||||
medium: { label: 'Medium', bg: 'bg-blue-100', text: 'text-blue-700' },
|
||||
high: { label: 'High', bg: 'bg-orange-100', text: 'text-orange-700' },
|
||||
urgent: { label: 'Urgent', bg: 'bg-red-100', text: 'text-red-700' },
|
||||
}
|
||||
|
||||
export default function IssueDetailPanel({ issue, onClose, onUpdate, teamMembers }) {
|
||||
export default function IssueDetailPanel({ issue, onClose, onUpdate, teamMembers, teams = [] }) {
|
||||
const { brands } = useContext(AppContext)
|
||||
const toast = useToast()
|
||||
const { t } = useLanguage()
|
||||
const [issueData, setIssueData] = useState(null)
|
||||
const [updates, setUpdates] = useState([])
|
||||
const [attachments, setAttachments] = useState([])
|
||||
@@ -32,6 +21,7 @@ export default function IssueDetailPanel({ issue, onClose, onUpdate, teamMembers
|
||||
|
||||
// Form state
|
||||
const [assignedTo, setAssignedTo] = useState('')
|
||||
const [teamId, setTeamId] = useState('')
|
||||
const [internalNotes, setInternalNotes] = useState('')
|
||||
const [resolutionSummary, setResolutionSummary] = useState('')
|
||||
const [newUpdate, setNewUpdate] = useState('')
|
||||
@@ -40,6 +30,7 @@ export default function IssueDetailPanel({ issue, onClose, onUpdate, teamMembers
|
||||
// Modals
|
||||
const [showResolveModal, setShowResolveModal] = useState(false)
|
||||
const [showDeclineModal, setShowDeclineModal] = useState(false)
|
||||
const [confirmDeleteAttId, setConfirmDeleteAttId] = useState(null)
|
||||
|
||||
const issueId = issue?.Id || issue?.id
|
||||
|
||||
@@ -54,6 +45,7 @@ export default function IssueDetailPanel({ issue, onClose, onUpdate, teamMembers
|
||||
setUpdates(data.updates || [])
|
||||
setAttachments(data.attachments || [])
|
||||
setAssignedTo(data.assigned_to_id || '')
|
||||
setTeamId(data.team_id || '')
|
||||
setInternalNotes(data.internal_notes || '')
|
||||
setResolutionSummary(data.resolution_summary || '')
|
||||
} catch (err) {
|
||||
@@ -72,7 +64,7 @@ export default function IssueDetailPanel({ issue, onClose, onUpdate, teamMembers
|
||||
await loadIssueDetails()
|
||||
} catch (err) {
|
||||
console.error('Failed to update status:', err)
|
||||
alert('Failed to update status')
|
||||
toast.error(t('issues.failedToUpdateStatus'))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
@@ -88,7 +80,7 @@ export default function IssueDetailPanel({ issue, onClose, onUpdate, teamMembers
|
||||
await loadIssueDetails()
|
||||
} catch (err) {
|
||||
console.error('Failed to resolve issue:', err)
|
||||
alert('Failed to resolve issue')
|
||||
toast.error(t('issues.failedToResolve'))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
@@ -104,7 +96,7 @@ export default function IssueDetailPanel({ issue, onClose, onUpdate, teamMembers
|
||||
await loadIssueDetails()
|
||||
} catch (err) {
|
||||
console.error('Failed to decline issue:', err)
|
||||
alert('Failed to decline issue')
|
||||
toast.error(t('issues.failedToDecline'))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
@@ -117,7 +109,7 @@ export default function IssueDetailPanel({ issue, onClose, onUpdate, teamMembers
|
||||
await onUpdate()
|
||||
} catch (err) {
|
||||
console.error('Failed to update assignment:', err)
|
||||
alert('Failed to update assignment')
|
||||
toast.error(t('issues.failedToUpdateAssignment'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +120,7 @@ export default function IssueDetailPanel({ issue, onClose, onUpdate, teamMembers
|
||||
await api.patch(`/issues/${issueId}`, { internal_notes: internalNotes })
|
||||
} catch (err) {
|
||||
console.error('Failed to save notes:', err)
|
||||
alert('Failed to save notes')
|
||||
toast.error(t('issues.failedToSaveNotes'))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
@@ -144,7 +136,7 @@ export default function IssueDetailPanel({ issue, onClose, onUpdate, teamMembers
|
||||
await loadIssueDetails()
|
||||
} catch (err) {
|
||||
console.error('Failed to add update:', err)
|
||||
alert('Failed to add update')
|
||||
toast.error(t('issues.failedToAddUpdate'))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
@@ -162,27 +154,26 @@ export default function IssueDetailPanel({ issue, onClose, onUpdate, teamMembers
|
||||
e.target.value = '' // Reset input
|
||||
} catch (err) {
|
||||
console.error('Failed to upload file:', err)
|
||||
alert('Failed to upload file')
|
||||
toast.error(t('issues.failedToUploadFile'))
|
||||
} finally {
|
||||
setUploadingFile(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteAttachment = async (attachmentId) => {
|
||||
if (!confirm('Delete this attachment?')) return
|
||||
try {
|
||||
await api.delete(`/issue-attachments/${attachmentId}`)
|
||||
await loadIssueDetails()
|
||||
} catch (err) {
|
||||
console.error('Failed to delete attachment:', err)
|
||||
alert('Failed to delete attachment')
|
||||
toast.error(t('issues.failedToDeleteAttachment'))
|
||||
}
|
||||
}
|
||||
|
||||
const copyTrackingLink = () => {
|
||||
const url = `${window.location.origin}/track/${issueData.tracking_token}`
|
||||
navigator.clipboard.writeText(url)
|
||||
alert('Tracking link copied to clipboard!')
|
||||
toast.success(t('issues.trackingLinkCopied'))
|
||||
}
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
@@ -283,6 +274,33 @@ export default function IssueDetailPanel({ issue, onClose, onUpdate, teamMembers
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Team */}
|
||||
{teams.length > 0 && (
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-text-primary mb-2">{t('issues.team')}</label>
|
||||
<select
|
||||
value={teamId}
|
||||
onChange={async (e) => {
|
||||
const val = e.target.value || null
|
||||
setTeamId(val || '')
|
||||
try {
|
||||
await api.patch(`/issues/${issueId}`, { team_id: val })
|
||||
await onUpdate()
|
||||
await loadIssueDetails()
|
||||
} catch (err) {
|
||||
console.error('Failed to update team:', err)
|
||||
}
|
||||
}}
|
||||
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"
|
||||
>
|
||||
<option value="">{t('issues.allTeams')}</option>
|
||||
{teams.map((team) => (
|
||||
<option key={team.id || team._id} value={team.id || team._id}>{team.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Brand */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-text-primary mb-2">Brand</label>
|
||||
@@ -504,7 +522,7 @@ export default function IssueDetailPanel({ issue, onClose, onUpdate, teamMembers
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
<button onClick={() => handleDeleteAttachment(att.Id || att.id)} className="p-1 hover:bg-surface-tertiary rounded">
|
||||
<button onClick={() => setConfirmDeleteAttId(att.Id || att.id)} className="p-1 hover:bg-surface-tertiary rounded">
|
||||
<Trash2 className="w-4 h-4 text-red-600" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -579,6 +597,19 @@ export default function IssueDetailPanel({ issue, onClose, onUpdate, teamMembers
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* Delete Attachment Confirmation */}
|
||||
<Modal
|
||||
isOpen={!!confirmDeleteAttId}
|
||||
onClose={() => setConfirmDeleteAttId(null)}
|
||||
title={t('issues.deleteAttachment')}
|
||||
isConfirm
|
||||
danger
|
||||
onConfirm={() => handleDeleteAttachment(confirmDeleteAttId)}
|
||||
confirmText={t('common.delete')}
|
||||
>
|
||||
{t('issues.deleteAttachmentDesc')}
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user