import { useState, useEffect, useContext } from 'react' import { Copy, Eye, Lock, Send, FileText, Trash2, Check, Clock, CheckCircle2, XCircle, FileEdit, Wrench, MessageSquare, Paperclip } from 'lucide-react' import UploadZone from './UploadZone' import { api, STATUS_CONFIG, PRIORITY_CONFIG } from '../utils/api' import TabbedModal from './TabbedModal' import Modal from './Modal' import { useToast } from './ToastContainer' import { AppContext } from '../App' import { useLanguage } from '../i18n/LanguageContext' import PortalSelect from './PortalSelect' 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([]) const [initialLoading, setInitialLoading] = useState(true) const [saving, setSaving] = useState(false) const [uploadingFile, setUploadingFile] = useState(false) const [activeTab, setActiveTab] = useState('details') // Form state const [assignedTo, setAssignedTo] = useState('') const [teamId, setTeamId] = useState('') const [internalNotes, setInternalNotes] = useState('') const [resolutionSummary, setResolutionSummary] = useState('') const [newUpdate, setNewUpdate] = useState('') const [updateIsPublic, setUpdateIsPublic] = useState(false) // Modals const [showResolveModal, setShowResolveModal] = useState(false) const [showDeclineModal, setShowDeclineModal] = useState(false) const [confirmDeleteAttId, setConfirmDeleteAttId] = useState(null) const issueId = issue?.Id || issue?.id useEffect(() => { if (issueId) loadIssueDetails() }, [issueId]) const loadIssueDetails = async () => { try { const data = await api.get(`/issues/${issueId}`) setIssueData(data) 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) { console.error('Failed to load issue:', err) } finally { setInitialLoading(false) } } const handleUpdateStatus = async (newStatus) => { if (saving) return try { setSaving(true) await api.patch(`/issues/${issueId}`, { status: newStatus }) await onUpdate() await loadIssueDetails() } catch (err) { console.error('Failed to update status:', err) toast.error(t('issues.failedToUpdateStatus')) } finally { setSaving(false) } } const handleResolve = async () => { if (saving || !resolutionSummary.trim()) return try { setSaving(true) await api.patch(`/issues/${issueId}`, { status: 'resolved', resolution_summary: resolutionSummary }) await onUpdate() setShowResolveModal(false) await loadIssueDetails() } catch (err) { console.error('Failed to resolve issue:', err) toast.error(t('issues.failedToResolve')) } finally { setSaving(false) } } const handleDecline = async () => { if (saving || !resolutionSummary.trim()) return try { setSaving(true) await api.patch(`/issues/${issueId}`, { status: 'declined', resolution_summary: resolutionSummary }) await onUpdate() setShowDeclineModal(false) await loadIssueDetails() } catch (err) { console.error('Failed to decline issue:', err) toast.error(t('issues.failedToDecline')) } finally { setSaving(false) } } const handleAssignmentChange = async (newAssignedTo) => { try { setAssignedTo(newAssignedTo) await api.patch(`/issues/${issueId}`, { assigned_to_id: newAssignedTo || null }) await onUpdate() } catch (err) { console.error('Failed to update assignment:', err) toast.error(t('issues.failedToUpdateAssignment')) } } const handleNotesChange = async () => { if (saving) return try { setSaving(true) await api.patch(`/issues/${issueId}`, { internal_notes: internalNotes }) } catch (err) { console.error('Failed to save notes:', err) toast.error(t('issues.failedToSaveNotes')) } finally { setSaving(false) } } const handleAddUpdate = async () => { if (!newUpdate.trim() || saving) return try { setSaving(true) await api.post(`/issues/${issueId}/updates`, { message: newUpdate, is_public: updateIsPublic }) setNewUpdate('') setUpdateIsPublic(false) await loadIssueDetails() } catch (err) { console.error('Failed to add update:', err) toast.error(t('issues.failedToAddUpdate')) } finally { setSaving(false) } } const handleFileUpload = async (e) => { const file = e.target.files?.[0] if (!file) return try { setUploadingFile(true) const formData = new FormData() formData.append('file', file) await api.upload(`/issues/${issueId}/attachments`, formData) await loadIssueDetails() e.target.value = '' // Reset input } catch (err) { console.error('Failed to upload file:', err) toast.error(t('issues.failedToUploadFile')) } finally { setUploadingFile(false) } } const handleDeleteAttachment = async (attachmentId) => { try { await api.delete(`/issue-attachments/${attachmentId}`) await loadIssueDetails() } catch (err) { console.error('Failed to delete attachment:', err) toast.error(t('issues.failedToDeleteAttachment')) } } const copyTrackingLink = () => { const url = `${window.location.origin}/track/${issueData.tracking_token}` navigator.clipboard.writeText(url) toast.success(t('issues.trackingLinkCopied')) } const formatDate = (dateStr) => { if (!dateStr) return '' const date = new Date(dateStr) return date.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' }) } const formatFileSize = (bytes) => { if (bytes < 1024) return bytes + ' B' if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB' return (bytes / (1024 * 1024)).toFixed(1) + ' MB' } if (initialLoading || !issueData) { return (
) } const statusConfig = STATUS_CONFIG[issueData.status] || STATUS_CONFIG.new const priorityConfig = PRIORITY_CONFIG[issueData.priority] || PRIORITY_CONFIG.medium const tabs = [ { key: 'details', label: t('issues.details') || 'Details', icon: FileEdit }, { key: 'actions', label: t('issues.actions') || 'Actions', icon: Wrench }, { key: 'updates', label: t('issues.updates') || 'Updates', icon: MessageSquare, badge: updates.length }, { key: 'attachments', label: t('issues.attachments') || 'Attachments', icon: Paperclip, badge: attachments.length }, ] return ( <>

{issueData.title}

{statusConfig.label} {priorityConfig.label} {issueData.type} {issueData.category} {issueData.brand_name && ( {issueData.brand_name} )}
} tabs={tabs} activeTab={activeTab} onTabChange={setActiveTab} footer={ <> } > {/* Details Tab */} {activeTab === 'details' && (
{/* Submitter Info */}

{t('issues.submitterInfo')}

{t('issues.nameLabel')} {issueData.submitter_name}
{t('issues.emailLabel')} {issueData.submitter_email}
{issueData.submitter_phone && (
{t('issues.phoneLabel')} {issueData.submitter_phone}
)}
{t('issues.submittedLabel')} {formatDate(issueData.created_at)}
{/* Description */}

{t('issues.description')}

{issueData.description || t('issues.noDescription')}

{/* Assigned To */}
handleAssignmentChange(val)} options={[{ value: '', label: t('issues.unassigned') }, ...teamMembers.map(member => ({ value: member.id || member._id, label: member.name }))]} 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" />
{/* Team */} {teams.length > 0 && (
{ const resolvedVal = val || null setTeamId(resolvedVal || '') try { await api.patch(`/issues/${issueId}`, { team_id: resolvedVal }) await onUpdate() await loadIssueDetails() } catch (err) { console.error('Failed to update team:', err) } }} options={[{ value: '', label: t('issues.allTeams') }, ...teams.map(team => ({ value: team.id || team._id, label: team.name }))]} 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" />
)} {/* Brand */}
{ const resolvedVal = val || null; try { await api.patch(`/issues/${issueId}`, { brand_id: resolvedVal }); loadIssueDetails(); onUpdate(); } catch {} }} options={[{ value: '', label: t('issues.noBrand') }, ...(brands || []).map(b => ({ value: b._id || b.Id, label: b.name }))]} 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" />
{/* Internal Notes */}