Add a ne model and card design across pages

This commit is contained in:
Urtzi Alfaro
2025-08-31 10:46:13 +02:00
parent ab21149acf
commit a8b73e22ea
14 changed files with 1865 additions and 820 deletions

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Plus, Download, AlertTriangle, Package, Clock, CheckCircle, Eye, Edit, Calendar, DollarSign } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui';
import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout';
import { InventoryForm, LowStockAlert } from '../../../../components/domain/inventory';
@@ -8,6 +8,7 @@ import { InventoryForm, LowStockAlert } from '../../../../components/domain/inve
const InventoryPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const [selectedItem, setSelectedItem] = useState<typeof mockInventoryItems[0] | null>(null);
const mockInventoryItems = [
@@ -83,65 +84,46 @@ const InventoryPage: React.FC = () => {
},
];
const getStockStatusBadge = (item: typeof mockInventoryItems[0]) => {
const getInventoryStatusConfig = (item: typeof mockInventoryItems[0]) => {
const { currentStock, minStock, status } = item;
if (status === 'expired') {
return (
<Badge
variant="error"
icon={<AlertTriangle size={12} />}
text="Caducado"
/>
);
return {
color: getStatusColor('expired'),
text: 'Caducado',
icon: AlertTriangle,
isCritical: true,
isHighlight: false
};
}
if (currentStock === 0) {
return (
<Badge
variant="error"
icon={<AlertTriangle size={12} />}
text="Sin Stock"
/>
);
return {
color: getStatusColor('out'),
text: 'Sin Stock',
icon: AlertTriangle,
isCritical: true,
isHighlight: false
};
}
if (currentStock <= minStock) {
return (
<Badge
variant="warning"
icon={<AlertTriangle size={12} />}
text="Stock Bajo"
/>
);
return {
color: getStatusColor('low'),
text: 'Stock Bajo',
icon: AlertTriangle,
isCritical: false,
isHighlight: true
};
}
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' },
return {
color: getStatusColor('normal'),
text: 'Normal',
icon: CheckCircle,
isCritical: false,
isHighlight: false
};
const config = categoryConfig[category as keyof typeof categoryConfig] || { color: 'default' };
return (
<Badge
variant={config.color as any}
text={category}
/>
);
};
const filteredItems = mockInventoryItems.filter(item => {
@@ -258,132 +240,60 @@ const InventoryPage: React.FC = () => {
{/* 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={() => {
{filteredItems.map((item) => {
const statusConfig = getInventoryStatusConfig(item);
const stockPercentage = Math.round((item.currentStock / item.maxStock) * 100);
const isExpiringSoon = new Date(item.expirationDate) < new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
const isExpired = new Date(item.expirationDate) < new Date();
return (
<StatusCard
key={item.id}
id={item.id}
statusIndicator={statusConfig}
title={item.name}
subtitle={`${item.category}${item.supplier}`}
primaryValue={item.currentStock}
primaryValueLabel={item.unit}
secondaryInfo={{
label: 'Valor total',
value: `${formatters.currency(item.currentStock * item.cost)} (${formatters.currency(item.cost)}/${item.unit})`
}}
progress={{
label: 'Nivel de stock',
percentage: stockPercentage,
color: statusConfig.color
}}
metadata={[
`Rango: ${item.minStock} - ${item.maxStock} ${item.unit}`,
`Caduca: ${new Date(item.expirationDate).toLocaleDateString('es-ES')}${isExpired ? ' (CADUCADO)' : isExpiringSoon ? ' (PRONTO)' : ''}`,
`Último restock: ${new Date(item.lastRestocked).toLocaleDateString('es-ES')}`
]}
actions={[
{
label: 'Ver',
icon: Eye,
variant: 'outline',
onClick: () => {
setSelectedItem(item);
setModalMode('view');
setShowForm(true);
}}
>
<Eye className="w-4 h-4 mr-1" />
Ver
</Button>
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
}
},
{
label: 'Editar',
icon: Edit,
variant: 'outline',
onClick: () => {
setSelectedItem(item);
setModalMode('edit');
setShowForm(true);
}}
>
<Edit className="w-4 h-4 mr-1" />
Editar
</Button>
</div>
</div>
</Card>
))}
}
}
]}
/>
);
})}
</div>
{/* Empty State */}
@@ -403,19 +313,107 @@ const InventoryPage: React.FC = () => {
</div>
)}
{/* Inventory Form Modal */}
{showForm && (
<InventoryForm
item={selectedItem}
{/* Inventory Item Modal */}
{showForm && selectedItem && (
<StatusModal
isOpen={showForm}
onClose={() => {
setShowForm(false);
setSelectedItem(null);
setModalMode('view');
}}
onSave={(item) => {
// Handle save logic
console.log('Saving item:', item);
setShowForm(false);
setSelectedItem(null);
mode={modalMode}
onModeChange={setModalMode}
title={selectedItem.name}
subtitle={`${selectedItem.category} - ${selectedItem.supplier}`}
statusIndicator={getInventoryStatusConfig(selectedItem)}
size="lg"
sections={[
{
title: 'Información Básica',
icon: Package,
fields: [
{
label: 'Nombre',
value: selectedItem.name,
highlight: true
},
{
label: 'Categoría',
value: selectedItem.category
},
{
label: 'Proveedor',
value: selectedItem.supplier
},
{
label: 'Unidad de medida',
value: selectedItem.unit
}
]
},
{
title: 'Stock y Niveles',
icon: Package,
fields: [
{
label: 'Stock actual',
value: `${selectedItem.currentStock} ${selectedItem.unit}`,
highlight: true
},
{
label: 'Stock mínimo',
value: `${selectedItem.minStock} ${selectedItem.unit}`
},
{
label: 'Stock máximo',
value: `${selectedItem.maxStock} ${selectedItem.unit}`
},
{
label: 'Porcentaje de stock',
value: Math.round((selectedItem.currentStock / selectedItem.maxStock) * 100),
type: 'percentage',
highlight: selectedItem.currentStock <= selectedItem.minStock
}
]
},
{
title: 'Información Financiera',
icon: DollarSign,
fields: [
{
label: 'Costo por unidad',
value: selectedItem.cost,
type: 'currency'
},
{
label: 'Valor total en stock',
value: selectedItem.currentStock * selectedItem.cost,
type: 'currency',
highlight: true
}
]
},
{
title: 'Fechas Importantes',
icon: Calendar,
fields: [
{
label: 'Último restock',
value: selectedItem.lastRestocked,
type: 'date'
},
{
label: 'Fecha de caducidad',
value: selectedItem.expirationDate,
type: 'date',
highlight: new Date(selectedItem.expirationDate) < new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
}
]
}
]}
onEdit={() => {
console.log('Editing inventory item:', selectedItem.id);
}}
/>
)}