Files
Hotel-Booking/Frontend/src/pages/staff/IncidentComplaintManagementPage.tsx
Iliyan Angelov 3d634b4fce updates
2025-12-04 01:07:34 +02:00

801 lines
34 KiB
TypeScript

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<Complaint[]>([]);
const [selectedComplaint, setSelectedComplaint] = useState<Complaint | null>(null);
const [complaintUpdates, setComplaintUpdates] = useState<ComplaintUpdate[]>([]);
const [showDetailModal, setShowDetailModal] = useState(false);
const [staffMembers, setStaffMembers] = useState<any[]>([]);
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<number | null>(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 <Loading fullScreen text="Loading complaints..." />;
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50 px-3 sm:px-4 md:px-6 lg:px-8 py-6 sm:py-8 md:py-10">
{/* Header */}
<div className="mb-6 sm:mb-8">
<div className="flex items-center gap-2 sm:gap-3 mb-2 sm:mb-3">
<div className="h-1 w-12 sm:w-16 md:w-20 bg-gradient-to-r from-red-400 via-orange-500 to-yellow-600 rounded-full"></div>
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 bg-clip-text text-transparent tracking-tight">
Incident & Complaint Management
</h1>
</div>
<p className="text-slate-600 mt-2 sm:mt-3 text-sm sm:text-base md:text-lg font-light">
Track, assign, and resolve guest complaints and incidents
</p>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-5 md:gap-6 mb-6 sm:mb-8">
<div className="bg-white/90 backdrop-blur-md rounded-xl shadow-xl border border-slate-200/60 p-4 sm:p-5 border-l-4 border-l-red-500">
<div className="flex items-center justify-between">
<div>
<p className="text-slate-500 text-xs font-semibold uppercase tracking-wider mb-1">Open</p>
<p className="text-2xl sm:text-3xl font-bold text-red-600">{openCount}</p>
</div>
<AlertTriangle className="w-8 h-8 sm:w-10 sm:h-10 text-red-500" />
</div>
</div>
<div className="bg-white/90 backdrop-blur-md rounded-xl shadow-xl border border-slate-200/60 p-4 sm:p-5 border-l-4 border-l-yellow-500">
<div className="flex items-center justify-between">
<div>
<p className="text-slate-500 text-xs font-semibold uppercase tracking-wider mb-1">In Progress</p>
<p className="text-2xl sm:text-3xl font-bold text-yellow-600">{inProgressCount}</p>
</div>
<Clock className="w-8 h-8 sm:w-10 sm:h-10 text-yellow-500" />
</div>
</div>
<div className="bg-white/90 backdrop-blur-md rounded-xl shadow-xl border border-slate-200/60 p-4 sm:p-5 border-l-4 border-l-orange-500">
<div className="flex items-center justify-between">
<div>
<p className="text-slate-500 text-xs font-semibold uppercase tracking-wider mb-1">Urgent</p>
<p className="text-2xl sm:text-3xl font-bold text-orange-600">{urgentCount}</p>
</div>
<ArrowUp className="w-8 h-8 sm:w-10 sm:h-10 text-orange-500" />
</div>
</div>
<div className="bg-white/90 backdrop-blur-md rounded-xl shadow-xl border border-slate-200/60 p-4 sm:p-5 border-l-4 border-l-blue-500">
<div className="flex items-center justify-between">
<div>
<p className="text-slate-500 text-xs font-semibold uppercase tracking-wider mb-1">Total</p>
<p className="text-2xl sm:text-3xl font-bold text-blue-600">{complaints.length}</p>
</div>
<AlertTriangle className="w-8 h-8 sm:w-10 sm:h-10 text-blue-500" />
</div>
</div>
</div>
{/* Filters */}
<div className="bg-white/90 backdrop-blur-md rounded-xl shadow-xl border border-slate-200/60 p-4 sm:p-5 md:p-6 mb-6 sm:mb-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg sm:text-xl font-bold text-slate-900 flex items-center gap-2">
<Filter className="w-5 h-5 text-blue-600" />
Filters
</h2>
<button
onClick={() => setExpandedFilters(!expandedFilters)}
className="flex items-center gap-2 px-3 py-2 text-sm text-blue-600 hover:text-blue-700 font-medium"
>
{expandedFilters ? <ArrowUp className="w-4 h-4" /> : <ArrowDown className="w-4 h-4" />}
{expandedFilters ? 'Hide' : 'Show'} Filters
</button>
</div>
{/* Search */}
<div className="mb-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-5 h-5" />
<input
type="text"
placeholder="Search by title, guest name, room, or description..."
value={filters.search}
onChange={(e) => 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"
/>
</div>
</div>
{expandedFilters && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Status</label>
<select
value={filters.status}
onChange={(e) => {
setFilters({ ...filters, status: e.target.value });
setCurrentPage(1);
}}
className="w-full px-3 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"
>
<option value="">All Statuses</option>
<option value="open">Open</option>
<option value="in_progress">In Progress</option>
<option value="resolved">Resolved</option>
<option value="closed">Closed</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Priority</label>
<select
value={filters.priority}
onChange={(e) => {
setFilters({ ...filters, priority: e.target.value });
setCurrentPage(1);
}}
className="w-full px-3 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"
>
<option value="">All Priorities</option>
<option value="urgent">Urgent</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Category</label>
<select
value={filters.category}
onChange={(e) => {
setFilters({ ...filters, category: e.target.value });
setCurrentPage(1);
}}
className="w-full px-3 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"
>
<option value="">All Categories</option>
<option value="room_quality">Room Quality</option>
<option value="service">Service</option>
<option value="billing">Billing</option>
<option value="noise">Noise</option>
<option value="cleanliness">Cleanliness</option>
<option value="maintenance">Maintenance</option>
<option value="other">Other</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Assigned To</label>
<select
value={filters.assigned_to}
onChange={(e) => {
setFilters({ ...filters, assigned_to: e.target.value });
setCurrentPage(1);
}}
className="w-full px-3 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"
>
<option value="">All Staff</option>
<option value="unassigned">Unassigned</option>
{staffMembers.map((staff) => (
<option key={staff.id} value={staff.id}>
{staff.full_name}
</option>
))}
</select>
</div>
</div>
)}
<div className="mt-4 flex gap-2">
<button
onClick={() => {
setFilters({
status: '',
priority: '',
category: '',
assigned_to: '',
search: '',
});
setCurrentPage(1);
}}
className="px-4 py-2 text-sm text-slate-600 hover:text-slate-800 font-medium"
>
Clear Filters
</button>
<button
onClick={fetchComplaints}
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium flex items-center gap-2"
>
<RefreshCw className="w-4 h-4" />
Refresh
</button>
</div>
</div>
{/* Complaints List */}
<div className="bg-white/90 backdrop-blur-md rounded-xl shadow-xl border border-slate-200/60 overflow-hidden">
{complaints.length === 0 ? (
<div className="p-8 sm:p-12">
<EmptyState
title="No complaints found"
description="There are no complaints matching your filters."
/>
</div>
) : (
<>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-slate-200">
<thead className="bg-slate-50">
<tr>
<th className="px-4 sm:px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
Complaint
</th>
<th className="px-4 sm:px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
Guest / Room
</th>
<th className="px-4 sm:px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
Status
</th>
<th className="px-4 sm:px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
Priority
</th>
<th className="px-4 sm:px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
Assigned To
</th>
<th className="px-4 sm:px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
Date
</th>
<th className="px-4 sm:px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-slate-200">
{complaints.map((complaint) => (
<tr
key={complaint.id}
className={`hover:bg-slate-50 transition-colors cursor-pointer ${
complaint.priority === 'urgent' && complaint.status !== 'resolved' && complaint.status !== 'closed'
? 'bg-red-50/50'
: ''
}`}
onClick={() => handleViewDetails(complaint)}
>
<td className="px-4 sm:px-6 py-4">
<div className="flex items-center gap-3">
<span className="text-2xl">{getCategoryIcon(complaint.category)}</span>
<div>
<div className="text-sm font-medium text-slate-900">{complaint.title}</div>
<div className="text-xs text-slate-500 capitalize">{complaint.category.replace('_', ' ')}</div>
</div>
</div>
</td>
<td className="px-4 sm:px-6 py-4 whitespace-nowrap">
<div className="text-sm text-slate-900">{complaint.guest_name || 'N/A'}</div>
{complaint.room_number && (
<div className="text-xs text-slate-500 flex items-center gap-1">
<MapPin className="w-3 h-3" />
Room {complaint.room_number}
</div>
)}
</td>
<td className="px-4 sm:px-6 py-4 whitespace-nowrap">
<span
className={`px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full border ${getStatusColor(
complaint.status
)}`}
>
{complaint.status.replace('_', ' ')}
</span>
</td>
<td className="px-4 sm:px-6 py-4 whitespace-nowrap">
<span
className={`px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full border ${getPriorityColor(
complaint.priority
)}`}
>
{complaint.priority}
</span>
</td>
<td className="px-4 sm:px-6 py-4 whitespace-nowrap text-sm text-slate-500">
{complaint.assigned_staff_name || (
<span className="text-yellow-600 font-medium">Unassigned</span>
)}
</td>
<td className="px-4 sm:px-6 py-4 whitespace-nowrap text-sm text-slate-500">
{formatRelativeTime(complaint.created_at)}
</td>
<td className="px-4 sm:px-6 py-4 whitespace-nowrap text-sm font-medium">
<button
onClick={(e) => {
e.stopPropagation();
handleViewDetails(complaint);
}}
className="text-blue-600 hover:text-blue-900 p-1.5 hover:bg-blue-50 rounded-lg transition-colors"
title="View details"
>
<Eye className="w-4 h-4" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{totalPages > 1 && (
<div className="px-4 sm:px-6 py-4 border-t border-slate-200">
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
/>
</div>
)}
</>
)}
</div>
{/* Detail Modal */}
{showDetailModal && selectedComplaint && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-slate-200">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-slate-900">Complaint Details</h2>
<button
onClick={() => {
setShowDetailModal(false);
setSelectedComplaint(null);
setComplaintUpdates([]);
setUpdateForm({ description: '', update_type: 'note' });
setResolveForm({ resolution_notes: '', compensation_amount: '' });
}}
className="text-slate-400 hover:text-slate-600"
>
<X className="w-6 h-6" />
</button>
</div>
</div>
<div className="p-6 space-y-6">
{/* Complaint Info */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-500 mb-1">Status</label>
<span
className={`px-3 py-1 inline-flex text-sm font-semibold rounded-full border ${getStatusColor(
selectedComplaint.status
)}`}
>
{selectedComplaint.status.replace('_', ' ')}
</span>
</div>
<div>
<label className="block text-sm font-medium text-slate-500 mb-1">Priority</label>
<span
className={`px-3 py-1 inline-flex text-sm font-semibold rounded-full border ${getPriorityColor(
selectedComplaint.priority
)}`}
>
{selectedComplaint.priority}
</span>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-500 mb-1">Title</label>
<p className="text-slate-900 font-medium">{selectedComplaint.title}</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-500 mb-1">Description</label>
<p className="text-slate-900 whitespace-pre-wrap">{selectedComplaint.description}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-500 mb-1">Guest</label>
<p className="text-slate-900">{selectedComplaint.guest_name || 'N/A'}</p>
</div>
{selectedComplaint.room_number && (
<div>
<label className="block text-sm font-medium text-slate-500 mb-1">Room</label>
<p className="text-slate-900">Room {selectedComplaint.room_number}</p>
</div>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-500 mb-1">Category</label>
<p className="text-slate-900 capitalize">{selectedComplaint.category.replace('_', ' ')}</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-500 mb-1">Created At</label>
<p className="text-slate-900">{formatDate(selectedComplaint.created_at)}</p>
</div>
</div>
{selectedComplaint.assigned_staff_name && (
<div>
<label className="block text-sm font-medium text-slate-500 mb-1">Assigned To</label>
<p className="text-slate-900">{selectedComplaint.assigned_staff_name}</p>
</div>
)}
{/* Updates Timeline */}
{complaintUpdates.length > 0 && (
<div>
<label className="block text-sm font-medium text-slate-500 mb-3">Updates Timeline</label>
<div className="space-y-3">
{complaintUpdates.map((update) => (
<div key={update.id} className="p-4 bg-slate-50 rounded-lg border border-slate-200">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-semibold text-slate-900">{update.updated_by_name || 'System'}</span>
<span className="text-xs text-slate-500">{formatRelativeTime(update.created_at)}</span>
</div>
<p className="text-sm text-slate-700">{update.description}</p>
</div>
))}
</div>
</div>
)}
{/* Actions */}
{selectedComplaint.status !== 'resolved' && selectedComplaint.status !== 'closed' && (
<div className="pt-4 border-t border-slate-200 space-y-4">
{!selectedComplaint.assigned_to && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Assign To</label>
<select
onChange={(e) => {
if (e.target.value) {
handleAssign(selectedComplaint.id, parseInt(e.target.value));
}
}}
className="w-full px-3 py-2 border-2 border-slate-200 rounded-lg focus:border-blue-400 focus:ring-4 focus:ring-blue-100"
>
<option value="">Select staff member...</option>
{staffMembers.map((staff) => (
<option key={staff.id} value={staff.id}>
{staff.full_name}
</option>
))}
</select>
</div>
)}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Add Update</label>
<textarea
value={updateForm.description}
onChange={(e) => setUpdateForm({ ...updateForm, description: e.target.value })}
rows={3}
placeholder="Add an update or note..."
className="w-full px-3 py-2 border-2 border-slate-200 rounded-lg focus:border-blue-400 focus:ring-4 focus:ring-blue-100"
/>
<button
onClick={handleAddUpdate}
className="mt-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium"
>
Add Update
</button>
</div>
{selectedComplaint.status !== 'resolved' && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Resolution Notes *</label>
<textarea
value={resolveForm.resolution_notes}
onChange={(e) => setResolveForm({ ...resolveForm, resolution_notes: e.target.value })}
rows={4}
placeholder="Enter resolution details..."
className="w-full px-3 py-2 border-2 border-slate-200 rounded-lg focus:border-blue-400 focus:ring-4 focus:ring-blue-100"
/>
<div className="mt-2">
<label className="block text-sm font-medium text-slate-700 mb-2">Compensation Amount (Optional)</label>
<input
type="number"
value={resolveForm.compensation_amount}
onChange={(e) => setResolveForm({ ...resolveForm, compensation_amount: e.target.value })}
placeholder="0.00"
className="w-full px-3 py-2 border-2 border-slate-200 rounded-lg focus:border-blue-400 focus:ring-4 focus:ring-blue-100"
/>
</div>
<button
onClick={handleResolve}
disabled={updatingComplaintId === selectedComplaint.id}
className="mt-2 w-full px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 font-medium disabled:opacity-50"
>
{updatingComplaintId === selectedComplaint.id ? 'Resolving...' : 'Resolve Complaint'}
</button>
</div>
)}
<div className="flex gap-2">
<button
onClick={() => handleUpdateStatus(selectedComplaint.id, 'in_progress')}
disabled={updatingComplaintId === selectedComplaint.id || selectedComplaint.status === 'in_progress'}
className="flex-1 px-4 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 font-medium disabled:opacity-50"
>
Mark In Progress
</button>
<button
onClick={() => handleUpdateStatus(selectedComplaint.id, 'closed')}
disabled={updatingComplaintId === selectedComplaint.id}
className="flex-1 px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 font-medium disabled:opacity-50"
>
Close
</button>
</div>
</div>
)}
{selectedComplaint.status === 'resolved' && selectedComplaint.resolution_notes && (
<div className="pt-4 border-t border-slate-200">
<label className="block text-sm font-medium text-slate-500 mb-1">Resolution Notes</label>
<p className="text-slate-900 whitespace-pre-wrap">{selectedComplaint.resolution_notes}</p>
</div>
)}
</div>
</div>
</div>
)}
</div>
);
};
export default IncidentComplaintManagementPage;