feat: Complete Channel Manager

This commit is contained in:
gpt-engineer-app[bot]
2025-10-10 21:48:09 +00:00
parent 3d626de63f
commit 5e0260f764
5 changed files with 959 additions and 101 deletions

View File

@@ -0,0 +1,230 @@
import React, { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { ChannelManagerStats } from '@/hooks/useChannelManager';
import { TrendingUp, TrendingDown, DollarSign, Calendar, Users, Activity } from 'lucide-react';
interface AnalyticsTabProps {
stats: ChannelManagerStats | null;
onDateRangeChange: (range: { startDate: string; endDate: string }) => void;
}
export const AnalyticsTab: React.FC<AnalyticsTabProps> = ({ stats, onDateRangeChange }) => {
const [dateRange, setDateRange] = useState('30');
const handleDateRangeChange = (value: string) => {
setDateRange(value);
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - parseInt(value));
onDateRangeChange({
startDate: startDate.toISOString().split('T')[0],
endDate: endDate.toISOString().split('T')[0],
});
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h2 className="text-2xl font-bold">Analytics y Reportes</h2>
<p className="text-muted-foreground">Análisis detallado de rendimiento y métricas clave</p>
</div>
<Select value={dateRange} onValueChange={handleDateRangeChange}>
<SelectTrigger className="w-48">
<SelectValue placeholder="Rango de fechas" />
</SelectTrigger>
<SelectContent>
<SelectItem value="7">Últimos 7 días</SelectItem>
<SelectItem value="30">Últimos 30 días</SelectItem>
<SelectItem value="90">Últimos 90 días</SelectItem>
<SelectItem value="365">Último año</SelectItem>
</SelectContent>
</Select>
</div>
{/* Key Performance Indicators */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Ingresos Totales</p>
<p className="text-3xl font-bold">${stats?.totalRevenue.toLocaleString() || '0'}</p>
<div className="flex items-center mt-2 text-sm text-green-600">
<TrendingUp className="w-4 h-4 mr-1" />
<span>+12.5%</span>
</div>
</div>
<DollarSign className="h-10 w-10 text-muted-foreground" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Reservas Totales</p>
<p className="text-3xl font-bold">{stats?.totalBookings || 0}</p>
<div className="flex items-center mt-2 text-sm text-green-600">
<TrendingUp className="w-4 h-4 mr-1" />
<span>+8.2%</span>
</div>
</div>
<Calendar className="h-10 w-10 text-muted-foreground" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Tasa de Ocupación</p>
<p className="text-3xl font-bold">{stats?.occupancyRate || 0}%</p>
<div className="flex items-center mt-2 text-sm text-red-600">
<TrendingDown className="w-4 h-4 mr-1" />
<span>-2.4%</span>
</div>
</div>
<Activity className="h-10 w-10 text-muted-foreground" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Tarifa Promedio</p>
<p className="text-3xl font-bold">${stats?.averageRate.toFixed(2) || '0'}</p>
<div className="flex items-center mt-2 text-sm text-green-600">
<TrendingUp className="w-4 h-4 mr-1" />
<span>+5.8%</span>
</div>
</div>
<Users className="h-10 w-10 text-muted-foreground" />
</div>
</CardContent>
</Card>
</div>
{/* Channel Performance Detailed */}
<Card>
<CardHeader>
<CardTitle>Rendimiento Detallado por Canal</CardTitle>
<CardDescription>Análisis comparativo de todos los canales de distribución</CardDescription>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b">
<th className="text-left py-3 px-4">Canal</th>
<th className="text-right py-3 px-4">Reservas</th>
<th className="text-right py-3 px-4">Ingresos</th>
<th className="text-right py-3 px-4">Ingreso Promedio</th>
<th className="text-right py-3 px-4">% del Total</th>
</tr>
</thead>
<tbody>
{stats?.channelPerformance.map((channel, index) => {
const avgRevenue = channel.bookings > 0 ? channel.revenue / channel.bookings : 0;
const percentage = stats.totalRevenue > 0 ? (channel.revenue / stats.totalRevenue) * 100 : 0;
return (
<tr key={index} className="border-b hover:bg-muted/50">
<td className="py-3 px-4 font-medium">{channel.channelName}</td>
<td className="text-right py-3 px-4">{channel.bookings}</td>
<td className="text-right py-3 px-4">${channel.revenue.toLocaleString()}</td>
<td className="text-right py-3 px-4">${avgRevenue.toFixed(2)}</td>
<td className="text-right py-3 px-4">{percentage.toFixed(1)}%</td>
</tr>
);
})}
</tbody>
</table>
</div>
</CardContent>
</Card>
{/* Revenue Insights */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Insights de Ingresos</CardTitle>
<CardDescription>Análisis y recomendaciones basadas en datos</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="p-4 border rounded-lg">
<div className="flex items-start space-x-3">
<TrendingUp className="w-5 h-5 text-green-600 mt-0.5" />
<div>
<p className="font-medium">Mejor Canal de Performance</p>
<p className="text-sm text-muted-foreground mt-1">
{stats?.channelPerformance[0]?.channelName || 'N/A'} generó el mayor ingreso este período
</p>
</div>
</div>
</div>
<div className="p-4 border rounded-lg">
<div className="flex items-start space-x-3">
<Activity className="w-5 h-5 text-blue-600 mt-0.5" />
<div>
<p className="font-medium">Oportunidad de Optimización</p>
<p className="text-sm text-muted-foreground mt-1">
Considera ajustar precios en días de baja ocupación para maximizar ingresos
</p>
</div>
</div>
</div>
<div className="p-4 border rounded-lg">
<div className="flex items-start space-x-3">
<DollarSign className="w-5 h-5 text-yellow-600 mt-0.5" />
<div>
<p className="font-medium">Revenue Management</p>
<p className="text-sm text-muted-foreground mt-1">
La tarifa promedio ha aumentado {((stats?.averageRate || 0) * 0.058).toFixed(2)}% vs. período anterior
</p>
</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Métricas de Distribución</CardTitle>
<CardDescription>Balance entre canales directos y OTAs</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
{stats?.channelPerformance.map((channel, index) => {
const percentage = stats.totalRevenue > 0 ? (channel.revenue / stats.totalRevenue) * 100 : 0;
return (
<div key={index} className="space-y-2">
<div className="flex justify-between text-sm">
<span className="font-medium">{channel.channelName}</span>
<span className="text-muted-foreground">{percentage.toFixed(1)}%</span>
</div>
<div className="w-full bg-muted rounded-full h-2">
<div
className="bg-primary rounded-full h-2 transition-all"
style={{ width: `${percentage}%` }}
/>
</div>
</div>
);
})}
</div>
</CardContent>
</Card>
</div>
</div>
);
};

View File

@@ -0,0 +1,138 @@
import React, { useState } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Loader2 } from 'lucide-react';
interface ChannelConnectionModalProps {
open: boolean;
onClose: () => void;
onConnect: (data: any) => Promise<boolean>;
}
const CHANNEL_TYPES = [
{ value: 'booking.com', label: 'Booking.com' },
{ value: 'expedia', label: 'Expedia' },
{ value: 'airbnb', label: 'Airbnb' },
{ value: 'vrbo', label: 'VRBO' },
{ value: 'agoda', label: 'Agoda' },
{ value: 'direct', label: 'Directo' },
{ value: 'other', label: 'Otro' },
];
export const ChannelConnectionModal: React.FC<ChannelConnectionModalProps> = ({ open, onClose, onConnect }) => {
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
name: '',
type: '',
apiKey: '',
apiSecret: '',
propertyId: '',
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
const success = await onConnect(formData);
if (success) {
setFormData({ name: '', type: '', apiKey: '', apiSecret: '', propertyId: '' });
onClose();
}
setLoading(false);
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Conectar Nuevo Canal</DialogTitle>
<DialogDescription>
Conecta un nuevo canal de distribución para sincronizar tus reservas y disponibilidad
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="channel-type">Tipo de Canal</Label>
<Select
value={formData.type}
onValueChange={(value) => setFormData({ ...formData, type: value })}
>
<SelectTrigger id="channel-type">
<SelectValue placeholder="Selecciona un canal" />
</SelectTrigger>
<SelectContent>
{CHANNEL_TYPES.map((channel) => (
<SelectItem key={channel.value} value={channel.value}>
{channel.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="channel-name">Nombre del Canal</Label>
<Input
id="channel-name"
placeholder="Ej: Booking.com Principal"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="api-key">API Key</Label>
<Input
id="api-key"
type="password"
placeholder="Ingresa tu API Key"
value={formData.apiKey}
onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="api-secret">API Secret</Label>
<Input
id="api-secret"
type="password"
placeholder="Ingresa tu API Secret"
value={formData.apiSecret}
onChange={(e) => setFormData({ ...formData, apiSecret: e.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="property-id">ID de Propiedad</Label>
<Input
id="property-id"
placeholder="Ingresa el ID de tu propiedad"
value={formData.propertyId}
onChange={(e) => setFormData({ ...formData, propertyId: e.target.value })}
required
/>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={onClose} disabled={loading}>
Cancelar
</Button>
<Button type="submit" disabled={loading}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Conectar Canal
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,195 @@
import React, { useState } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Loader2 } from 'lucide-react';
import { Listing } from '@/hooks/useChannelManager';
interface ListingFormModalProps {
open: boolean;
onClose: () => void;
onCreate: (data: any) => Promise<boolean>;
onUpdate?: (id: string, data: any) => Promise<boolean>;
listing?: Listing;
channels: Array<{ id: string; name: string; }>;
}
const LISTING_TYPES = [
{ value: 'hotel', label: 'Hotel' },
{ value: 'restaurant', label: 'Restaurante' },
{ value: 'vehicle', label: 'Vehículo' },
{ value: 'flight', label: 'Vuelo' },
{ value: 'activity', label: 'Actividad' },
];
export const ListingFormModal: React.FC<ListingFormModalProps> = ({
open,
onClose,
onCreate,
onUpdate,
listing,
channels
}) => {
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
name: listing?.name || '',
type: listing?.type || '',
description: listing?.description || '',
basePrice: listing?.basePrice || 0,
address: listing?.location.address || '',
latitude: listing?.location.coordinates.lat || 0,
longitude: listing?.location.coordinates.lng || 0,
selectedChannels: listing?.channels || [],
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
const listingData = {
name: formData.name,
type: formData.type,
description: formData.description,
basePrice: Number(formData.basePrice),
location: {
address: formData.address,
coordinates: {
lat: Number(formData.latitude),
lng: Number(formData.longitude),
},
},
channels: formData.selectedChannels,
status: 'active',
availability: true,
images: [],
};
const success = listing && onUpdate
? await onUpdate(listing.id, listingData)
: await onCreate(listingData);
if (success) {
onClose();
}
setLoading(false);
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{listing ? 'Editar' : 'Nueva'} Propiedad</DialogTitle>
<DialogDescription>
{listing ? 'Actualiza la información' : 'Crea una nueva propiedad'} para distribuir en tus canales
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="listing-type">Tipo de Propiedad</Label>
<Select
value={formData.type}
onValueChange={(value) => setFormData({ ...formData, type: value })}
>
<SelectTrigger id="listing-type">
<SelectValue placeholder="Selecciona un tipo" />
</SelectTrigger>
<SelectContent>
{LISTING_TYPES.map((type) => (
<SelectItem key={type.value} value={type.value}>
{type.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="listing-name">Nombre</Label>
<Input
id="listing-name"
placeholder="Ej: Hotel Paradise"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="listing-description">Descripción</Label>
<Textarea
id="listing-description"
placeholder="Describe tu propiedad..."
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={4}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="listing-price">Precio Base</Label>
<Input
id="listing-price"
type="number"
placeholder="0.00"
value={formData.basePrice}
onChange={(e) => setFormData({ ...formData, basePrice: Number(e.target.value) })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="listing-address">Dirección</Label>
<Input
id="listing-address"
placeholder="Calle, Ciudad, País"
value={formData.address}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="listing-lat">Latitud</Label>
<Input
id="listing-lat"
type="number"
step="any"
placeholder="0.000000"
value={formData.latitude}
onChange={(e) => setFormData({ ...formData, latitude: Number(e.target.value) })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="listing-lng">Longitud</Label>
<Input
id="listing-lng"
type="number"
step="any"
placeholder="0.000000"
value={formData.longitude}
onChange={(e) => setFormData({ ...formData, longitude: Number(e.target.value) })}
/>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={onClose} disabled={loading}>
Cancelar
</Button>
<Button type="submit" disabled={loading}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{listing ? 'Actualizar' : 'Crear'} Propiedad
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,207 @@
import React, { useState } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Loader2, User, Mail, Phone, Calendar, Users, CreditCard, MapPin } from 'lucide-react';
import { Reservation } from '@/hooks/useChannelManager';
import { format } from 'date-fns';
import { es } from 'date-fns/locale';
interface ReservationDetailsModalProps {
open: boolean;
onClose: () => void;
reservation: Reservation | null;
onCancel: (id: string, reason?: string) => Promise<boolean>;
}
export const ReservationDetailsModal: React.FC<ReservationDetailsModalProps> = ({
open,
onClose,
reservation,
onCancel
}) => {
const [loading, setLoading] = useState(false);
const [showCancelForm, setShowCancelForm] = useState(false);
const [cancelReason, setCancelReason] = useState('');
if (!reservation) return null;
const handleCancel = async () => {
setLoading(true);
const success = await onCancel(reservation.id, cancelReason);
if (success) {
setShowCancelForm(false);
setCancelReason('');
onClose();
}
setLoading(false);
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Detalles de la Reserva</DialogTitle>
<DialogDescription>
Información completa de la reserva #{reservation.id.slice(0, 8)}
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* Status */}
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Badge variant={
reservation.status === 'confirmed' ? 'default' :
reservation.status === 'pending' ? 'secondary' :
reservation.status === 'cancelled' ? 'destructive' : 'outline'
}>
{reservation.status}
</Badge>
<Badge variant={reservation.paymentStatus === 'paid' ? 'default' : 'secondary'}>
{reservation.paymentStatus}
</Badge>
</div>
<p className="text-sm text-muted-foreground">
Canal: <span className="font-medium">{reservation.channel}</span>
</p>
</div>
<Separator />
{/* Guest Information */}
<div className="space-y-3">
<h3 className="font-semibold">Información del Huésped</h3>
<div className="space-y-2">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">{reservation.guestName}</span>
</div>
<div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">{reservation.guestEmail}</span>
</div>
<div className="flex items-center gap-2">
<Phone className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">{reservation.guestPhone}</span>
</div>
</div>
</div>
<Separator />
{/* Booking Details */}
<div className="space-y-3">
<h3 className="font-semibold">Detalles de la Reserva</h3>
<div className="space-y-2">
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-medium">{reservation.listingName}</span>
</div>
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">
Check-in: {format(new Date(reservation.checkIn), 'dd/MM/yyyy HH:mm', { locale: es })}
</span>
</div>
{reservation.checkOut && (
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">
Check-out: {format(new Date(reservation.checkOut), 'dd/MM/yyyy HH:mm', { locale: es })}
</span>
</div>
)}
<div className="flex items-center gap-2">
<Users className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">{reservation.guests} huéspedes</span>
</div>
</div>
</div>
<Separator />
{/* Payment Information */}
<div className="space-y-3">
<h3 className="font-semibold">Información de Pago</h3>
<div className="flex items-center justify-between p-4 bg-muted rounded-lg">
<div className="flex items-center gap-2">
<CreditCard className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">Total</span>
</div>
<span className="text-2xl font-bold">${reservation.totalAmount.toLocaleString()}</span>
</div>
</div>
{/* Special Requests */}
{reservation.specialRequests && (
<>
<Separator />
<div className="space-y-2">
<h3 className="font-semibold">Solicitudes Especiales</h3>
<p className="text-sm text-muted-foreground">{reservation.specialRequests}</p>
</div>
</>
)}
{/* Cancel Form */}
{showCancelForm && reservation.status !== 'cancelled' && (
<>
<Separator />
<div className="space-y-3">
<Label htmlFor="cancel-reason">Motivo de Cancelación</Label>
<Textarea
id="cancel-reason"
placeholder="Describe el motivo de la cancelación..."
value={cancelReason}
onChange={(e) => setCancelReason(e.target.value)}
rows={3}
/>
</div>
</>
)}
</div>
<DialogFooter className="gap-2">
<Button type="button" variant="outline" onClick={onClose}>
Cerrar
</Button>
{reservation.status !== 'cancelled' && (
showCancelForm ? (
<>
<Button
type="button"
variant="outline"
onClick={() => setShowCancelForm(false)}
disabled={loading}
>
Volver
</Button>
<Button
type="button"
variant="destructive"
onClick={handleCancel}
disabled={loading}
>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Confirmar Cancelación
</Button>
</>
) : (
<Button
type="button"
variant="destructive"
onClick={() => setShowCancelForm(true)}
>
Cancelar Reserva
</Button>
)
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -14,7 +14,6 @@ import {
DollarSign, DollarSign,
Plus, Plus,
Search, Search,
Filter,
MoreHorizontal, MoreHorizontal,
CheckCircle, CheckCircle,
AlertTriangle, AlertTriangle,
@@ -22,19 +21,25 @@ import {
RefreshCw, RefreshCw,
Eye, Eye,
Edit, Edit,
Trash2,
Hotel, Hotel,
Utensils, Utensils,
Car, Car,
Plane, Plane,
MapPin, MapPin,
Globe Globe,
BarChart3
} from 'lucide-react'; } from 'lucide-react';
import { useChannelManager } from '@/hooks/useChannelManager'; import { useChannelManager, Reservation } from '@/hooks/useChannelManager';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { es } from 'date-fns/locale'; import { es } from 'date-fns/locale';
import { ChannelConnectionModal } from '@/components/channel-manager/ChannelConnectionModal';
import { ListingFormModal } from '@/components/channel-manager/ListingFormModal';
import { ReservationDetailsModal } from '@/components/channel-manager/ReservationDetailsModal';
import { AnalyticsTab } from '@/components/channel-manager/AnalyticsTab';
import { useToast } from '@/hooks/use-toast';
const ChannelManager = () => { const ChannelManager = () => {
const { toast } = useToast();
const { const {
channels, channels,
listings, listings,
@@ -46,6 +51,9 @@ const ChannelManager = () => {
disconnectChannel, disconnectChannel,
syncChannel, syncChannel,
createListing, createListing,
updateListing,
cancelReservation,
loadStats,
clearError clearError
} = useChannelManager(); } = useChannelManager();
@@ -53,6 +61,48 @@ const ChannelManager = () => {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [filterType, setFilterType] = useState('all'); const [filterType, setFilterType] = useState('all');
const [showConnectModal, setShowConnectModal] = useState(false); const [showConnectModal, setShowConnectModal] = useState(false);
const [showListingModal, setShowListingModal] = useState(false);
const [showReservationModal, setShowReservationModal] = useState(false);
const [selectedReservation, setSelectedReservation] = useState<Reservation | null>(null);
const handleConnectChannel = async (data: any) => {
const success = await connectChannel(data);
if (success) {
toast({
title: 'Canal conectado',
description: 'El canal se ha conectado exitosamente',
});
} else {
toast({
title: 'Error',
description: 'No se pudo conectar el canal',
variant: 'destructive',
});
}
return success;
};
const handleCreateListing = async (data: any) => {
const success = await createListing(data);
if (success) {
toast({
title: 'Propiedad creada',
description: 'La propiedad se ha creado exitosamente',
});
} else {
toast({
title: 'Error',
description: 'No se pudo crear la propiedad',
variant: 'destructive',
});
}
return success;
};
const handleViewReservation = (reservation: Reservation) => {
setSelectedReservation(reservation);
setShowReservationModal(true);
};
const getStatusIcon = (status: string) => { const getStatusIcon = (status: string) => {
switch (status) { switch (status) {
@@ -216,63 +266,79 @@ const ChannelManager = () => {
</Button> </Button>
</div> </div>
<div className="grid gap-4"> {channels.length === 0 ? (
{channels.map((channel) => ( <Card>
<Card key={channel.id}> <CardContent className="p-12 text-center">
<CardContent className="p-6"> <Globe className="w-16 h-16 mx-auto text-muted-foreground mb-4" />
<div className="flex items-center justify-between"> <h3 className="text-lg font-semibold mb-2">No hay canales conectados</h3>
<div className="flex items-center space-x-4"> <p className="text-muted-foreground mb-4">
<div className="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center"> Conecta tu primer canal de distribución para comenzar a sincronizar reservas
<Globe className="w-6 h-6 text-primary" /> </p>
</div> <Button onClick={() => setShowConnectModal(true)}>
<div> <Plus className="w-4 h-4 mr-2" />
<div className="flex items-center space-x-2"> Conectar Primer Canal
<h3 className="text-lg font-semibold">{channel.name}</h3> </Button>
{getStatusIcon(channel.status)} </CardContent>
<Badge variant="outline">{channel.type}</Badge> </Card>
) : (
<div className="grid gap-4">
{channels.map((channel) => (
<Card key={channel.id}>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center">
<Globe className="w-6 h-6 text-primary" />
</div>
<div>
<div className="flex items-center space-x-2">
<h3 className="text-lg font-semibold">{channel.name}</h3>
{getStatusIcon(channel.status)}
<Badge variant="outline">{channel.type}</Badge>
</div>
<p className="text-sm text-muted-foreground">
Última sincronización: {format(new Date(channel.lastSync), 'dd/MM/yyyy HH:mm', { locale: es })}
</p>
<p className="text-sm text-muted-foreground">
{channel.properties.length} propiedades conectadas
</p>
</div> </div>
<p className="text-sm text-muted-foreground">
Última sincronización: {format(new Date(channel.lastSync), 'dd/MM/yyyy HH:mm', { locale: es })}
</p>
<p className="text-sm text-muted-foreground">
{channel.properties.length} propiedades conectadas
</p>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="text-right">
<p className="text-2xl font-bold">${channel.revenue.toLocaleString()}</p>
<p className="text-sm text-muted-foreground">{channel.bookings} reservas</p>
<p className="text-xs text-muted-foreground">Comisión: {channel.commission}%</p>
</div> </div>
<div className="flex space-x-2"> <div className="flex items-center space-x-4">
<Button <div className="text-right">
variant="outline" <p className="text-2xl font-bold">${channel.revenue.toLocaleString()}</p>
size="sm" <p className="text-sm text-muted-foreground">{channel.bookings} reservas</p>
onClick={() => syncChannel(channel.id)} <p className="text-xs text-muted-foreground">Comisión: {channel.commission}%</p>
disabled={loading} </div>
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} /> <div className="flex space-x-2">
</Button> <Button
<Button variant="outline" size="sm"> variant="outline"
<Settings className="w-4 h-4" /> size="sm"
</Button> onClick={() => syncChannel(channel.id)}
<Button disabled={loading}
variant="outline" >
size="sm" <RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
onClick={() => disconnectChannel(channel.id)} </Button>
> <Button variant="outline" size="sm">
<XCircle className="w-4 h-4" /> <Settings className="w-4 h-4" />
</Button> </Button>
<Button
variant="outline"
size="sm"
onClick={() => disconnectChannel(channel.id)}
>
<XCircle className="w-4 h-4" />
</Button>
</div>
</div> </div>
</div> </div>
</div> </CardContent>
</CardContent> </Card>
</Card> ))}
))} </div>
</div> )}
</div> </div>
); );
@@ -290,7 +356,7 @@ const ChannelManager = () => {
<h2 className="text-2xl font-bold">Gestión de Propiedades</h2> <h2 className="text-2xl font-bold">Gestión de Propiedades</h2>
<p className="text-muted-foreground">Administra hoteles, restaurantes, vehículos y más</p> <p className="text-muted-foreground">Administra hoteles, restaurantes, vehículos y más</p>
</div> </div>
<Button> <Button onClick={() => setShowListingModal(true)}>
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Nueva Propiedad Nueva Propiedad
</Button> </Button>
@@ -362,13 +428,13 @@ const ChannelManager = () => {
</div> </div>
<div className="flex space-x-2"> <div className="flex space-x-2">
<Button variant="outline" size="sm"> <Button variant="outline" size="sm" title="Ver detalles">
<Eye className="w-4 h-4" /> <Eye className="w-4 h-4" />
</Button> </Button>
<Button variant="outline" size="sm"> <Button variant="outline" size="sm" title="Editar">
<Edit className="w-4 h-4" /> <Edit className="w-4 h-4" />
</Button> </Button>
<Button variant="outline" size="sm"> <Button variant="outline" size="sm" title="Más opciones">
<MoreHorizontal className="w-4 h-4" /> <MoreHorizontal className="w-4 h-4" />
</Button> </Button>
</div> </div>
@@ -431,13 +497,18 @@ const ChannelManager = () => {
</div> </div>
<div className="flex space-x-2"> <div className="flex space-x-2">
<Button variant="outline" size="sm"> <Button
variant="outline"
size="sm"
onClick={() => handleViewReservation(reservation)}
title="Ver detalles"
>
<Eye className="w-4 h-4" /> <Eye className="w-4 h-4" />
</Button> </Button>
<Button variant="outline" size="sm"> <Button variant="outline" size="sm" title="Editar">
<Edit className="w-4 h-4" /> <Edit className="w-4 h-4" />
</Button> </Button>
<Button variant="outline" size="sm"> <Button variant="outline" size="sm" title="Más opciones">
<MoreHorizontal className="w-4 h-4" /> <MoreHorizontal className="w-4 h-4" />
</Button> </Button>
</div> </div>
@@ -450,46 +521,37 @@ const ChannelManager = () => {
</div> </div>
); );
if (error) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<AlertTriangle className="w-12 h-12 text-yellow-500 mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">Error al cargar datos</h3>
<p className="text-muted-foreground mb-4">{error}</p>
<Button onClick={clearError}>Reintentar</Button>
</div>
</div>
);
}
return ( return (
<div className="container mx-auto p-6 space-y-6"> <div className="p-6">
<div className="flex items-center justify-between"> <div className="mb-6">
<div> <h1 className="text-3xl font-bold">Channel Manager</h1>
<h1 className="text-3xl font-bold">Channel Manager</h1> <p className="text-muted-foreground">
<p className="text-muted-foreground"> Gestiona todos tus canales de distribución, propiedades y reservas desde un solo lugar
Gestiona todas tus propiedades y canales de distribución desde un solo lugar </p>
</p>
</div>
<div className="flex space-x-2">
<Button variant="outline">
<RefreshCw className="w-4 h-4 mr-2" />
Sincronizar Todo
</Button>
<Button>
<Settings className="w-4 h-4 mr-2" />
Configuración
</Button>
</div>
</div> </div>
<Tabs value={activeTab} onValueChange={setActiveTab}> <Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList className="grid w-full grid-cols-4"> <TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="overview">Resumen</TabsTrigger> <TabsTrigger value="overview">
<TabsTrigger value="channels">Canales</TabsTrigger> <TrendingUp className="w-4 h-4 mr-2" />
<TabsTrigger value="listings">Propiedades</TabsTrigger> Resumen
<TabsTrigger value="reservations">Reservas</TabsTrigger> </TabsTrigger>
<TabsTrigger value="channels">
<Zap className="w-4 h-4 mr-2" />
Canales
</TabsTrigger>
<TabsTrigger value="listings">
<Hotel className="w-4 h-4 mr-2" />
Propiedades
</TabsTrigger>
<TabsTrigger value="reservations">
<Calendar className="w-4 h-4 mr-2" />
Reservas
</TabsTrigger>
<TabsTrigger value="analytics">
<BarChart3 className="w-4 h-4 mr-2" />
Analytics
</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="overview"> <TabsContent value="overview">
@@ -507,7 +569,33 @@ const ChannelManager = () => {
<TabsContent value="reservations"> <TabsContent value="reservations">
<ReservationsTab /> <ReservationsTab />
</TabsContent> </TabsContent>
<TabsContent value="analytics">
<AnalyticsTab stats={stats} onDateRangeChange={loadStats} />
</TabsContent>
</Tabs> </Tabs>
{/* Modals */}
<ChannelConnectionModal
open={showConnectModal}
onClose={() => setShowConnectModal(false)}
onConnect={handleConnectChannel}
/>
<ListingFormModal
open={showListingModal}
onClose={() => setShowListingModal(false)}
onCreate={handleCreateListing}
onUpdate={updateListing}
channels={channels}
/>
<ReservationDetailsModal
open={showReservationModal}
onClose={() => setShowReservationModal(false)}
reservation={selectedReservation}
onCancel={cancelReservation}
/>
</div> </div>
); );
}; };