Improve the frontend modals

This commit is contained in:
Urtzi Alfaro
2025-10-27 16:33:26 +01:00
parent 61376b7a9f
commit 858d985c92
143 changed files with 9289 additions and 2306 deletions

View File

@@ -1,13 +1,18 @@
import React, { useState, useEffect, useMemo } from 'react';
import { Package, Clock, Users, AlertCircle, Plus } from 'lucide-react';
import { Package, Clock, Users, AlertCircle, Plus, ClipboardCheck } from 'lucide-react';
import { AddModal } from '../../ui/AddModal/AddModal';
import {
ProductionBatchCreate,
ProductionPriorityEnum
} from '../../../api/types/production';
import { Card } from '../../ui';
import { Badge } from '../../ui';
import { ProcessStage } from '../../../api/types/qualityTemplates';
import type { RecipeResponse } from '../../../api/types/recipes';
import { useTranslation } from 'react-i18next';
import { useRecipes } from '../../../api/hooks/recipes';
import { useIngredients } from '../../../api/hooks/inventory';
import { recipesService } from '../../../api/services/recipes';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { statusColors } from '../../../styles/colors';
@@ -30,11 +35,43 @@ export const CreateProductionBatchModal: React.FC<CreateProductionBatchModalProp
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const [loading, setLoading] = useState(false);
const [selectedRecipe, setSelectedRecipe] = useState<RecipeResponse | null>(null);
const [loadingRecipe, setLoadingRecipe] = useState(false);
// API Data
const { data: recipes = [], isLoading: recipesLoading } = useRecipes(tenantId);
const { data: ingredients = [], isLoading: ingredientsLoading } = useIngredients(tenantId);
// Stage labels for display
const STAGE_LABELS: Record<ProcessStage, string> = {
[ProcessStage.MIXING]: 'Mezclado',
[ProcessStage.PROOFING]: 'Fermentación',
[ProcessStage.SHAPING]: 'Formado',
[ProcessStage.BAKING]: 'Horneado',
[ProcessStage.COOLING]: 'Enfriado',
[ProcessStage.PACKAGING]: 'Empaquetado',
[ProcessStage.FINISHING]: 'Acabado'
};
// Load recipe details when recipe is selected
const handleRecipeChange = async (recipeId: string) => {
if (!recipeId) {
setSelectedRecipe(null);
return;
}
setLoadingRecipe(true);
try {
const recipe = await recipesService.getRecipe(tenantId, recipeId);
setSelectedRecipe(recipe);
} catch (error) {
console.error('Error loading recipe:', error);
setSelectedRecipe(null);
} finally {
setLoadingRecipe(false);
}
};
// Filter finished products (ingredients that are finished products)
const finishedProducts = useMemo(() => ingredients.filter(ing =>
ing.type === 'finished_product' ||
@@ -141,7 +178,8 @@ export const CreateProductionBatchModal: React.FC<CreateProductionBatchModalProp
type: 'select' as const,
options: recipeOptions,
placeholder: 'Seleccionar receta...',
span: 2
span: 2,
onChange: (value: string) => handleRecipeChange(value)
},
{
label: 'Número de Lote',
@@ -252,6 +290,62 @@ export const CreateProductionBatchModal: React.FC<CreateProductionBatchModalProp
}
], [productOptions, recipeOptions, t]);
// Quality Requirements Preview Component
const qualityRequirementsPreview = selectedRecipe && (
<Card className="mt-4 p-4 bg-blue-50 border-blue-200">
<h4 className="font-medium text-[var(--text-primary)] mb-3 flex items-center gap-2">
<ClipboardCheck className="w-5 h-5 text-blue-600" />
Controles de Calidad Requeridos
</h4>
{selectedRecipe.quality_check_configuration && selectedRecipe.quality_check_configuration.stages ? (
<div className="space-y-3">
{Object.entries(selectedRecipe.quality_check_configuration.stages).map(([stage, config]: [string, any]) => {
if (!config.template_ids || config.template_ids.length === 0) return null;
return (
<div key={stage} className="flex items-center gap-2 text-sm">
<Badge variant="info">{STAGE_LABELS[stage as ProcessStage]}</Badge>
<span className="text-[var(--text-secondary)]">
{config.template_ids.length} control{config.template_ids.length > 1 ? 'es' : ''}
</span>
{config.blocking && (
<Badge variant="warning" size="sm">Bloqueante</Badge>
)}
{config.is_required && (
<Badge variant="error" size="sm">Requerido</Badge>
)}
</div>
);
})}
<div className="mt-3 pt-3 border-t border-blue-200">
<p className="text-sm text-[var(--text-secondary)]">
<span className="font-medium">Umbral de calidad mínimo:</span>{' '}
{selectedRecipe.quality_check_configuration.overall_quality_threshold || 7.0}/10
</p>
{selectedRecipe.quality_check_configuration.critical_stage_blocking && (
<p className="text-sm text-[var(--text-secondary)] mt-1">
<span className="font-medium text-orange-600"> Bloqueo crítico activado:</span>{' '}
El lote no puede avanzar si fallan checks críticos
</p>
)}
</div>
</div>
) : (
<div className="text-sm text-[var(--text-secondary)]">
<p className="mb-2">Esta receta no tiene controles de calidad configurados.</p>
<a
href={`/app/database/recipes`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
Configurar controles de calidad
</a>
</div>
)}
</Card>
);
return (
<AddModal
isOpen={isOpen}
@@ -263,6 +357,7 @@ export const CreateProductionBatchModal: React.FC<CreateProductionBatchModalProp
size="xl"
loading={loading}
onSave={handleSave}
additionalContent={qualityRequirementsPreview}
/>
);
};

