Add new page designs
This commit is contained in:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user