Improve the frontend modals
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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: 'Sí' },
|
||||
{ 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;
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user