Fix: Improve table layout functionality
This commit is contained in:
@@ -34,6 +34,7 @@ import Invoices from "./pages/dashboard/Invoices";
|
|||||||
import InvoiceDetail from "./pages/dashboard/InvoiceDetail";
|
import InvoiceDetail from "./pages/dashboard/InvoiceDetail";
|
||||||
import HotelManagement from "./pages/dashboard/HotelManagement";
|
import HotelManagement from "./pages/dashboard/HotelManagement";
|
||||||
import RestaurantPOS from "./pages/dashboard/RestaurantPOS";
|
import RestaurantPOS from "./pages/dashboard/RestaurantPOS";
|
||||||
|
import Personalization from "./pages/dashboard/Personalization";
|
||||||
import NotFound from "./pages/NotFound";
|
import NotFound from "./pages/NotFound";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
@@ -255,6 +256,13 @@ const AppRouter = () => (
|
|||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
|
<Route path="/dashboard/personalization" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<DashboardLayout>
|
||||||
|
<Personalization />
|
||||||
|
</DashboardLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
{/* Catch-all route */}
|
{/* Catch-all route */}
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Hotel,
|
Hotel,
|
||||||
UtensilsCrossed
|
UtensilsCrossed,
|
||||||
|
Brain
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
@@ -63,6 +64,7 @@ const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
{ icon: Plus, label: 'Channel Manager', path: '/dashboard/channel-manager' },
|
{ icon: Plus, label: 'Channel Manager', path: '/dashboard/channel-manager' },
|
||||||
{ icon: Hotel, label: 'Hotel Management', path: '/dashboard/hotel-management' },
|
{ icon: Hotel, label: 'Hotel Management', path: '/dashboard/hotel-management' },
|
||||||
{ icon: UtensilsCrossed, label: 'Restaurant POS', path: '/dashboard/restaurant-pos' },
|
{ icon: UtensilsCrossed, label: 'Restaurant POS', path: '/dashboard/restaurant-pos' },
|
||||||
|
{ icon: Brain, label: 'Personalization', path: '/dashboard/personalization' },
|
||||||
{ icon: Wallet, label: 'Wallet', path: '/dashboard/wallet' },
|
{ icon: Wallet, label: 'Wallet', path: '/dashboard/wallet' },
|
||||||
{ icon: MessageSquare, label: 'Message', path: '/dashboard/messages', badge: '2' },
|
{ icon: MessageSquare, label: 'Message', path: '/dashboard/messages', badge: '2' },
|
||||||
];
|
];
|
||||||
|
|||||||
181
src/components/personalization/AIRecommendations.tsx
Normal file
181
src/components/personalization/AIRecommendations.tsx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Brain, Star, MapPin, TrendingUp, ThumbsUp, ThumbsDown } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface Recommendation {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
userName: string;
|
||||||
|
type: 'destination' | 'activity' | 'restaurant' | 'hotel';
|
||||||
|
item: string;
|
||||||
|
confidence: number;
|
||||||
|
reason: string;
|
||||||
|
matchScore: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AIRecommendations = () => {
|
||||||
|
const [recommendations] = useState<Recommendation[]>([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
userId: 'user_123',
|
||||||
|
userName: 'María García',
|
||||||
|
type: 'destination',
|
||||||
|
item: 'Barcelona, España',
|
||||||
|
confidence: 95,
|
||||||
|
reason: 'Basado en historial de viajes a ciudades culturales',
|
||||||
|
matchScore: 95
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
userId: 'user_123',
|
||||||
|
userName: 'María García',
|
||||||
|
type: 'activity',
|
||||||
|
item: 'Tour Gastronómico Sagrada Familia',
|
||||||
|
confidence: 88,
|
||||||
|
reason: 'Usuario interesado en gastronomía y arquitectura',
|
||||||
|
matchScore: 88
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
userId: 'user_456',
|
||||||
|
userName: 'Carlos López',
|
||||||
|
type: 'hotel',
|
||||||
|
item: 'Hotel Boutique Centro Histórico',
|
||||||
|
confidence: 92,
|
||||||
|
reason: 'Prefiere alojamientos boutique en zonas céntricas',
|
||||||
|
matchScore: 92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
userId: 'user_789',
|
||||||
|
userName: 'Ana Martínez',
|
||||||
|
type: 'restaurant',
|
||||||
|
item: 'Restaurante Vegetariano "Green Life"',
|
||||||
|
confidence: 90,
|
||||||
|
reason: 'Historial de preferencias vegetarianas y sostenibles',
|
||||||
|
matchScore: 90
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getTypeIcon = (type: Recommendation['type']) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'destination': return MapPin;
|
||||||
|
case 'activity': return Star;
|
||||||
|
case 'restaurant': return Star;
|
||||||
|
case 'hotel': return MapPin;
|
||||||
|
default: return Star;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTypeLabel = (type: Recommendation['type']) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'destination': return 'Destino';
|
||||||
|
case 'activity': return 'Actividad';
|
||||||
|
case 'restaurant': return 'Restaurante';
|
||||||
|
case 'hotel': return 'Hotel';
|
||||||
|
default: return type;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getConfidenceColor = (confidence: number) => {
|
||||||
|
if (confidence >= 90) return 'bg-green-500';
|
||||||
|
if (confidence >= 75) return 'bg-yellow-500';
|
||||||
|
return 'bg-orange-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFeedback = (recId: string, positive: boolean) => {
|
||||||
|
toast.success(positive ? 'Feedback positivo registrado' : 'Feedback negativo registrado');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Brain className="h-4 w-4" />
|
||||||
|
<span>Motor de IA analizando {recommendations.length} recomendaciones activas</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
{recommendations.map((rec) => {
|
||||||
|
const Icon = getTypeIcon(rec.type);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={rec.id}>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<Icon className="h-4 w-4 text-primary" />
|
||||||
|
<Badge variant="outline">{getTypeLabel(rec.type)}</Badge>
|
||||||
|
</div>
|
||||||
|
<h4 className="font-semibold">{rec.item}</h4>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Para: {rec.userName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className={`w-2 h-2 rounded-full ${getConfidenceColor(rec.confidence)}`}></div>
|
||||||
|
<span className="text-sm font-medium">{rec.confidence}%</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground">confianza</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-muted rounded-lg">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<Brain className="h-4 w-4 text-primary mt-0.5" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-xs font-medium mb-1">Razón de la recomendación:</div>
|
||||||
|
<div className="text-xs text-muted-foreground">{rec.reason}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-2">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<TrendingUp className="h-3 w-3 text-muted-foreground" />
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Match Score: {rec.matchScore}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
onClick={() => handleFeedback(rec.id, true)}
|
||||||
|
>
|
||||||
|
<ThumbsUp className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
onClick={() => handleFeedback(rec.id, false)}
|
||||||
|
>
|
||||||
|
<ThumbsDown className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{recommendations.length === 0 && (
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
<Brain className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||||
|
<p>No hay recomendaciones activas</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AIRecommendations;
|
||||||
180
src/components/personalization/BehaviorAnalytics.tsx
Normal file
180
src/components/personalization/BehaviorAnalytics.tsx
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { TrendingUp, Eye, MousePointer, Clock, MapPin } from 'lucide-react';
|
||||||
|
|
||||||
|
const BehaviorAnalytics = () => {
|
||||||
|
const behaviorData = {
|
||||||
|
topDestinations: [
|
||||||
|
{ name: 'Barcelona', views: 1250, bookings: 85 },
|
||||||
|
{ name: 'Madrid', views: 1120, bookings: 72 },
|
||||||
|
{ name: 'Valencia', views: 890, bookings: 58 },
|
||||||
|
{ name: 'Sevilla', views: 780, bookings: 45 }
|
||||||
|
],
|
||||||
|
userJourney: [
|
||||||
|
{ step: 'Landing Page', users: 1000, dropoff: 0 },
|
||||||
|
{ step: 'Búsqueda', users: 850, dropoff: 15 },
|
||||||
|
{ step: 'Detalle', users: 680, dropoff: 20 },
|
||||||
|
{ step: 'Reserva', users: 425, dropoff: 37.5 },
|
||||||
|
{ step: 'Pago', users: 340, dropoff: 20 }
|
||||||
|
],
|
||||||
|
peakHours: [
|
||||||
|
{ hour: '09:00', activity: 45 },
|
||||||
|
{ hour: '12:00', activity: 78 },
|
||||||
|
{ hour: '15:00', activity: 65 },
|
||||||
|
{ hour: '18:00', activity: 92 },
|
||||||
|
{ hour: '21:00', activity: 58 }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
{/* Top Destinations */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<MapPin className="h-5 w-5" />
|
||||||
|
Destinos Más Vistos
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{behaviorData.topDestinations.map((dest, idx) => (
|
||||||
|
<div key={idx} className="space-y-1">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="font-medium">{dest.name}</span>
|
||||||
|
<Badge variant="secondary">{dest.views} vistas</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-primary"
|
||||||
|
style={{ width: `${(dest.bookings / dest.views) * 100}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{dest.bookings} reservas
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* User Journey Funnel */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<TrendingUp className="h-5 w-5" />
|
||||||
|
Embudo de Conversión
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{behaviorData.userJourney.map((step, idx) => (
|
||||||
|
<div key={idx} className="space-y-1">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm font-medium">{step.step}</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm">{step.users} usuarios</span>
|
||||||
|
{step.dropoff > 0 && (
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
-{step.dropoff}%
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-gradient-to-r from-green-500 to-blue-500"
|
||||||
|
style={{ width: `${(step.users / 1000) * 100}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Peak Activity Hours */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<Clock className="h-5 w-5" />
|
||||||
|
Horas de Mayor Actividad
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex justify-between items-end h-40 gap-4">
|
||||||
|
{behaviorData.peakHours.map((hour, idx) => (
|
||||||
|
<div key={idx} className="flex-1 flex flex-col items-center gap-2">
|
||||||
|
<div className="w-full flex items-end justify-center h-32">
|
||||||
|
<div
|
||||||
|
className="w-full bg-primary rounded-t-lg transition-all hover:bg-primary/80"
|
||||||
|
style={{ height: `${hour.activity}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">{hour.hour}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Quick Stats */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Eye className="h-8 w-8 text-blue-500" />
|
||||||
|
<div>
|
||||||
|
<div className="text-2xl font-bold">4,890</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Páginas vistas</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<MousePointer className="h-8 w-8 text-green-500" />
|
||||||
|
<div>
|
||||||
|
<div className="text-2xl font-bold">68%</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Click-through rate</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Clock className="h-8 w-8 text-purple-500" />
|
||||||
|
<div>
|
||||||
|
<div className="text-2xl font-bold">4:32</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Tiempo promedio</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<TrendingUp className="h-8 w-8 text-orange-500" />
|
||||||
|
<div>
|
||||||
|
<div className="text-2xl font-bold">34%</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Tasa conversión</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BehaviorAnalytics;
|
||||||
179
src/components/personalization/SegmentManagement.tsx
Normal file
179
src/components/personalization/SegmentManagement.tsx
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||||
|
import { Users, Plus, Target, TrendingUp } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface Segment {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
userCount: number;
|
||||||
|
criteria: string[];
|
||||||
|
conversionRate: number;
|
||||||
|
avgSpending: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SegmentManagement = () => {
|
||||||
|
const [segments] = useState<Segment[]>([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'Viajeros Culturales',
|
||||||
|
description: 'Usuarios interesados en historia, arte y cultura',
|
||||||
|
userCount: 450,
|
||||||
|
criteria: ['Visita museos', 'Busca tours culturales', 'Presupuesto medio-alto'],
|
||||||
|
conversionRate: 42,
|
||||||
|
avgSpending: 850
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Amantes de la Playa',
|
||||||
|
description: 'Prefieren destinos costeros y actividades acuáticas',
|
||||||
|
userCount: 380,
|
||||||
|
criteria: ['Busca playas', 'Reserva resorts', 'Temporada verano'],
|
||||||
|
conversionRate: 38,
|
||||||
|
avgSpending: 720
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: 'Aventureros',
|
||||||
|
description: 'Buscan experiencias de aventura y naturaleza',
|
||||||
|
userCount: 290,
|
||||||
|
criteria: ['Actividades outdoor', 'Deportes extremos', 'Ecoturismo'],
|
||||||
|
conversionRate: 45,
|
||||||
|
avgSpending: 950
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
name: 'Gastronómicos',
|
||||||
|
description: 'Viajeros enfocados en experiencias culinarias',
|
||||||
|
userCount: 320,
|
||||||
|
criteria: ['Tours gastronómicos', 'Restaurantes gourmet', 'Cocina local'],
|
||||||
|
conversionRate: 48,
|
||||||
|
avgSpending: 1100
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const createSegment = () => {
|
||||||
|
toast.success('Segmento creado correctamente');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold">Segmentos Activos</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{segments.length} segmentos de usuarios configurados
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button className="gap-2">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Nuevo Segmento
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Crear Nuevo Segmento</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="segment-name">Nombre del Segmento</Label>
|
||||||
|
<Input id="segment-name" placeholder="Ej: Viajeros de Lujo" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="segment-desc">Descripción</Label>
|
||||||
|
<Input id="segment-desc" placeholder="Descripción del segmento" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="segment-criteria">Criterios (separados por coma)</Label>
|
||||||
|
<Input
|
||||||
|
id="segment-criteria"
|
||||||
|
placeholder="Alta capacidad de gasto, Hoteles 5 estrellas, ..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button onClick={createSegment} className="w-full">
|
||||||
|
Crear Segmento
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
{segments.map((segment) => (
|
||||||
|
<Card key={segment.id}>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<Target className="h-5 w-5 text-primary" />
|
||||||
|
{segment.name}
|
||||||
|
</CardTitle>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
{segment.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Users className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium">Usuarios</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-2xl font-bold">{segment.userCount}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium">Criterios de segmentación:</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{segment.criteria.map((criterion, idx) => (
|
||||||
|
<Badge key={idx} variant="outline">{criterion}</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="p-3 bg-green-50 rounded-lg">
|
||||||
|
<div className="flex items-center gap-1 mb-1">
|
||||||
|
<TrendingUp className="h-3 w-3 text-green-600" />
|
||||||
|
<span className="text-xs text-green-600">Conversión</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xl font-bold text-green-700">
|
||||||
|
{segment.conversionRate}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-blue-50 rounded-lg">
|
||||||
|
<div className="flex items-center gap-1 mb-1">
|
||||||
|
<span className="text-xs text-blue-600">Gasto Promedio</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xl font-bold text-blue-700">
|
||||||
|
€{segment.avgSpending}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<Button variant="outline" size="sm" className="flex-1">
|
||||||
|
Ver Usuarios
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" className="flex-1">
|
||||||
|
Editar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SegmentManagement;
|
||||||
154
src/components/personalization/UserPreferences.tsx
Normal file
154
src/components/personalization/UserPreferences.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { User, Heart, Tag, Bell } from 'lucide-react';
|
||||||
|
|
||||||
|
interface UserProfile {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
preferences: {
|
||||||
|
destinations: string[];
|
||||||
|
activities: string[];
|
||||||
|
budget: string;
|
||||||
|
travelStyle: string;
|
||||||
|
};
|
||||||
|
notifications: {
|
||||||
|
email: boolean;
|
||||||
|
push: boolean;
|
||||||
|
sms: boolean;
|
||||||
|
};
|
||||||
|
interests: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserPreferences = () => {
|
||||||
|
const [users] = useState<UserProfile[]>([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'María García',
|
||||||
|
email: 'maria@example.com',
|
||||||
|
preferences: {
|
||||||
|
destinations: ['Europa', 'Asia'],
|
||||||
|
activities: ['Cultura', 'Gastronomía', 'Historia'],
|
||||||
|
budget: 'Medio-Alto',
|
||||||
|
travelStyle: 'Cultural'
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
email: true,
|
||||||
|
push: true,
|
||||||
|
sms: false
|
||||||
|
},
|
||||||
|
interests: ['Arte', 'Museos', 'Cocina Local', 'Arquitectura']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Carlos López',
|
||||||
|
email: 'carlos@example.com',
|
||||||
|
preferences: {
|
||||||
|
destinations: ['Caribe', 'Mediterráneo'],
|
||||||
|
activities: ['Playa', 'Deportes', 'Relax'],
|
||||||
|
budget: 'Alto',
|
||||||
|
travelStyle: 'Lujo'
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
email: true,
|
||||||
|
push: false,
|
||||||
|
sms: true
|
||||||
|
},
|
||||||
|
interests: ['Golf', 'Spa', 'Gastronomía Gourmet', 'Vinos']
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
{users.map((user) => (
|
||||||
|
<Card key={user.id}>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||||
|
<User className="h-5 w-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold">{user.name}</h4>
|
||||||
|
<p className="text-xs text-muted-foreground">{user.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Heart className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium">Destinos Favoritos</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{user.preferences.destinations.map((dest, idx) => (
|
||||||
|
<Badge key={idx} variant="secondary">{dest}</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Tag className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium">Intereses</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{user.interests.map((interest, idx) => (
|
||||||
|
<Badge key={idx} variant="outline">{interest}</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Presupuesto:</span>
|
||||||
|
<div className="font-medium">{user.preferences.budget}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Estilo:</span>
|
||||||
|
<div className="font-medium">{user.preferences.travelStyle}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-3 border-t">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Bell className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium">Notificaciones</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor={`email-${user.id}`} className="text-xs">Email</Label>
|
||||||
|
<Switch id={`email-${user.id}`} checked={user.notifications.email} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor={`push-${user.id}`} className="text-xs">Push</Label>
|
||||||
|
<Switch id={`push-${user.id}`} checked={user.notifications.push} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor={`sms-${user.id}`} className="text-xs">SMS</Label>
|
||||||
|
<Switch id={`sms-${user.id}`} checked={user.notifications.sms} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button variant="outline" className="w-full" size="sm">
|
||||||
|
Editar Preferencias
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserPreferences;
|
||||||
@@ -151,7 +151,22 @@ const TableConfiguration = () => {
|
|||||||
{tables.map((table) => (
|
{tables.map((table) => (
|
||||||
<div
|
<div
|
||||||
key={table.id}
|
key={table.id}
|
||||||
className="absolute cursor-pointer hover:scale-110 transition-transform"
|
className="absolute cursor-move hover:scale-110 transition-transform"
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => {
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
e.dataTransfer.setData('tableId', table.id);
|
||||||
|
}}
|
||||||
|
onDragEnd={(e) => {
|
||||||
|
const rect = e.currentTarget.parentElement?.getBoundingClientRect();
|
||||||
|
if (rect) {
|
||||||
|
const x = Math.max(0, Math.min(e.clientX - rect.left - 40, rect.width - 80));
|
||||||
|
const y = Math.max(0, Math.min(e.clientY - rect.top - 40, rect.height - 80));
|
||||||
|
setTables(tables.map(t =>
|
||||||
|
t.id === table.id ? { ...t, position: { x, y } } : t
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
left: `${table.position.x}px`,
|
left: `${table.position.x}px`,
|
||||||
top: `${table.position.y}px`
|
top: `${table.position.y}px`
|
||||||
|
|||||||
146
src/pages/dashboard/Personalization.tsx
Normal file
146
src/pages/dashboard/Personalization.tsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Target, Brain, TrendingUp, Users } from 'lucide-react';
|
||||||
|
import UserPreferences from '@/components/personalization/UserPreferences';
|
||||||
|
import AIRecommendations from '@/components/personalization/AIRecommendations';
|
||||||
|
import BehaviorAnalytics from '@/components/personalization/BehaviorAnalytics';
|
||||||
|
import SegmentManagement from '@/components/personalization/SegmentManagement';
|
||||||
|
|
||||||
|
const Personalization = () => {
|
||||||
|
const [totalUsers] = useState(1250);
|
||||||
|
const [activeSegments] = useState(8);
|
||||||
|
const [recommendationAccuracy] = useState(87);
|
||||||
|
const [engagement] = useState(72);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold flex items-center gap-2">
|
||||||
|
<Target className="h-8 w-8 text-primary" />
|
||||||
|
Personalización con IA
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground mt-1">
|
||||||
|
Recomendaciones inteligentes y segmentación de usuarios
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Overview */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Usuarios Activos</CardTitle>
|
||||||
|
<Users className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{totalUsers.toLocaleString()}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">perfiles analizados</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Segmentos</CardTitle>
|
||||||
|
<Target className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{activeSegments}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">grupos activos</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Precisión IA</CardTitle>
|
||||||
|
<Brain className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{recommendationAccuracy}%</div>
|
||||||
|
<p className="text-xs text-muted-foreground">accuracy de recomendaciones</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Engagement</CardTitle>
|
||||||
|
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{engagement}%</div>
|
||||||
|
<p className="text-xs text-muted-foreground">tasa de interacción</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<Tabs defaultValue="recommendations" className="space-y-4">
|
||||||
|
<TabsList className="grid w-full grid-cols-4">
|
||||||
|
<TabsTrigger value="recommendations">Recomendaciones</TabsTrigger>
|
||||||
|
<TabsTrigger value="preferences">Preferencias</TabsTrigger>
|
||||||
|
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
||||||
|
<TabsTrigger value="segments">Segmentos</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="recommendations" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Recomendaciones con IA</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Sistema de recomendaciones personalizadas basado en comportamiento
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<AIRecommendations />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="preferences" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Preferencias de Usuario</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Gestiona las preferencias y configuraciones de personalización
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<UserPreferences />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="analytics" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Análisis de Comportamiento</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Insights sobre patrones de uso y preferencias de usuarios
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<BehaviorAnalytics />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="segments" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Gestión de Segmentos</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Crea y gestiona segmentos de usuarios para campañas dirigidas
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<SegmentManagement />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Personalization;
|
||||||
Reference in New Issue
Block a user