File: /var/www/igsms.viitorcloud.co/igsmsportal/views/OfficerDashboard.tsx
import React, { useState, useMemo } from 'react';
import { Grievance, GrievanceStatus, User } from '../types';
import { STATUS_COLORS, PRIORITY_COLORS } from '../constants';
import { summarizeGrievance } from '../services/geminiService';
interface OfficerDashboardProps {
activeTab: string;
user: User;
grievances: Grievance[];
onUpdateStatus: (id: string, status: GrievanceStatus) => void;
}
type SortKey = 'ticketNumber' | 'district' | 'status';
type SortDirection = 'asc' | 'desc';
const OfficerDashboard: React.FC<OfficerDashboardProps> = ({ activeTab, user, grievances, onUpdateStatus }) => {
const [selectedTicket, setSelectedTicket] = useState<Grievance | null>(null);
const [summary, setSummary] = useState<string>('');
const [isSummarizing, setIsSummarizing] = useState(false);
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: SortDirection }>({ key: 'ticketNumber', direction: 'asc' });
// Explicitly matching user request for functional tabs
const filteredByTab = useMemo(() => {
switch (activeTab) {
case 'pending': // "Active Queue"
return grievances.filter(g =>
g.status === GrievanceStatus.PENDING ||
g.status === GrievanceStatus.ASSIGNED ||
g.status === GrievanceStatus.IN_PROGRESS
);
case 'resolved': // "Completed"
return grievances.filter(g =>
g.status === GrievanceStatus.RESOLVED ||
g.status === GrievanceStatus.CLOSED
);
case 'escalated': // "Critical Alerts"
return grievances.filter(g =>
g.status === GrievanceStatus.ESCALATED ||
g.priority === 'URGENT'
);
default:
return grievances; // 'dashboard' shows all in department (or system in prototype)
}
}, [grievances, activeTab]);
const sortedGrievances = useMemo(() => {
const sortableItems = [...filteredByTab];
sortableItems.sort((a, b) => {
let valA = a[sortConfig.key] || '';
let valB = b[sortConfig.key] || '';
if (valA < valB) return sortConfig.direction === 'asc' ? -1 : 1;
if (valA > valB) return sortConfig.direction === 'asc' ? 1 : -1;
return 0;
});
return sortableItems;
}, [filteredByTab, sortConfig]);
const requestSort = (key: SortKey) => {
setSortConfig({ key, direction: sortConfig.key === key && sortConfig.direction === 'asc' ? 'desc' : 'asc' });
};
const getDaysSince = (dateString: string) => {
const diff = Math.floor((Date.now() - new Date(dateString).getTime()) / (1000 * 3600 * 24));
return diff === 0 ? 'Today' : `${diff} days ago`;
};
const getHeaderContent = () => {
switch (activeTab) {
case 'pending': return { title: 'Active Queue', desc: 'Tickets currently under review and field audit' };
case 'resolved': return { title: 'Completed', desc: 'Resolved issues and officially closed cases' };
case 'escalated': return { title: 'Critical Alerts', desc: 'Urgent priority and escalated items requiring immediate action' };
default: return { title: 'My Workspace', desc: 'Consolidated view of all system grievances (Prototype Mode)' };
}
};
const header = getHeaderContent();
return (
<div className="animate-fade-in space-y-8 py-4">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h2 className="text-3xl font-black text-slate-900 tracking-tight">{header.title}</h2>
<p className="text-slate-500 font-medium mt-1">{header.desc}</p>
</div>
<div className="flex gap-3">
<div className="bg-white px-5 py-3 rounded-2xl border border-slate-200 flex items-center gap-3 shadow-sm">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">In view</span>
<span className="text-lg font-black text-blue-600">{sortedGrievances.length}</span>
</div>
<button className="bg-slate-900 text-white px-6 py-3 rounded-2xl text-[10px] font-black hover:bg-slate-800 transition-all shadow-lg flex items-center gap-2 uppercase tracking-widest">
<i className="fa-solid fa-file-export"></i> Export Report
</button>
</div>
</div>
<div className="bg-white rounded-[2.5rem] shadow-sm border border-slate-200 overflow-hidden min-h-[450px]">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-slate-100">
<thead className="bg-slate-50/50">
<tr>
{['ticketNumber', 'district', 'status'].map((k) => (
<th key={k} onClick={() => requestSort(k as SortKey)} className="px-8 py-6 text-left text-[10px] font-black text-slate-400 uppercase tracking-[0.2em] cursor-pointer hover:text-blue-600 transition-colors">
{k.replace('Number', '')} <i className={`fa-solid fa-sort ml-2 opacity-20`}></i>
</th>
))}
<th className="px-8 py-6 text-right text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">Manage</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{sortedGrievances.length > 0 ? (
sortedGrievances.map((g) => (
<tr key={g.id} className="group hover:bg-slate-50/50 transition-all cursor-pointer" onClick={() => setSelectedTicket(g)}>
<td className="px-8 py-6">
<div className="flex items-center gap-4">
<div className={`w-2 h-8 rounded-full ${g.priority === 'URGENT' ? 'bg-red-500 animate-pulse' : 'bg-slate-100'}`}></div>
<div>
<div className="text-sm font-black text-blue-600 tracking-tight mb-0.5">{g.ticketNumber}</div>
<div className="text-xs font-bold text-slate-800 line-clamp-1">{g.title}</div>
</div>
</div>
</td>
<td className="px-8 py-6">
<div className="text-xs font-black text-slate-700">{g.district}</div>
<div className="text-[10px] font-bold text-slate-400 mt-1 uppercase tracking-tight">{g.taluka}</div>
</td>
<td className="px-8 py-6">
<span className={`text-[9px] px-3 py-1.5 rounded-xl font-black uppercase tracking-wider ${STATUS_COLORS[g.status]}`}>{g.status.replace('_', ' ')}</span>
</td>
<td className="px-8 py-6 text-right">
<button className="w-10 h-10 bg-white border border-slate-200 rounded-xl inline-flex items-center justify-center text-slate-400 group-hover:text-blue-600 group-hover:border-blue-200 transition-all shadow-sm">
<i className="fa-solid fa-arrow-right-long"></i>
</button>
</td>
</tr>
))
) : (
<tr>
<td colSpan={4} className="px-8 py-32 text-center">
<div className="flex flex-col items-center">
<div className="w-20 h-20 bg-slate-50 rounded-[2rem] flex items-center justify-center text-slate-200 mb-6 border border-slate-100">
<i className="fa-solid fa-folder-open text-3xl"></i>
</div>
<p className="text-sm font-black text-slate-900 uppercase tracking-widest">Workspace is clear</p>
<p className="text-xs font-bold text-slate-400 mt-2">No tickets found matching your current view.</p>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{selectedTicket && (
<div className="fixed inset-0 z-50 flex items-center justify-end bg-slate-900/70 transition-all animate-fade-in">
<div className="w-full max-w-2xl h-full bg-white shadow-2xl animate-slide-in-right flex flex-col rounded-l-[3rem] border-l border-slate-200 relative overflow-hidden">
<div className="p-8 border-b border-slate-100 flex justify-between items-start bg-slate-50">
<div>
<div className="flex items-center gap-3 mb-2">
<span className="text-[10px] font-black text-blue-600 bg-blue-50 border border-blue-100 px-3 py-1 rounded-lg uppercase tracking-widest">{selectedTicket.ticketNumber}</span>
<span className={`text-[10px] px-3 py-1 rounded-lg font-black uppercase tracking-wider ${PRIORITY_COLORS[selectedTicket.priority]}`}>{selectedTicket.priority} Priority</span>
</div>
<h3 className="text-2xl font-black text-slate-900 leading-tight pr-8">{selectedTicket.title}</h3>
</div>
<button onClick={() => { setSelectedTicket(null); setSummary(''); }} className="w-12 h-12 rounded-2xl bg-white border border-slate-200 text-slate-400 hover:text-slate-900 hover:border-slate-300 transition-all flex items-center justify-center shadow-sm">
<i className="fa-solid fa-xmark"></i>
</button>
</div>
<div className="p-10 space-y-10 flex-1 overflow-y-auto bg-white">
<div className="bg-slate-50 rounded-[2.5rem] p-8 border border-slate-100 flex items-center justify-between shadow-sm">
<div className="flex items-center gap-5">
<div className="w-14 h-14 bg-white rounded-2xl flex items-center justify-center text-blue-600 shadow-sm border border-slate-100">
<i className="fa-solid fa-id-card text-xl"></i>
</div>
<div>
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Citizen Details</p>
<p className="font-black text-slate-900 text-lg">{selectedTicket.citizenName}</p>
<p className="text-xs font-bold text-slate-500 tracking-wider">+91 {selectedTicket.contactNumber}</p>
</div>
</div>
<div className="text-right">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Timeline</p>
<p className="font-black text-slate-900 text-sm">{new Date(selectedTicket.createdAt).toLocaleDateString()}</p>
<p className="text-[10px] font-black text-blue-600 mt-1 uppercase tracking-tighter">{getDaysSince(selectedTicket.createdAt)}</p>
</div>
</div>
<div>
<div className="flex items-center gap-3 mb-4">
<div className="w-1 h-4 bg-orange-500 rounded-full"></div>
<p className="text-[10px] font-black text-slate-800 uppercase tracking-[0.2em]">Statement of Issue</p>
</div>
<div className="p-8 bg-slate-50 rounded-[2.5rem] border border-slate-100 text-sm font-bold text-slate-600 leading-relaxed italic">
"{selectedTicket.description}"
</div>
</div>
<div className="bg-blue-50 border border-blue-100 rounded-[2.5rem] p-8 space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-white flex items-center justify-center shadow-sm text-blue-600">
<i className="fa-solid fa-sparkles"></i>
</div>
<p className="text-[10px] font-black text-blue-900 uppercase tracking-widest">Case Intelligence</p>
</div>
<button
onClick={async () => {
setIsSummarizing(true);
setSummary(await summarizeGrievance(selectedTicket.description) || '');
setIsSummarizing(false);
}}
disabled={isSummarizing}
className="px-6 py-2.5 bg-blue-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-blue-700 transition-all disabled:opacity-50 shadow-lg shadow-blue-500/20"
>
{isSummarizing ? <i className="fa-solid fa-spinner animate-spin"></i> : 'Run AI Analysis'}
</button>
</div>
{summary && (
<div className="space-y-4 animate-fade-in p-6 bg-white rounded-[2rem] border border-blue-200/50">
<div className="text-blue-900 font-bold text-xs leading-relaxed whitespace-pre-line">
{summary}
</div>
</div>
)}
</div>
<div className="grid grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-4">Jurisdiction</p>
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-slate-50 rounded-xl flex items-center justify-center text-slate-400 border border-slate-100">
<i className="fa-solid fa-map-location-dot"></i>
</div>
<p className="text-xs font-black text-slate-900">{selectedTicket.district}</p>
</div>
</div>
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-4">SLA Deadline</p>
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-slate-50 rounded-xl flex items-center justify-center text-slate-400 border border-slate-100">
<i className="fa-solid fa-hourglass-half"></i>
</div>
<p className="text-xs font-black text-slate-900">{new Date(selectedTicket.slaDeadline).toLocaleDateString()}</p>
</div>
</div>
</div>
</div>
<div className="p-10 border-t border-slate-100 bg-slate-50/50 flex flex-col gap-4">
<div className="flex gap-4">
<button onClick={() => onUpdateStatus(selectedTicket.id, GrievanceStatus.RESOLVED)} className="flex-1 py-5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-[1.5rem] font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-emerald-500/20 transition-all">
Resolve Issue
</button>
<button onClick={() => onUpdateStatus(selectedTicket.id, GrievanceStatus.ESCALATED)} className="px-10 py-5 bg-white border border-slate-200 text-rose-600 hover:bg-rose-50 rounded-[1.5rem] font-black text-xs uppercase tracking-[0.2em] transition-all">
Escalate
</button>
</div>
<div className="flex gap-4">
<button onClick={() => onUpdateStatus(selectedTicket.id, GrievanceStatus.IN_PROGRESS)} className="flex-1 py-4 bg-slate-900 text-white rounded-[1.5rem] font-black text-[10px] uppercase tracking-widest transition-all">
Mark In-Progress
</button>
<button className="px-10 py-4 bg-white border border-slate-200 text-slate-400 rounded-[1.5rem] font-black text-[10px] uppercase tracking-widest transition-all">
Officer Remark
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default OfficerDashboard;