From aaac624bf919672bd24a6162e3cbc4da0ae3e9ea Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 15:47:12 +0000 Subject: [PATCH] Implement Tourist Guide System --- src/App.tsx | 20 ++ src/components/DashboardLayout.tsx | 12 +- src/components/politur/EmergencyMap.tsx | 124 ++++++++ src/components/politur/IncidentDetails.tsx | 140 +++++++++ .../dashboard/politur/EmergencyDashboard.tsx | 271 ++++++++++++++++++ src/pages/dashboard/politur/Reports.tsx | 204 +++++++++++++ src/types/politur.ts | 13 + 7 files changed, 783 insertions(+), 1 deletion(-) create mode 100644 src/components/politur/EmergencyMap.tsx create mode 100644 src/components/politur/IncidentDetails.tsx create mode 100644 src/pages/dashboard/politur/EmergencyDashboard.tsx create mode 100644 src/pages/dashboard/politur/Reports.tsx create mode 100644 src/types/politur.ts diff --git a/src/App.tsx b/src/App.tsx index 95ad3e5..f51a256 100644 --- a/src/App.tsx +++ b/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 = () => ( } /> + {/* POLITUR Routes */} + + + + + + } /> + + + + + + + } /> + {/* Catch-all route */} } /> diff --git a/src/components/DashboardLayout.tsx b/src/components/DashboardLayout.tsx index f4eaa63..4992d8e 100644 --- a/src/components/DashboardLayout.tsx +++ b/src/components/DashboardLayout.tsx @@ -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'), diff --git a/src/components/politur/EmergencyMap.tsx b/src/components/politur/EmergencyMap.tsx new file mode 100644 index 0000000..274a7d2 --- /dev/null +++ b/src/components/politur/EmergencyMap.tsx @@ -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 = ({ emergencies, onSelectEmergency }) => { + const mapContainer = useRef(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 ( +
+ {/* Map will be rendered here */} +
+ ); +}; + +export default EmergencyMap; diff --git a/src/components/politur/IncidentDetails.tsx b/src/components/politur/IncidentDetails.tsx new file mode 100644 index 0000000..583801e --- /dev/null +++ b/src/components/politur/IncidentDetails.tsx @@ -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 = ({ + 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 ( + + + + + Detalles de la Emergencia + + {emergency.priority.toUpperCase()} + + + + +
+ {/* Tourist Info */} +
+
+ +

Información del Turista

+
+
+

Nombre: {emergency.touristName}

+

Teléfono: {emergency.phone}

+
+
+ + {/* Location Info */} +
+
+ +

Ubicación

+
+

{emergency.address}

+ +
+ + {/* Incident Info */} +
+
+ +

Descripción del Incidente

+
+

{emergency.description}

+
+ + Reportado: {new Date(emergency.timestamp).toLocaleString('es-ES')} +
+
+ + {/* Status */} + {emergency.assignedOfficer && ( +
+

+ Oficial asignado: {emergency.assignedOfficer} +

+
+ )} + + {/* Actions */} +
+ + + +
+ + {emergency.status === 'pending' && ( + + )} + + {emergency.status === 'assigned' && ( + + )} +
+
+
+ ); +}; + +export default IncidentDetails; diff --git a/src/pages/dashboard/politur/EmergencyDashboard.tsx b/src/pages/dashboard/politur/EmergencyDashboard.tsx new file mode 100644 index 0000000..d4a7ae3 --- /dev/null +++ b/src/pages/dashboard/politur/EmergencyDashboard.tsx @@ -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([ + { + 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(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 ( +
+ {/* Header Stats */} +
+ + + + Emergencias Activas + + + +
+ {emergencies.filter(e => e.status !== 'resolved').length} +
+
+
+ + + + + Críticas + + + +
+ {emergencies.filter(e => e.priority === 'critical').length} +
+
+
+ + + + + En Progreso + + + +
+ {emergencies.filter(e => e.status === 'in_progress').length} +
+
+
+ + + + + Resueltas Hoy + + + +
+ {emergencies.filter(e => e.status === 'resolved').length} +
+
+
+
+ +
+ {/* Map */} + + + + + Mapa de Emergencias + + + + + + + + {/* Emergency List */} + + + + + Alertas Activas + + + +
+ {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) => ( +
setSelectedEmergency(emergency)} + > +
+
+ {getTypeIcon(emergency.type)} +
+

{emergency.touristName}

+

{emergency.address}

+
+
+ + {emergency.priority.toUpperCase()} + +
+ +

{emergency.description}

+ +
+
+ + {getTimeSince(emergency.timestamp)} +
+ +
+ {emergency.status === 'pending' && ( + + )} + {emergency.status === 'assigned' && ( + <> + + + + )} +
+
+
+ ))} +
+
+
+
+ + {/* Incident Details Modal */} + {selectedEmergency && ( + setSelectedEmergency(null)} + onAssign={() => handleAssignOfficer(selectedEmergency.id)} + onResolve={() => handleResolve(selectedEmergency.id)} + /> + )} +
+ ); +}; + +export default EmergencyDashboard; diff --git a/src/pages/dashboard/politur/Reports.tsx b/src/pages/dashboard/politur/Reports.tsx new file mode 100644 index 0000000..180cb9a --- /dev/null +++ b/src/pages/dashboard/politur/Reports.tsx @@ -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([ + { + 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 Abierto; + case 'under_review': + return En Revisión; + case 'closed': + return Cerrado; + 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 ( +
+
+

Reportes y Denuncias

+ +
+ + {/* Stats */} +
+ + + + Reportes Abiertos + + + +
+ {reports.filter(r => r.status === 'open').length} +
+
+
+ + + + + En Revisión + + + +
+ {reports.filter(r => r.status === 'under_review').length} +
+
+
+ + + + + Cerrados + + + +
+ {reports.filter(r => r.status === 'closed').length} +
+
+
+
+ + {/* Search and Filters */} + + +
+
+
+ + setSearchTerm(e.target.value)} + /> +
+
+ + +
+
+ +
+ {filteredReports.map((report) => ( +
+
+
+
+

{report.reportNumber}

+ {getStatusBadge(report.status)} +
+

{report.touristName}

+
+ +
+ +
+
+ Tipo: +

{report.type}

+
+
+ Categoría: +

{report.category}

+
+
+ Fecha: +

{new Date(report.date).toLocaleDateString('es-ES')}

+
+
+ Oficial: +

{report.officer}

+
+
+ +

{report.description}

+
+ ))} +
+
+
+
+ ); +}; + +export default Reports; diff --git a/src/types/politur.ts b/src/types/politur.ts new file mode 100644 index 0000000..d1fb941 --- /dev/null +++ b/src/types/politur.ts @@ -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; +}