Continue with next module
This commit is contained in:
29
src/App.tsx
29
src/App.tsx
@@ -52,6 +52,10 @@ import PersonalizationPage from "./pages/dashboard/config/PersonalizationPage";
|
|||||||
// POLITUR pages
|
// POLITUR pages
|
||||||
import EmergencyDashboard from "./pages/dashboard/politur/EmergencyDashboard";
|
import EmergencyDashboard from "./pages/dashboard/politur/EmergencyDashboard";
|
||||||
import PolReports from "./pages/dashboard/politur/Reports";
|
import PolReports from "./pages/dashboard/politur/Reports";
|
||||||
|
// Guides pages
|
||||||
|
import GuideDashboard from "./pages/dashboard/guides/GuideDashboard";
|
||||||
|
import ItineraryBuilder from "./pages/dashboard/guides/ItineraryBuilder";
|
||||||
|
import ContentLibrary from "./pages/dashboard/guides/ContentLibrary";
|
||||||
// Commerce pages (for retail stores)
|
// Commerce pages (for retail stores)
|
||||||
import CommerceStore from "./pages/dashboard/commerce/Store";
|
import CommerceStore from "./pages/dashboard/commerce/Store";
|
||||||
import CommercePOS from "./pages/dashboard/commerce/POSTerminal";
|
import CommercePOS from "./pages/dashboard/commerce/POSTerminal";
|
||||||
@@ -590,6 +594,31 @@ const AppRouter = () => (
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
|
|
||||||
|
{/* Guides Routes */}
|
||||||
|
<Route path="/dashboard/guides" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<DashboardLayout>
|
||||||
|
<GuideDashboard />
|
||||||
|
</DashboardLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
|
<Route path="/dashboard/guides/itinerary" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<DashboardLayout>
|
||||||
|
<ItineraryBuilder />
|
||||||
|
</DashboardLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
|
<Route path="/dashboard/guides/library" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<DashboardLayout>
|
||||||
|
<ContentLibrary />
|
||||||
|
</DashboardLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
{/* Catch-all route */}
|
{/* Catch-all route */}
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ import {
|
|||||||
Leaf,
|
Leaf,
|
||||||
Store,
|
Store,
|
||||||
Server,
|
Server,
|
||||||
ShieldAlert
|
ShieldAlert,
|
||||||
|
UserCircle
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
@@ -186,6 +187,16 @@ const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
{ icon: FileText, label: 'Reportes', path: '/dashboard/politur/reports' }
|
{ icon: FileText, label: 'Reportes', path: '/dashboard/politur/reports' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: UserCircle,
|
||||||
|
label: 'Guías Turísticos',
|
||||||
|
path: '/dashboard/guides',
|
||||||
|
subItems: [
|
||||||
|
{ icon: Home, label: 'Dashboard', path: '/dashboard/guides' },
|
||||||
|
{ icon: BookOpen, label: 'Crear Itinerario', path: '/dashboard/guides/itinerary' },
|
||||||
|
{ icon: BookOpen, label: 'Biblioteca', path: '/dashboard/guides/library' }
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: Store,
|
icon: Store,
|
||||||
label: t('commerce'),
|
label: t('commerce'),
|
||||||
|
|||||||
271
src/pages/dashboard/guides/ContentLibrary.tsx
Normal file
271
src/pages/dashboard/guides/ContentLibrary.tsx
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { BookOpen, Search, Plus, Play, Download, Edit, Trash2, Volume2 } from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
|
||||||
|
interface ContentItem {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
type: 'historical' | 'cultural' | 'monument' | 'audio';
|
||||||
|
category: string;
|
||||||
|
description: string;
|
||||||
|
audioUrl?: string;
|
||||||
|
duration?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContentLibrary = () => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [contents] = useState<ContentItem[]>([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Historia de la Catedral Primada',
|
||||||
|
type: 'historical',
|
||||||
|
category: 'Arquitectura Colonial',
|
||||||
|
description: 'Primera catedral de América, construcción iniciada en 1514...',
|
||||||
|
audioUrl: '#',
|
||||||
|
duration: '3:45'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: 'Alcázar de Colón',
|
||||||
|
type: 'monument',
|
||||||
|
category: 'Monumentos',
|
||||||
|
description: 'Palacio virreinal construido entre 1510 y 1514...',
|
||||||
|
audioUrl: '#',
|
||||||
|
duration: '4:20'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
title: 'Cultura Taína',
|
||||||
|
type: 'cultural',
|
||||||
|
category: 'Cultura Precolombina',
|
||||||
|
description: 'Los taínos fueron los habitantes originales de La Española...',
|
||||||
|
audioUrl: '#',
|
||||||
|
duration: '5:15'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
title: 'Audio Guía: Zona Colonial',
|
||||||
|
type: 'audio',
|
||||||
|
category: 'Audio Guías',
|
||||||
|
description: 'Recorrido completo por la Zona Colonial de Santo Domingo',
|
||||||
|
audioUrl: '#',
|
||||||
|
duration: '12:30'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getTypeIcon = (type: string) => {
|
||||||
|
const icons = {
|
||||||
|
historical: '📚',
|
||||||
|
cultural: '🎭',
|
||||||
|
monument: '🏛️',
|
||||||
|
audio: '🎧'
|
||||||
|
};
|
||||||
|
return icons[type] || '📄';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTypeBadge = (type: string) => {
|
||||||
|
const variants = {
|
||||||
|
historical: 'default' as const,
|
||||||
|
cultural: 'secondary' as const,
|
||||||
|
monument: 'outline' as const,
|
||||||
|
audio: 'default' as const
|
||||||
|
};
|
||||||
|
return <Badge variant={variants[type]}>{type}</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredContents = contents.filter(content =>
|
||||||
|
content.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
content.category.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900">Biblioteca de Contenidos</h2>
|
||||||
|
<p className="text-gray-600">Gestiona tu contenido histórico y cultural</p>
|
||||||
|
</div>
|
||||||
|
<Button>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Nuevo Contenido
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium text-gray-600">
|
||||||
|
Total Contenidos
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-blue-600">
|
||||||
|
{contents.length}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium text-gray-600">
|
||||||
|
Audio Guías
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-purple-600">
|
||||||
|
{contents.filter(c => c.type === 'audio').length}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium text-gray-600">
|
||||||
|
Históricos
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-orange-600">
|
||||||
|
{contents.filter(c => c.type === 'historical').length}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium text-gray-600">
|
||||||
|
Monumentos
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-green-600">
|
||||||
|
{contents.filter(c => c.type === 'monument').length}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<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 contenido..."
|
||||||
|
className="pl-10"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline">
|
||||||
|
<Volume2 className="w-4 h-4 mr-2" />
|
||||||
|
Generar Audio con IA
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Tabs defaultValue="all" className="w-full">
|
||||||
|
<TabsList className="grid w-full grid-cols-5">
|
||||||
|
<TabsTrigger value="all">Todos</TabsTrigger>
|
||||||
|
<TabsTrigger value="historical">Histórico</TabsTrigger>
|
||||||
|
<TabsTrigger value="cultural">Cultural</TabsTrigger>
|
||||||
|
<TabsTrigger value="monument">Monumentos</TabsTrigger>
|
||||||
|
<TabsTrigger value="audio">Audio</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="all" className="space-y-3 mt-4">
|
||||||
|
{filteredContents.map((content) => (
|
||||||
|
<div
|
||||||
|
key={content.id}
|
||||||
|
className="border rounded-lg p-4 hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span className="text-3xl">{getTypeIcon(content.type)}</span>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<h3 className="font-semibold">{content.title}</h3>
|
||||||
|
{getTypeBadge(content.type)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600">{content.category}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{content.audioUrl && (
|
||||||
|
<Button size="sm" variant="outline">
|
||||||
|
<Play className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button size="sm" variant="outline">
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline">
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="ghost">
|
||||||
|
<Trash2 className="w-4 h-4 text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-700 mb-2">{content.description}</p>
|
||||||
|
|
||||||
|
{content.duration && (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||||
|
<Volume2 className="w-4 h-4" />
|
||||||
|
Duración: {content.duration}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{['historical', 'cultural', 'monument', 'audio'].map(type => (
|
||||||
|
<TabsContent key={type} value={type} className="space-y-3 mt-4">
|
||||||
|
{filteredContents
|
||||||
|
.filter(c => c.type === type)
|
||||||
|
.map((content) => (
|
||||||
|
<div
|
||||||
|
key={content.id}
|
||||||
|
className="border rounded-lg p-4 hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
{/* Same content structure as "all" tab */}
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span className="text-3xl">{getTypeIcon(content.type)}</span>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold mb-1">{content.title}</h3>
|
||||||
|
<p className="text-sm text-gray-600">{content.category}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{content.audioUrl && (
|
||||||
|
<Button size="sm" variant="outline">
|
||||||
|
<Play className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button size="sm" variant="outline">
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-700">{content.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</TabsContent>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContentLibrary;
|
||||||
278
src/pages/dashboard/guides/GuideDashboard.tsx
Normal file
278
src/pages/dashboard/guides/GuideDashboard.tsx
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { User, Calendar, MapPin, DollarSign, Star, BookOpen, TrendingUp } from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
|
interface Tour {
|
||||||
|
id: string;
|
||||||
|
touristName: string;
|
||||||
|
date: string;
|
||||||
|
time: string;
|
||||||
|
type: string;
|
||||||
|
status: 'pending' | 'confirmed' | 'completed' | 'cancelled';
|
||||||
|
price: number;
|
||||||
|
location: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GuideDashboard = () => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [tours] = useState<Tour[]>([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
touristName: 'John Smith',
|
||||||
|
date: '2024-01-20',
|
||||||
|
time: '09:00',
|
||||||
|
type: 'Zona Colonial Tour',
|
||||||
|
status: 'confirmed',
|
||||||
|
price: 75,
|
||||||
|
location: 'Santo Domingo'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
touristName: 'Maria Garcia',
|
||||||
|
date: '2024-01-20',
|
||||||
|
time: '14:00',
|
||||||
|
type: 'City Highlights',
|
||||||
|
status: 'pending',
|
||||||
|
price: 60,
|
||||||
|
location: 'Santo Domingo'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
touristName: 'Robert Johnson',
|
||||||
|
date: '2024-01-21',
|
||||||
|
time: '10:00',
|
||||||
|
type: 'Beach & Culture',
|
||||||
|
status: 'confirmed',
|
||||||
|
price: 90,
|
||||||
|
location: 'Boca Chica'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
upcomingTours: tours.filter(t => t.status === 'confirmed').length,
|
||||||
|
pendingRequests: tours.filter(t => t.status === 'pending').length,
|
||||||
|
monthEarnings: tours.filter(t => t.status === 'completed').reduce((acc, t) => acc + t.price, 0),
|
||||||
|
rating: 4.8,
|
||||||
|
totalReviews: 127
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = (status: string) => {
|
||||||
|
const variants = {
|
||||||
|
pending: 'secondary' as const,
|
||||||
|
confirmed: 'default' as const,
|
||||||
|
completed: 'outline' as const,
|
||||||
|
cancelled: 'destructive' as const
|
||||||
|
};
|
||||||
|
return <Badge variant={variants[status]}>{status}</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900">Panel de Guía Turístico</h2>
|
||||||
|
<p className="text-gray-600">Gestiona tus tours y disponibilidad</p>
|
||||||
|
</div>
|
||||||
|
<Button>
|
||||||
|
<Calendar className="w-4 h-4 mr-2" />
|
||||||
|
Ver Calendario
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium text-gray-600">
|
||||||
|
Tours Próximos
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-blue-600">
|
||||||
|
{stats.upcomingTours}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium text-gray-600">
|
||||||
|
Solicitudes
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-orange-600">
|
||||||
|
{stats.pendingRequests}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium text-gray-600">
|
||||||
|
Ganancias del Mes
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-green-600">
|
||||||
|
${stats.monthEarnings}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium text-gray-600">
|
||||||
|
Calificación
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="text-3xl font-bold text-yellow-600">
|
||||||
|
{stats.rating}
|
||||||
|
</div>
|
||||||
|
<Star className="w-6 h-6 fill-yellow-400 text-yellow-400" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium text-gray-600">
|
||||||
|
Reseñas
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-purple-600">
|
||||||
|
{stats.totalReviews}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
{/* Upcoming Tours */}
|
||||||
|
<Card className="lg:col-span-2">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Calendar className="w-5 h-5" />
|
||||||
|
Tours Próximos
|
||||||
|
</span>
|
||||||
|
<Button variant="outline" size="sm">Ver Todos</Button>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{tours.map((tour) => (
|
||||||
|
<div
|
||||||
|
key={tour.id}
|
||||||
|
className="border rounded-lg p-4 hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold">{tour.type}</h4>
|
||||||
|
<p className="text-sm text-gray-600">{tour.touristName}</p>
|
||||||
|
</div>
|
||||||
|
{getStatusBadge(tour.status)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-2 text-sm mb-3">
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-500">Fecha:</span>
|
||||||
|
<p className="font-medium">{new Date(tour.date).toLocaleDateString('es-ES')}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-500">Hora:</span>
|
||||||
|
<p className="font-medium">{tour.time}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-500">Precio:</span>
|
||||||
|
<p className="font-medium">${tour.price}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-600 mb-3">
|
||||||
|
<MapPin className="w-4 h-4" />
|
||||||
|
{tour.location}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tour.status === 'pending' && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={() => {
|
||||||
|
toast({
|
||||||
|
title: "Tour Confirmado",
|
||||||
|
description: "El tour ha sido aceptado exitosamente",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Aceptar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
Rechazar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tour.status === 'confirmed' && (
|
||||||
|
<Button size="sm" variant="outline" className="w-full">
|
||||||
|
Ver Detalles
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Acciones Rápidas</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Button className="w-full justify-start" variant="outline">
|
||||||
|
<Calendar className="w-4 h-4 mr-2" />
|
||||||
|
Gestionar Disponibilidad
|
||||||
|
</Button>
|
||||||
|
<Button className="w-full justify-start" variant="outline">
|
||||||
|
<BookOpen className="w-4 h-4 mr-2" />
|
||||||
|
Crear Itinerario
|
||||||
|
</Button>
|
||||||
|
<Button className="w-full justify-start" variant="outline">
|
||||||
|
<User className="w-4 h-4 mr-2" />
|
||||||
|
Editar Perfil
|
||||||
|
</Button>
|
||||||
|
<Button className="w-full justify-start" variant="outline">
|
||||||
|
<DollarSign className="w-4 h-4 mr-2" />
|
||||||
|
Ver Ganancias
|
||||||
|
</Button>
|
||||||
|
<Button className="w-full justify-start" variant="outline">
|
||||||
|
<Star className="w-4 h-4 mr-2" />
|
||||||
|
Mis Reseñas
|
||||||
|
</Button>
|
||||||
|
<Button className="w-full justify-start" variant="outline">
|
||||||
|
<TrendingUp className="w-4 h-4 mr-2" />
|
||||||
|
Estadísticas
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GuideDashboard;
|
||||||
263
src/pages/dashboard/guides/ItineraryBuilder.tsx
Normal file
263
src/pages/dashboard/guides/ItineraryBuilder.tsx
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Plus, MapPin, Clock, DollarSign, Save, Wand2, Image, Trash2 } from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
|
interface ItineraryStop {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
duration: number;
|
||||||
|
order: number;
|
||||||
|
image?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ItineraryBuilder = () => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [itineraryName, setItineraryName] = useState('');
|
||||||
|
const [totalPrice, setTotalPrice] = useState(75);
|
||||||
|
const [stops, setStops] = useState<ItineraryStop[]>([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'Catedral Primada de América',
|
||||||
|
description: 'Primera catedral construida en América (1514-1540)',
|
||||||
|
duration: 45,
|
||||||
|
order: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Alcázar de Colón',
|
||||||
|
description: 'Palacio virreinal construido en 1510',
|
||||||
|
duration: 60,
|
||||||
|
order: 2
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const addStop = () => {
|
||||||
|
const newStop: ItineraryStop = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
duration: 30,
|
||||||
|
order: stops.length + 1
|
||||||
|
};
|
||||||
|
setStops([...stops, newStop]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeStop = (id: string) => {
|
||||||
|
setStops(stops.filter(s => s.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateStop = (id: string, field: keyof ItineraryStop, value: any) => {
|
||||||
|
setStops(stops.map(s =>
|
||||||
|
s.id === id ? { ...s, [field]: value } : s
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateWithAI = () => {
|
||||||
|
toast({
|
||||||
|
title: "Generando con IA",
|
||||||
|
description: "Creando itinerario personalizado...",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveItinerary = () => {
|
||||||
|
toast({
|
||||||
|
title: "Itinerario Guardado",
|
||||||
|
description: "Tu itinerario ha sido guardado exitosamente",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalDuration = stops.reduce((acc, stop) => acc + stop.duration, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900">Constructor de Itinerarios</h2>
|
||||||
|
<p className="text-gray-600">Crea tours personalizados para tus clientes</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" onClick={generateWithAI}>
|
||||||
|
<Wand2 className="w-4 h-4 mr-2" />
|
||||||
|
Generar con IA
|
||||||
|
</Button>
|
||||||
|
<Button onClick={saveItinerary}>
|
||||||
|
<Save className="w-4 h-4 mr-2" />
|
||||||
|
Guardar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
{/* Itinerary Details */}
|
||||||
|
<Card className="lg:col-span-2">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Detalles del Itinerario</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">Nombre del Tour</label>
|
||||||
|
<Input
|
||||||
|
placeholder="Ej: Tour Histórico Zona Colonial"
|
||||||
|
value={itineraryName}
|
||||||
|
onChange={(e) => setItineraryName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">Precio (USD)</label>
|
||||||
|
<div className="relative">
|
||||||
|
<DollarSign className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
className="pl-10"
|
||||||
|
value={totalPrice}
|
||||||
|
onChange={(e) => setTotalPrice(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">Duración Total</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Clock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||||
|
<Input
|
||||||
|
value={`${Math.floor(totalDuration / 60)}h ${totalDuration % 60}m`}
|
||||||
|
className="pl-10"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t pt-4 mt-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="font-semibold">Paradas del Tour</h3>
|
||||||
|
<Button size="sm" variant="outline" onClick={addStop}>
|
||||||
|
<Plus className="w-4 h-4 mr-1" />
|
||||||
|
Agregar Parada
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{stops.map((stop, index) => (
|
||||||
|
<div key={stop.id} className="border rounded-lg p-4 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-8 h-8 bg-primary text-white rounded-full flex items-center justify-center font-semibold">
|
||||||
|
{index + 1}
|
||||||
|
</div>
|
||||||
|
<h4 className="font-medium">Parada {index + 1}</h4>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => removeStop(stop.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
placeholder="Nombre del lugar"
|
||||||
|
value={stop.name}
|
||||||
|
onChange={(e) => updateStop(stop.id, 'name', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Descripción e información histórica..."
|
||||||
|
value={stop.description}
|
||||||
|
onChange={(e) => updateStop(stop.id, 'description', e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-gray-600 mb-1 block">Duración (minutos)</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={stop.duration}
|
||||||
|
onChange={(e) => updateStop(stop.id, 'duration', Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-gray-600 mb-1 block">Imagen</label>
|
||||||
|
<Button size="sm" variant="outline" className="w-full">
|
||||||
|
<Image className="w-4 h-4 mr-1" />
|
||||||
|
Subir
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Preview */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Vista Previa</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="aspect-video bg-gray-200 rounded-lg flex items-center justify-center">
|
||||||
|
<Image className="w-12 h-12 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-lg mb-1">
|
||||||
|
{itineraryName || 'Nombre del Tour'}
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-4 text-sm text-gray-600">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
|
{Math.floor(totalDuration / 60)}h {totalDuration % 60}m
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<DollarSign className="w-4 h-4" />
|
||||||
|
${totalPrice}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t pt-4">
|
||||||
|
<h4 className="font-medium mb-3">Itinerario</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{stops.map((stop, index) => (
|
||||||
|
<div key={stop.id} className="flex gap-3">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="w-6 h-6 bg-primary text-white rounded-full flex items-center justify-center text-xs font-semibold">
|
||||||
|
{index + 1}
|
||||||
|
</div>
|
||||||
|
{index < stops.length - 1 && (
|
||||||
|
<div className="w-0.5 h-12 bg-gray-300 my-1" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 pb-4">
|
||||||
|
<h5 className="font-medium text-sm">{stop.name || 'Sin nombre'}</h5>
|
||||||
|
<p className="text-xs text-gray-600 line-clamp-2">{stop.description}</p>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">{stop.duration} min</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ItineraryBuilder;
|
||||||
Reference in New Issue
Block a user