Add Commerce module and enhance Admin Panel sections
This commit is contained in:
@@ -54,7 +54,8 @@ import {
|
||||
Shield,
|
||||
Radio,
|
||||
Sparkles,
|
||||
Leaf
|
||||
Leaf,
|
||||
Store
|
||||
} from 'lucide-react';
|
||||
|
||||
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
@@ -159,6 +160,7 @@ const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
{ icon: Shield, label: 'Security', path: '/dashboard/security' },
|
||||
{ icon: Car, label: 'Vehicle Management', path: '/dashboard/vehicle-management' },
|
||||
{ icon: Leaf, label: 'Sustainability', path: '/dashboard/sustainability' },
|
||||
{ icon: Store, label: 'Comercios', path: '/dashboard/establishments' },
|
||||
{ icon: Wallet, label: 'Wallet', path: '/dashboard/wallet' },
|
||||
{ icon: MessageSquare, label: 'Message', path: '/dashboard/messages', badge: '2' },
|
||||
];
|
||||
|
||||
@@ -539,9 +539,93 @@ const ContentTab: React.FC<ContentTabProps> = ({ isSuperAdmin, activeSubTab = 'd
|
||||
<TabsContent value="guides" className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold">Gestión de Guías Turísticos</h3>
|
||||
<Badge variant="secondary" className="text-sm">
|
||||
{Array.isArray(guides) ? guides.length : 0} Guías Registrados
|
||||
</Badge>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="flex items-center gap-2">
|
||||
<Plus className="w-4 h-4" />
|
||||
Nuevo Guía
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Crear Nuevo Guía</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<div>
|
||||
<Label>Número de Licencia</Label>
|
||||
<Input
|
||||
value={newGuide.licenseNumber}
|
||||
onChange={(e) => setNewGuide({ ...newGuide, licenseNumber: e.target.value })}
|
||||
placeholder="Ej: GUIDE-2024-001"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Especialidades (separadas por coma)</Label>
|
||||
<Input
|
||||
value={newGuide.specialties.join(', ')}
|
||||
onChange={(e) => setNewGuide({
|
||||
...newGuide,
|
||||
specialties: e.target.value.split(',').map(s => s.trim())
|
||||
})}
|
||||
placeholder="Historia, Naturaleza, Aventura"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Idiomas (separados por coma)</Label>
|
||||
<Input
|
||||
value={newGuide.languages.join(', ')}
|
||||
onChange={(e) => setNewGuide({
|
||||
...newGuide,
|
||||
languages: e.target.value.split(',').map(l => l.trim())
|
||||
})}
|
||||
placeholder="Español, Inglés, Francés"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Tarifa por Hora ($)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={newGuide.hourlyRate}
|
||||
onChange={(e) => setNewGuide({ ...newGuide, hourlyRate: parseFloat(e.target.value) || 0 })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Tarifa Diaria ($)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={newGuide.dailyRate}
|
||||
onChange={(e) => setNewGuide({ ...newGuide, dailyRate: parseFloat(e.target.value) || 0 })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Biografía</Label>
|
||||
<Textarea
|
||||
value={newGuide.bio}
|
||||
onChange={(e) => setNewGuide({ ...newGuide, bio: e.target.value })}
|
||||
placeholder="Experiencia y descripción del guía..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={async () => {
|
||||
try {
|
||||
await apiClient.post('/tourism/guides', {
|
||||
...newGuide,
|
||||
userId: 1
|
||||
});
|
||||
setNewGuide({ licenseNumber: '', specialties: [''], languages: [''], hourlyRate: 0, dailyRate: 0, bio: '', certifications: {} });
|
||||
loadData();
|
||||
toast({ title: 'Guía creado', description: 'El guía se creó correctamente.' });
|
||||
} catch (error: any) {
|
||||
toast({ title: 'Error', description: error?.message || 'No se pudo crear el guía.' });
|
||||
}
|
||||
}}>
|
||||
Crear Guía
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
@@ -565,6 +649,32 @@ const ContentTab: React.FC<ContentTabProps> = ({ isSuperAdmin, activeSubTab = 'd
|
||||
<Badge variant={guide.isVerified ? "default" : "secondary"}>
|
||||
{guide.isVerified ? 'Verificado' : 'Pendiente'}
|
||||
</Badge>
|
||||
{isSuperAdmin && (
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => console.log('Edit guide:', guide.id)}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await apiClient.delete(`/tourism/guides/${guide.id}`);
|
||||
loadData();
|
||||
toast({ title: 'Guía eliminado', description: 'Se eliminó correctamente.' });
|
||||
} catch (error) {
|
||||
toast({ title: 'Error', description: 'No se pudo eliminar el guía.' });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -589,9 +699,88 @@ const ContentTab: React.FC<ContentTabProps> = ({ isSuperAdmin, activeSubTab = 'd
|
||||
<TabsContent value="taxis" className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold">Gestión de Taxis</h3>
|
||||
<Badge variant="secondary" className="text-sm">
|
||||
{Array.isArray(taxis) ? taxis.length : 0} Taxis Disponibles
|
||||
</Badge>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="flex items-center gap-2">
|
||||
<Plus className="w-4 h-4" />
|
||||
Nuevo Taxi
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Registrar Nuevo Taxi</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<div>
|
||||
<Label>Número de Licencia</Label>
|
||||
<Input
|
||||
value={newTaxi.licenseNumber}
|
||||
onChange={(e) => setNewTaxi({ ...newTaxi, licenseNumber: e.target.value })}
|
||||
placeholder="Ej: TAXI-2024-001"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Placa del Vehículo</Label>
|
||||
<Input
|
||||
value={newTaxi.vehiclePlate}
|
||||
onChange={(e) => setNewTaxi({ ...newTaxi, vehiclePlate: e.target.value })}
|
||||
placeholder="ABC-123"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Modelo</Label>
|
||||
<Input
|
||||
value={newTaxi.vehicleModel}
|
||||
onChange={(e) => setNewTaxi({ ...newTaxi, vehicleModel: e.target.value })}
|
||||
placeholder="Toyota Corolla"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label>Año</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={newTaxi.vehicleYear}
|
||||
onChange={(e) => setNewTaxi({ ...newTaxi, vehicleYear: parseInt(e.target.value) || new Date().getFullYear() })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Color</Label>
|
||||
<Input
|
||||
value={newTaxi.vehicleColor}
|
||||
onChange={(e) => setNewTaxi({ ...newTaxi, vehicleColor: e.target.value })}
|
||||
placeholder="Blanco"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Capacidad</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={newTaxi.vehicleCapacity}
|
||||
onChange={(e) => setNewTaxi({ ...newTaxi, vehicleCapacity: parseInt(e.target.value) || 4 })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={async () => {
|
||||
try {
|
||||
await apiClient.post('/tourism/taxis', {
|
||||
...newTaxi,
|
||||
userId: 1 // You should get this from auth
|
||||
});
|
||||
setNewTaxi({ licenseNumber: '', vehiclePlate: '', vehicleModel: '', vehicleYear: new Date().getFullYear(), vehicleColor: '', vehicleCapacity: 4 });
|
||||
loadData();
|
||||
toast({ title: 'Taxi registrado', description: 'El taxi se registró correctamente.' });
|
||||
} catch (error: any) {
|
||||
toast({ title: 'Error', description: error?.message || 'No se pudo registrar el taxi.' });
|
||||
}
|
||||
}}>
|
||||
Registrar Taxi
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
@@ -615,6 +804,32 @@ const ContentTab: React.FC<ContentTabProps> = ({ isSuperAdmin, activeSubTab = 'd
|
||||
<Badge variant={taxi.isAvailable ? "default" : "secondary"}>
|
||||
{taxi.isAvailable ? 'Disponible' : 'Ocupado'}
|
||||
</Badge>
|
||||
{isSuperAdmin && (
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => console.log('Edit taxi:', taxi.id)}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await apiClient.delete(`/tourism/taxis/${taxi.id}`);
|
||||
loadData();
|
||||
toast({ title: 'Taxi eliminado', description: 'Se eliminó correctamente.' });
|
||||
} catch (error) {
|
||||
toast({ title: 'Error', description: 'No se pudo eliminar el taxi.' });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
85
src/components/establishments/BusinessAnalytics.tsx
Normal file
85
src/components/establishments/BusinessAnalytics.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { TrendingUp, Users, DollarSign, Eye } from 'lucide-react';
|
||||
|
||||
const BusinessAnalytics = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Comercios</CardTitle>
|
||||
<TrendingUp className="h-4 w-4 text-blue-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">769</div>
|
||||
<p className="text-xs text-gray-600 mt-1">+12% este mes</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Visitas Totales</CardTitle>
|
||||
<Eye className="h-4 w-4 text-purple-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">45.2K</div>
|
||||
<p className="text-xs text-gray-600 mt-1">Este mes</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Usuarios Activos</CardTitle>
|
||||
<Users className="h-4 w-4 text-green-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">2,345</div>
|
||||
<p className="text-xs text-gray-600 mt-1">Propietarios</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Ingresos</CardTitle>
|
||||
<DollarSign className="h-4 w-4 text-orange-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">$124K</div>
|
||||
<p className="text-xs text-gray-600 mt-1">Este mes</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Comercios Más Populares</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ name: 'Restaurante El Faro', visits: '2.4K', category: 'Restaurante' },
|
||||
{ name: 'Hotel Paradise', visits: '1.8K', category: 'Hotel' },
|
||||
{ name: 'Boutique Fashion', visits: '1.5K', category: 'Tienda' },
|
||||
{ name: 'Spa Wellness', visits: '1.2K', category: 'Servicios' },
|
||||
{ name: 'Club Nocturno', visits: '980', category: 'Entretenimiento' }
|
||||
].map((item, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between border-b pb-2">
|
||||
<div>
|
||||
<p className="font-medium">{item.name}</p>
|
||||
<p className="text-sm text-gray-600">{item.category}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-semibold">{item.visits}</p>
|
||||
<p className="text-xs text-gray-600">visitas</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusinessAnalytics;
|
||||
47
src/components/establishments/BusinessCategories.tsx
Normal file
47
src/components/establishments/BusinessCategories.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { UtensilsCrossed, Hotel, ShoppingBag, Music, Wrench, Building } from 'lucide-react';
|
||||
|
||||
const categories = [
|
||||
{ id: 1, name: 'Restaurantes', icon: UtensilsCrossed, count: 145, color: 'bg-orange-100 text-orange-600' },
|
||||
{ id: 2, name: 'Hoteles', icon: Hotel, count: 67, color: 'bg-blue-100 text-blue-600' },
|
||||
{ id: 3, name: 'Tiendas', icon: ShoppingBag, count: 234, color: 'bg-purple-100 text-purple-600' },
|
||||
{ id: 4, name: 'Entretenimiento', icon: Music, count: 89, color: 'bg-pink-100 text-pink-600' },
|
||||
{ id: 5, name: 'Servicios', icon: Wrench, count: 156, color: 'bg-green-100 text-green-600' },
|
||||
{ id: 6, name: 'Otros', icon: Building, count: 78, color: 'bg-gray-100 text-gray-600' }
|
||||
];
|
||||
|
||||
const BusinessCategories = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{categories.map((category) => {
|
||||
const Icon = category.icon;
|
||||
return (
|
||||
<Card key={category.id} className="hover:shadow-lg transition-shadow cursor-pointer">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-12 h-12 rounded-lg ${category.color} flex items-center justify-center`}>
|
||||
<Icon className="h-6 w-6" />
|
||||
</div>
|
||||
<CardTitle className="text-lg">{category.name}</CardTitle>
|
||||
</div>
|
||||
<Badge variant="secondary">{category.count}</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-600">
|
||||
{category.count} establecimientos registrados en esta categoría
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusinessCategories;
|
||||
306
src/components/establishments/BusinessList.tsx
Normal file
306
src/components/establishments/BusinessList.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardDescription, 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 { Textarea } from '@/components/ui/textarea';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Store, Plus, Edit, Trash2, MapPin, Phone, Globe, Clock, Star, Search } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { apiClient } from '@/services/adminApi';
|
||||
|
||||
const BusinessList = () => {
|
||||
const [businesses, setBusinesses] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const { toast } = useToast();
|
||||
|
||||
const [newBusiness, setNewBusiness] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
category: '',
|
||||
address: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
website: '',
|
||||
coordinates: { x: 0, y: 0 },
|
||||
openingHours: {
|
||||
monday: '9:00-18:00',
|
||||
tuesday: '9:00-18:00',
|
||||
wednesday: '9:00-18:00',
|
||||
thursday: '9:00-18:00',
|
||||
friday: '9:00-18:00',
|
||||
saturday: '10:00-14:00',
|
||||
sunday: 'Cerrado'
|
||||
},
|
||||
images: ['']
|
||||
});
|
||||
|
||||
const loadBusinesses = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await apiClient.get('/establishments?page=1&limit=50');
|
||||
setBusinesses(Array.isArray(data) ? data : (data as any)?.establishments || []);
|
||||
} catch (error) {
|
||||
console.error('Error loading businesses:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadBusinesses();
|
||||
}, []);
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
await apiClient.post('/establishments', {
|
||||
...newBusiness,
|
||||
coordinates: `(${newBusiness.coordinates.x},${newBusiness.coordinates.y})`,
|
||||
images: newBusiness.images.filter(img => img.trim() !== '')
|
||||
});
|
||||
setNewBusiness({
|
||||
name: '', description: '', category: '', address: '', phone: '', email: '', website: '',
|
||||
coordinates: { x: 0, y: 0 }, images: [''],
|
||||
openingHours: {
|
||||
monday: '9:00-18:00', tuesday: '9:00-18:00', wednesday: '9:00-18:00',
|
||||
thursday: '9:00-18:00', friday: '9:00-18:00', saturday: '10:00-14:00', sunday: 'Cerrado'
|
||||
}
|
||||
});
|
||||
loadBusinesses();
|
||||
toast({ title: 'Comercio creado', description: 'El establecimiento se creó correctamente.' });
|
||||
} catch (error: any) {
|
||||
toast({ title: 'Error', description: error?.message || 'No se pudo crear el comercio.' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await apiClient.delete(`/establishments/${id}`);
|
||||
loadBusinesses();
|
||||
toast({ title: 'Comercio eliminado', description: 'Se eliminó correctamente.' });
|
||||
} catch (error) {
|
||||
toast({ title: 'Error', description: 'No se pudo eliminar el comercio.' });
|
||||
}
|
||||
};
|
||||
|
||||
const filteredBusinesses = businesses.filter(b =>
|
||||
b.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
b.category?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="Buscar comercios..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Nuevo Comercio
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Registrar Nuevo Comercio</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Nombre del Comercio</Label>
|
||||
<Input
|
||||
value={newBusiness.name}
|
||||
onChange={(e) => setNewBusiness({ ...newBusiness, name: e.target.value })}
|
||||
placeholder="Restaurante El Caribe"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Categoría</Label>
|
||||
<Select value={newBusiness.category} onValueChange={(v) => setNewBusiness({ ...newBusiness, category: v })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecciona categoría" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="restaurant">Restaurante</SelectItem>
|
||||
<SelectItem value="hotel">Hotel</SelectItem>
|
||||
<SelectItem value="store">Tienda</SelectItem>
|
||||
<SelectItem value="entertainment">Entretenimiento</SelectItem>
|
||||
<SelectItem value="services">Servicios</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Descripción</Label>
|
||||
<Textarea
|
||||
value={newBusiness.description}
|
||||
onChange={(e) => setNewBusiness({ ...newBusiness, description: e.target.value })}
|
||||
placeholder="Describe el comercio..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Dirección</Label>
|
||||
<Input
|
||||
value={newBusiness.address}
|
||||
onChange={(e) => setNewBusiness({ ...newBusiness, address: e.target.value })}
|
||||
placeholder="Calle Principal #123"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Teléfono</Label>
|
||||
<Input
|
||||
value={newBusiness.phone}
|
||||
onChange={(e) => setNewBusiness({ ...newBusiness, phone: e.target.value })}
|
||||
placeholder="+1 809-555-0123"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Email</Label>
|
||||
<Input
|
||||
type="email"
|
||||
value={newBusiness.email}
|
||||
onChange={(e) => setNewBusiness({ ...newBusiness, email: e.target.value })}
|
||||
placeholder="contacto@negocio.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Sitio Web</Label>
|
||||
<Input
|
||||
value={newBusiness.website}
|
||||
onChange={(e) => setNewBusiness({ ...newBusiness, website: e.target.value })}
|
||||
placeholder="https://www.negocio.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Latitud</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="any"
|
||||
value={newBusiness.coordinates.y}
|
||||
onChange={(e) => setNewBusiness({
|
||||
...newBusiness,
|
||||
coordinates: { ...newBusiness.coordinates, y: parseFloat(e.target.value) || 0 }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Longitud</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="any"
|
||||
value={newBusiness.coordinates.x}
|
||||
onChange={(e) => setNewBusiness({
|
||||
...newBusiness,
|
||||
coordinates: { ...newBusiness.coordinates, x: parseFloat(e.target.value) || 0 }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>URLs de Imágenes (separadas por coma)</Label>
|
||||
<Textarea
|
||||
value={newBusiness.images.join(', ')}
|
||||
onChange={(e) => setNewBusiness({
|
||||
...newBusiness,
|
||||
images: e.target.value.split(',').map(url => url.trim())
|
||||
})}
|
||||
placeholder="https://ejemplo.com/imagen1.jpg, https://ejemplo.com/imagen2.jpg"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleCreate} className="w-full">
|
||||
Crear Comercio
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{filteredBusinesses.map((business) => (
|
||||
<Card key={business.id} className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 rounded-lg bg-orange-100 flex items-center justify-center">
|
||||
<Store className="h-8 w-8 text-orange-600" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>{business.name}</CardTitle>
|
||||
<CardDescription className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="outline">{business.category}</Badge>
|
||||
{business.isVerified && (
|
||||
<Badge className="bg-green-100 text-green-800">Verificado</Badge>
|
||||
)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="flex items-center gap-1">
|
||||
<Star className="h-3 w-3 fill-yellow-500 text-yellow-500" />
|
||||
{business.rating || '4.5'}
|
||||
</Badge>
|
||||
<Button variant="outline" size="sm" onClick={() => console.log('Edit:', business.id)}>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => handleDelete(business.id)}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-600 mb-4">{business.description}</p>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<MapPin className="h-4 w-4" />
|
||||
{business.address || 'Sin dirección'}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<Phone className="h-4 w-4" />
|
||||
{business.phone || 'Sin teléfono'}
|
||||
</div>
|
||||
{business.website && (
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<Globe className="h-4 w-4" />
|
||||
<a href={business.website} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
||||
Sitio web
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<Clock className="h-4 w-4" />
|
||||
{business.isOpen ? 'Abierto' : 'Cerrado'}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusinessList;
|
||||
98
src/components/establishments/BusinessVerification.tsx
Normal file
98
src/components/establishments/BusinessVerification.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CheckCircle, XCircle, Clock, FileText } from 'lucide-react';
|
||||
|
||||
const pendingVerifications = [
|
||||
{ id: 1, name: 'Restaurante La Marina', category: 'Restaurante', submitted: '2025-10-08', documents: 3 },
|
||||
{ id: 2, name: 'Hotel Vista Mar', category: 'Hotel', submitted: '2025-10-09', documents: 5 },
|
||||
{ id: 3, name: 'Boutique Fashion', category: 'Tienda', submitted: '2025-10-10', documents: 2 }
|
||||
];
|
||||
|
||||
const BusinessVerification = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Pendientes</CardTitle>
|
||||
<Clock className="h-4 w-4 text-yellow-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">12</div>
|
||||
<p className="text-xs text-gray-600 mt-1">Solicitudes por revisar</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Aprobados</CardTitle>
|
||||
<CheckCircle className="h-4 w-4 text-green-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">245</div>
|
||||
<p className="text-xs text-gray-600 mt-1">Comercios verificados</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Rechazados</CardTitle>
|
||||
<XCircle className="h-4 w-4 text-red-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">8</div>
|
||||
<p className="text-xs text-gray-600 mt-1">Este mes</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Solicitudes Pendientes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{pendingVerifications.map((item) => (
|
||||
<div key={item.id} className="border rounded-lg p-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-lg bg-yellow-100 flex items-center justify-center">
|
||||
<FileText className="h-6 w-6 text-yellow-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold">{item.name}</h3>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="outline" className="text-xs">{item.category}</Badge>
|
||||
<span className="text-xs text-gray-600">
|
||||
Enviado: {item.submitted}
|
||||
</span>
|
||||
<span className="text-xs text-gray-600">
|
||||
{item.documents} documentos
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline">
|
||||
Revisar
|
||||
</Button>
|
||||
<Button size="sm" variant="default" className="bg-green-600 hover:bg-green-700">
|
||||
<CheckCircle className="h-4 w-4 mr-1" />
|
||||
Aprobar
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive">
|
||||
<XCircle className="h-4 w-4 mr-1" />
|
||||
Rechazar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusinessVerification;
|
||||
Reference in New Issue
Block a user