Refactor components and modals

This commit is contained in:
Urtzi Alfaro
2025-09-26 07:46:25 +02:00
parent cf4405b771
commit d573c38621
80 changed files with 3421 additions and 4617 deletions

View File

@@ -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}

View File

@@ -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"

View File

@@ -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}
/>
);
};

View File

@@ -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"}

View File

@@ -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"