Refactor: Implement emergency system plan
This commit is contained in:
@@ -21,18 +21,11 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { toast } from 'sonner';
|
import { toast } from '@/hooks/use-toast';
|
||||||
import { useEmergencyData } from '@/hooks/useEmergencyData';
|
import { useEmergencyData } from '@/hooks/useEmergencyData';
|
||||||
import { Incident } from '@/services/emergencyApi';
|
import { Incident } from '@/services/emergencyApi';
|
||||||
|
|
||||||
interface EmergencyTabProps {
|
const EmergencyTab: React.FC = () => {
|
||||||
incidents: any[];
|
|
||||||
stats: any;
|
|
||||||
isAdmin: boolean;
|
|
||||||
isSuperAdmin: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EmergencyTab: React.FC<EmergencyTabProps> = () => {
|
|
||||||
const {
|
const {
|
||||||
stats,
|
stats,
|
||||||
incidents,
|
incidents,
|
||||||
@@ -69,13 +62,24 @@ const EmergencyTab: React.FC<EmergencyTabProps> = () => {
|
|||||||
try {
|
try {
|
||||||
const result = await activatePanicButton('panic');
|
const result = await activatePanicButton('panic');
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success('¡Alerta de pánico enviada! POLITUR ha sido notificado.');
|
toast({
|
||||||
|
title: "¡Alerta de pánico enviada!",
|
||||||
|
description: "POLITUR ha sido notificado.",
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.error || 'Error activando alerta de pánico');
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: result.error || 'Error activando alerta de pánico',
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error activating panic button:', error);
|
console.error('Error activating panic button:', error);
|
||||||
toast.error('Error activando alerta de pánico');
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Error activando alerta de pánico",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -187,11 +191,64 @@ const EmergencyTab: React.FC<EmergencyTabProps> = () => {
|
|||||||
<SelectItem value="resolved">Resuelto</SelectItem>
|
<SelectItem value="resolved">Resuelto</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
<SelectValue placeholder="Tipo" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Todos los tipos</SelectItem>
|
||||||
|
<SelectItem value="theft">Robo</SelectItem>
|
||||||
|
<SelectItem value="assault">Asalto</SelectItem>
|
||||||
|
<SelectItem value="accident">Accidente</SelectItem>
|
||||||
|
<SelectItem value="medical">Médico</SelectItem>
|
||||||
|
<SelectItem value="other">Otro</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{filteredIncidents.length > 0 ? (
|
||||||
|
filteredIncidents.map((incident) => (
|
||||||
|
<Card key={incident.id} className="cursor-pointer hover:shadow-md transition-shadow">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<h4 className="font-semibold text-lg">{incident.title}</h4>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Badge variant={incident.priority === 'critical' ? 'destructive' :
|
||||||
|
incident.priority === 'high' ? 'secondary' : 'outline'}>
|
||||||
|
{incident.priority}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant={incident.status === 'resolved' ? 'default' : 'outline'}>
|
||||||
|
{incident.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-600 mb-3">{incident.description}</p>
|
||||||
|
<div className="flex justify-between items-center text-sm text-gray-500">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<MapPin className="w-4 h-4" />
|
||||||
|
{incident.location.address || `${incident.location.latitude}, ${incident.location.longitude}`}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
|
{new Date(incident.createdAt).toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{incident.assignedOfficer && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<UserCheck className="w-4 h-4" />
|
||||||
|
{incident.assignedOfficer.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<Shield className="w-16 h-16 text-blue-500 mx-auto mb-4" />
|
<Shield className="w-16 h-16 text-blue-500 mx-auto mb-4" />
|
||||||
<h3 className="text-lg font-semibold mb-2">Sistema de Emergencias Implementado</h3>
|
<h3 className="text-lg font-semibold mb-2">Sistema de Emergencias Activo</h3>
|
||||||
<p className="text-gray-600 mb-4">
|
<p className="text-gray-600 mb-4">
|
||||||
✅ Panel de emergencias en tiempo real<br/>
|
✅ Panel de emergencias en tiempo real<br/>
|
||||||
✅ Gestión de incidentes<br/>
|
✅ Gestión de incidentes<br/>
|
||||||
@@ -200,24 +257,102 @@ const EmergencyTab: React.FC<EmergencyTabProps> = () => {
|
|||||||
✅ Botón de pánico integrado
|
✅ Botón de pánico integrado
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Mostrando {filteredIncidents.length} incidentes | {emergencyAlerts.length} alertas activas
|
{error ? `Usando datos de demostración - ${error}` : 'No hay incidentes que mostrar'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="alerts" className="space-y-4">
|
<TabsContent value="alerts" className="space-y-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{emergencyAlerts.length > 0 ? (
|
||||||
|
emergencyAlerts.map((alert) => (
|
||||||
|
<Card key={alert.id} className="border-red-200 bg-red-50">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<h4 className="font-semibold text-lg text-red-800">
|
||||||
|
Alerta de {alert.type === 'panic' ? 'Pánico' : alert.type}
|
||||||
|
</h4>
|
||||||
|
<Badge variant="destructive">{alert.priority}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center text-sm">
|
||||||
|
<div className="flex items-center gap-4 text-red-700">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<MapPin className="w-4 h-4" />
|
||||||
|
{alert.location.address || `${alert.location.latitude}, ${alert.location.longitude}`}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
|
{new Date(alert.createdAt).toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => deactivateEmergencyAlert(alert.id)}
|
||||||
|
className="bg-red-600 hover:bg-red-700"
|
||||||
|
>
|
||||||
|
Desactivar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
<div className="text-center p-8">
|
<div className="text-center p-8">
|
||||||
<AlertCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
<AlertCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
||||||
<h3 className="text-lg font-semibold mb-2">Alertas de Emergencia</h3>
|
<h3 className="text-lg font-semibold mb-2">Alertas de Emergencia</h3>
|
||||||
<p className="text-gray-600">{emergencyAlerts.length} alertas activas</p>
|
<p className="text-gray-600">No hay alertas activas en este momento</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="officers" className="space-y-4">
|
<TabsContent value="officers" className="space-y-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{officers.length > 0 ? (
|
||||||
|
officers.map((officer) => (
|
||||||
|
<Card key={officer.id}>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-lg">{officer.name}</h4>
|
||||||
|
<p className="text-gray-600">{officer.rank} - Badge: {officer.badge}</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant={officer.status === 'available' ? 'default' :
|
||||||
|
officer.status === 'busy' ? 'secondary' : 'outline'}>
|
||||||
|
{officer.status === 'available' ? 'Disponible' :
|
||||||
|
officer.status === 'busy' ? 'Ocupado' : 'Fuera de servicio'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center text-sm text-gray-500">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Phone className="w-4 h-4" />
|
||||||
|
{officer.phone}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Activity className="w-4 h-4" />
|
||||||
|
{officer.assignedIncidents} incidentes asignados
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{officer.location && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<MapPin className="w-4 h-4" />
|
||||||
|
Ubicación activa
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
<div className="text-center p-8">
|
<div className="text-center p-8">
|
||||||
<Shield className="w-16 h-16 text-blue-500 mx-auto mb-4" />
|
<Shield className="w-16 h-16 text-blue-500 mx-auto mb-4" />
|
||||||
<h3 className="text-lg font-semibold mb-2">Personal POLITUR</h3>
|
<h3 className="text-lg font-semibold mb-2">Personal POLITUR</h3>
|
||||||
<p className="text-gray-600">{officers.length} oficiales registrados</p>
|
<p className="text-gray-600">No hay oficiales registrados</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
|||||||
@@ -17,17 +17,20 @@ export const useEmergencyData = () => {
|
|||||||
|
|
||||||
const loadEmergencyData = async () => {
|
const loadEmergencyData = async () => {
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
|
console.log('Emergency system: User not authenticated');
|
||||||
setError('Usuario no autenticado');
|
setError('Usuario no autenticado');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('Emergency system: Loading data for user role:', user?.role);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
if (isOfficer) {
|
if (isOfficer) {
|
||||||
// Load data for officers and admins
|
// Load data for officers and admins
|
||||||
|
console.log('Emergency system: Loading officer/admin data');
|
||||||
const [incidentsData, alertsData, officersData, statsData] = await Promise.all([
|
const [incidentsData, alertsData, officersData, statsData] = await Promise.all([
|
||||||
isAdmin ? emergencyApi.getAllIncidents({ page: 1, limit: 50 }) : emergencyApi.getMyIncidents(),
|
isAdmin ? emergencyApi.getAllIncidents({ page: 1, limit: 50 }) : emergencyApi.getMyIncidents(),
|
||||||
emergencyApi.getActiveEmergencyAlerts(),
|
emergencyApi.getActiveEmergencyAlerts(),
|
||||||
@@ -35,21 +38,31 @@ export const useEmergencyData = () => {
|
|||||||
emergencyApi.getSecurityStats(),
|
emergencyApi.getSecurityStats(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
console.log('Emergency system: Data loaded successfully', {
|
||||||
|
incidents: incidentsData,
|
||||||
|
alerts: alertsData,
|
||||||
|
officers: officersData,
|
||||||
|
stats: statsData
|
||||||
|
});
|
||||||
|
|
||||||
setIncidents(isAdmin ? (incidentsData as any)?.incidents || incidentsData || [] : incidentsData as Incident[]);
|
setIncidents(isAdmin ? (incidentsData as any)?.incidents || incidentsData || [] : incidentsData as Incident[]);
|
||||||
setEmergencyAlerts(alertsData);
|
setEmergencyAlerts(alertsData || []);
|
||||||
setOfficers(officersData);
|
setOfficers(officersData || []);
|
||||||
setStats(statsData);
|
setStats(statsData);
|
||||||
} else {
|
} else {
|
||||||
// Regular users can only see their own reported incidents
|
// Regular users can only see their own reported incidents
|
||||||
|
console.log('Emergency system: Loading user incidents');
|
||||||
const myIncidents = await emergencyApi.getMyIncidents();
|
const myIncidents = await emergencyApi.getMyIncidents();
|
||||||
setIncidents(myIncidents);
|
console.log('Emergency system: User incidents loaded', myIncidents);
|
||||||
|
setIncidents(myIncidents || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error loading emergency data:', error);
|
console.error('Emergency system: Error loading data:', error);
|
||||||
setError(error.message);
|
setError(`Error de conexión: ${error.message}`);
|
||||||
|
|
||||||
// Use mock data for development/testing
|
// Use comprehensive mock data for development/testing
|
||||||
|
console.log('Emergency system: Using mock data due to API error');
|
||||||
setStats({
|
setStats({
|
||||||
totalIncidents: 15,
|
totalIncidents: 15,
|
||||||
activeIncidents: 3,
|
activeIncidents: 3,
|
||||||
@@ -71,6 +84,111 @@ export const useEmergencyData = () => {
|
|||||||
low: 5
|
low: 5
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mock incidents data
|
||||||
|
const mockIncidents: Incident[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'theft',
|
||||||
|
priority: 'high',
|
||||||
|
status: 'pending',
|
||||||
|
title: 'Robo en Plaza Central',
|
||||||
|
description: 'Reporte de robo a turista en la plaza principal',
|
||||||
|
location: {
|
||||||
|
latitude: 18.4861,
|
||||||
|
longitude: -69.9312,
|
||||||
|
address: 'Plaza Central, Punta Cana'
|
||||||
|
},
|
||||||
|
reportedBy: {
|
||||||
|
id: '1',
|
||||||
|
name: 'María González',
|
||||||
|
phone: '+1809-555-0123'
|
||||||
|
},
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: 'medical',
|
||||||
|
priority: 'critical',
|
||||||
|
status: 'assigned',
|
||||||
|
title: 'Emergencia Médica en Hotel',
|
||||||
|
description: 'Turista con problemas cardíacos',
|
||||||
|
location: {
|
||||||
|
latitude: 18.4856,
|
||||||
|
longitude: -69.9289,
|
||||||
|
address: 'Hotel Paradise, Punta Cana'
|
||||||
|
},
|
||||||
|
reportedBy: {
|
||||||
|
id: '2',
|
||||||
|
name: 'Hotel Paradise Staff',
|
||||||
|
phone: '+1809-555-0456'
|
||||||
|
},
|
||||||
|
assignedOfficer: {
|
||||||
|
id: 'off1',
|
||||||
|
name: 'Sgt. Carlos Medina',
|
||||||
|
badge: 'POL-001'
|
||||||
|
},
|
||||||
|
createdAt: new Date(Date.now() - 30 * 60 * 1000).toISOString(),
|
||||||
|
updatedAt: new Date(Date.now() - 15 * 60 * 1000).toISOString()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock emergency alerts
|
||||||
|
const mockAlerts: EmergencyAlert[] = [
|
||||||
|
{
|
||||||
|
id: 'alert1',
|
||||||
|
type: 'panic',
|
||||||
|
priority: 'critical',
|
||||||
|
status: 'active',
|
||||||
|
location: {
|
||||||
|
latitude: 18.4870,
|
||||||
|
longitude: -69.9320,
|
||||||
|
address: 'Bavaro Beach'
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
id: '3',
|
||||||
|
name: 'Ana Pérez',
|
||||||
|
phone: '+1809-555-0789'
|
||||||
|
},
|
||||||
|
createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock officers
|
||||||
|
const mockOfficers: Officer[] = [
|
||||||
|
{
|
||||||
|
id: 'off1',
|
||||||
|
name: 'Sgt. Carlos Medina',
|
||||||
|
badge: 'POL-001',
|
||||||
|
rank: 'Sargento',
|
||||||
|
status: 'busy',
|
||||||
|
location: {
|
||||||
|
latitude: 18.4856,
|
||||||
|
longitude: -69.9289
|
||||||
|
},
|
||||||
|
phone: '+1809-555-1001',
|
||||||
|
assignedIncidents: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'off2',
|
||||||
|
name: 'Of. María Rodríguez',
|
||||||
|
badge: 'POL-002',
|
||||||
|
rank: 'Oficial',
|
||||||
|
status: 'available',
|
||||||
|
location: {
|
||||||
|
latitude: 18.4861,
|
||||||
|
longitude: -69.9312
|
||||||
|
},
|
||||||
|
phone: '+1809-555-1002',
|
||||||
|
assignedIncidents: 0
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
setIncidents(mockIncidents);
|
||||||
|
setEmergencyAlerts(mockAlerts);
|
||||||
|
setOfficers(mockOfficers);
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ const AdminDashboard = () => {
|
|||||||
case 'navigation':
|
case 'navigation':
|
||||||
return <GeolocationTab activeSubTab="navigation" />;
|
return <GeolocationTab activeSubTab="navigation" />;
|
||||||
case 'emergency':
|
case 'emergency':
|
||||||
return <EmergencyTab {...tabProps} />;
|
return <EmergencyTab />;
|
||||||
case 'support':
|
case 'support':
|
||||||
return <SupportTab {...tabProps} />;
|
return <SupportTab {...tabProps} />;
|
||||||
case 'config':
|
case 'config':
|
||||||
|
|||||||
Reference in New Issue
Block a user