229 lines
8.3 KiB
TypeScript
229 lines
8.3 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
||
|
|
import { Plus, Search, Filter, Download, AlertTriangle } from 'lucide-react';
|
||
|
|
import { Button, Input, Card, Badge } from '../../../../components/ui';
|
||
|
|
import { PageHeader } from '../../../../components/layout';
|
||
|
|
import { InventoryTable, 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 mockInventoryItems = [
|
||
|
|
{
|
||
|
|
id: '1',
|
||
|
|
name: 'Harina de Trigo',
|
||
|
|
category: 'Harinas',
|
||
|
|
currentStock: 45,
|
||
|
|
minStock: 20,
|
||
|
|
maxStock: 100,
|
||
|
|
unit: 'kg',
|
||
|
|
cost: 1.20,
|
||
|
|
supplier: 'Molinos del Sur',
|
||
|
|
lastRestocked: '2024-01-20',
|
||
|
|
expirationDate: '2024-06-30',
|
||
|
|
status: 'normal',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '2',
|
||
|
|
name: 'Levadura Fresca',
|
||
|
|
category: 'Levaduras',
|
||
|
|
currentStock: 8,
|
||
|
|
minStock: 10,
|
||
|
|
maxStock: 25,
|
||
|
|
unit: 'kg',
|
||
|
|
cost: 8.50,
|
||
|
|
supplier: 'Levaduras SA',
|
||
|
|
lastRestocked: '2024-01-25',
|
||
|
|
expirationDate: '2024-02-15',
|
||
|
|
status: 'low',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '3',
|
||
|
|
name: 'Mantequilla',
|
||
|
|
category: 'Lácteos',
|
||
|
|
currentStock: 15,
|
||
|
|
minStock: 5,
|
||
|
|
maxStock: 30,
|
||
|
|
unit: 'kg',
|
||
|
|
cost: 5.80,
|
||
|
|
supplier: 'Lácteos Frescos',
|
||
|
|
lastRestocked: '2024-01-24',
|
||
|
|
expirationDate: '2024-02-10',
|
||
|
|
status: 'normal',
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
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,
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="p-6 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>
|
||
|
|
}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* 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>
|
||
|
|
|
||
|
|
{/* Low Stock Alert */}
|
||
|
|
{lowStockItems.length > 0 && (
|
||
|
|
<LowStockAlert items={lowStockItems} />
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Filters and Search */}
|
||
|
|
<Card className="p-6">
|
||
|
|
<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>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Inventory Table */}
|
||
|
|
<Card>
|
||
|
|
<InventoryTable
|
||
|
|
data={mockInventoryItems}
|
||
|
|
onEdit={(item) => {
|
||
|
|
setSelectedItem(item);
|
||
|
|
setShowForm(true);
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Inventory Form Modal */}
|
||
|
|
{showForm && (
|
||
|
|
<InventoryForm
|
||
|
|
item={selectedItem}
|
||
|
|
onClose={() => {
|
||
|
|
setShowForm(false);
|
||
|
|
setSelectedItem(null);
|
||
|
|
}}
|
||
|
|
onSave={(item) => {
|
||
|
|
// Handle save logic
|
||
|
|
console.log('Saving item:', item);
|
||
|
|
setShowForm(false);
|
||
|
|
setSelectedItem(null);
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default InventoryPage;
|