Add new page designs

This commit is contained in:
Urtzi Alfaro
2025-08-30 19:11:15 +02:00
parent 221781731c
commit 62b1ab9cb1
12 changed files with 2129 additions and 1240 deletions

View File

@@ -1,15 +1,14 @@
import React, { useState } from 'react';
import { Plus, Search, Filter, Download, AlertTriangle } from 'lucide-react';
import { Button, Input, Card, Badge } from '../../../../components/ui';
import { Plus, Download, AlertTriangle, Package, Clock, CheckCircle, Eye, Edit, Calendar, DollarSign } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout';
import { InventoryTable, InventoryForm, LowStockAlert } from '../../../../components/domain/inventory';
import { InventoryForm, LowStockAlert } from '../../../../components/domain/inventory';
const InventoryPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [selectedItem, setSelectedItem] = useState(null);
const [filterCategory, setFilterCategory] = useState('all');
const [filterStatus, setFilterStatus] = useState('all');
const [selectedItem, setSelectedItem] = useState<typeof mockInventoryItems[0] | null>(null);
const mockInventoryItems = [
{
@@ -54,157 +53,355 @@ const InventoryPage: React.FC = () => {
expirationDate: '2024-02-10',
status: 'normal',
},
{
id: '4',
name: 'Azúcar Blanco',
category: 'Azúcares',
currentStock: 0,
minStock: 15,
maxStock: 50,
unit: 'kg',
cost: 0.95,
supplier: 'Distribuidora Central',
lastRestocked: '2024-01-10',
expirationDate: '2024-12-31',
status: 'out',
},
{
id: '5',
name: 'Leche Entera',
category: 'Lácteos',
currentStock: 3,
minStock: 10,
maxStock: 40,
unit: 'L',
cost: 1.45,
supplier: 'Lácteos Frescos',
lastRestocked: '2024-01-22',
expirationDate: '2024-01-28',
status: 'expired',
},
];
const lowStockItems = mockInventoryItems.filter(item => item.status === 'low');
const stats = {
totalItems: mockInventoryItems.length,
lowStockItems: lowStockItems.length,
totalValue: mockInventoryItems.reduce((sum, item) => sum + (item.currentStock * item.cost), 0),
needsReorder: lowStockItems.length,
const getStockStatusBadge = (item: typeof mockInventoryItems[0]) => {
const { currentStock, minStock, status } = item;
if (status === 'expired') {
return (
<Badge
variant="error"
icon={<AlertTriangle size={12} />}
text="Caducado"
/>
);
}
if (currentStock === 0) {
return (
<Badge
variant="error"
icon={<AlertTriangle size={12} />}
text="Sin Stock"
/>
);
}
if (currentStock <= minStock) {
return (
<Badge
variant="warning"
icon={<AlertTriangle size={12} />}
text="Stock Bajo"
/>
);
}
return (
<Badge
variant="success"
icon={<CheckCircle size={12} />}
text="Normal"
/>
);
};
const getCategoryBadge = (category: string) => {
const categoryConfig = {
'Harinas': { color: 'default' },
'Levaduras': { color: 'info' },
'Lácteos': { color: 'secondary' },
'Grasas': { color: 'warning' },
'Azúcares': { color: 'primary' },
'Especias': { color: 'success' },
};
const config = categoryConfig[category as keyof typeof categoryConfig] || { color: 'default' };
return (
<Badge
variant={config.color as any}
text={category}
/>
);
};
const filteredItems = mockInventoryItems.filter(item => {
const matchesSearch = item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.category.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.supplier.toLowerCase().includes(searchTerm.toLowerCase());
return matchesSearch;
});
const lowStockItems = mockInventoryItems.filter(item =>
item.currentStock <= item.minStock || item.status === 'low' || item.status === 'out' || item.status === 'expired'
);
const mockInventoryStats = {
totalItems: mockInventoryItems.length,
lowStockItems: lowStockItems.length,
outOfStock: mockInventoryItems.filter(item => item.currentStock === 0).length,
expiringSoon: mockInventoryItems.filter(item => item.status === 'expired').length,
totalValue: mockInventoryItems.reduce((sum, item) => sum + (item.currentStock * item.cost), 0),
categories: [...new Set(mockInventoryItems.map(item => item.category))].length,
};
const inventoryStats = [
{
title: 'Total Artículos',
value: mockInventoryStats.totalItems,
variant: 'default' as const,
icon: Package,
},
{
title: 'Stock Bajo',
value: mockInventoryStats.lowStockItems,
variant: 'warning' as const,
icon: AlertTriangle,
},
{
title: 'Sin Stock',
value: mockInventoryStats.outOfStock,
variant: 'error' as const,
icon: AlertTriangle,
},
{
title: 'Por Caducar',
value: mockInventoryStats.expiringSoon,
variant: 'error' as const,
icon: Clock,
},
{
title: 'Valor Total',
value: formatters.currency(mockInventoryStats.totalValue),
variant: 'success' as const,
icon: DollarSign,
},
{
title: 'Categorías',
value: mockInventoryStats.categories,
variant: 'info' as const,
icon: Package,
},
];
return (
<div className="p-6 space-y-6">
<div className="space-y-6">
<PageHeader
title="Gestión de Inventario"
description="Controla el stock de ingredientes y materias primas"
action={
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Nuevo Artículo
</Button>
}
actions={[
{
id: "export",
label: "Exportar",
variant: "outline" as const,
icon: Download,
onClick: () => console.log('Export inventory')
},
{
id: "new",
label: "Nuevo Artículo",
variant: "primary" as const,
icon: Plus,
onClick: () => setShowForm(true)
}
]}
/>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Artículos</p>
<p className="text-3xl font-bold text-[var(--text-primary)]">{stats.totalItems}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-[var(--color-info)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4-8-4m16 0v10l-8 4-8-4V7" />
</svg>
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Stock Bajo</p>
<p className="text-3xl font-bold text-[var(--color-error)]">{stats.lowStockItems}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-error)]/10 rounded-full flex items-center justify-center">
<AlertTriangle className="h-6 w-6 text-[var(--color-error)]" />
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Valor Total</p>
<p className="text-3xl font-bold text-[var(--color-success)]">{stats.totalValue.toFixed(2)}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-[var(--color-success)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" />
</svg>
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Necesita Reorden</p>
<p className="text-3xl font-bold text-[var(--color-primary)]">{stats.needsReorder}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-[var(--color-primary)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
</div>
</Card>
</div>
{/* Stats Grid */}
<StatsGrid
stats={inventoryStats}
columns={6}
/>
{/* Low Stock Alert */}
{lowStockItems.length > 0 && (
<LowStockAlert items={lowStockItems} />
)}
{/* Filters and Search */}
<Card className="p-6">
{/* Simplified Controls */}
<Card className="p-4">
<div className="flex flex-col sm: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-[var(--text-tertiary)] h-4 w-4" />
<Input
placeholder="Buscar artículos..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="all">Todas las categorías</option>
<option value="Harinas">Harinas</option>
<option value="Levaduras">Levaduras</option>
<option value="Lácteos">Lácteos</option>
<option value="Grasas">Grasas</option>
<option value="Azúcares">Azúcares</option>
<option value="Especias">Especias</option>
</select>
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
>
<option value="all">Todos los estados</option>
<option value="normal">Stock normal</option>
<option value="low">Stock bajo</option>
<option value="out">Sin stock</option>
<option value="expired">Caducado</option>
</select>
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Más filtros
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
<Input
placeholder="Buscar artículos por nombre, categoría o proveedor..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full"
/>
</div>
<Button variant="outline" onClick={() => console.log('Export filtered')}>
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
</Card>
{/* Inventory Table */}
<Card>
<InventoryTable
data={mockInventoryItems}
onEdit={(item) => {
setSelectedItem(item);
setShowForm(true);
}}
/>
</Card>
{/* Inventory Items Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredItems.map((item) => (
<Card key={item.id} className="p-4">
<div className="space-y-4">
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 bg-[var(--color-primary)]/10 p-2 rounded-lg">
<Package className="w-4 h-4 text-[var(--text-tertiary)]" />
</div>
<div>
<div className="font-medium text-[var(--text-primary)]">
{item.name}
</div>
<div className="text-sm text-[var(--text-secondary)]">
{item.supplier}
</div>
</div>
</div>
{getStockStatusBadge(item)}
</div>
{/* Category and Stock */}
<div className="flex items-center justify-between">
<div>
{getCategoryBadge(item.category)}
</div>
<div className="text-right">
<div className="text-lg font-bold text-[var(--text-primary)]">
{item.currentStock} {item.unit}
</div>
<div className="text-xs text-[var(--text-tertiary)]">
Mín: {item.minStock} | Máx: {item.maxStock}
</div>
</div>
</div>
{/* Value and Dates */}
<div className="flex items-center justify-between text-sm">
<div>
<div className="text-[var(--text-secondary)]">Costo unitario:</div>
<div className="font-medium text-[var(--text-primary)]">
{formatters.currency(item.cost)}
</div>
</div>
<div className="text-right">
<div className="text-[var(--text-secondary)]">Valor total:</div>
<div className="font-medium text-[var(--text-primary)]">
{formatters.currency(item.currentStock * item.cost)}
</div>
</div>
</div>
{/* Dates */}
<div className="space-y-2 text-xs">
<div className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">Último restock:</span>
<span className="text-[var(--text-primary)]">
{new Date(item.lastRestocked).toLocaleDateString('es-ES')}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">Caducidad:</span>
<span className={`font-medium ${
new Date(item.expirationDate) < new Date()
? 'text-[var(--color-error)]'
: new Date(item.expirationDate) < new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
? 'text-[var(--color-warning)]'
: 'text-[var(--text-primary)]'
}`}>
{new Date(item.expirationDate).toLocaleDateString('es-ES')}
</span>
</div>
</div>
{/* Stock Level Progress */}
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-[var(--text-secondary)]">Nivel de stock</span>
<span className="text-[var(--text-primary)]">
{Math.round((item.currentStock / item.maxStock) * 100)}%
</span>
</div>
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
item.currentStock <= item.minStock
? 'bg-[var(--color-error)]'
: item.currentStock <= item.minStock * 1.5
? 'bg-[var(--color-warning)]'
: 'bg-[var(--color-success)]'
}`}
style={{ width: `${Math.min((item.currentStock / item.maxStock) * 100, 100)}%` }}
/>
</div>
</div>
{/* Actions */}
<div className="flex gap-2 pt-2 border-t border-[var(--border-primary)]">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
setSelectedItem(item);
setShowForm(true);
}}
>
<Eye className="w-4 h-4 mr-1" />
Ver
</Button>
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
setSelectedItem(item);
setShowForm(true);
}}
>
<Edit className="w-4 h-4 mr-1" />
Editar
</Button>
</div>
</div>
</Card>
))}
</div>
{/* Empty State */}
{filteredItems.length === 0 && (
<div className="text-center py-12">
<Package className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
No se encontraron artículos
</h3>
<p className="text-[var(--text-secondary)] mb-4">
Intenta ajustar la búsqueda o agregar un nuevo artículo al inventario
</p>
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Nuevo Artículo
</Button>
</div>
)}
{/* Inventory Form Modal */}
{showForm && (