feat: Complete Channel Manager
This commit is contained in:
@@ -14,7 +14,6 @@ import {
|
||||
DollarSign,
|
||||
Plus,
|
||||
Search,
|
||||
Filter,
|
||||
MoreHorizontal,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
@@ -22,19 +21,25 @@ import {
|
||||
RefreshCw,
|
||||
Eye,
|
||||
Edit,
|
||||
Trash2,
|
||||
Hotel,
|
||||
Utensils,
|
||||
Car,
|
||||
Plane,
|
||||
MapPin,
|
||||
Globe
|
||||
Globe,
|
||||
BarChart3
|
||||
} from 'lucide-react';
|
||||
import { useChannelManager } from '@/hooks/useChannelManager';
|
||||
import { useChannelManager, Reservation } from '@/hooks/useChannelManager';
|
||||
import { format } from 'date-fns';
|
||||
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 { toast } = useToast();
|
||||
const {
|
||||
channels,
|
||||
listings,
|
||||
@@ -46,6 +51,9 @@ const ChannelManager = () => {
|
||||
disconnectChannel,
|
||||
syncChannel,
|
||||
createListing,
|
||||
updateListing,
|
||||
cancelReservation,
|
||||
loadStats,
|
||||
clearError
|
||||
} = useChannelManager();
|
||||
|
||||
@@ -53,6 +61,48 @@ const ChannelManager = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [filterType, setFilterType] = useState('all');
|
||||
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) => {
|
||||
switch (status) {
|
||||
@@ -216,63 +266,79 @@ const ChannelManager = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{channels.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="p-12 text-center">
|
||||
<Globe className="w-16 h-16 mx-auto text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">No hay canales conectados</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Conecta tu primer canal de distribución para comenzar a sincronizar reservas
|
||||
</p>
|
||||
<Button onClick={() => setShowConnectModal(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Conectar Primer Canal
|
||||
</Button>
|
||||
</CardContent>
|
||||
</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>
|
||||
<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 className="flex space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => syncChannel(channel.id)}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Settings className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => disconnectChannel(channel.id)}
|
||||
>
|
||||
<XCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
<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 className="flex space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => syncChannel(channel.id)}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Settings className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => disconnectChannel(channel.id)}
|
||||
>
|
||||
<XCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -290,7 +356,7 @@ const ChannelManager = () => {
|
||||
<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>
|
||||
</div>
|
||||
<Button>
|
||||
<Button onClick={() => setShowListingModal(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Nueva Propiedad
|
||||
</Button>
|
||||
@@ -362,13 +428,13 @@ const ChannelManager = () => {
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Button variant="outline" size="sm" title="Editar">
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Button variant="outline" size="sm" title="Más opciones">
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -431,13 +497,18 @@ const ChannelManager = () => {
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Button variant="outline" size="sm" title="Editar">
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Button variant="outline" size="sm" title="Más opciones">
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -450,46 +521,37 @@ const ChannelManager = () => {
|
||||
</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 (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Channel Manager</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Gestiona todas tus propiedades y canales de distribución desde un solo lugar
|
||||
</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 className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold">Channel Manager</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Gestiona todos tus canales de distribución, propiedades y reservas desde un solo lugar
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="overview">Resumen</TabsTrigger>
|
||||
<TabsTrigger value="channels">Canales</TabsTrigger>
|
||||
<TabsTrigger value="listings">Propiedades</TabsTrigger>
|
||||
<TabsTrigger value="reservations">Reservas</TabsTrigger>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-5">
|
||||
<TabsTrigger value="overview">
|
||||
<TrendingUp className="w-4 h-4 mr-2" />
|
||||
Resumen
|
||||
</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>
|
||||
|
||||
<TabsContent value="overview">
|
||||
@@ -507,9 +569,35 @@ const ChannelManager = () => {
|
||||
<TabsContent value="reservations">
|
||||
<ReservationsTab />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="analytics">
|
||||
<AnalyticsTab stats={stats} onDateRangeChange={loadStats} />
|
||||
</TabsContent>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelManager;
|
||||
export default ChannelManager;
|
||||
|
||||
Reference in New Issue
Block a user