Fix UI for inventory page

This commit is contained in:
Urtzi Alfaro
2025-09-15 15:31:27 +02:00
parent 36cfc88f93
commit 65a53c6d16
10 changed files with 953 additions and 378 deletions

View File

@@ -0,0 +1,389 @@
import React from 'react';
import { Layers, Settings, Calculator, FileText, Calendar } from 'lucide-react';
import { StatusModal, StatusModalSection, getStatusColor } from '../../ui';
import { IngredientResponse, ProductType, UnitOfMeasure, IngredientCategory, ProductCategory, IngredientCreate, IngredientUpdate } from '../../../api/types/inventory';
export interface InventoryModalProps {
isOpen: boolean;
onClose: () => void;
mode: 'create' | 'view' | 'edit';
onModeChange?: (mode: 'view' | 'edit') => void;
selectedItem?: IngredientResponse | null;
formData: Partial<IngredientCreate & IngredientUpdate & { initial_stock?: number; product_type?: string }>;
onFieldChange: (sectionIndex: number, fieldIndex: number, value: string | number) => void;
onSave: () => Promise<void>;
onCancel: () => void;
loading?: boolean;
}
/**
* Unified Inventory Modal Component
* Handles create, view, and edit modes with consistent styling and UX
*/
export const InventoryModal: React.FC<InventoryModalProps> = ({
isOpen,
onClose,
mode,
onModeChange,
selectedItem,
formData,
onFieldChange,
onSave,
onCancel,
loading = false
}) => {
const isCreating = mode === 'create';
const isEditing = mode === 'edit';
const isViewing = mode === 'view';
// Helper functions to get dropdown options
const getUnitOfMeasureOptions = () => {
return Object.entries(UnitOfMeasure).map(([, value]) => ({
label: value,
value: value
}));
};
const getCategoryOptions = (productType?: ProductType) => {
if (productType === ProductType.INGREDIENT) {
return Object.entries(IngredientCategory).map(([, value]) => ({
label: value.charAt(0).toUpperCase() + value.slice(1).replace('_', ' '),
value: value
}));
} else {
return Object.entries(ProductCategory).map(([, value]) => ({
label: value.charAt(0).toUpperCase() + value.slice(1).replace('_', ' '),
value: value
}));
}
};
// Get status indicator based on mode and item
const getStatusIndicator = () => {
if (isCreating) {
return undefined; // No status indicator for create mode to reduce clutter
}
if (!selectedItem) return undefined;
const { stock_status } = selectedItem;
switch (stock_status) {
case 'out_of_stock':
return {
color: getStatusColor('cancelled'),
text: 'Sin Stock',
icon: Calendar,
isCritical: true,
isHighlight: false
};
case 'low_stock':
return {
color: getStatusColor('pending'),
text: 'Stock Bajo',
icon: Calendar,
isCritical: false,
isHighlight: true
};
case 'overstock':
return {
color: getStatusColor('info'),
text: 'Sobrestock',
icon: Calendar,
isCritical: false,
isHighlight: false
};
case 'in_stock':
default:
return {
color: getStatusColor('completed'),
text: 'Normal',
icon: Calendar,
isCritical: false,
isHighlight: false
};
}
};
// Get modal title based on mode
const getTitle = () => {
if (isCreating) return 'Nuevo Artículo';
return selectedItem?.name || 'Artículo de Inventario';
};
// Get modal subtitle based on mode
const getSubtitle = () => {
if (isCreating) return ''; // No subtitle for create mode
if (!selectedItem) return '';
const category = selectedItem.category || '';
const description = selectedItem.description || '';
return `${category}${description ? ` - ${description}` : ''}`;
};
// Define modal sections with create-mode simplification
const sections: StatusModalSection[] = isCreating ? [
// SIMPLIFIED CREATE MODE - Only essential fields
{
title: 'Información Básica',
icon: Layers,
fields: [
{
label: 'Tipo de producto',
value: formData.product_type || ProductType.INGREDIENT,
type: 'select',
editable: true,
required: true,
placeholder: 'Seleccionar tipo',
options: [
{ label: 'Ingrediente', value: ProductType.INGREDIENT },
{ label: 'Producto Final', value: ProductType.FINISHED_PRODUCT }
]
},
{
label: 'Nombre',
value: formData.name || '',
highlight: true,
editable: true,
required: true,
placeholder: 'Ej: Harina de trigo o Croissant de chocolate'
},
{
label: 'Categoría',
value: formData.category || '',
type: 'select',
editable: true,
required: true,
placeholder: 'Seleccionar categoría',
options: getCategoryOptions(formData.product_type as ProductType || ProductType.INGREDIENT)
},
{
label: 'Unidad de medida',
value: formData.unit_of_measure || '',
type: 'select',
editable: true,
required: true,
placeholder: 'Seleccionar unidad',
options: getUnitOfMeasureOptions()
}
]
},
{
title: 'Configuración de Stock',
icon: Settings,
fields: [
{
label: 'Stock inicial (opcional)',
value: formData.initial_stock || '',
type: 'number' as const,
editable: true,
required: false,
placeholder: 'Ej: 50 (si ya tienes este producto)'
},
{
label: 'Umbral mínimo',
value: formData.low_stock_threshold || 10,
type: 'number' as const,
editable: true,
required: true,
placeholder: 'Ej: 10'
},
{
label: 'Punto de reorden',
value: formData.reorder_point || 20,
type: 'number' as const,
editable: true,
required: true,
placeholder: 'Ej: 20'
}
]
}
] : [
// FULL VIEW/EDIT MODE - Complete information
{
title: 'Información Básica',
icon: Layers,
fields: [
{
label: 'Nombre',
value: formData.name || selectedItem?.name || '',
highlight: true,
editable: isEditing,
required: true,
placeholder: 'Nombre del ingrediente'
},
{
label: 'Categoría',
value: formData.category || selectedItem?.category || '',
type: 'select',
editable: isEditing,
placeholder: 'Seleccionar categoría',
options: getCategoryOptions(selectedItem?.product_type || ProductType.INGREDIENT)
},
{
label: 'Descripción',
value: formData.description || selectedItem?.description || '',
editable: isEditing,
placeholder: 'Descripción del producto'
},
{
label: 'Unidad de medida',
value: formData.unit_of_measure || selectedItem?.unit_of_measure || '',
type: 'select',
editable: isEditing,
required: true,
placeholder: 'Seleccionar unidad',
options: getUnitOfMeasureOptions()
}
]
},
{
title: 'Gestión de Stock',
icon: Settings,
fields: [
{
label: 'Stock actual',
value: `${Number(selectedItem?.current_stock) || 0} ${selectedItem?.unit_of_measure || ''}`,
highlight: true
},
{
label: 'Umbral mínimo',
value: formData.low_stock_threshold || selectedItem?.low_stock_threshold || 10,
type: 'number' as const,
editable: isEditing,
required: true,
placeholder: 'Cantidad mínima'
},
{
label: 'Stock máximo',
value: formData.max_stock_level || selectedItem?.max_stock_level || '',
type: 'number' as const,
editable: isEditing,
placeholder: 'Cantidad máxima (opcional)'
},
{
label: 'Punto de reorden',
value: formData.reorder_point || selectedItem?.reorder_point || 20,
type: 'number' as const,
editable: isEditing,
required: true,
placeholder: 'Punto de reorden'
},
{
label: 'Cantidad a reordenar',
value: formData.reorder_quantity || selectedItem?.reorder_quantity || 50,
type: 'number' as const,
editable: isEditing,
required: true,
placeholder: 'Cantidad por pedido'
}
]
},
{
title: 'Información Financiera',
icon: Calculator,
fields: [
{
label: 'Costo promedio por unidad',
value: formData.average_cost || selectedItem?.average_cost || 0,
type: 'currency',
editable: isEditing,
placeholder: 'Costo por unidad'
},
{
label: 'Valor total en stock',
value: (Number(selectedItem?.current_stock) || 0) * (Number(formData.average_cost || selectedItem?.average_cost) || 0),
type: 'currency' as const,
highlight: true
}
]
},
{
title: 'Información Adicional',
icon: Calendar,
fields: [
{
label: 'Último restock',
value: selectedItem?.last_restocked || 'Sin historial',
type: selectedItem?.last_restocked ? 'datetime' as const : 'text' as const
},
{
label: 'Vida útil (días)',
value: formData.shelf_life_days || selectedItem?.shelf_life_days || '',
type: 'number',
editable: isEditing,
placeholder: 'Días de vida útil'
},
{
label: 'Requiere refrigeración',
value: (formData.requires_refrigeration !== undefined ? formData.requires_refrigeration : selectedItem?.requires_refrigeration) ? 'Sí' : 'No',
type: 'select',
editable: isEditing,
options: [
{ label: 'No', value: 'false' },
{ label: 'Sí', value: 'true' }
]
},
{
label: 'Requiere congelación',
value: (formData.requires_freezing !== undefined ? formData.requires_freezing : selectedItem?.requires_freezing) ? 'Sí' : 'No',
type: 'select',
editable: isEditing,
options: [
{ label: 'No', value: 'false' },
{ label: 'Sí', value: 'true' }
]
},
{
label: 'Producto estacional',
value: (formData.is_seasonal !== undefined ? formData.is_seasonal : selectedItem?.is_seasonal) ? 'Sí' : 'No',
type: 'select',
editable: isEditing,
options: [
{ label: 'No', value: 'false' },
{ label: 'Sí', value: 'true' }
]
},
{
label: 'Creado',
value: selectedItem?.created_at,
type: 'datetime' as const
}
]
},
{
title: 'Notas',
icon: FileText,
fields: [
{
label: 'Observaciones',
value: formData.notes || selectedItem?.notes || '',
span: 2 as const,
editable: isEditing,
placeholder: 'Notas adicionales sobre el producto'
}
]
}
];
return (
<StatusModal
isOpen={isOpen}
onClose={onClose}
mode={isViewing ? 'view' : 'edit'}
onModeChange={onModeChange}
title={getTitle()}
subtitle={getSubtitle()}
statusIndicator={getStatusIndicator()}
size="lg"
sections={sections}
onFieldChange={onFieldChange}
onSave={onSave}
onCancel={onCancel}
loading={loading}
showDefaultActions={true}
/>
);
};
export default InventoryModal;

View File

@@ -3,6 +3,7 @@ export { default as InventoryTable, type InventoryTableProps } from './Inventory
export { default as StockLevelIndicator, type StockLevelIndicatorProps, useStockLevels } from './StockLevelIndicator';
export { default as InventoryForm, type InventoryFormProps } from './InventoryForm';
export { default as LowStockAlert, type LowStockAlertProps } from './LowStockAlert';
export { default as InventoryModal, type InventoryModalProps } from './InventoryModal';
// Re-export related types from inventory types
export type {