Initial commit from remix

This commit is contained in:
gpt-engineer-app[bot]
2025-09-25 16:01:00 +00:00
commit 5ddc52658d
149 changed files with 32798 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
import React from 'react';
import { Settings, Cog, Database, Wifi, Shield } from 'lucide-react';
interface ConfigTabProps {
isAdmin: boolean;
isSuperAdmin: boolean;
}
const ConfigTab: React.FC<ConfigTabProps> = ({ isSuperAdmin }) => {
if (!isSuperAdmin) {
return (
<div className="bg-white rounded-lg shadow p-8 text-center">
<Shield className="w-16 h-16 text-red-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">Acceso Restringido</h3>
<p className="text-gray-600">Solo los Super Administradores pueden acceder a la configuración del sistema.</p>
</div>
);
}
return (
<div className="space-y-6">
<h2 className="text-xl font-semibold text-gray-900">Configuración del Sistema</h2>
<div className="bg-white rounded-lg shadow p-8 text-center">
<Settings className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Configuración del Sistema
</h3>
<p className="text-gray-600 mb-4">
Esta sección está en desarrollo y se implementará según las especificaciones del informe.
</p>
<div className="text-sm text-gray-500">
Funcionalidades pendientes:
<ul className="mt-2 space-y-1">
<li> Configuración de API</li>
<li> Parámetros del sistema</li>
<li> Gestión de integrations</li>
<li> Configuración de seguridad</li>
<li> Logs de auditoría</li>
</ul>
</div>
</div>
</div>
);
};
export default ConfigTab;

View File

