Files
karibeo_backend_admin/src/pages/dashboard/Collections.tsx
2026-03-12 10:54:46 -04:00

519 lines
19 KiB
TypeScript

import { useState } from 'react';
import { useCollections } from '@/hooks/useCollections';
import {
FolderHeart,
Plus,
Trash2,
Edit2,
Eye,
Globe,
Lock,
RefreshCw,
Search,
MoreVertical,
Image as ImageIcon,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
const Collections = () => {
const {
collections,
stats,
loading,
error,
loadCollections,
createCollection,
updateCollection,
deleteCollection,
getCollectionById,
selectedCollection,
setSelectedCollection,
} = useCollections();
const [searchTerm, setSearchTerm] = useState('');
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [collectionToDelete, setCollectionToDelete] = useState<string | null>(null);
// Form state
const [formData, setFormData] = useState({
name: '',
description: '',
coverImageUrl: '',
color: '#3b82f6',
isPublic: false,
});
const filteredCollections = collections.filter(col =>
col.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
col.description?.toLowerCase().includes(searchTerm.toLowerCase())
);
const handleRefresh = async () => {
await loadCollections();
};
const handleCreate = async () => {
const result = await createCollection(formData);
if (result) {
setIsCreateDialogOpen(false);
resetForm();
}
};
const handleEdit = async () => {
if (!selectedCollection) return;
const success = await updateCollection(selectedCollection.id, formData);
if (success) {
setIsEditDialogOpen(false);
resetForm();
}
};
const handleDelete = async () => {
if (!collectionToDelete) return;
const success = await deleteCollection(collectionToDelete);
if (success) {
setIsDeleteDialogOpen(false);
setCollectionToDelete(null);
}
};
const openEditDialog = async (collectionId: string) => {
const collection = await getCollectionById(collectionId);
if (collection) {
setFormData({
name: collection.name,
description: collection.description || '',
coverImageUrl: collection.coverImageUrl || '',
color: collection.color || '#3b82f6',
isPublic: collection.isPublic,
});
setIsEditDialogOpen(true);
}
};
const openDeleteDialog = (collectionId: string) => {
setCollectionToDelete(collectionId);
setIsDeleteDialogOpen(true);
};
const resetForm = () => {
setFormData({
name: '',
description: '',
coverImageUrl: '',
color: '#3b82f6',
isPublic: false,
});
setSelectedCollection(null);
};
if (loading && collections.length === 0) {
return (
<div className="body-content">
<div className="container-xxl">
<div className="flex items-center justify-center min-h-96">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
<p className="mt-4 text-muted-foreground">Cargando colecciones...</p>
</div>
</div>
</div>
</div>
);
}
return (
<div className="body-content">
<div className="container-xxl">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-6">
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
<FolderHeart className="h-6 w-6 text-primary" />
Gestión de Colecciones
</h1>
<p className="text-muted-foreground mt-1">
Administra las colecciones de lugares favoritos
</p>
</div>
<div className="flex gap-2 mt-4 md:mt-0">
<Button onClick={handleRefresh} variant="outline">
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
Actualizar
</Button>
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
Nueva Colección
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Crear Colección</DialogTitle>
<DialogDescription>
Crea una nueva colección para organizar lugares.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="name">Nombre</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Mi colección"
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Descripción</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Describe tu colección..."
/>
</div>
<div className="space-y-2">
<Label htmlFor="coverImage">URL de imagen de portada</Label>
<Input
id="coverImage"
value={formData.coverImageUrl}
onChange={(e) => setFormData({ ...formData, coverImageUrl: e.target.value })}
placeholder="https://..."
/>
</div>
<div className="space-y-2">
<Label htmlFor="color">Color</Label>
<Input
id="color"
type="color"
value={formData.color}
onChange={(e) => setFormData({ ...formData, color: e.target.value })}
className="h-10 w-20"
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="isPublic">Colección pública</Label>
<Switch
id="isPublic"
checked={formData.isPublic}
onCheckedChange={(checked) => setFormData({ ...formData, isPublic: checked })}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsCreateDialogOpen(false)}>
Cancelar
</Button>
<Button onClick={handleCreate} disabled={!formData.name}>
Crear
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
{/* Stats Cards */}
{stats && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Total Colecciones
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.totalCollections}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Total Items
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.totalItems}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-1">
<Globe className="h-3 w-3" />
Públicas
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.publicCollections}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-1">
<Lock className="h-3 w-3" />
Privadas
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.privateCollections}</div>
</CardContent>
</Card>
</div>
)}
{/* Search */}
<Card className="mb-6">
<CardContent className="pt-6">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar colecciones..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</CardContent>
</Card>
{/* Error State */}
{error && (
<Card className="mb-6 border-destructive">
<CardContent className="pt-6">
<p className="text-destructive">{error}</p>
<Button onClick={handleRefresh} variant="outline" className="mt-2">
Reintentar
</Button>
</CardContent>
</Card>
)}
{/* Empty State */}
{!loading && filteredCollections.length === 0 && (
<Card>
<CardContent className="pt-6">
<div className="text-center py-12">
<FolderHeart className="h-16 w-16 text-muted-foreground mx-auto mb-4" />
<h3 className="text-xl font-semibold mb-2">No hay colecciones</h3>
<p className="text-muted-foreground mb-4">
{searchTerm
? 'No se encontraron colecciones con ese término de búsqueda.'
: 'Crea tu primera colección para organizar lugares.'}
</p>
<Button onClick={() => setIsCreateDialogOpen(true)}>
<Plus className="h-4 w-4 mr-2" />
Crear Colección
</Button>
</div>
</CardContent>
</Card>
)}
{/* Collections Grid */}
{filteredCollections.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredCollections.map((collection) => (
<Card key={collection.id} className="overflow-hidden">
<div
className="h-32 relative"
style={{
backgroundColor: collection.color || '#3b82f6',
backgroundImage: collection.coverImageUrl
? `url(${collection.coverImageUrl})`
: undefined,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
>
{!collection.coverImageUrl && (
<div className="absolute inset-0 flex items-center justify-center">
<ImageIcon className="h-12 w-12 text-white/50" />
</div>
)}
<div className="absolute top-2 right-2">
<Badge variant={collection.isPublic ? 'default' : 'secondary'}>
{collection.isPublic ? (
<><Globe className="h-3 w-3 mr-1" /> Pública</>
) : (
<><Lock className="h-3 w-3 mr-1" /> Privada</>
)}
</Badge>
</div>
</div>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="text-lg">{collection.name}</CardTitle>
<CardDescription className="line-clamp-2">
{collection.description || 'Sin descripción'}
</CardDescription>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => getCollectionById(collection.id)}>
<Eye className="h-4 w-4 mr-2" />
Ver detalles
</DropdownMenuItem>
<DropdownMenuItem onClick={() => openEditDialog(collection.id)}>
<Edit2 className="h-4 w-4 mr-2" />
Editar
</DropdownMenuItem>
<DropdownMenuItem
className="text-destructive"
onClick={() => openDeleteDialog(collection.id)}
>
<Trash2 className="h-4 w-4 mr-2" />
Eliminar
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between text-sm text-muted-foreground">
<span>{collection.itemCount || 0} items</span>
<span>{new Date(collection.createdAt).toLocaleDateString('es-ES')}</span>
</div>
</CardContent>
</Card>
))}
</div>
)}
{/* Edit Dialog */}
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Editar Colección</DialogTitle>
<DialogDescription>
Modifica los detalles de la colección.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="edit-name">Nombre</Label>
<Input
id="edit-name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-description">Descripción</Label>
<Textarea
id="edit-description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-coverImage">URL de imagen de portada</Label>
<Input
id="edit-coverImage"
value={formData.coverImageUrl}
onChange={(e) => setFormData({ ...formData, coverImageUrl: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-color">Color</Label>
<Input
id="edit-color"
type="color"
value={formData.color}
onChange={(e) => setFormData({ ...formData, color: e.target.value })}
className="h-10 w-20"
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="edit-isPublic">Colección pública</Label>
<Switch
id="edit-isPublic"
checked={formData.isPublic}
onCheckedChange={(checked) => setFormData({ ...formData, isPublic: checked })}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>
Cancelar
</Button>
<Button onClick={handleEdit} disabled={!formData.name}>
Guardar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Dialog */}
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>¿Eliminar colección?</AlertDialogTitle>
<AlertDialogDescription>
Esta acción no se puede deshacer. La colección y todos sus items serán eliminados permanentemente.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Eliminar
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Footer */}
<div className="mt-6 text-center text-sm text-muted-foreground">
Mostrando {filteredCollections.length} de {collections.length} colecciones
</div>
</div>
</div>
);
};
export default Collections;