Implement Tourist Guide System
This commit is contained in:
20
src/App.tsx
20
src/App.tsx
@@ -49,6 +49,9 @@ import Integrations from "./pages/dashboard/config/Integrations";
|
||||
import Audit from "./pages/dashboard/config/Audit";
|
||||
import SecurityCenter from "./pages/dashboard/config/SecurityCenter";
|
||||
import PersonalizationPage from "./pages/dashboard/config/PersonalizationPage";
|
||||
// POLITUR pages
|
||||
import EmergencyDashboard from "./pages/dashboard/politur/EmergencyDashboard";
|
||||
import PolReports from "./pages/dashboard/politur/Reports";
|
||||
// Commerce pages (for retail stores)
|
||||
import CommerceStore from "./pages/dashboard/commerce/Store";
|
||||
import CommercePOS from "./pages/dashboard/commerce/POSTerminal";
|
||||
@@ -570,6 +573,23 @@ const AppRouter = () => (
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
{/* POLITUR Routes */}
|
||||
<Route path="/dashboard/politur/emergency" element={
|
||||
<ProtectedRoute>
|
||||
<DashboardLayout>
|
||||
<EmergencyDashboard />
|
||||
</DashboardLayout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/dashboard/politur/reports" element={
|
||||
<ProtectedRoute>
|
||||
<DashboardLayout>
|
||||
<PolReports />
|
||||
</DashboardLayout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
{/* Catch-all route */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
|
||||
@@ -57,7 +57,8 @@ import {
|
||||
Sparkles,
|
||||
Leaf,
|
||||
Store,
|
||||
Server
|
||||
Server,
|
||||
ShieldAlert
|
||||
} from 'lucide-react';
|
||||
|
||||
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
@@ -176,6 +177,15 @@ const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
},
|
||||
{ icon: Car, label: 'Vehicle Management', path: '/dashboard/vehicle-management' },
|
||||
{ icon: Leaf, label: 'Sustainability', path: '/dashboard/sustainability' },
|
||||
{
|
||||
icon: ShieldAlert,
|
||||
label: 'POLITUR',
|
||||
path: '/dashboard/politur',
|
||||
subItems: [
|
||||
{ icon: AlertTriangle, label: 'Emergencias', path: '/dashboard/politur/emergency' },
|
||||
{ icon: FileText, label: 'Reportes', path: '/dashboard/politur/reports' }
|
||||
]
|
||||
},
|
||||
{
|
||||
icon: Store,
|
||||
label: t('commerce'),
|
||||
|
||||
124
src/components/politur/EmergencyMap.tsx
Normal file
124
src/components/politur/EmergencyMap.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Emergency } from '@/types/politur';
|
||||
|
||||
interface EmergencyMapProps {
|
||||
emergencies: Emergency[];
|
||||
onSelectEmergency: (emergency: Emergency) => void;
|
||||
}
|
||||
|
||||
const EmergencyMap: React.FC<EmergencyMapProps> = ({ emergencies, onSelectEmergency }) => {
|
||||
const mapContainer = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapContainer.current) return;
|
||||
|
||||
// Simulated map - in production, integrate with Mapbox or Google Maps
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = mapContainer.current.clientWidth;
|
||||
canvas.height = 400;
|
||||
canvas.style.width = '100%';
|
||||
canvas.style.height = '400px';
|
||||
canvas.style.borderRadius = '8px';
|
||||
canvas.style.backgroundColor = '#f0f0f0';
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// Draw simple map representation
|
||||
ctx.fillStyle = '#e0e0e0';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Draw grid
|
||||
ctx.strokeStyle = '#d0d0d0';
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < canvas.width; i += 50) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(i, 0);
|
||||
ctx.lineTo(i, canvas.height);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let i = 0; i < canvas.height; i += 50) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, i);
|
||||
ctx.lineTo(canvas.width, i);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw title
|
||||
ctx.fillStyle = '#666';
|
||||
ctx.font = '16px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Mapa de Santo Domingo', canvas.width / 2, 30);
|
||||
ctx.font = '12px Arial';
|
||||
ctx.fillText('(Integración con Mapbox próximamente)', canvas.width / 2, 50);
|
||||
|
||||
// Draw emergency markers
|
||||
emergencies.forEach((emergency, index) => {
|
||||
const x = 100 + (index * 150) % (canvas.width - 200);
|
||||
const y = 100 + Math.floor(index / 4) * 100;
|
||||
|
||||
// Marker circle
|
||||
const color = emergency.priority === 'critical' ? '#ef4444' :
|
||||
emergency.priority === 'high' ? '#f97316' :
|
||||
emergency.priority === 'medium' ? '#eab308' : '#22c55e';
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 15, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Marker border
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.stroke();
|
||||
|
||||
// Pulse effect for critical
|
||||
if (emergency.priority === 'critical') {
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.globalAlpha = 0.3;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 25, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// Label
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.font = '11px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(emergency.touristName.split(' ')[0], x, y + 35);
|
||||
});
|
||||
|
||||
mapContainer.current.innerHTML = '';
|
||||
mapContainer.current.appendChild(canvas);
|
||||
|
||||
// Add click handler
|
||||
canvas.addEventListener('click', (e) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) * (canvas.width / rect.width);
|
||||
const y = (e.clientY - rect.top) * (canvas.height / rect.height);
|
||||
|
||||
emergencies.forEach((emergency, index) => {
|
||||
const markerX = 100 + (index * 150) % (canvas.width - 200);
|
||||
const markerY = 100 + Math.floor(index / 4) * 100;
|
||||
const distance = Math.sqrt(Math.pow(x - markerX, 2) + Math.pow(y - markerY, 2));
|
||||
|
||||
if (distance < 20) {
|
||||
onSelectEmergency(emergency);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
canvas.style.cursor = 'pointer';
|
||||
|
||||
}, [emergencies, onSelectEmergency]);
|
||||
|
||||
return (
|
||||
<div ref={mapContainer} className="w-full h-[400px] rounded-lg overflow-hidden">
|
||||
{/* Map will be rendered here */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmergencyMap;
|
||||
140
src/components/politur/IncidentDetails.tsx
Normal file
140
src/components/politur/IncidentDetails.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import { X, Phone, Video, Navigation, MapPin, Clock, User, FileText } from 'lucide-react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Emergency } from '@/types/politur';
|
||||
|
||||
interface IncidentDetailsProps {
|
||||
emergency: Emergency;
|
||||
onClose: () => void;
|
||||
onAssign: () => void;
|
||||
onResolve: () => void;
|
||||
}
|
||||
|
||||
const IncidentDetails: React.FC<IncidentDetailsProps> = ({
|
||||
emergency,
|
||||
onClose,
|
||||
onAssign,
|
||||
onResolve
|
||||
}) => {
|
||||
const openNavigation = () => {
|
||||
const url = `https://www.google.com/maps/dir/?api=1&destination=${emergency.location.lat},${emergency.location.lng}`;
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={true} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center justify-between">
|
||||
<span>Detalles de la Emergencia</span>
|
||||
<Badge variant={emergency.priority === 'critical' ? 'destructive' : 'default'}>
|
||||
{emergency.priority.toUpperCase()}
|
||||
</Badge>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Tourist Info */}
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<User className="w-5 h-5 text-gray-600" />
|
||||
<h3 className="font-semibold">Información del Turista</h3>
|
||||
</div>
|
||||
<div className="space-y-1 text-sm">
|
||||
<p><strong>Nombre:</strong> {emergency.touristName}</p>
|
||||
<p><strong>Teléfono:</strong> {emergency.phone}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Location Info */}
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<MapPin className="w-5 h-5 text-gray-600" />
|
||||
<h3 className="font-semibold">Ubicación</h3>
|
||||
</div>
|
||||
<p className="text-sm mb-2">{emergency.address}</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={openNavigation}
|
||||
>
|
||||
<Navigation className="w-4 h-4 mr-2" />
|
||||
Navegar hacia la ubicación
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Incident Info */}
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<FileText className="w-5 h-5 text-gray-600" />
|
||||
<h3 className="font-semibold">Descripción del Incidente</h3>
|
||||
</div>
|
||||
<p className="text-sm mb-2">{emergency.description}</p>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>Reportado: {new Date(emergency.timestamp).toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
{emergency.assignedOfficer && (
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<p className="text-sm">
|
||||
<strong>Oficial asignado:</strong> {emergency.assignedOfficer}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={() => window.location.href = `tel:${emergency.phone}`}
|
||||
>
|
||||
<Phone className="w-4 h-4 mr-2" />
|
||||
Llamar
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => {/* Video call implementation */}}
|
||||
>
|
||||
<Video className="w-4 h-4 mr-2" />
|
||||
Videollamada
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{emergency.status === 'pending' && (
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
onAssign();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Asignarme esta emergencia
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{emergency.status === 'assigned' && (
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
onResolve();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Marcar como Resuelta
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default IncidentDetails;
|
||||
271
src/pages/dashboard/politur/EmergencyDashboard.tsx
Normal file
271
src/pages/dashboard/politur/EmergencyDashboard.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { AlertTriangle, MapPin, Phone, Video, Clock, CheckCircle, XCircle, Navigation } from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import EmergencyMap from '@/components/politur/EmergencyMap';
|
||||
import IncidentDetails from '@/components/politur/IncidentDetails';
|
||||
import { Emergency } from '@/types/politur';
|
||||
|
||||
|
||||
const EmergencyDashboard = () => {
|
||||
const { toast } = useToast();
|
||||
const [emergencies, setEmergencies] = useState<Emergency[]>([
|
||||
{
|
||||
id: '1',
|
||||
touristName: 'John Smith',
|
||||
location: { lat: 18.4861, lng: -69.9312 },
|
||||
address: 'Zona Colonial, Santo Domingo',
|
||||
type: 'security',
|
||||
priority: 'high',
|
||||
status: 'pending',
|
||||
description: 'Robo reportado en la zona',
|
||||
timestamp: new Date().toISOString(),
|
||||
phone: '+1-555-0123'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
touristName: 'Maria Garcia',
|
||||
location: { lat: 18.4721, lng: -69.8933 },
|
||||
address: 'Malecón, Santo Domingo',
|
||||
type: 'medical',
|
||||
priority: 'critical',
|
||||
status: 'assigned',
|
||||
description: 'Emergencia médica - dolor de pecho',
|
||||
timestamp: new Date(Date.now() - 300000).toISOString(),
|
||||
assignedOfficer: 'Oficial Rodriguez',
|
||||
phone: '+1-555-0456'
|
||||
}
|
||||
]);
|
||||
const [selectedEmergency, setSelectedEmergency] = useState<Emergency | null>(null);
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'critical': return 'destructive';
|
||||
case 'high': return 'default';
|
||||
case 'medium': return 'secondary';
|
||||
default: return 'outline';
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'medical': return '🏥';
|
||||
case 'security': return '🚨';
|
||||
case 'accident': return '🚗';
|
||||
default: return '⚠️';
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssignOfficer = (emergencyId: string) => {
|
||||
setEmergencies(prev => prev.map(e =>
|
||||
e.id === emergencyId
|
||||
? { ...e, status: 'assigned', assignedOfficer: 'Tu' }
|
||||
: e
|
||||
));
|
||||
toast({
|
||||
title: "Asignado",
|
||||
description: "Te has asignado a esta emergencia",
|
||||
});
|
||||
};
|
||||
|
||||
const handleResolve = (emergencyId: string) => {
|
||||
setEmergencies(prev => prev.map(e =>
|
||||
e.id === emergencyId
|
||||
? { ...e, status: 'resolved' }
|
||||
: e
|
||||
));
|
||||
toast({
|
||||
title: "Resuelto",
|
||||
description: "La emergencia ha sido marcada como resuelta",
|
||||
});
|
||||
};
|
||||
|
||||
const getTimeSince = (timestamp: string) => {
|
||||
const minutes = Math.floor((Date.now() - new Date(timestamp).getTime()) / 60000);
|
||||
if (minutes < 1) return 'Hace un momento';
|
||||
if (minutes < 60) return `Hace ${minutes} min`;
|
||||
return `Hace ${Math.floor(minutes / 60)} hrs`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-gray-600">
|
||||
Emergencias Activas
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-red-600">
|
||||
{emergencies.filter(e => e.status !== 'resolved').length}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-gray-600">
|
||||
Críticas
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-orange-600">
|
||||
{emergencies.filter(e => e.priority === 'critical').length}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-gray-600">
|
||||
En Progreso
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-blue-600">
|
||||
{emergencies.filter(e => e.status === 'in_progress').length}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-gray-600">
|
||||
Resueltas Hoy
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-green-600">
|
||||
{emergencies.filter(e => e.status === 'resolved').length}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Map */}
|
||||
<Card className="lg:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<MapPin className="w-5 h-5" />
|
||||
Mapa de Emergencias
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<EmergencyMap
|
||||
emergencies={emergencies}
|
||||
onSelectEmergency={setSelectedEmergency}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Emergency List */}
|
||||
<Card className="lg:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5" />
|
||||
Alertas Activas
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 max-h-[500px] overflow-y-auto">
|
||||
{emergencies
|
||||
.filter(e => e.status !== 'resolved')
|
||||
.sort((a, b) => {
|
||||
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
||||
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
||||
})
|
||||
.map((emergency) => (
|
||||
<div
|
||||
key={emergency.id}
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${
|
||||
selectedEmergency?.id === emergency.id
|
||||
? 'ring-2 ring-primary bg-primary/5'
|
||||
: 'hover:bg-gray-50'
|
||||
}`}
|
||||
onClick={() => setSelectedEmergency(emergency)}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl">{getTypeIcon(emergency.type)}</span>
|
||||
<div>
|
||||
<h4 className="font-semibold">{emergency.touristName}</h4>
|
||||
<p className="text-sm text-gray-600">{emergency.address}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={getPriorityColor(emergency.priority)}>
|
||||
{emergency.priority.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-700 mb-3">{emergency.description}</p>
|
||||
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-1 text-gray-500">
|
||||
<Clock className="w-4 h-4" />
|
||||
{getTimeSince(emergency.timestamp)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{emergency.status === 'pending' && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAssignOfficer(emergency.id);
|
||||
}}
|
||||
>
|
||||
Asignarme
|
||||
</Button>
|
||||
)}
|
||||
{emergency.status === 'assigned' && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.location.href = `tel:${emergency.phone}`;
|
||||
}}
|
||||
>
|
||||
<Phone className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleResolve(emergency.id);
|
||||
}}
|
||||
>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Incident Details Modal */}
|
||||
{selectedEmergency && (
|
||||
<IncidentDetails
|
||||
emergency={selectedEmergency}
|
||||
onClose={() => setSelectedEmergency(null)}
|
||||
onAssign={() => handleAssignOfficer(selectedEmergency.id)}
|
||||
onResolve={() => handleResolve(selectedEmergency.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmergencyDashboard;
|
||||
204
src/pages/dashboard/politur/Reports.tsx
Normal file
204
src/pages/dashboard/politur/Reports.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FileText, Search, Filter, Download, Eye } from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
interface Report {
|
||||
id: string;
|
||||
reportNumber: string;
|
||||
touristName: string;
|
||||
type: string;
|
||||
category: string;
|
||||
date: string;
|
||||
status: 'open' | 'under_review' | 'closed';
|
||||
officer: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const Reports = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [reports] = useState<Report[]>([
|
||||
{
|
||||
id: '1',
|
||||
reportNumber: 'POL-2024-001',
|
||||
touristName: 'John Smith',
|
||||
type: 'Robo',
|
||||
category: 'Seguridad',
|
||||
date: '2024-01-15',
|
||||
status: 'closed',
|
||||
officer: 'Oficial Rodriguez',
|
||||
description: 'Robo de cartera en Zona Colonial'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
reportNumber: 'POL-2024-002',
|
||||
touristName: 'Maria Garcia',
|
||||
type: 'Accidente',
|
||||
category: 'Tránsito',
|
||||
date: '2024-01-16',
|
||||
status: 'under_review',
|
||||
officer: 'Oficial Martinez',
|
||||
description: 'Accidente de tránsito menor en el Malecón'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
reportNumber: 'POL-2024-003',
|
||||
touristName: 'Robert Johnson',
|
||||
type: 'Pérdida',
|
||||
category: 'Documentos',
|
||||
date: '2024-01-17',
|
||||
status: 'open',
|
||||
officer: 'Oficial Fernandez',
|
||||
description: 'Pérdida de pasaporte en área hotelera'
|
||||
}
|
||||
]);
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'open':
|
||||
return <Badge variant="default">Abierto</Badge>;
|
||||
case 'under_review':
|
||||
return <Badge variant="secondary">En Revisión</Badge>;
|
||||
case 'closed':
|
||||
return <Badge variant="outline">Cerrado</Badge>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredReports = reports.filter(report =>
|
||||
report.touristName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
report.reportNumber.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
report.type.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold text-gray-900">Reportes y Denuncias</h2>
|
||||
<Button>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
Nuevo Reporte
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-gray-600">
|
||||
Reportes Abiertos
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-blue-600">
|
||||
{reports.filter(r => r.status === 'open').length}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-gray-600">
|
||||
En Revisión
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-orange-600">
|
||||
{reports.filter(r => r.status === 'under_review').length}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium text-gray-600">
|
||||
Cerrados
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-green-600">
|
||||
{reports.filter(r => r.status === 'closed').length}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Search and Filters */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
placeholder="Buscar por nombre, número o tipo..."
|
||||
className="pl-10"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
Filtros
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exportar
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{filteredReports.map((report) => (
|
||||
<div
|
||||
key={report.id}
|
||||
className="border rounded-lg p-4 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold">{report.reportNumber}</h3>
|
||||
{getStatusBadge(report.status)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">{report.touristName}</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline">
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
Ver
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm mb-2">
|
||||
<div>
|
||||
<span className="text-gray-500">Tipo:</span>
|
||||
<p className="font-medium">{report.type}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">Categoría:</span>
|
||||
<p className="font-medium">{report.category}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">Fecha:</span>
|
||||
<p className="font-medium">{new Date(report.date).toLocaleDateString('es-ES')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">Oficial:</span>
|
||||
<p className="font-medium">{report.officer}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-700">{report.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Reports;
|
||||
13
src/types/politur.ts
Normal file
13
src/types/politur.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export interface Emergency {
|
||||
id: string;
|
||||
touristName: string;
|
||||
location: { lat: number; lng: number };
|
||||
address: string;
|
||||
type: 'medical' | 'security' | 'accident' | 'other';
|
||||
priority: 'low' | 'medium' | 'high' | 'critical';
|
||||
status: 'pending' | 'assigned' | 'in_progress' | 'resolved';
|
||||
description: string;
|
||||
timestamp: string;
|
||||
assignedOfficer?: string;
|
||||
phone: string;
|
||||
}
|
||||
Reference in New Issue
Block a user