View File

@@ -219,14 +219,14 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
name: 'description',
type: 'textarea' as const,
placeholder: 'Describe qué evalúa esta plantilla de calidad',
span: 2
span: 2 as const
},
{
label: 'Instrucciones para el Personal',
name: 'instructions',
type: 'textarea' as const,
placeholder: 'Instrucciones detalladas para realizar este control de calidad',
span: 2,
span: 2 as const,
helpText: 'Pasos específicos que debe seguir el operario'
}
]
@@ -282,7 +282,7 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
type: 'text' as const,
placeholder: 'Se seleccionarán las etapas donde aplicar',
helpText: 'Las etapas se configuran mediante la selección múltiple',
span: 2
span: 2 as const
}
]
},
@@ -297,7 +297,7 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
options: recipeOptions,
placeholder: 'Seleccionar receta para asociar automáticamente',
helpText: 'Si seleccionas una receta, esta plantilla se aplicará automáticamente a sus lotes de producción',
span: 2
span: 2 as const
}
]
},
@@ -322,20 +322,20 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
name: 'is_active',
type: 'select' as const,
options: [
{ value: true, label: 'Activa' },
{ value: false, label: 'Inactiva' }
{ value: 'true', label: '' },
{ value: 'false', label: 'No' }
],
defaultValue: true
defaultValue: 'true'
},
{
label: 'Control Requerido',
name: 'is_required',
type: 'select' as const,
options: [
{ value: false, label: 'Opcional' },
{ value: true, label: 'Requerido' }
{ value: 'false', label: 'Opcional' },
{ value: 'true', label: 'Requerido' }
],
defaultValue: false,
defaultValue: 'false',
helpText: 'Si es requerido, debe completarse obligatoriamente'
},
{
@@ -343,10 +343,10 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
name: 'is_critical',
type: 'select' as const,
options: [
{ value: false, label: 'Normal' },
{ value: true, label: 'Crítico' }
{ value: 'false', label: 'Normal' },
{ value: 'true', label: 'Crítico' }
],
defaultValue: false,
defaultValue: 'false',
helpText: 'Si es crítico, bloquea la producción si falla'
}
]
@@ -378,4 +378,4 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
);
};
export default CreateQualityTemplateModal;
export default CreateQualityTemplateModal;

View File

