Add a ne model and card design across pages
This commit is contained in:
@@ -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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user