import React, { useState, useEffect } from 'react'; import { AlertTriangle, CheckCircle, Clock, XCircle, Filter, Search, Eye, Edit, MessageSquare, User, Calendar, MapPin, RefreshCw, X, ArrowUp, ArrowDown, } from 'lucide-react'; import { toast } from 'react-toastify'; import Loading from '../../shared/components/Loading'; import EmptyState from '../../shared/components/EmptyState'; import { formatDate, formatRelativeTime } from '../../shared/utils/format'; import { logger } from '../../shared/utils/logger'; import complaintService, { Complaint, ComplaintUpdate } from '../../features/complaints/services/complaintService'; import userService from '../../features/auth/services/userService'; import Pagination from '../../shared/components/Pagination'; const IncidentComplaintManagementPage: React.FC = () => { const [loading, setLoading] = useState(true); const [complaints, setComplaints] = useState([]); const [selectedComplaint, setSelectedComplaint] = useState(null); const [complaintUpdates, setComplaintUpdates] = useState([]); const [showDetailModal, setShowDetailModal] = useState(false); const [staffMembers, setStaffMembers] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [filters, setFilters] = useState({ status: '', priority: '', category: '', assigned_to: '', search: '', }); const [expandedFilters, setExpandedFilters] = useState(false); const [updatingComplaintId, setUpdatingComplaintId] = useState(null); const [updateForm, setUpdateForm] = useState({ description: '', update_type: 'note', }); const [resolveForm, setResolveForm] = useState({ resolution_notes: '', compensation_amount: '', }); useEffect(() => { fetchComplaints(); fetchStaffMembers(); }, [currentPage, filters.status, filters.priority, filters.category, filters.assigned_to]); const fetchComplaints = async () => { try { setLoading(true); const params: any = { page: currentPage, limit: 20, }; if (filters.status) params.status = filters.status; if (filters.priority) params.priority = filters.priority; if (filters.category) params.category = filters.category; if (filters.assigned_to) params.assigned_to = parseInt(filters.assigned_to); const response = await complaintService.getComplaints(params); if (response.status === 'success' && response.data?.complaints) { let filtered = response.data.complaints; // Client-side search filter if (filters.search) { const searchLower = filters.search.toLowerCase(); filtered = filtered.filter( (complaint: Complaint) => complaint.title?.toLowerCase().includes(searchLower) || complaint.guest_name?.toLowerCase().includes(searchLower) || complaint.description?.toLowerCase().includes(searchLower) || complaint.room_number?.toString().includes(searchLower) ); } setComplaints(filtered); setTotalPages(response.data.pagination?.total_pages || 1); } } catch (error: any) { logger.error('Error fetching complaints', error); toast.error('Failed to load complaints'); } finally { setLoading(false); } }; const fetchStaffMembers = async () => { try { const response = await userService.getUsers({ role: 'staff', limit: 100 }); if (response.data?.users) { setStaffMembers(response.data.users); } } catch (error) { logger.error('Failed to fetch staff members', error); } }; const handleViewDetails = async (complaint: Complaint) => { try { const response = await complaintService.getComplaint(complaint.id); if (response.status === 'success' && response.data) { setSelectedComplaint(response.data.complaint); setComplaintUpdates(response.data.updates || []); setShowDetailModal(true); } } catch (error: any) { toast.error('Failed to load complaint details'); } }; const handleUpdateStatus = async (complaintId: number, newStatus: string) => { try { setUpdatingComplaintId(complaintId); await complaintService.updateComplaint(complaintId, { status: newStatus }); toast.success(`Complaint marked as ${newStatus}`); fetchComplaints(); if (selectedComplaint?.id === complaintId) { const updated = await complaintService.getComplaint(complaintId); if (updated.status === 'success') { setSelectedComplaint(updated.data.complaint); } } } catch (error: any) { toast.error(error.response?.data?.detail || 'Failed to update complaint'); } finally { setUpdatingComplaintId(null); } }; const handleAssign = async (complaintId: number, staffId?: number) => { try { setUpdatingComplaintId(complaintId); await complaintService.updateComplaint(complaintId, { assigned_to: staffId }); toast.success('Complaint assigned successfully'); fetchComplaints(); if (selectedComplaint?.id === complaintId) { const updated = await complaintService.getComplaint(complaintId); if (updated.status === 'success') { setSelectedComplaint(updated.data.complaint); } } } catch (error: any) { toast.error(error.response?.data?.detail || 'Failed to assign complaint'); } finally { setUpdatingComplaintId(null); } }; const handleAddUpdate = async () => { if (!selectedComplaint || !updateForm.description.trim()) { toast.error('Please enter update description'); return; } try { await complaintService.addComplaintUpdate(selectedComplaint.id, { description: updateForm.description, update_type: updateForm.update_type, }); toast.success('Update added successfully'); setUpdateForm({ description: '', update_type: 'note' }); // Refresh complaint details const updated = await complaintService.getComplaint(selectedComplaint.id); if (updated.status === 'success') { setSelectedComplaint(updated.data.complaint); setComplaintUpdates(updated.data.updates || []); } } catch (error: any) { toast.error(error.response?.data?.detail || 'Failed to add update'); } }; const handleResolve = async () => { if (!selectedComplaint || !resolveForm.resolution_notes.trim()) { toast.error('Please enter resolution notes'); return; } try { setUpdatingComplaintId(selectedComplaint.id); await complaintService.resolveComplaint(selectedComplaint.id, { resolution_notes: resolveForm.resolution_notes, compensation_amount: resolveForm.compensation_amount ? parseFloat(resolveForm.compensation_amount) : undefined, }); toast.success('Complaint resolved successfully'); setResolveForm({ resolution_notes: '', compensation_amount: '' }); fetchComplaints(); setShowDetailModal(false); setSelectedComplaint(null); } catch (error: any) { toast.error(error.response?.data?.detail || 'Failed to resolve complaint'); } finally { setUpdatingComplaintId(null); } }; const getStatusColor = (status: string) => { switch (status) { case 'open': return 'bg-red-100 text-red-800 border-red-300'; case 'in_progress': return 'bg-yellow-100 text-yellow-800 border-yellow-300'; case 'resolved': return 'bg-green-100 text-green-800 border-green-300'; case 'closed': return 'bg-gray-100 text-gray-800 border-gray-300'; default: return 'bg-gray-100 text-gray-800 border-gray-300'; } }; const getPriorityColor = (priority: string) => { switch (priority) { case 'urgent': return 'bg-red-100 text-red-800 border-red-300'; case 'high': return 'bg-orange-100 text-orange-800 border-orange-300'; case 'medium': return 'bg-yellow-100 text-yellow-800 border-yellow-300'; case 'low': return 'bg-blue-100 text-blue-800 border-blue-300'; default: return 'bg-gray-100 text-gray-800 border-gray-300'; } }; const getCategoryIcon = (category: string) => { switch (category) { case 'room_quality': return '๐Ÿจ'; case 'service': return '๐Ÿ‘ฅ'; case 'billing': return '๐Ÿ’ฐ'; case 'noise': return '๐Ÿ”Š'; case 'cleanliness': return '๐Ÿงน'; case 'maintenance': return '๐Ÿ”ง'; default: return '๐Ÿ“‹'; } }; const openCount = complaints.filter(c => c.status === 'open').length; const inProgressCount = complaints.filter(c => c.status === 'in_progress').length; const urgentCount = complaints.filter(c => c.priority === 'urgent' && c.status !== 'resolved' && c.status !== 'closed').length; if (loading && complaints.length === 0) { return ; } return (
{/* Header */}

Incident & Complaint Management

Track, assign, and resolve guest complaints and incidents

{/* Stats Cards */}

Open

{openCount}

In Progress

{inProgressCount}

Urgent

{urgentCount}

Total

{complaints.length}

{/* Filters */}

Filters

{/* Search */}
setFilters({ ...filters, search: e.target.value })} className="w-full pl-10 pr-4 py-2.5 border-2 border-slate-200 rounded-xl focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all duration-200 text-slate-700" />
{expandedFilters && (
)}
{/* Complaints List */}
{complaints.length === 0 ? (
) : ( <>
{complaints.map((complaint) => ( handleViewDetails(complaint)} > ))}
Complaint Guest / Room Status Priority Assigned To Date Actions
{getCategoryIcon(complaint.category)}
{complaint.title}
{complaint.category.replace('_', ' ')}
{complaint.guest_name || 'N/A'}
{complaint.room_number && (
Room {complaint.room_number}
)}
{complaint.status.replace('_', ' ')} {complaint.priority} {complaint.assigned_staff_name || ( Unassigned )} {formatRelativeTime(complaint.created_at)}
{totalPages > 1 && (
)} )}
{/* Detail Modal */} {showDetailModal && selectedComplaint && (

Complaint Details

{/* Complaint Info */}
{selectedComplaint.status.replace('_', ' ')}
{selectedComplaint.priority}

{selectedComplaint.title}

{selectedComplaint.description}

{selectedComplaint.guest_name || 'N/A'}

{selectedComplaint.room_number && (

Room {selectedComplaint.room_number}

)}

{selectedComplaint.category.replace('_', ' ')}

{formatDate(selectedComplaint.created_at)}

{selectedComplaint.assigned_staff_name && (

{selectedComplaint.assigned_staff_name}

)} {/* Updates Timeline */} {complaintUpdates.length > 0 && (
{complaintUpdates.map((update) => (
{update.updated_by_name || 'System'} {formatRelativeTime(update.created_at)}

{update.description}

))}
)} {/* Actions */} {selectedComplaint.status !== 'resolved' && selectedComplaint.status !== 'closed' && (
{!selectedComplaint.assigned_to && (
)}