@@ -0,0 +1,764 @@
import React, { useState } from 'react';
import {
FileText,
Globe,
Edit,
Plus,
MapPin,
Camera,
Bot,
Eye,
Trash2,
Star,
Users,
Calendar,
DollarSign,
Image,
Settings,
Car,
Clock
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import { useToast } from '@/hooks/use-toast';
import { apiClient } from '@/services/adminApi';
import GeolocationTab from './GeolocationTab';
interface ContentTabProps {
isSuperAdmin: boolean;
activeSubTab?: string;
}
const ContentTab: React.FC<ContentTabProps> = ({ isSuperAdmin, activeSubTab = 'destinations' }) => {
const [activeTab, setActiveTab] = useState(activeSubTab || 'destinations');
React.useEffect(() => {
setActiveTab(activeSubTab || 'destinations');
}, [activeSubTab]);
const [destinations, setDestinations] = useState<any[]>([]);
const [places, setPlaces] = useState<any[]>([]);
const [guides, setGuides] = useState<any[]>([]);
const [taxis, setTaxis] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [newDestination, setNewDestination] = useState({
name: '',
description: '',
category: '',
countryId: 1,
coordinates: { x: 0, y: 0 },
images: ['']
});
const [newPlace, setNewPlace] = useState({
name: '',
description: '',
destinationId: '',
category: '',
address: '',
phone: '',
website: '',
coordinates: { x: 0, y: 0 },
entranceFee: 0,
openingHours: {
monday: '9:00-17:00',
tuesday: '9:00-17:00',
wednesday: '9:00-17:00',
thursday: '9:00-17:00',
friday: '9:00-17:00',
saturday: '9:00-17:00',
sunday: '9:00-17:00'
},
historicalInfo: '',
images: ['']
});
const [newGuide, setNewGuide] = useState({
licenseNumber: '',
specialties: [''],
languages: [''],
hourlyRate: 0,
dailyRate: 0,
bio: '',
certifications: {}
});
const [newTaxi, setNewTaxi] = useState({
licenseNumber: '',
vehiclePlate: '',
vehicleModel: '',
vehicleYear: new Date().getFullYear(),
vehicleColor: '',
vehicleCapacity: 4
});
// Promocional & IA forms
const [newCampaign, setNewCampaign] = useState({
title: '',
message: '',
segment: 'all'
});
const [aiGuideConfig, setAiGuideConfig] = useState({
name: '',
language: 'es',
personality: ''
});
// Load initial data
const loadData = async () => {
setLoading(true);
try {
const [destData, placesData, guidesData, taxisData] = await Promise.all([
apiClient.get('/tourism/destinations?page=1&limit=50'),
apiClient.get('/tourism/places?page=1&limit=50'),
apiClient.get('/tourism/guides?page=1&limit=50'),
apiClient.get('/tourism/taxis/available')
]);
setDestinations((destData as any)?.destinations || (Array.isArray(destData) ? (destData as any) : []) );
setPlaces((placesData as any)?.places || (Array.isArray(placesData) ? (placesData as any) : []) );
setGuides((guidesData as any)?.guides || (Array.isArray(guidesData) ? (guidesData as any) : []) );
const taxisArr = Array.isArray(taxisData) ? taxisData : (taxisData as any)?.taxis || (taxisData as any)?.drivers || (taxisData as any)?.data || [];
setTaxis(taxisArr as any[]);
} catch (error) {
console.error('Error loading data:', error);
} finally {
setLoading(false);
}
};
React.useEffect(() => {
loadData();
}, []);
const { toast } = useToast();
const handleCreateDestination = async () => {
try {
await apiClient.post('/tourism/destinations', {
name: newDestination.name,
description: newDestination.description,
category: newDestination.category,
countryId: newDestination.countryId,
coordinates: `(${newDestination.coordinates.x},${newDestination.coordinates.y})`,
images: newDestination.images.filter(img => img.trim() !== '')
});
setNewDestination({ name: '', description: '', category: '', countryId: 1, coordinates: { x: 0, y: 0 }, images: [''] });
loadData();
toast({ title: 'Destino creado', description: 'El destino se creó correctamente.' });
} catch (error: any) {
console.error('Error creating destination:', error);
toast({ title: 'Error', description: error?.message || 'No se pudo crear el destino.' });
}
};
const handleCreatePlace = async () => {
try {
await apiClient.post('/tourism/places', {
name: newPlace.name,
description: newPlace.description,
destinationId: parseInt(newPlace.destinationId),
category: newPlace.category,
address: newPlace.address,
phone: newPlace.phone,
website: newPlace.website,
coordinates: `(${newPlace.coordinates.x},${newPlace.coordinates.y})`,
entranceFee: newPlace.entranceFee,
openingHours: newPlace.openingHours,
historicalInfo: newPlace.historicalInfo,
images: newPlace.images.filter(img => img.trim() !== '')
});
setNewPlace({
name: '', description: '', destinationId: '', category: '', address: '', phone: '', website: '',
coordinates: { x: 0, y: 0 }, entranceFee: 0, historicalInfo: '', images: [''],
openingHours: {
monday: '9:00-17:00', tuesday: '9:00-17:00', wednesday: '9:00-17:00', thursday: '9:00-17:00',
friday: '9:00-17:00', saturday: '9:00-17:00', sunday: '9:00-17:00'
}
});
loadData();
toast({ title: 'Lugar creado', description: 'El lugar se creó correctamente.' });
} catch (error: any) {
console.error('Error creating place:', error);
toast({ title: 'Error', description: error?.message || 'No se pudo crear el lugar.' });
}
};
const handleDeleteDestination = async (id: string) => {
try {
await apiClient.delete(`/tourism/destinations/${id}`);
} catch (error: any) {
console.error('Error deleting destination:', error);
// Algunos endpoints devuelven 204 sin cuerpo
}
await loadData();
toast({ title: 'Destino eliminado', description: 'Se eliminó correctamente.' });
};
const handleDeletePlace = async (id: string) => {
try {
await apiClient.delete(`/tourism/places/${id}`);
} catch (error: any) {
console.error('Error deleting place:', error);
// Algunos endpoints devuelven 204 sin cuerpo
}
await loadData();
toast({ title: 'Lugar eliminado', description: 'Se eliminó correctamente.' });
};
const handleCreateCampaign = async () => {
try {
await apiClient.post('/notifications', {
title: newCampaign.title,
message: newCampaign.message,
segment: newCampaign.segment || 'all',
type: 'promotion'
});
toast({ title: 'Campaña creada', description: 'La campaña promocional fue creada.' });
setNewCampaign({ title: '', message: '', segment: 'all' });
} catch (error: any) {
console.error('Error creating campaign:', error);
toast({ title: 'Error', description: error?.message || 'No se pudo crear la campaña.' });
}
};
const handleSetupAIGuide = async () => {
try {
await apiClient.post('/ai-generator/generate', {
template: 'virtual-guide',
inputs: {
name: aiGuideConfig.name,
language: aiGuideConfig.language,
personality: aiGuideConfig.personality
}
});
toast({ title: 'Guía IA creada', description: 'La guía virtual fue configurada.' });
setAiGuideConfig({ name: '', language: 'es', personality: '' });
} catch (error: any) {
console.error('Error setting up AI guide:', error);
toast({ title: 'Error', description: error?.message || 'No se pudo configurar la guía IA.' });
}
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold text-gray-900">Gestión de Contenido Turístico</h2>
<div className="flex gap-2">
<Badge variant="secondary" className="text-sm">
{destinations.length} Destinos
</Badge>
<Badge variant="secondary" className="text-sm">
{places.length} Lugares
</Badge>
</div>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsContent value="destinations" className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">Gestión de Destinos Turísticos</h3>
{isSuperAdmin && (
<Dialog>
<DialogTrigger asChild>
<Button className="flex items-center gap-2">
<Plus className="w-4 h-4" />
Nuevo Destino
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Crear Nuevo Destino</DialogTitle>
</DialogHeader>
<div className="grid gap-4">
<div>
<Label htmlFor="dest-name">Nombre del Destino</Label>
<Input
id="dest-name"
value={newDestination.name}
onChange={(e) => setNewDestination({ ...newDestination, name: e.target.value })}
placeholder="Ej: Punta Cana"
/>
</div>
<div>
<Label htmlFor="dest-category">Categoría</Label>
<Select value={newDestination.category} onValueChange={(value) => setNewDestination({ ...newDestination, category: value })}>
<SelectTrigger>
<SelectValue placeholder="Selecciona una categoría" />
</SelectTrigger>
<SelectContent>
<SelectItem value="beach">Playa</SelectItem>
<SelectItem value="cultural">Cultural</SelectItem>
<SelectItem value="nature">Naturaleza</SelectItem>
<SelectItem value="luxury">Lujo</SelectItem>
<SelectItem value="adventure">Aventura</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="dest-description">Descripción</Label>
<Textarea
id="dest-description"
value={newDestination.description}
onChange={(e) => setNewDestination({ ...newDestination, description: e.target.value })}
placeholder="Describe el destino turístico..."
rows={3}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="dest-lat">Latitud</Label>
<Input
id="dest-lat"
type="number"
step="any"
value={newDestination.coordinates.y}
onChange={(e) => setNewDestination({
...newDestination,
coordinates: { ...newDestination.coordinates, y: parseFloat(e.target.value) || 0 }
})}
placeholder="18.4861"
/>
</div>
<div>
<Label htmlFor="dest-lng">Longitud</Label>
<Input
id="dest-lng"
type="number"
step="any"
value={newDestination.coordinates.x}
onChange={(e) => setNewDestination({
...newDestination,
coordinates: { ...newDestination.coordinates, x: parseFloat(e.target.value) || 0 }
})}
placeholder="-69.9312"
/>
</div>
</div>
<div>
<Label htmlFor="dest-image">URLs de Imágenes (separadas por coma)</Label>
<Textarea
id="dest-image"
value={newDestination.images.join(', ')}
onChange={(e) => setNewDestination({
...newDestination,
images: e.target.value.split(',').map(url => url.trim())
})}
placeholder="https://ejemplo.com/imagen1.jpg, https://ejemplo.com/imagen2.jpg"
rows={2}
/>
</div>
<Button onClick={handleCreateDestination} className="w-full">
Crear Destino
</Button>
</div>
</DialogContent>
</Dialog>
)}
</div>
<div className="grid gap-4">
{destinations.map((destination) => (
<Card key={destination.id} className="hover:shadow-lg transition-shadow">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div className="flex items-center space-x-4">
<img
src={destination.images?.[0] || '/placeholder.svg'}
alt={destination.name}
className="w-16 h-16 rounded-lg object-cover"
/>
<div>
<CardTitle className="text-lg">{destination.name}</CardTitle>
<p className="text-sm text-muted-foreground">{destination.category}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<Badge variant="outline" className="flex items-center gap-1">
<Star className="w-3 h-3" />
{destination.rating || 4.5}
</Badge>
{isSuperAdmin && (
<div className="flex gap-1">
<Button
variant="outline"
size="sm"
onClick={() => console.log('Edit destination:', destination.id)}
>
<Edit className="w-4 h-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDeleteDestination(destination.id)}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
)}
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-gray-600 mb-3">{destination.description}</p>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary" className="text-xs">
{destination.active ? 'Activo' : 'Inactivo'}
</Badge>
{destination.country_id && (
<Badge variant="outline" className="text-xs">
País ID: {destination.country_id}
</Badge>
)}
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
<TabsContent value="places" className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">Gestión de Lugares de Interés</h3>
<Dialog>
<DialogTrigger asChild>
<Button className="flex items-center gap-2">
<Plus className="w-4 h-4" />
Nuevo Lugar
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Crear Nuevo Lugar</DialogTitle>
</DialogHeader>
<div className="grid gap-4">
<div>
<Label htmlFor="place-name">Nombre del Lugar</Label>
<Input
id="place-name"
value={newPlace.name}
onChange={(e) => setNewPlace({ ...newPlace, name: e.target.value })}
placeholder="Ej: Hoyo Azul"
/>
</div>
<div>
<Label htmlFor="place-destination">Destino</Label>
<Select value={newPlace.destinationId} onValueChange={(value) => setNewPlace({ ...newPlace, destinationId: value })}>
<SelectTrigger>
<SelectValue placeholder="Selecciona un destino" />
</SelectTrigger>
<SelectContent>
{destinations.map((dest) => (
<SelectItem key={dest.id} value={dest.id.toString()}>
{dest.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="place-category">Categoría</Label>
<Select value={newPlace.category} onValueChange={(value) => setNewPlace({ ...newPlace, category: value })}>
<SelectTrigger>
<SelectValue placeholder="Selecciona una categoría" />
</SelectTrigger>
<SelectContent>
<SelectItem value="attraction">Atracción</SelectItem>
<SelectItem value="restaurant">Restaurante</SelectItem>
<SelectItem value="hotel">Hotel</SelectItem>
<SelectItem value="beach">Playa</SelectItem>
<SelectItem value="adventure">Aventura</SelectItem>
</SelectContent>
</Select>
</div>
<Button onClick={handleCreatePlace} className="w-full">
Crear Lugar
</Button>
</div>
</DialogContent>
</Dialog>
</div>
<div className="grid gap-4">
{places.map((place) => (
<Card key={place.id} className="hover:shadow-lg transition-shadow">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div className="flex items-center space-x-4">
<img
src={place.images?.[0] || '/placeholder.svg'}
alt={place.name}
className="w-16 h-16 rounded-lg object-cover"
/>
<div>
<CardTitle className="text-lg">{place.name}</CardTitle>
<p className="text-sm text-muted-foreground">{place.category}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<Badge variant="outline" className="flex items-center gap-1">
<Star className="w-3 h-3" />
{place.rating || 4.5}
</Badge>
<div className="flex gap-1">
<Button
variant="outline"
size="sm"
onClick={() => console.log('Edit place:', place.id)}
>
<Edit className="w-4 h-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDeletePlace(place.id)}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-gray-600">{place.description}</p>
<p className="text-xs text-gray-500 mt-2">{place.address}</p>
{place.phone && (
<p className="text-xs text-gray-500 mt-1">📞 {place.phone}</p>
)}
{place.website && (
<p className="text-xs text-gray-500 mt-1">🌐 {place.website}</p>
)}
</CardContent>
</Card>
))}
</div>
</TabsContent>
<TabsContent value="ai-guides" className="space-y-4">
<div className="text-center py-12">
<Bot className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">Guías Virtuales con IA</h3>
<p className="text-gray-600 mb-4">Configura y entrena asistentes virtuales para guiar a los turistas</p>
<Button className="flex items-center gap-2 mx-auto">
<Settings className="w-4 h-4" />
Configurar IA
</Button>
</div>
</TabsContent>
<TabsContent value="guides" className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">Gestión de Guías Turísticos</h3>
<Badge variant="secondary" className="text-sm">
{Array.isArray(guides) ? guides.length : 0} Guías Registrados
</Badge>
</div>
<div className="grid gap-4">
{(Array.isArray(guides) ? guides : []).map((guide) => (
<Card key={guide.id} className="hover:shadow-lg transition-shadow">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div className="flex items-center space-x-4">
<div className="w-16 h-16 rounded-lg bg-primary/10 flex items-center justify-center">
<Users className="w-8 h-8 text-primary" />
</div>
<div>
<CardTitle className="text-lg">{guide.user?.firstName} {guide.user?.lastName}</CardTitle>
<p className="text-sm text-muted-foreground">Licencia: {guide.licenseNumber}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<Badge variant="outline" className="flex items-center gap-1">
<Star className="w-3 h-3" />
{guide.rating}
</Badge>
<Badge variant={guide.isVerified ? "default" : "secondary"}>
{guide.isVerified ? 'Verificado' : 'Pendiente'}
</Badge>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-gray-600 mb-2">{guide.bio}</p>
<div className="flex flex-wrap gap-2 mb-2">
{guide.specialties?.map((specialty: string, idx: number) => (
<Badge key={idx} variant="outline" className="text-xs">
{specialty}
</Badge>
))}
</div>
<div className="flex justify-between text-sm text-gray-500">
<span>Tarifa por hora: ${guide.hourlyRate}</span>
<span>Tours realizados: {guide.totalTours}</span>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
<TabsContent value="taxis" className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">Gestión de Taxis</h3>
<Badge variant="secondary" className="text-sm">
{Array.isArray(taxis) ? taxis.length : 0} Taxis Disponibles
</Badge>
</div>
<div className="grid gap-4">
{(Array.isArray(taxis) ? taxis : []).map((taxi) => (
<Card key={taxi.id} className="hover:shadow-lg transition-shadow">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div className="flex items-center space-x-4">
<div className="w-16 h-16 rounded-lg bg-primary/10 flex items-center justify-center">
<Car className="w-8 h-8 text-primary" />
</div>
<div>
<CardTitle className="text-lg">{taxi.user?.firstName} {taxi.user?.lastName}</CardTitle>
<p className="text-sm text-muted-foreground">{taxi.vehicleModel} {taxi.vehicleYear}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<Badge variant="outline" className="flex items-center gap-1">
<Star className="w-3 h-3" />
{taxi.rating}
</Badge>
<Badge variant={taxi.isAvailable ? "default" : "secondary"}>
{taxi.isAvailable ? 'Disponible' : 'Ocupado'}
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium">Placa:</span> {taxi.vehiclePlate}
</div>
<div>
<span className="font-medium">Capacidad:</span> {taxi.vehicleCapacity} pasajeros
</div>
<div>
<span className="font-medium">Color:</span> {taxi.vehicleColor}
</div>
<div>
<span className="font-medium">Viajes:</span> {taxi.totalTrips}
</div>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
<TabsContent value="geolocation" className="space-y-4">
<GeolocationTab activeSubTab="geofences" />
</TabsContent>
<TabsContent value="promotional" className="space-y-4">
<div className="text-center py-12">
<Image className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">Contenido Promocional</h3>
<p className="text-gray-600 mb-4">Gestiona banners, ofertas especiales y campañas promocionales</p>
<Dialog>
<DialogTrigger asChild>
<Button className="flex items-center gap-2 mx-auto">
<Plus className="w-4 h-4" />
Crear Campaña
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Crear Nueva Campaña Promocional</DialogTitle>
</DialogHeader>
<div className="grid gap-4">
<div>
<Label>Título</Label>
<Input value={newCampaign.title} onChange={(e) => setNewCampaign({ ...newCampaign, title: e.target.value })} />
</div>
<div>
<Label>Mensaje</Label>
<Textarea rows={3} value={newCampaign.message} onChange={(e) => setNewCampaign({ ...newCampaign, message: e.target.value })} />
</div>
<div>
<Label>Segmento</Label>
<Select value={newCampaign.segment} onValueChange={(v) => setNewCampaign({ ...newCampaign, segment: v })}>
<SelectTrigger>
<SelectValue placeholder="Selecciona segmento" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
<SelectItem value="tourists">Turistas</SelectItem>
<SelectItem value="guides">Guías</SelectItem>
<SelectItem value="business">Comercios</SelectItem>
</SelectContent>
</Select>
</div>
<Button onClick={handleCreateCampaign}>Crear Campaña</Button>
</div>
</DialogContent>
</Dialog>
</div>
</TabsContent>
<TabsContent value="ai-guides" className="space-y-4">
<div className="text-center py-12">
<Bot className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">Guías Virtuales con IA</h3>
<p className="text-gray-600 mb-4">Configura y entrena asistentes virtuales para guiar a los turistas</p>
<Dialog>
<DialogTrigger asChild>
<Button className="flex items-center gap-2 mx-auto">
<Settings className="w-4 h-4" />
Configurar IA
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Configuración de Guías IA</DialogTitle>
</DialogHeader>
<div className="grid gap-4">
<div>
<Label>Nombre</Label>
<Input value={aiGuideConfig.name} onChange={(e) => setAiGuideConfig({ ...aiGuideConfig, name: e.target.value })} />
</div>
<div>
<Label>Idioma</Label>
<Select value={aiGuideConfig.language} onValueChange={(v) => setAiGuideConfig({ ...aiGuideConfig, language: v })}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="es">Español</SelectItem>
<SelectItem value="en">Inglés</SelectItem>
<SelectItem value="fr">Francés</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Personalidad</Label>
<Textarea rows={3} value={aiGuideConfig.personality} onChange={(e) => setAiGuideConfig({ ...aiGuideConfig, personality: e.target.value })} />
</div>
<Button onClick={handleSetupAIGuide}>Crear Guía IA</Button>
</div>
</DialogContent>
</Dialog>
</div>
</TabsContent>
<TabsContent value="ar-content" className="space-y-4">
<div className="text-center py-12">
<Camera className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">Contenido de Realidad Aumentada</h3>
<p className="text-gray-600 mb-4">Crea experiencias inmersivas con realidad aumentada para destinos</p>
<Button className="flex items-center gap-2 mx-auto">
<Eye className="w-4 h-4" />
Crear Experiencia AR
</Button>
</div>
</TabsContent>
</Tabs>
</div>
);
};
export default ContentTab;

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { AlertTriangle, Shield, Phone, MapPin } from 'lucide-react';
interface EmergencyTabProps {
incidents: any[];
stats: any;
isAdmin: boolean;
isSuperAdmin: boolean;
}
const EmergencyTab: React.FC<EmergencyTabProps> = ({ incidents, stats }) => {
return (
<div className="space-y-6">
<h2 className="text-xl font-semibold text-gray-900">Sistema de Emergencias y POLITUR</h2>
<div className="bg-white rounded-lg shadow p-8 text-center">
<AlertTriangle className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Sistema de Emergencias y POLITUR
</h3>
<p className="text-gray-600 mb-4">
Esta sección está en desarrollo y se implementará según las especificaciones del informe.
</p>
<div className="text-sm text-gray-500">
Funcionalidades pendientes:
<ul className="mt-2 space-y-1">
<li> Panel de emergencias en tiempo real</li>
<li> Gestión de incidentes</li>
<li> Comunicación con POLITUR</li>
<li> Geolocalización de emergencias</li>
<li> Botón de pánico integrado</li>
</ul>
</div>
</div>
</div>
);
};
export default EmergencyTab;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,959 @@
import React, { useState, useEffect } from 'react';
import {
MapPin,
Plus,
Settings,
Shield,
AlertTriangle,
Activity,
Navigation,
Target,
Eye,
Trash2,
Edit
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
import { Loader } from '@googlemaps/js-api-loader';
import { useToast } from '@/hooks/use-toast';
import { Label } from '@/components/ui/label';
import { apiClient } from '@/services/adminApi';
import { useAuth } from '@/contexts/AuthContext';
import { ResponsiveContainer, AreaChart, Area, BarChart as RBarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';
interface GeolocationTabProps {
activeSubTab: string;
}
const GeolocationTab: React.FC<GeolocationTabProps> = ({ activeSubTab }) => {
const [activeTab, setActiveTab] = useState(activeSubTab);
useEffect(() => { setActiveTab(activeSubTab); }, [activeSubTab]);
const [geofences, setGeofences] = useState<any[]>([]);
const [analytics, setAnalytics] = useState<any>(null);
const [loading, setLoading] = useState(false);
const { isLoading: authLoading, isAuthenticated } = useAuth();
const [newGeofence, setNewGeofence] = useState({
name: '',
latitude: 0,
longitude: 0,
radius: 500,
type: 'tourist-zone',
description: '',
entryMessage: '',
exitMessage: ''
});
const [locationTest, setLocationTest] = useState({
latitude: 18.4735,
longitude: -69.8849,
activity: 'walking'
});
const [routeTest, setRouteTest] = useState({
startLat: 18.4735,
startLng: -69.8849,
endLat: 18.4620,
endLng: -69.9071,
includeAttractions: true
});
const [panicTest, setPanicTest] = useState({
latitude: 18.4740,
longitude: -69.8855,
message: 'Necesito ayuda'
});
// Maps & API key
const { toast } = useToast();
const [mapsApiKey, setMapsApiKey] = useState<string>(() => localStorage.getItem('googleMapsApiKey') || '');
const [showApiKeyInput, setShowApiKeyInput] = useState(!localStorage.getItem('googleMapsApiKey'));
const [googleObj, setGoogleObj] = useState<any>(null);
const testMapRef = React.useRef<HTMLDivElement>(null);
const navMapRef = React.useRef<HTMLDivElement>(null);
const [testMap, setTestMap] = useState<any>(null);
const [navMap, setNavMap] = useState<any>(null);
const [directionsRenderer, setDirectionsRenderer] = useState<any>(null);
const loadGoogle = async () => {
if (googleObj) return googleObj;
// Reusar instancia existente si ya está cargada
if (typeof window !== 'undefined' && (window as any).google?.maps) {
setGoogleObj((window as any).google);
return (window as any).google;
}
// Si el script ya existe (posiblemente con otra API key), esperar su carga y reutilizarlo
const existingScript = typeof document !== 'undefined'
? (document.getElementById('__googleMapsScriptId') as HTMLScriptElement | null)
: null;
if (existingScript) {
await new Promise<void>((resolve, reject) => {
if ((window as any).google?.maps) return resolve();
existingScript.addEventListener('load', () => resolve(), { once: true });
existingScript.addEventListener('error', () => reject(new Error('Fallo al cargar Google Maps script')), { once: true });
});
setGoogleObj((window as any).google);
return (window as any).google;
}
if (!mapsApiKey) {
toast({
title: 'Google Maps no configurado',
description: 'Necesitas configurar una API key de Google Maps. Ingresa tu clave abajo.'
});
setShowApiKeyInput(true);
return null;
}
try {
const loader = new Loader({ apiKey: mapsApiKey, version: 'weekly', libraries: ['places'], id: '__googleMapsScriptIdKaribeo' });
const g = await loader.load();
setGoogleObj(g);
return g;
} catch (err: any) {
console.error('Error cargando Google Maps:', err);
const msg = err?.message || '';
if (msg.includes('RefererNotAllowedMapError')) {
toast({
title: 'Dominio no autorizado',
description: `Autoriza el dominio ${window.location.origin} en Google Cloud Console (restricción por HTTP Referrer).`,
variant: 'destructive'
});
setShowApiKeyInput(true);
} else if (msg.includes('Loader must not be called again with different options')) {
// Si ya existe una instancia con otra configuración, reusar google si está disponible
if ((window as any).google?.maps) {
setGoogleObj((window as any).google);
return (window as any).google;
}
} else {
toast({
title: 'Error con Google Maps',
description: msg || 'Error desconocido con la API de Google Maps.'
});
}
return null;
}
};
const handleSaveApiKey = () => {
if (mapsApiKey.trim()) {
localStorage.setItem('googleMapsApiKey', mapsApiKey.trim());
setShowApiKeyInput(false);
toast({ title: 'API Key guardada', description: 'Intenta usar los mapas nuevamente.' });
}
};
// Espera hasta que el elemento esté en el DOM y tenga tamaño visible
const waitForVisible = (el: HTMLElement | null, maxTries = 60): Promise<void> => {
return new Promise((resolve) => {
let tries = 0;
const check = () => {
if (el && el.isConnected && el.clientHeight > 0 && el.clientWidth > 0) {
resolve();
return;
}
if (tries++ >= maxTries) {
resolve();
return;
}
requestAnimationFrame(check);
};
check();
});
};
const initTestMap = async () => {
const g = await loadGoogle();
if (!g) return;
if (testMapRef.current && testMapRef.current instanceof HTMLElement && !testMap) {
await waitForVisible(testMapRef.current);
try {
const m = new g.maps.Map(testMapRef.current as HTMLElement, {
center: { lat: locationTest.latitude, lng: locationTest.longitude },
zoom: 13,
});
setTestMap(m);
} catch (e) {
console.warn('Error inicializando Test Map:', e);
}
}
};
const initNavMap = async () => {
const g = await loadGoogle();
if (!g) return;
if (navMapRef.current && navMapRef.current instanceof HTMLElement && !navMap) {
await waitForVisible(navMapRef.current);
try {
const m = new g.maps.Map(navMapRef.current as HTMLElement, { center: { lat: routeTest.startLat, lng: routeTest.startLng }, zoom: 12 });
setNavMap(m);
const dr = new g.maps.DirectionsRenderer({ map: m });
setDirectionsRenderer(dr);
} catch (e) {
console.warn('Error inicializando Nav Map:', e);
}
}
};
useEffect(() => {
if (activeTab === 'testing') initTestMap();
if (activeTab === 'navigation') initNavMap();
// Reintentar si se guarda la API key
}, [activeTab, mapsApiKey]);
// Load geofences
const loadGeofences = async () => {
try {
const data = await apiClient.get('/geolocation/geofences');
setGeofences(Array.isArray(data) ? data : []);
} catch (error) {
console.error('Error loading geofences:', error);
}
};
// Load analytics
const loadAnalytics = async () => {
try {
const data = await apiClient.get('/geolocation/analytics?timeframe=7d');
setAnalytics(data);
} catch (error) {
console.error('Error loading analytics:', error);
}
};
useEffect(() => {
const hasToken = !!(typeof window !== 'undefined' && (localStorage.getItem('karibeo-token') || localStorage.getItem('karibeo_token')));
if (!authLoading && (isAuthenticated || hasToken)) {
loadGeofences();
loadAnalytics();
}
}, [authLoading, isAuthenticated]);
// Create geofence
const handleCreateGeofence = async () => {
try {
await apiClient.post('/geolocation/geofences', {
name: newGeofence.name,
latitude: newGeofence.latitude,
longitude: newGeofence.longitude,
radius: newGeofence.radius,
type: newGeofence.type,
description: newGeofence.description,
entryMessage: newGeofence.entryMessage,
exitMessage: newGeofence.exitMessage
});
setNewGeofence({
name: '', latitude: 0, longitude: 0, radius: 500, type: 'tourist-zone',
description: '', entryMessage: '', exitMessage: ''
});
loadGeofences();
toast({ title: 'Geofence creado', description: 'Se creó correctamente.' });
} catch (error: any) {
console.error('Error creating geofence:', error);
toast({ title: 'Error', description: error?.message || 'No se pudo crear el geofence.' });
}
};
const handleDeleteGeofence = async (id: string) => {
let success = false;
try {
await apiClient.delete(`/geolocation/geofences/${id}`);
success = true;
} catch (error: any) {
// Many backends don't implement DELETE; try to soft-deactivate
try {
await apiClient.patch(`/geolocation/geofences/${id}`, { isActive: false });
success = true;
} catch (e2) {
console.warn('Delete/patch not supported, removing locally:', id, e2);
// Final fallback: remove locally to avoid UX dead-ends
setGeofences((prev) => prev.filter((g) => g.id !== id));
toast({ title: 'Eliminado localmente', description: 'El backend no permite eliminar este geofence. Se ocultó en la interfaz.' });
return;
}
}
if (success) {
await loadGeofences();
toast({ title: 'Geofence eliminado', description: 'Se eliminó o desactivó correctamente.' });
}
};
// Test location update
const handleLocationUpdate = async () => {
try {
const result = await apiClient.post('/geolocation/location/update', {
latitude: locationTest.latitude,
longitude: locationTest.longitude,
accuracy: 10,
speed: 5,
activity: locationTest.activity
});
console.log('Location update result:', result);
toast({ title: 'Ubicación actualizada', description: 'Se envió la ubicación de prueba.' });
// Center marker on test map
if (testMap && googleObj) {
const marker = new googleObj.maps.Marker({ position: { lat: locationTest.latitude, lng: locationTest.longitude }, map: testMap });
testMap.setCenter(marker.getPosition());
}
} catch (error: any) {
console.error('Error updating location:', error);
toast({ title: 'Error', description: error?.message || 'No se pudo actualizar la ubicación.' });
}
};
// Test geofence check
const handleGeofenceCheck = async () => {
try {
const result = await apiClient.post('/geolocation/geofences/check', {
latitude: locationTest.latitude,
longitude: locationTest.longitude
});
console.log('Geofence check result:', result);
const matched = (result as any)?.geofences?.length || 0;
toast({ title: 'Verificación de geofences', description: `Coincidencias: ${matched}` });
} catch (error: any) {
console.error('Error checking geofences:', error);
toast({ title: 'Error', description: error?.message || 'No se pudo verificar geofences.' });
}
};
// Test smart suggestions
const handleSmartSuggestions = async () => {
try {
const result = await apiClient.post('/geolocation/suggestions/smart', {
latitude: locationTest.latitude,
longitude: locationTest.longitude,
activity: locationTest.activity
});
console.log('Smart suggestions result:', result);
toast({ title: 'Sugerencias IA', description: 'Sugerencias recibidas, revisa la consola.' });
} catch (error: any) {
console.error('Error getting smart suggestions:', error);
toast({ title: 'Error', description: error?.message || 'No se pudo obtener sugerencias.' });
}
};
// Test panic button
const handlePanicButton = async () => {
try {
// Enviar alerta a POLITUR (backend de seguridad)
const result = await apiClient.post('/security/emergency-alerts', {
type: 'emergency',
channel: 'politur',
location: { latitude: panicTest.latitude, longitude: panicTest.longitude },
message: panicTest.message || 'Alerta de emergencia',
});
console.log('Panic button result:', result);
toast({ title: 'Emergencia', description: 'Alerta enviada a POLITUR.' });
} catch (error: any) {
console.error('Error triggering panic button:', error);
toast({ title: 'Error', description: error?.message || 'No se pudo activar el botón de pánico.' });
}
};
// Test route planning
const handleRoutePlanning = async () => {
try {
const result = await apiClient.post('/geolocation/navigation/route', {
startLat: routeTest.startLat,
startLng: routeTest.startLng,
endLat: routeTest.endLat,
endLng: routeTest.endLng,
includeAttractions: routeTest.includeAttractions
});
console.log('Route planning result:', result);
toast({ title: 'Ruta planificada', description: 'Ruta generada correctamente.' });
// Draw route on Google Maps
if (navMap && googleObj) {
const ds = new googleObj.maps.DirectionsService();
const dr = directionsRenderer || new googleObj.maps.DirectionsRenderer({ map: navMap });
setDirectionsRenderer(dr);
const req = {
origin: { lat: routeTest.startLat, lng: routeTest.startLng },
destination: { lat: routeTest.endLat, lng: routeTest.endLng },
travelMode: googleObj.maps.TravelMode.DRIVING,
} as any;
// Usar callback para máxima compatibilidad
ds.route(req, (res: any, status: any) => {
if (status === 'OK' || status === 200) {
dr.setDirections(res);
} else {
console.warn('DirectionsService status:', status);
toast({ title: 'Ruta no disponible', description: `Google Directions status: ${status}` });
}
});
} else {
toast({ title: 'Mapa no inicializado', description: 'Abre la pestaña Navegación para inicializar el mapa antes de planificar.' });
}
} catch (error: any) {
console.error('Error planning route:', error);
toast({ title: 'Error', description: error?.message || 'No se pudo planificar la ruta.' });
}
};
// Test nearby attractions
const handleNearbyAttractions = async () => {
try {
const result = await apiClient.get(`/geolocation/nearby/attractions?latitude=${locationTest.latitude}&longitude=${locationTest.longitude}&radius=500`);
console.log('Nearby attractions result:', result);
const count = Array.isArray(result) ? result.length : ((result as any)?.length || (result as any)?.data?.length || 0);
toast({ title: 'Atracciones cercanas', description: `Resultados: ${count}` });
} catch (error: any) {
console.error('Error getting nearby attractions:', error);
toast({ title: 'Error', description: error?.message || 'No se pudo obtener atracciones.' });
}
};
// Test safety zones
const handleSafetyZones = async () => {
try {
const result = await apiClient.get(`/geolocation/safety/zones?latitude=${locationTest.latitude}&longitude=${locationTest.longitude}`);
console.log('Safety zones result:', result);
toast({ title: 'Zonas de seguridad', description: 'Consulta completada. Revisa la consola.' });
} catch (error: any) {
console.error('Error getting safety zones:', error);
toast({ title: 'Error', description: error?.message || 'No se pudo obtener zonas de seguridad.' });
}
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold text-gray-900">Gestión de Geolocalización</h2>
<div className="flex gap-2">
<Badge variant="secondary" className="text-sm">
{geofences.length} Geofences
</Badge>
<Badge variant="secondary" className="text-sm">
{analytics?.totalUsers || 0} Usuarios Activos
</Badge>
</div>
</div>
{/* API Key Configuration */}
{showApiKeyInput && (
<Card className="border-orange-200 bg-orange-50">
<CardHeader>
<CardTitle className="text-orange-800">Configurar Google Maps API Key</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-orange-700">
Para usar los mapas, necesitas una API key de Google Maps.
<a href="https://console.cloud.google.com/google/maps-apis/credentials" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline ml-1">
Consigue una aquí
</a>
</p>
<div className="flex gap-2">
<Input
placeholder="Ingresa tu Google Maps API Key"
value={mapsApiKey}
onChange={(e) => setMapsApiKey(e.target.value)}
className="flex-1"
/>
<Button onClick={handleSaveApiKey} disabled={!mapsApiKey.trim()}>
Guardar
</Button>
</div>
<p className="text-xs text-orange-600">
Asegúrate de autorizar el dominio: <code className="bg-orange-100 px-1 rounded">{window.location.origin}</code> en tu Google Cloud Console
</p>
</CardContent>
</Card>
)}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsContent value="geofences" className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">Gestión de Geofences</h3>
<Dialog>
<DialogTrigger asChild>
<Button className="flex items-center gap-2">
<Plus className="w-4 h-4" />
Nuevo Geofence
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Crear Nuevo Geofence</DialogTitle>
</DialogHeader>
<div className="grid gap-4">
<div>
<Label htmlFor="geo-name">Nombre</Label>
<Input
id="geo-name"
value={newGeofence.name}
onChange={(e) => setNewGeofence({ ...newGeofence, name: e.target.value })}
placeholder="Ej: Zona Colonial"
/>
</div>
<div>
<Label htmlFor="geo-type">Tipo</Label>
<Select value={newGeofence.type} onValueChange={(value) => setNewGeofence({ ...newGeofence, type: value })}>
<SelectTrigger>
<SelectValue placeholder="Selecciona el tipo" />
</SelectTrigger>
<SelectContent>
<SelectItem value="tourist-zone">Zona Turística</SelectItem>
<SelectItem value="attraction">Atracción</SelectItem>
<SelectItem value="safety-alert">Alerta de Seguridad</SelectItem>
<SelectItem value="restricted">Zona Restringida</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="geo-lat">Latitud</Label>
<Input
id="geo-lat"
type="number"
step="any"
value={newGeofence.latitude}
onChange={(e) => setNewGeofence({ ...newGeofence, latitude: parseFloat(e.target.value) || 0 })}
placeholder="18.4735"
/>
</div>
<div>
<Label htmlFor="geo-lng">Longitud</Label>
<Input
id="geo-lng"
type="number"
step="any"
value={newGeofence.longitude}
onChange={(e) => setNewGeofence({ ...newGeofence, longitude: parseFloat(e.target.value) || 0 })}
placeholder="-69.8849"
/>
</div>
</div>
<div>
<Label htmlFor="geo-radius">Radio (metros)</Label>
<Input
id="geo-radius"
type="number"
value={newGeofence.radius}
onChange={(e) => setNewGeofence({ ...newGeofence, radius: parseInt(e.target.value) || 500 })}
placeholder="500"
/>
</div>
<div>
<Label htmlFor="geo-description">Descripción</Label>
<Textarea
id="geo-description"
value={newGeofence.description}
onChange={(e) => setNewGeofence({ ...newGeofence, description: e.target.value })}
placeholder="Descripción del área..."
rows={2}
/>
</div>
<div>
<Label htmlFor="geo-entry">Mensaje de Entrada</Label>
<Input
id="geo-entry"
value={newGeofence.entryMessage}
onChange={(e) => setNewGeofence({ ...newGeofence, entryMessage: e.target.value })}
placeholder="Mensaje al entrar al área"
/>
</div>
<div>
<Label htmlFor="geo-exit">Mensaje de Salida</Label>
<Input
id="geo-exit"
value={newGeofence.exitMessage}
onChange={(e) => setNewGeofence({ ...newGeofence, exitMessage: e.target.value })}
placeholder="Mensaje al salir del área"
/>
</div>
<Button onClick={handleCreateGeofence} className="w-full">
Crear Geofence
</Button>
</div>
</DialogContent>
</Dialog>
</div>
<div className="grid gap-4">
{geofences.map((geofence) => (
<Card key={geofence.id} className="hover:shadow-lg transition-shadow">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div className="flex items-center space-x-4">
<div className="w-16 h-16 rounded-lg bg-primary/10 flex items-center justify-center">
<Target className="w-8 h-8 text-primary" />
</div>
<div>
<CardTitle className="text-lg">{geofence.name}</CardTitle>
<p className="text-sm text-muted-foreground">{geofence.type}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<Badge variant="outline" className="flex items-center gap-1">
<Activity className="w-3 h-3" />
{geofence.entryCount} entradas
</Badge>
<div className="flex gap-1">
<Button variant="outline" size="sm">
<Edit className="w-4 h-4" />
</Button>
<Button variant="outline" size="sm" onClick={() => handleDeleteGeofence(geofence.id)}>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-gray-600 mb-3">{geofence.description}</p>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary" className="text-xs">
Radio: {geofence.radius}m
</Badge>
<Badge variant={geofence.isActive ? "default" : "secondary"} className="text-xs">
{geofence.isActive ? 'Activo' : 'Inactivo'}
</Badge>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
<TabsContent value="analytics" className="space-y-4">
<h3 className="text-lg font-semibold">Analíticas de Geolocalización</h3>
{analytics && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Usuarios Totales</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{analytics.totalUsers}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Usuarios Activos</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{analytics.activeUsers}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Zonas Populares</CardTitle>
<MapPin className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{analytics.popularZones?.length || 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Geofences</CardTitle>
<Target className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{analytics.geofenceStats?.length || 0}</div>
</CardContent>
</Card>
</div>
)}
{/* Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Área: Usuarios activos últimos 7 días */}
<Card>
<CardHeader>
<CardTitle>Usuarios activos (últimos 7 días)</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
{(() => {
const labels = analytics?.dailyLabels || ['Lun','Mar','Mié','Jue','Vie','Sáb','Dom'];
const values = (analytics?.dailyActiveUsers || [12,18,14,22,17,25,21]).map((n: any) => Number(n) || 0);
const data = labels.map((name: string, i: number) => ({ name, value: values[i] ?? 0 }));
return (
<AreaChart data={data} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="colorValue" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#F84525" stopOpacity={0.4}/>
<stop offset="95%" stopColor="#F84525" stopOpacity={0}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis allowDecimals={false} />
<Tooltip />
<Legend />
<Area type="monotone" dataKey="value" stroke="#F84525" fill="url(#colorValue)" name="Usuarios activos" />
</AreaChart>
);
})()}
</ResponsiveContainer>
</div>
</CardContent>
</Card>
{/* Barras: Entradas por geofence */}
<Card>
<CardHeader>
<CardTitle>Entradas por geofence</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
{(() => {
const stats = analytics?.geofenceStats || [];
const labels = (stats.length ? stats.map((s: any) => s.name) : ['A','B','C','D']);
const values = (stats.length ? stats.map((s: any) => Number(s.entries) || 0) : [10,8,6,12]);
const data = labels.map((name: string, i: number) => ({ name, entries: values[i] ?? 0 }));
return (
<RBarChart data={data} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis allowDecimals={false} />
<Tooltip />
<Legend />
<Bar dataKey="entries" fill="#F84525" name="Entradas" />
</RBarChart>
);
})()}
</ResponsiveContainer>
</div>
</CardContent>
</Card>
</div>
{analytics?.geofenceStats && (
<Card>
<CardHeader>
<CardTitle>Estadísticas por Geofence</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{analytics.geofenceStats.map((stat: any, index: number) => (
<div key={index} className="flex justify-between items-center">
<span className="text-sm font-medium">{stat.name}</span>
<div className="flex items-center gap-2">
<Badge variant="outline">{stat.type}</Badge>
<span className="text-sm text-muted-foreground">{stat.entries} entradas</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
</TabsContent>
<TabsContent value="testing" className="space-y-4">
<h3 className="text-lg font-semibold">Pruebas de Funcionalidad</h3>
<Card>
<CardHeader>
<CardTitle>Prueba de Ubicación (requiere Maps)</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div>
<Label>Latitud</Label>
<Input
type="number"
step="any"
value={locationTest.latitude}
onChange={(e) => setLocationTest({ ...locationTest, latitude: parseFloat(e.target.value) })}
/>
</div>
<div>
<Label>Longitud</Label>
<Input
type="number"
step="any"
value={locationTest.longitude}
onChange={(e) => setLocationTest({ ...locationTest, longitude: parseFloat(e.target.value) })}
/>
</div>
<div>
<Label>Actividad</Label>
<Select value={locationTest.activity} onValueChange={(value) => setLocationTest({ ...locationTest, activity: value })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="walking">Caminando</SelectItem>
<SelectItem value="driving">Conduciendo</SelectItem>
<SelectItem value="dining">Comiendo</SelectItem>
<SelectItem value="shopping">Comprando</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex gap-2">
<Button onClick={handleLocationUpdate}>Actualizar Ubicación</Button>
<Button onClick={handleGeofenceCheck}>Verificar Geofences</Button>
<Button onClick={handleSmartSuggestions}>Sugerencias IA</Button>
<Button onClick={handleNearbyAttractions}>Atracciones Cercanas</Button>
<Button onClick={handleSafetyZones}>Zonas de Seguridad</Button>
</div>
<div className="mt-4">
<div ref={testMapRef} className="w-full h-64 rounded-lg border relative">
{!mapsApiKey && (
<div className="absolute inset-0 flex items-center justify-center text-center p-4">
<p className="text-sm text-muted-foreground">Mapa deshabilitado. Configura una Google Maps API key restringida por dominio y autoriza este dominio en Google Cloud Console.</p>
</div>
)}
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="emergency" className="space-y-4">
<h3 className="text-lg font-semibold">Sistema de Emergencias</h3>
<Card>
<CardHeader>
<CardTitle>Prueba de Botón de Pánico</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Latitud</Label>
<Input
type="number"
step="any"
value={panicTest.latitude}
onChange={(e) => setPanicTest({ ...panicTest, latitude: parseFloat(e.target.value) })}
/>
</div>
<div>
<Label>Longitud</Label>
<Input
type="number"
step="any"
value={panicTest.longitude}
onChange={(e) => setPanicTest({ ...panicTest, longitude: parseFloat(e.target.value) })}
/>
</div>
</div>
<div>
<Label>Mensaje de Emergencia</Label>
<Textarea
value={panicTest.message}
onChange={(e) => setPanicTest({ ...panicTest, message: e.target.value })}
placeholder="Describe la emergencia..."
/>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" className="flex items-center gap-2">
<AlertTriangle className="w-4 h-4" />
Activar Botón de Pánico
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirmar activación</AlertDialogTitle>
<AlertDialogDescription>
Se notificará a los servicios de emergencia con tu ubicación.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction onClick={handlePanicButton}>Activar</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="navigation" className="space-y-4">
<h3 className="text-lg font-semibold">Sistema de Navegación</h3>
<Card>
<CardHeader>
<CardTitle>Planificación de Rutas</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Latitud Origen</Label>
<Input
type="number"
step="any"
value={routeTest.startLat}
onChange={(e) => setRouteTest({ ...routeTest, startLat: parseFloat(e.target.value) })}
/>
</div>
<div>
<Label>Longitud Origen</Label>
<Input
type="number"
step="any"
value={routeTest.startLng}
onChange={(e) => setRouteTest({ ...routeTest, startLng: parseFloat(e.target.value) })}
/>
</div>
<div>
<Label>Latitud Destino</Label>
<Input
type="number"
step="any"
value={routeTest.endLat}
onChange={(e) => setRouteTest({ ...routeTest, endLat: parseFloat(e.target.value) })}
/>
</div>
<div>
<Label>Longitud Destino</Label>
<Input
type="number"
step="any"
value={routeTest.endLng}
onChange={(e) => setRouteTest({ ...routeTest, endLng: parseFloat(e.target.value) })}
/>
</div>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
checked={routeTest.includeAttractions}
onChange={(e) => setRouteTest({ ...routeTest, includeAttractions: e.target.checked })}
/>
<Label>Incluir atracciones en la ruta</Label>
</div>
<Button onClick={handleRoutePlanning} className="flex items-center gap-2">
<Navigation className="w-4 h-4" />
Planificar Ruta
</Button>
<div className="mt-4">
<div ref={navMapRef} className="w-full h-72 rounded-lg border relative">
{!mapsApiKey && (
<div className="absolute inset-0 flex items-center justify-center text-center p-4">
<p className="text-sm text-muted-foreground">Mapa deshabilitado. Configura una Google Maps API key restringida por dominio y autoriza este dominio en Google Cloud Console.</p>
</div>
)}
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
};
export default GeolocationTab;

View File

@@ -0,0 +1,135 @@
import React from 'react';
import {
Users,
DollarSign,
Calendar,
Activity,
TrendingUp,
AlertTriangle,
Bell,
CheckCircle,
Clock,
BarChart3
} from 'lucide-react';
interface OverviewTabProps {
stats: any;
users: any[];
incidents: any[];
isAdmin: boolean;
isSuperAdmin: boolean;
refreshData: () => void;
}
const OverviewTab: React.FC<OverviewTabProps> = ({ stats, users, incidents, refreshData }) => {
return (
<div className="space-y-8">
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center">
<div className="p-2 bg-blue-100 rounded-lg">
<Users className="w-6 h-6 text-blue-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Total Usuarios</p>
<p className="text-2xl font-semibold text-gray-900">{stats?.totalUsers || 0}</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center">
<div className="p-2 bg-green-100 rounded-lg">
<DollarSign className="w-6 h-6 text-green-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Ingresos Totales</p>
<p className="text-2xl font-semibold text-gray-900">${stats?.totalRevenue || 0}</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center">
<div className="p-2 bg-purple-100 rounded-lg">
<Calendar className="w-6 h-6 text-purple-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Reservas Totales</p>
<p className="text-2xl font-semibold text-gray-900">{stats?.totalBookings || 0}</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center">
<div className="p-2 bg-orange-100 rounded-lg">
<Activity className="w-6 h-6 text-orange-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Servicios Activos</p>
<p className="text-2xl font-semibold text-gray-900">{stats?.activeServices || 0}</p>
</div>
</div>
</div>
</div>
{/* Alert Cards */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">Verificaciones Pendientes</h3>
<span className="bg-yellow-100 text-yellow-800 text-xs font-medium px-2.5 py-0.5 rounded">
{stats?.pendingVerifications || 0} pendientes
</span>
</div>
<p className="text-gray-600">Proveedores de servicios esperando aprobación</p>
<button className="mt-4 bg-orange-500 text-white px-4 py-2 rounded-lg hover:bg-orange-600">
Revisar Verificaciones
</button>
</div>
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">Alertas de Emergencia</h3>
<span className="bg-red-100 text-red-800 text-xs font-medium px-2.5 py-0.5 rounded">
{stats?.emergencyAlerts || 0} activas
</span>
</div>
<p className="text-gray-600">Situaciones que requieren atención inmediata</p>
<button className="mt-4 bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600">
Ver Emergencias
</button>
</div>
</div>
{/* Recent Activity */}
<div className="bg-white rounded-lg shadow">
<div className="px-6 py-4 border-b">
<h3 className="text-lg font-semibold text-gray-900">Actividad Reciente</h3>
</div>
<div className="p-6">
<div className="space-y-4">
{users.slice(0, 5).map((user, index) => (
<div key={user.id || index} className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gray-200 rounded-full flex items-center justify-center">
<Users className="w-4 h-4 text-gray-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-900">{user.name}</p>
<p className="text-xs text-gray-500">Nuevo usuario {(typeof user.role === 'string' ? user.role : user.role?.name) || user.type}</p>
</div>
</div>
<span className="text-xs text-gray-500">Hace 2 horas</span>
</div>
))}
</div>
</div>
</div>
</div>
);
};
export default OverviewTab;

View File

@@ -0,0 +1,238 @@
import React, { useState, useEffect } from 'react';
import {
Building,
Hotel,
UtensilsCrossed,
Car,
Store,
Users,
MapPin,
Star,
Phone,
Mail,
Plus,
Search,
Edit,
Trash2,
Eye,
CheckCircle,
XCircle,
Clock,
Map,
Shield
} from 'lucide-react';
import { useAdminData } from '@/hooks/useAdminData';
interface ServicesTabProps {
establishments: any[];
isAdmin: boolean;
isSuperAdmin: boolean;
loadEstablishments: (type?: string) => void;
}
const ServicesTab: React.FC<ServicesTabProps> = ({
establishments,
isAdmin,
loadEstablishments
}) => {
const { updateEstablishment, deleteEstablishment } = useAdminData();
const [activeFilter, setActiveFilter] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
loadEstablishments(activeFilter === 'all' ? undefined : activeFilter);
}, [activeFilter]);
const serviceTypes = [
{ id: 'all', label: 'Todos', icon: Building, count: establishments.length },
{ id: 'hotel', label: 'Hoteles', icon: Hotel, count: establishments.filter(e => e.type === 'hotel').length },
{ id: 'restaurant', label: 'Restaurantes', icon: UtensilsCrossed, count: establishments.filter(e => e.type === 'restaurant').length },
{ id: 'taxi', label: 'Taxis', icon: Car, count: establishments.filter(e => e.type === 'taxi').length },
{ id: 'shop', label: 'Tiendas', icon: Store, count: establishments.filter(e => e.type === 'shop').length },
{ id: 'guide', label: 'Guías Turísticos', icon: Map, count: establishments.filter(e => e.type === 'guide').length },
{ id: 'security', label: 'Politur', icon: Shield, count: establishments.filter(e => e.type === 'security').length },
];
const filteredEstablishments = establishments.filter(establishment =>
establishment.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
establishment.description?.toLowerCase().includes(searchTerm.toLowerCase())
);
const handleUpdateStatus = async (id: string, status: 'active' | 'suspended' | 'pending') => {
const result = await updateEstablishment(id, { status });
if (result.success) {
loadEstablishments(activeFilter === 'all' ? undefined : activeFilter);
}
};
const handleDelete = async (id: string) => {
if (window.confirm('¿Estás seguro de que quieres eliminar este establecimiento?')) {
const result = await deleteEstablishment(id);
if (result.success) {
loadEstablishments(activeFilter === 'all' ? undefined : activeFilter);
}
}
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold text-gray-900">Proveedores de Servicios</h2>
<div className="flex space-x-2">
<button className="bg-orange-500 text-white px-4 py-2 rounded-lg hover:bg-orange-600 flex items-center space-x-2">
<Plus className="w-4 h-4" />
<span>Aprobar Pendientes</span>
</button>
</div>
</div>
{/* Service Type Filters */}
<div className="grid services-grid-mobile md:grid-cols-4 lg:grid-cols-7 gap-4">
{serviceTypes.map((type) => {
const Icon = type.icon;
return (
<button
key={type.id}
onClick={() => setActiveFilter(type.id)}
className={`p-4 rounded-lg border-2 transition-colors ${
activeFilter === type.id
? 'border-orange-500 bg-orange-50 text-orange-700'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<div className="flex items-center justify-center mb-2">
<Icon className="w-6 h-6" />
</div>
<div className="text-sm font-medium">{type.label}</div>
<div className="text-xs text-gray-500">{type.count} activos</div>
</button>
);
})}
</div>
{/* Search */}
<div className="bg-white rounded-lg shadow p-6">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
type="text"
placeholder="Buscar establecimientos..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
</div>
</div>
{/* Establishments Grid */}
<div className="grid mobile-grid md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
{filteredEstablishments.map((establishment) => (
<div key={establishment.id} className="bg-white rounded-lg shadow-md overflow-hidden">
{/* Image */}
<div className="h-48 bg-gray-200 relative">
<img
src={establishment.imageUrl || '/api/placeholder/400/300'}
alt={establishment.name}
className="w-full h-full object-cover"
/>
<div className="absolute top-2 right-2">
<span className={`px-2 py-1 text-xs font-semibold rounded-full ${
establishment.status === 'active' ? 'bg-green-100 text-green-800' :
establishment.status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
'bg-red-100 text-red-800'
}`}>
{establishment.status}
</span>
</div>
</div>
{/* Content */}
<div className="p-4">
<div className="flex items-start justify-between mb-2">
<h3 className="text-lg font-semibold text-gray-900 truncate">
{establishment.name}
</h3>
<div className="flex items-center space-x-1">
<Star className="w-4 h-4 text-yellow-400 fill-current" />
<span className="text-sm text-gray-600">{establishment.rating || 0}</span>
</div>
</div>
<p className="text-sm text-gray-600 mb-3 line-clamp-2">
{establishment.description}
</p>
<div className="space-y-2 mb-4">
<div className="flex items-center space-x-2 text-sm text-gray-500">
<MapPin className="w-4 h-4" />
<span className="truncate">{establishment.location?.address}</span>
</div>
{establishment.owner && (
<div className="flex items-center space-x-2 text-sm text-gray-500">
<Users className="w-4 h-4" />
<span>{establishment.owner.name}</span>
</div>
)}
</div>
{/* Actions */}
<div className="flex items-center justify-between">
<div className="flex space-x-2">
<button
className="text-blue-600 hover:text-blue-900"
title="Ver detalles"
>
<Eye className="w-4 h-4" />
</button>
<button
className="text-green-600 hover:text-green-900"
title="Editar"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(establishment.id)}
className="text-red-600 hover:text-red-900"
title="Eliminar"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
{establishment.status === 'pending' && (
<div className="flex space-x-1">
<button
onClick={() => handleUpdateStatus(establishment.id, 'active')}
className="p-1 text-green-600 hover:text-green-900"
title="Aprobar"
>
<CheckCircle className="w-4 h-4" />
</button>
<button
onClick={() => handleUpdateStatus(establishment.id, 'suspended')}
className="p-1 text-red-600 hover:text-red-900"
title="Rechazar"
>
<XCircle className="w-4 h-4" />
</button>
</div>
)}
</div>
</div>
</div>
))}
</div>
{filteredEstablishments.length === 0 && (
<div className="text-center py-12">
<Building className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No hay establecimientos</h3>
<p className="text-gray-500">No se encontraron establecimientos que coincidan con los filtros aplicados.</p>
</div>
)}
</div>
);
};
export default ServicesTab;

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { Phone, MessageSquare, HeadphonesIcon } from 'lucide-react';
interface SupportTabProps {
isAdmin: boolean;
isSuperAdmin: boolean;
}
const SupportTab: React.FC<SupportTabProps> = ({ }) => {
return (
<div className="space-y-6">
<h2 className="text-xl font-semibold text-gray-900">Centro de Soporte y Tickets</h2>
<div className="bg-white rounded-lg shadow p-8 text-center">
<HeadphonesIcon className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Centro de Soporte y Tickets
</h3>
<p className="text-gray-600 mb-4">
Esta sección está en desarrollo y se implementará según las especificaciones del informe.
</p>
<div className="text-sm text-gray-500">
Funcionalidades pendientes:
<ul className="mt-2 space-y-1">
<li> Sistema de tickets de soporte</li>
<li> Chat en vivo</li>
<li> Base de conocimientos</li>
<li> Métricas de soporte</li>
<li> Escalación automática</li>
</ul>
</div>
</div>
</div>
);
};
export default SupportTab;

View File

@@ -0,0 +1,408 @@
import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import {
Search,
Plus,
Edit,
Trash2,
Eye,
UserPlus,
Shield,
Users,
Car,
UtensilsCrossed,
Hotel,
MapPin,
CheckCircle,
XCircle,
AlertTriangle
} from 'lucide-react';
import { toast } from 'sonner';
interface UsersTabProps {
users: any[];
isAdmin: boolean;
isSuperAdmin: boolean;
loadUsers: (page?: number, limit?: number, role?: string) => void;
createUser?: (userData: any) => Promise<{ success: boolean; error?: string }>;
updateUser?: (id: string, userData: any) => Promise<{ success: boolean; error?: string }>;
deleteUser?: (id: string) => Promise<{ success: boolean; error?: string }>;
}
const UsersTab: React.FC<UsersTabProps> = ({
users,
isAdmin,
isSuperAdmin,
loadUsers
}) => {
const [searchTerm, setSearchTerm] = useState('');
const [roleFilter, setRoleFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
const [showCreateModal, setShowCreateModal] = useState(false);
const [mockUsers, setMockUsers] = useState([
{
id: '1',
name: 'Super Admin',
email: 'superadmin@karibeo.com',
role: 'super_admin',
status: 'active',
verified: true,
lastLogin: '2024-01-15 10:30:00',
avatar: '/api/placeholder/40/40',
createdAt: '2023-01-01'
},
{
id: '2',
name: 'Admin User',
email: 'admin@karibeo.com',
role: 'admin',
status: 'active',
verified: true,
lastLogin: '2024-01-14 15:45:00',
avatar: '/api/placeholder/40/40',
createdAt: '2023-02-01'
},
{
id: '3',
name: 'Juan Pérez',
email: 'juan.perez@karibeo.com',
role: 'tourist',
status: 'active',
verified: true,
lastLogin: '2024-01-13 09:15:00',
avatar: '/api/placeholder/40/40',
createdAt: '2023-03-15'
}
]);
useEffect(() => {
loadUsers(1, 50, roleFilter === 'all' ? undefined : roleFilter);
}, [roleFilter]);
const handleCreateUser = async (formData: any) => {
const newUser = {
id: (mockUsers.length + 1).toString(),
name: formData.name,
email: formData.email,
role: formData.role,
status: 'active',
verified: false,
lastLogin: 'Nunca',
avatar: '/api/placeholder/40/40',
createdAt: new Date().toISOString().split('T')[0]
};
setMockUsers([...mockUsers, newUser]);
setShowCreateModal(false);
toast.success('Usuario creado exitosamente');
};
const handleDeleteUser = async (userId: string) => {
setMockUsers(mockUsers.filter(user => user.id !== userId));
toast.success('Usuario eliminado exitosamente');
};
const getRoleName = (role: any): string => (typeof role === 'string' ? role : role?.name || '');
const filteredUsers = (users.length > 0 ? users : mockUsers).filter(user => {
const matchesSearch = user.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email?.toLowerCase().includes(searchTerm.toLowerCase());
const normalizedRole = getRoleName(user.role);
const matchesRole = roleFilter === 'all' || normalizedRole === roleFilter;
const matchesStatus = statusFilter === 'all' || user.status === statusFilter;
return matchesSearch && matchesRole && matchesStatus;
});
const getRoleIcon = (role: string) => {
const iconMap = {
'super_admin': Shield,
'admin': Shield,
'tourist': Users,
'guide': MapPin,
'hotel': Hotel,
'taxi': Car,
'restaurant': UtensilsCrossed
};
const Icon = iconMap[role as keyof typeof iconMap] || Users;
return <Icon className="w-4 h-4" />;
};
const getRoleBadgeColor = (role: string) => {
const colorMap = {
'super_admin': 'bg-purple-100 text-purple-800',
'admin': 'bg-blue-100 text-blue-800',
'tourist': 'bg-green-100 text-green-800',
'guide': 'bg-yellow-100 text-yellow-800',
'hotel': 'bg-indigo-100 text-indigo-800',
'taxi': 'bg-red-100 text-red-800',
'restaurant': 'bg-orange-100 text-orange-800'
};
return colorMap[role as keyof typeof colorMap] || 'bg-gray-100 text-gray-800';
};
const getStatusIcon = (status: string, verified: boolean) => {
if (status === 'active' && verified) return <CheckCircle className="w-4 h-4 text-green-500" />;
if (status === 'active' && !verified) return <AlertTriangle className="w-4 h-4 text-yellow-500" />;
return <XCircle className="w-4 h-4 text-red-500" />;
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">Gestión de Usuarios</h2>
<p className="text-gray-600">Administra usuarios, roles y permisos del sistema</p>
</div>
<Dialog open={showCreateModal} onOpenChange={setShowCreateModal}>
<DialogTrigger asChild>
<Button>
<UserPlus className="w-4 h-4 mr-2" />
Nuevo Usuario
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Crear Nuevo Usuario</DialogTitle>
</DialogHeader>
<UserForm
onSubmit={handleCreateUser}
onCancel={() => setShowCreateModal(false)}
isSuperAdmin={isSuperAdmin}
/>
</DialogContent>
</Dialog>
</div>
<Card>
<CardContent className="pt-6">
<div className="flex flex-wrap gap-4">
<div className="flex-1 min-w-64">
<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 o email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<Select value={roleFilter} onValueChange={setRoleFilter}>
<SelectTrigger className="w-48">
<SelectValue placeholder="Filtrar por rol" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos los roles</SelectItem>
<SelectItem value="super_admin">Super Admin</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="tourist">Turista</SelectItem>
<SelectItem value="guide">Guía</SelectItem>
<SelectItem value="hotel">Hotel</SelectItem>
<SelectItem value="taxi">Taxi</SelectItem>
<SelectItem value="restaurant">Restaurante</SelectItem>
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-48">
<SelectValue placeholder="Filtrar por estado" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos los estados</SelectItem>
<SelectItem value="active">Activo</SelectItem>
<SelectItem value="pending">Pendiente</SelectItem>
<SelectItem value="suspended">Suspendido</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Lista de Usuarios ({filteredUsers.length})</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b">
<th className="text-left py-3 px-4 font-medium text-gray-700">Usuario</th>
<th className="text-left py-3 px-4 font-medium text-gray-700">Rol</th>
<th className="text-left py-3 px-4 font-medium text-gray-700">Estado</th>
<th className="text-left py-3 px-4 font-medium text-gray-700">Última Conexión</th>
<th className="text-left py-3 px-4 font-medium text-gray-700">Registrado</th>
<th className="text-right py-3 px-4 font-medium text-gray-700">Acciones</th>
</tr>
</thead>
<tbody>
{filteredUsers.map((user) => (
<tr key={user.id} className="border-b hover:bg-gray-50">
<td className="py-3 px-4">
<div className="flex items-center space-x-3">
<Avatar>
<AvatarImage src={user.avatar} />
<AvatarFallback>
{user.name?.split(' ').map((n: string) => n[0]).join('').toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<div className="font-medium text-gray-900">{user.name}</div>
<div className="text-sm text-gray-600">{user.email}</div>
</div>
</div>
</td>
<td className="py-3 px-4">
<Badge className={getRoleBadgeColor(getRoleName(user.role))}>
{getRoleIcon(getRoleName(user.role))}
<span className="ml-1 capitalize">{getRoleName(user.role).replace('_', ' ')}</span>
</Badge>
</td>
<td className="py-3 px-4">
<div className="flex items-center space-x-2">
{getStatusIcon(user.status, user.verified)}
<span className="capitalize">{user.status}</span>
{!user.verified && <span className="text-xs text-yellow-600">(No verificado)</span>}
</div>
</td>
<td className="py-3 px-4 text-sm text-gray-600">
{user.lastLogin === 'Nunca' ? 'Nunca' : new Date(user.lastLogin).toLocaleDateString()}
</td>
<td className="py-3 px-4 text-sm text-gray-600">
{new Date(user.createdAt).toLocaleDateString()}
</td>
<td className="py-3 px-4">
<div className="flex items-center justify-end space-x-2">
<Button variant="ghost" size="sm">
<Eye className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm">
<Edit className="w-4 h-4" />
</Button>
{isSuperAdmin && getRoleName(user.role) !== 'super_admin' && (
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteUser(user.id)}
>
<Trash2 className="w-4 h-4 text-red-500" />
</Button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
);
};
const UserForm = ({
onSubmit,
onCancel,
user = null,
isSuperAdmin = false
}: {
onSubmit: (data: any) => void;
onCancel: () => void;
user?: any;
isSuperAdmin: boolean;
}) => {
const [formData, setFormData] = useState({
name: user?.name || '',
email: user?.email || '',
role: (typeof user?.role === 'string' ? user?.role : user?.role?.name) || 'tourist',
phone: user?.profile?.phone || '',
address: user?.profile?.address || '',
status: user?.status || 'active'
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(formData);
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="name">Nombre Completo</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
</div>
<div>
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
/>
</div>
<div>
<Label htmlFor="role">Rol</Label>
<Select value={formData.role} onValueChange={(value) => setFormData({ ...formData, role: value })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="tourist">Turista</SelectItem>
<SelectItem value="guide">Guía Turístico</SelectItem>
<SelectItem value="hotel">Hotel</SelectItem>
<SelectItem value="taxi">Taxista</SelectItem>
<SelectItem value="restaurant">Restaurante</SelectItem>
{isSuperAdmin && (
<>
<SelectItem value="admin">Administrador</SelectItem>
<SelectItem value="super_admin">Super Administrador</SelectItem>
</>
)}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="phone">Teléfono</Label>
<Input
id="phone"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
/>
</div>
<div className="md:col-span-2">
<Label htmlFor="address">Dirección</Label>
<Input
id="address"
value={formData.address}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
/>
</div>
</div>
<div className="flex justify-end space-x-2 pt-4">
<Button type="button" variant="outline" onClick={onCancel}>
Cancelar
</Button>
<Button type="submit">
{user ? 'Actualizar' : 'Crear'} Usuario
</Button>
</div>
</form>
);
};
export default UsersTab;