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' ? : } if (loading) { return (
{viewMode === 'board' ? : }
) } return (
{/* Header */}

Issues

Track and manage issue submissions

{/* View switcher */}
{[ { mode: 'board', icon: LayoutGrid, label: t('issues.board') }, { mode: 'list', icon: List, label: t('issues.list') }, ].map(({ mode, icon: Icon, label }) => ( ))}
{/* Status Counts */}
{Object.entries(STATUS_CONFIG).map(([status, config]) => (
updateFilter('status', filters.status === status ? '' : status)} >
{config.label}
{counts[status] || 0}
))}
{/* Search & Filters - always visible inline */}
{/* Search */}
setSearchTerm(e.target.value)} className="w-full pl-10 pr-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 bg-surface" />
{hasActiveFilters && ( )}
{/* Board View */} {viewMode === 'board' && ( filteredIssues.length === 0 ? ( ) : (
{STATUS_ORDER.map(status => { const config = STATUS_CONFIG[status] const columnIssues = filteredIssues.filter(i => i.status === status) return (
handleDragOver(e, status)} onDragLeave={handleDragLeave} onDrop={e => handleDrop(e, status)} > {/* Column header */}
{config.label} {columnIssues.length}
{/* Cards */}
{columnIssues.length === 0 ? (
{t('issues.noIssuesInColumn')}
) : ( columnIssues.map(issue => (
handleDragStart(e, issue)} onDragEnd={handleDragEnd} >
)) )}
) })}
) )} {/* List View */} {viewMode === 'list' && ( sortedIssues.length === 0 ? ( ) : (
{sortedIssues.map(issue => { const priorityConfig = PRIORITY_CONFIG[issue.priority] || PRIORITY_CONFIG.medium const statusConfig = STATUS_CONFIG[issue.status] || STATUS_CONFIG.new return ( setSelectedIssue(issue)} className="hover:bg-surface-secondary cursor-pointer transition-colors" > ) })}
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)}
) )} {/* Detail Panel */} {selectedIssue && ( setSelectedIssue(null)} onUpdate={loadData} teamMembers={teamMembers} /> )}
) }