import { useState, useEffect, useContext, useMemo } from 'react'
import { AlertCircle, Search, LayoutGrid, List, ChevronUp, ChevronDown } from 'lucide-react'
import { AppContext } from '../App'
import { api } from '../utils/api'
import { useLanguage } from '../i18n/LanguageContext'
import { useToast } from '../components/ToastContainer'
import IssueDetailPanel from '../components/IssueDetailPanel'
import IssueCard from '../components/IssueCard'
import EmptyState from '../components/EmptyState'
import { SkeletonTable, SkeletonKanbanBoard } from '../components/SkeletonLoader'
const TYPE_OPTIONS = [
{ value: 'request', label: 'Request' },
{ value: 'correction', label: 'Correction' },
{ value: 'complaint', label: 'Complaint' },
{ value: 'suggestion', label: 'Suggestion' },
{ value: 'other', label: 'Other' },
]
const PRIORITY_CONFIG = {
low: { label: 'Low', bg: 'bg-surface-tertiary', text: 'text-text-secondary', dot: 'bg-text-tertiary' },
medium: { label: 'Medium', bg: 'bg-blue-100', text: 'text-blue-700', dot: 'bg-blue-500' },
high: { label: 'High', bg: 'bg-orange-100', text: 'text-orange-700', dot: 'bg-orange-500' },
urgent: { label: 'Urgent', bg: 'bg-red-100', text: 'text-red-700', dot: 'bg-red-500' },
}
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 STATUS_ORDER = ['new', 'acknowledged', 'in_progress', 'resolved', 'declined']
export default function Issues() {
const { t } = useLanguage()
const toast = useToast()
const { brands } = useContext(AppContext)
const [issues, setIssues] = useState([])
const [counts, setCounts] = useState({})
const [loading, setLoading] = useState(true)
const [selectedIssue, setSelectedIssue] = useState(null)
const [searchTerm, setSearchTerm] = useState('')
const [filters, setFilters] = useState({ status: '', category: '', type: '', priority: '', brand: '' })
const [categories, setCategories] = useState([])
const [teamMembers, setTeamMembers] = useState([])
// View mode
const [viewMode, setViewMode] = useState('board')
// Drag and drop
const [draggedIssue, setDraggedIssue] = useState(null)
const [dragOverCol, setDragOverCol] = useState(null)
// List sorting
const [sortBy, setSortBy] = useState('created_at')
const [sortDir, setSortDir] = useState('desc')
useEffect(() => { loadData() }, [])
const loadData = async () => {
try {
setLoading(true)
const [issuesData, categoriesData, teamData] = await Promise.all([
api.get('/issues'),
api.get('/issues/categories'),
api.get('/users/team'),
])
setIssues(issuesData.issues || [])
setCounts(issuesData.counts || {})
setCategories(categoriesData || [])
setTeamMembers(Array.isArray(teamData) ? teamData : teamData.data || [])
} catch (err) {
console.error('Failed to load issues:', err)
} finally {
setLoading(false)
}
}
// Filtering
const filteredIssues = useMemo(() => {
let filtered = [...issues]
if (searchTerm) {
const term = searchTerm.toLowerCase()
filtered = filtered.filter(i =>
i.title?.toLowerCase().includes(term) ||
i.submitter_name?.toLowerCase().includes(term) ||
i.submitter_email?.toLowerCase().includes(term) ||
i.category?.toLowerCase().includes(term)
)
}
if (filters.status) filtered = filtered.filter(i => i.status === filters.status)
if (filters.category) filtered = filtered.filter(i => i.category === filters.category)
if (filters.type) filtered = filtered.filter(i => i.type === filters.type)
if (filters.priority) filtered = filtered.filter(i => i.priority === filters.priority)
if (filters.brand) filtered = filtered.filter(i => String(i.brand_id) === String(filters.brand))
return filtered
}, [issues, searchTerm, filters])
// List sorting
const sortedIssues = useMemo(() => {
const priorityOrder = { urgent: 0, high: 1, medium: 2, low: 3 }
const statusOrder = { new: 0, acknowledged: 1, in_progress: 2, resolved: 3, declined: 4 }
return [...filteredIssues].sort((a, b) => {
let cmp = 0
if (sortBy === 'created_at') {
cmp = (a.created_at || a.CreatedAt || '').localeCompare(b.created_at || b.CreatedAt || '')
} else if (sortBy === 'title') {
cmp = (a.title || '').localeCompare(b.title || '')
} else if (sortBy === 'priority') {
cmp = (priorityOrder[a.priority] ?? 2) - (priorityOrder[b.priority] ?? 2)
} else if (sortBy === 'status') {
cmp = (statusOrder[a.status] ?? 0) - (statusOrder[b.status] ?? 0)
}
return sortDir === 'asc' ? cmp : -cmp
})
}, [filteredIssues, sortBy, sortDir])
const updateFilter = (key, value) => setFilters(f => ({ ...f, [key]: value }))
const clearFilters = () => {
setFilters({ status: '', category: '', type: '', priority: '', brand: '' })
setSearchTerm('')
}
const hasActiveFilters = Object.values(filters).some(Boolean) || searchTerm
const getAssigneeName = (assignedToId) => {
if (!assignedToId) return '—'
const member = teamMembers.find(m => m.id === assignedToId || m._id === assignedToId)
return member?.name || '—'
}
const formatDate = (dateStr) => {
if (!dateStr) return '—'
return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
}
// Drag and drop handlers
const handleMoveIssue = async (issueId, newStatus) => {
try {
await api.patch(`/issues/${issueId}`, { status: newStatus })
toast.success(t('issues.statusUpdated'))
loadData()
} catch (err) {
console.error('Move issue failed:', err)
toast.error('Failed to update status')
}
}
const handleDragStart = (e, issue) => {
setDraggedIssue(issue)
e.dataTransfer.effectAllowed = 'move'
setTimeout(() => { if (e.target) e.target.style.opacity = '0.4' }, 0)
}
const handleDragEnd = (e) => {
e.target.style.opacity = '1'
setDraggedIssue(null)
setDragOverCol(null)
}
const handleDragOver = (e, colStatus) => {
e.preventDefault()
e.dataTransfer.dropEffect = 'move'
setDragOverCol(colStatus)
}
const handleDragLeave = (e) => {
if (!e.currentTarget.contains(e.relatedTarget)) setDragOverCol(null)
}
const handleDrop = (e, colStatus) => {
e.preventDefault()
setDragOverCol(null)
if (draggedIssue && draggedIssue.status !== colStatus) {
handleMoveIssue(draggedIssue.Id || draggedIssue.id, colStatus)
}
setDraggedIssue(null)
}
const toggleSort = (col) => {
if (sortBy === col) setSortDir(d => d === 'asc' ? 'desc' : 'asc')
else { setSortBy(col); setSortDir('asc') }
}
const SortIcon = ({ col }) => {
if (sortBy !== col) return null
return sortDir === 'asc'
?
Track and manage issue submissions
| toggleSort('title')}>
Title |
Submitter | Brand | Category | Type | toggleSort('priority')}>
Priority |
toggleSort('status')}>
Status |
Assigned To | toggleSort('created_at')}>
Created |
|---|---|---|---|---|---|---|---|---|
| {issue.title} |
{issue.submitter_name}
{issue.submitter_email}
|
{issue.brand_name || issue.brandName || '—'} | {issue.category || '—'} | {TYPE_OPTIONS.find(t => t.value === issue.type)?.label || issue.type} | {priorityConfig.label} | {statusConfig.label} | {getAssigneeName(issue.assigned_to_id)} | {formatDate(issue.created_at)} |