Refactor components and modals
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Package, Euro, Calendar, FileText, Thermometer } from 'lucide-react';
|
||||
import { StatusModal } from '../../ui/StatusModal/StatusModal';
|
||||
import { EditViewModal } from '../../ui/EditViewModal/EditViewModal';
|
||||
import { IngredientResponse, StockCreate, ProductionStage } from '../../../api/types/inventory';
|
||||
import { Button } from '../../ui/Button';
|
||||
import { useSuppliers } from '../../../api/hooks/suppliers';
|
||||
import { useCurrentTenant } from '../../../stores/tenant.store';
|
||||
import { useInventoryEnums } from '../../../utils/inventoryEnumHelpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { statusColors } from '../../../styles/colors';
|
||||
|
||||
interface AddStockModalProps {
|
||||
@@ -60,8 +60,14 @@ export const AddStockModal: React.FC<AddStockModalProps> = ({
|
||||
});
|
||||
const suppliers = (suppliersData || []).filter(supplier => supplier.status === 'active');
|
||||
|
||||
// Get inventory enum helpers
|
||||
const inventoryEnums = useInventoryEnums();
|
||||
// Get translations
|
||||
const { t } = useTranslation(['inventory', 'common']);
|
||||
|
||||
// Get production stage options using direct i18n
|
||||
const productionStageOptions = Object.values(ProductionStage).map(value => ({
|
||||
value,
|
||||
label: t(`inventory:production_stage.${value}`)
|
||||
}));
|
||||
|
||||
// Create supplier options for select
|
||||
const supplierOptions = [
|
||||
@@ -251,7 +257,7 @@ export const AddStockModal: React.FC<AddStockModalProps> = ({
|
||||
value: formData.production_stage || ProductionStage.RAW_INGREDIENT,
|
||||
type: 'select' as const,
|
||||
editable: true,
|
||||
options: inventoryEnums.getProductionStageOptions()
|
||||
options: productionStageOptions
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -382,7 +388,7 @@ export const AddStockModal: React.FC<AddStockModalProps> = ({
|
||||
];
|
||||
|
||||
return (
|
||||
<StatusModal
|
||||
<EditViewModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
mode={mode}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Package, AlertTriangle, Clock, Archive, Thermometer, Plus, Edit, Trash2, CheckCircle, X, Save } from 'lucide-react';
|
||||
import { StatusModal } from '../../ui/StatusModal/StatusModal';
|
||||
import { EditViewModal } from '../../ui/EditViewModal/EditViewModal';
|
||||
import { IngredientResponse, StockResponse, StockUpdate } from '../../../api/types/inventory';
|
||||
import { formatters } from '../../ui/Stats/StatsPresets';
|
||||
import { statusColors } from '../../../styles/colors';
|
||||
@@ -431,7 +431,7 @@ export const BatchModal: React.FC<BatchModalProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<StatusModal
|
||||
<EditViewModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
mode="view"
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Plus, Package, Calculator, Settings } from 'lucide-react';
|
||||
import { StatusModal } from '../../ui/StatusModal/StatusModal';
|
||||
import { AddModal } from '../../ui/AddModal/AddModal';
|
||||
import { IngredientCreate, UnitOfMeasure, IngredientCategory, ProductCategory } from '../../../api/types/inventory';
|
||||
import { useInventoryEnums } from '../../../utils/inventoryEnumHelpers';
|
||||
import { statusColors } from '../../../styles/colors';
|
||||
|
||||
interface CreateIngredientModalProps {
|
||||
@@ -21,138 +20,58 @@ export const CreateIngredientModal: React.FC<CreateIngredientModalProps> = ({
|
||||
onClose,
|
||||
onCreateIngredient
|
||||
}) => {
|
||||
const { t } = useTranslation(['inventory']);
|
||||
const [formData, setFormData] = useState<IngredientCreate>({
|
||||
name: '',
|
||||
description: '',
|
||||
category: '',
|
||||
unit_of_measure: 'kg',
|
||||
low_stock_threshold: 10,
|
||||
reorder_point: 20,
|
||||
max_stock_level: 100,
|
||||
is_seasonal: false,
|
||||
average_cost: 0,
|
||||
notes: ''
|
||||
});
|
||||
|
||||
const { t } = useTranslation(['inventory', 'common']);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [mode, setMode] = useState<'overview' | 'edit'>('edit');
|
||||
|
||||
// Get enum options using helpers
|
||||
const inventoryEnums = useInventoryEnums();
|
||||
// Get enum options using direct i18n implementation
|
||||
const ingredientCategoryOptions = Object.values(IngredientCategory).map(value => ({
|
||||
value,
|
||||
label: t(`inventory:ingredient_category.${value}`)
|
||||
})).sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
const productCategoryOptions = Object.values(ProductCategory).map(value => ({
|
||||
value,
|
||||
label: t(`inventory:product_category.${value}`)
|
||||
}));
|
||||
|
||||
// Combine ingredient and product categories
|
||||
const categoryOptions = [
|
||||
...inventoryEnums.getIngredientCategoryOptions(),
|
||||
...inventoryEnums.getProductCategoryOptions()
|
||||
...ingredientCategoryOptions,
|
||||
...productCategoryOptions
|
||||
];
|
||||
|
||||
const unitOptions = inventoryEnums.getUnitOfMeasureOptions();
|
||||
const unitOptions = Object.values(UnitOfMeasure).map(value => ({
|
||||
value,
|
||||
label: t(`inventory:unit_of_measure.${value}`)
|
||||
}));
|
||||
|
||||
const handleFieldChange = (sectionIndex: number, fieldIndex: number, value: string | number | boolean) => {
|
||||
// Map field positions to form data fields
|
||||
const fieldMappings = [
|
||||
// Basic Information section
|
||||
['name', 'description', 'category', 'unit_of_measure'],
|
||||
// Cost and Quantities section
|
||||
['average_cost', 'low_stock_threshold', 'reorder_point', 'max_stock_level'],
|
||||
// Additional Information section
|
||||
['notes']
|
||||
];
|
||||
|
||||
const fieldName = fieldMappings[sectionIndex]?.[fieldIndex] as keyof IngredientCreate;
|
||||
if (fieldName) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[fieldName]: value
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
// Validation
|
||||
if (!formData.name?.trim()) {
|
||||
alert(t('inventory:validation.name_required', 'El nombre es requerido'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.category) {
|
||||
alert(t('inventory:validation.category_required', 'La categoría es requerida'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.unit_of_measure) {
|
||||
alert(t('inventory:validation.unit_required', 'La unidad de medida es requerida'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.low_stock_threshold || formData.low_stock_threshold < 0) {
|
||||
alert(t('inventory:validation.min_greater_than_zero', 'El umbral de stock bajo debe ser un número positivo'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.reorder_point || formData.reorder_point < 0) {
|
||||
alert(t('inventory:validation.min_greater_than_zero', 'El punto de reorden debe ser un número positivo'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.reorder_point <= formData.low_stock_threshold) {
|
||||
alert(t('inventory:validation.max_greater_than_min', 'El punto de reorden debe ser mayor que el umbral de stock bajo'));
|
||||
return;
|
||||
}
|
||||
const handleSave = async (formData: Record<string, any>) => {
|
||||
// Transform form data to IngredientCreate format
|
||||
const ingredientData: IngredientCreate = {
|
||||
name: formData.name,
|
||||
description: formData.description || '',
|
||||
category: formData.category,
|
||||
unit_of_measure: formData.unit_of_measure,
|
||||
low_stock_threshold: Number(formData.low_stock_threshold),
|
||||
reorder_point: Number(formData.reorder_point),
|
||||
max_stock_level: Number(formData.max_stock_level),
|
||||
is_seasonal: false,
|
||||
average_cost: Number(formData.average_cost) || 0,
|
||||
notes: formData.notes || ''
|
||||
};
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
if (onCreateIngredient) {
|
||||
await onCreateIngredient(formData);
|
||||
await onCreateIngredient(ingredientData);
|
||||
}
|
||||
|
||||
// Reset form
|
||||
setFormData({
|
||||
name: '',
|
||||
description: '',
|
||||
category: '',
|
||||
unit_of_measure: 'kg',
|
||||
low_stock_threshold: 10,
|
||||
reorder_point: 20,
|
||||
max_stock_level: 100,
|
||||
shelf_life_days: undefined,
|
||||
requires_refrigeration: false,
|
||||
requires_freezing: false,
|
||||
is_seasonal: false,
|
||||
average_cost: 0,
|
||||
notes: ''
|
||||
});
|
||||
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Error creating ingredient:', error);
|
||||
alert('Error al crear el artículo. Por favor, intenta de nuevo.');
|
||||
throw error; // Let AddModal handle error display
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
// Reset form to initial values
|
||||
setFormData({
|
||||
name: '',
|
||||
description: '',
|
||||
category: '',
|
||||
unit_of_measure: 'kg',
|
||||
low_stock_threshold: 10,
|
||||
reorder_point: 20,
|
||||
max_stock_level: 100,
|
||||
shelf_life_days: undefined,
|
||||
requires_refrigeration: false,
|
||||
requires_freezing: false,
|
||||
is_seasonal: false,
|
||||
average_cost: 0,
|
||||
notes: ''
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
const statusConfig = {
|
||||
color: statusColors.inProgress.primary,
|
||||
text: t('inventory:actions.add_item', 'Nuevo Artículo'),
|
||||
@@ -168,34 +87,36 @@ export const CreateIngredientModal: React.FC<CreateIngredientModalProps> = ({
|
||||
fields: [
|
||||
{
|
||||
label: t('inventory:fields.name', 'Nombre'),
|
||||
value: formData.name,
|
||||
name: 'name',
|
||||
type: 'text' as const,
|
||||
editable: true,
|
||||
required: true,
|
||||
placeholder: 'Ej: Harina de trigo 000'
|
||||
placeholder: 'Ej: Harina de trigo 000',
|
||||
validation: (value: string | number) => {
|
||||
const str = String(value).trim();
|
||||
return str.length < 2 ? 'El nombre debe tener al menos 2 caracteres' : null;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('inventory:fields.description', 'Descripción'),
|
||||
value: formData.description || '',
|
||||
name: 'description',
|
||||
type: 'text' as const,
|
||||
editable: true,
|
||||
placeholder: 'Descripción opcional del artículo'
|
||||
},
|
||||
{
|
||||
label: 'Categoría',
|
||||
value: formData.category,
|
||||
name: 'category',
|
||||
type: 'select' as const,
|
||||
editable: true,
|
||||
required: true,
|
||||
options: categoryOptions
|
||||
options: categoryOptions,
|
||||
placeholder: 'Seleccionar categoría...'
|
||||
},
|
||||
{
|
||||
label: 'Unidad de Medida',
|
||||
value: formData.unit_of_measure,
|
||||
name: 'unit_of_measure',
|
||||
type: 'select' as const,
|
||||
editable: true,
|
||||
required: true,
|
||||
options: unitOptions
|
||||
options: unitOptions,
|
||||
defaultValue: 'kg'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -205,33 +126,49 @@ export const CreateIngredientModal: React.FC<CreateIngredientModalProps> = ({
|
||||
fields: [
|
||||
{
|
||||
label: 'Costo Promedio',
|
||||
value: formData.average_cost || 0,
|
||||
name: 'average_cost',
|
||||
type: 'currency' as const,
|
||||
editable: true,
|
||||
placeholder: '0.00'
|
||||
placeholder: '0.00',
|
||||
defaultValue: 0,
|
||||
validation: (value: string | number) => {
|
||||
const num = Number(value);
|
||||
return num < 0 ? 'El costo no puede ser negativo' : null;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Umbral Stock Bajo',
|
||||
value: formData.low_stock_threshold,
|
||||
name: 'low_stock_threshold',
|
||||
type: 'number' as const,
|
||||
editable: true,
|
||||
required: true,
|
||||
placeholder: '10'
|
||||
placeholder: '10',
|
||||
defaultValue: 10,
|
||||
validation: (value: string | number) => {
|
||||
const num = Number(value);
|
||||
return num < 0 ? 'El umbral debe ser un número positivo' : null;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Punto de Reorden',
|
||||
value: formData.reorder_point,
|
||||
name: 'reorder_point',
|
||||
type: 'number' as const,
|
||||
editable: true,
|
||||
required: true,
|
||||
placeholder: '20'
|
||||
placeholder: '20',
|
||||
defaultValue: 20,
|
||||
validation: (value: string | number) => {
|
||||
const num = Number(value);
|
||||
return num < 0 ? 'El punto de reorden debe ser un número positivo' : null;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Stock Máximo',
|
||||
value: formData.max_stock_level || 0,
|
||||
name: 'max_stock_level',
|
||||
type: 'number' as const,
|
||||
editable: true,
|
||||
placeholder: '100'
|
||||
placeholder: '100',
|
||||
defaultValue: 100,
|
||||
validation: (value: string | number) => {
|
||||
const num = Number(value);
|
||||
return num < 0 ? 'El stock máximo debe ser un número positivo' : null;
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -241,30 +178,26 @@ export const CreateIngredientModal: React.FC<CreateIngredientModalProps> = ({
|
||||
fields: [
|
||||
{
|
||||
label: 'Notas',
|
||||
value: formData.notes || '',
|
||||
type: 'text' as const,
|
||||
editable: true,
|
||||
placeholder: 'Notas adicionales'
|
||||
name: 'notes',
|
||||
type: 'textarea' as const,
|
||||
placeholder: 'Notas adicionales',
|
||||
span: 2 // Full width
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<StatusModal
|
||||
<AddModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
mode={mode}
|
||||
title="Crear Nuevo Artículo"
|
||||
subtitle="Agregar un nuevo artículo al inventario"
|
||||
statusIndicator={statusConfig}
|
||||
sections={sections}
|
||||
size="lg"
|
||||
loading={loading}
|
||||
showDefaultActions={true}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
onFieldChange={handleFieldChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Package, AlertTriangle, CheckCircle, Clock, Euro, Edit, Info, Thermometer, Calendar, Tag, Save, X, TrendingUp } from 'lucide-react';
|
||||
import { StatusModal } from '../../ui/StatusModal/StatusModal';
|
||||
import { EditViewModal } from '../../ui/EditViewModal/EditViewModal';
|
||||
import { IngredientResponse } from '../../../api/types/inventory';
|
||||
import { formatters } from '../../ui/Stats/StatsPresets';
|
||||
import { statusColors } from '../../../styles/colors';
|
||||
@@ -284,7 +284,7 @@ export const ShowInfoModal: React.FC<ShowInfoModalProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<StatusModal
|
||||
<EditViewModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
mode={isEditing ? "edit" : "view"}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Clock, TrendingDown, Package, AlertCircle, RotateCcw, X } from 'lucide-react';
|
||||
import { StatusModal } from '../../ui/StatusModal/StatusModal';
|
||||
import { EditViewModal } from '../../ui/EditViewModal/EditViewModal';
|
||||
import { IngredientResponse, StockMovementResponse } from '../../../api/types/inventory';
|
||||
import { formatters } from '../../ui/Stats/StatsPresets';
|
||||
import { statusColors } from '../../../styles/colors';
|
||||
@@ -214,7 +214,7 @@ export const StockHistoryModal: React.FC<StockHistoryModalProps> = ({
|
||||
];
|
||||
|
||||
return (
|
||||
<StatusModal
|
||||
<EditViewModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
mode="view"
|
||||
|
||||
Reference in New Issue
Block a user