@@ -18,25 +18,25 @@ interface EditQualityTemplateModalProps {
isLoading?: boolean;
}
const QUALITY_CHECK_TYPE_OPTIONS = [
{ value: QualityCheckType.VISUAL, label: 'Visual - Inspección visual' },
{ value: QualityCheckType.MEASUREMENT, label: 'Medición - Medidas precisas' },
{ value: QualityCheckType.TEMPERATURE, label: 'Temperatura - Control térmico' },
{ value: QualityCheckType.WEIGHT, label: 'Peso - Control de peso' },
{ value: QualityCheckType.BOOLEAN, label: 'Sí/No - Verificación binaria' },
{ value: QualityCheckType.TIMING, label: 'Tiempo - Control temporal' }
];
const PROCESS_STAGE_OPTIONS = [
{ value: ProcessStage.MIXING, label: 'Mezclado' },
{ value: ProcessStage.PROOFING, label: 'Fermentación' },
{ value: ProcessStage.PROOFING, label: 'Fermentación' },
{ value: ProcessStage.SHAPING, label: 'Formado' },
{ value: ProcessStage.BAKING, label: 'Horneado' },
{ value: ProcessStage.COOLING, label: 'Enfriado' },
{ value: ProcessStage.PACKAGING, label: 'Empaquetado' },
{ value: ProcessStage.PACKAGING, label: 'Empaquetado' },
{ value: ProcessStage.FINISHING, label: 'Acabado' }
];
const QUALITY_CHECK_TYPE_OPTIONS = [
{ value: QualityCheckType.VISUAL, label: 'Visual - Inspección visual' },
{ value: QualityCheckType.MEASUREMENT, label: 'Medición - Medidas precisas' },
{ value: QualityCheckType.TEMPERATURE, label: 'Temperatura - Control térmico' },
{ value: QualityCheckType.WEIGHT, label: 'Peso - Control de peso' },
{ value: QualityCheckType.BOOLEAN, label: 'Sí/No - Verificación binaria' },
{ value: QualityCheckType.TIMING, label: 'Tiempo - Control temporal' }
];
const CATEGORY_OPTIONS_KEYS = [
{ value: '', key: '' },
{ value: 'appearance', key: 'appearance' },
@@ -104,13 +104,13 @@ export const EditQualityTemplateModal: React.FC<EditQualityTemplateModalProps> =
);
// Helper function to get translated category label
const getCategoryLabel = (category: string | null | undefined): string => {
if (!category) return 'Sin categoría';
const getCategoryLabel = (category: string | null | undefined): string => {
if (!category) return t('production.quality.categories.appearance', 'Sin categoría');
const translationKey = `production.quality.categories.${category}`;
const translated = t(translationKey);
// If translation is same as key, it means no translation exists, return the original
return translated === translationKey ? category : translated;
};
};
// Build category options with translations
const getCategoryOptions = () => {

View File

@@ -21,6 +21,7 @@ import { ProductionBatchResponse } from '../../../api/types/production';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { useQualityTemplatesForStage, useExecuteQualityCheck } from '../../../api/hooks/qualityTemplates';
import { ProcessStage, type QualityCheckTemplate, type QualityCheckExecutionRequest } from '../../../api/types/qualityTemplates';
import { useTranslation } from 'react-i18next';
export interface QualityCheckModalProps {
isOpen: boolean;
@@ -695,4 +696,4 @@ export const QualityCheckModal: React.FC<QualityCheckModalProps> = ({
);
};
export default QualityCheckModal;
export default QualityCheckModal;

View File

@@ -51,50 +51,50 @@ interface QualityTemplateManagerProps {
className?: string;
}
const QUALITY_CHECK_TYPE_CONFIG = {
const QUALITY_CHECK_TYPE_CONFIG = (t: (key: string) => string) => ({
[QualityCheckType.VISUAL]: {
icon: Eye,
label: 'Visual',
label: t('production.quality.check_types.visual', 'Visual'),
color: 'bg-blue-500',
description: 'Inspección visual'
description: t('production.quality.check_types.visual_description', 'Inspección visual')
},
[QualityCheckType.MEASUREMENT]: {
icon: Settings,
label: 'Medición',
label: t('production.quality.check_types.measurement', 'Medición'),
color: 'bg-green-500',
description: 'Mediciones precisas'
description: t('production.quality.check_types.measurement_description', 'Mediciones precisas')
},
[QualityCheckType.TEMPERATURE]: {
icon: Thermometer,
label: 'Temperatura',
label: t('production.quality.check_types.temperature', 'Temperatura'),
color: 'bg-red-500',
description: 'Control de temperatura'
description: t('production.quality.check_types.temperature_description', 'Control de temperatura')
},
[QualityCheckType.WEIGHT]: {
icon: Scale,
label: 'Peso',
label: t('production.quality.check_types.weight', 'Peso'),
color: 'bg-purple-500',
description: 'Control de peso'
description: t('production.quality.check_types.weight_description', 'Control de peso')
},
[QualityCheckType.BOOLEAN]: {
icon: CheckCircle,
label: 'Sí/No',
label: t('production.quality.check_types.boolean', 'Sí/No'),
color: 'bg-gray-500',
description: 'Verificación binaria'
description: t('production.quality.check_types.boolean_description', 'Verificación binaria')
},
[QualityCheckType.TIMING]: {
icon: Timer,
label: 'Tiempo',
label: t('production.quality.check_types.timing', 'Tiempo'),
color: 'bg-orange-500',
description: 'Control de tiempo'
description: t('production.quality.check_types.timing_description', 'Control de tiempo')
},
[QualityCheckType.CHECKLIST]: {
icon: FileCheck,
label: 'Lista de verificación',
label: t('production.quality.check_types.checklist', 'Lista de verificación'),
color: 'bg-indigo-500',
description: 'Checklist de verificación'
description: t('production.quality.check_types.checklist_description', 'Checklist de verificación')
}
};
});
const PROCESS_STAGE_LABELS = {
[ProcessStage.MIXING]: 'Mezclado',
@@ -166,11 +166,22 @@ export const QualityTemplateManager: React.FC<QualityTemplateManagerProps> = ({
const templateStats = useMemo(() => {
const templates = templatesData?.templates || [];
// Calculate unique categories
const uniqueCategories = new Set(templates.map(t => t.category).filter(Boolean));
// Calculate average weight
const activeTemplates = templates.filter(t => t.is_active);
const averageWeight = activeTemplates.length > 0
? activeTemplates.reduce((sum, t) => sum + (t.weight || 0), 0) / activeTemplates.length
: 0;
return {
total: templates.length,
active: templates.filter(t => t.is_active).length,
critical: templates.filter(t => t.is_critical).length,
required: templates.filter(t => t.is_required).length,
categories: uniqueCategories.size,
averageWeight: parseFloat(averageWeight.toFixed(1)),
byType: Object.values(QualityCheckType).map(type => ({
type,
count: templates.filter(t => t.check_type === type).length
@@ -221,8 +232,9 @@ export const QualityTemplateManager: React.FC<QualityTemplateManagerProps> = ({
}
};
const getTemplateStatusConfig = (template: QualityCheckTemplate) => {
const typeConfig = QUALITY_CHECK_TYPE_CONFIG[template.check_type];
const getTemplateStatusConfig = (template: QualityCheckTemplate) => {
const typeConfigs = QUALITY_CHECK_TYPE_CONFIG(t);
const typeConfig = typeConfigs[template.check_type];
return {
color: template.is_active ? typeConfig.color : '#6b7280',
@@ -298,9 +310,21 @@ export const QualityTemplateManager: React.FC<QualityTemplateManagerProps> = ({
value: templateStats.required,
variant: 'warning',
icon: Tag
},
{
title: 'Categorías',
value: templateStats.categories,
variant: 'info',
icon: Tag
},
{
title: 'Peso Promedio',
value: templateStats.averageWeight,
variant: 'info',
icon: Scale
}
]}
columns={4}
columns={3}
/>
{/* Search and Filter Controls */}
@@ -316,7 +340,7 @@ export const QualityTemplateManager: React.FC<QualityTemplateManagerProps> = ({
value: selectedCheckType,
onChange: (value) => setSelectedCheckType(value as QualityCheckType | ''),
placeholder: 'Todos los tipos',
options: Object.entries(QUALITY_CHECK_TYPE_CONFIG).map(([type, config]) => ({
options: Object.entries(QUALITY_CHECK_TYPE_CONFIG(t)).map(([type, config]) => ({
value: type,
label: config.label
}))
@@ -471,4 +495,4 @@ export const QualityTemplateManager: React.FC<QualityTemplateManagerProps> = ({
);
};
export default QualityTemplateManager;
export default QualityTemplateManager;