Add improved production UI 3

This commit is contained in:
Urtzi Alfaro
2025-09-23 19:24:22 +02:00
parent 7f871fc933
commit 7892c5a739
47 changed files with 6211 additions and 267 deletions

View File

@@ -0,0 +1,372 @@
import React, { useState, useEffect } from 'react';
import {
CheckCircle,
AlertTriangle,
Settings,
Plus,
Trash2,
Save
} from 'lucide-react';
import {
Modal,
Button,
Card,
Badge,
Select
} from '../../ui';
import { LoadingSpinner } from '../../shared';
import { useCurrentTenant } from '../../../stores/tenant.store';
import { useQualityTemplatesForRecipe } from '../../../api/hooks/qualityTemplates';
import {
ProcessStage,
type QualityCheckTemplate,
type RecipeQualityConfiguration,
type ProcessStageQualityConfig
} from '../../../api/types/qualityTemplates';
import type { RecipeResponse } from '../../../api/types/recipes';
interface QualityCheckConfigurationModalProps {
isOpen: boolean;
onClose: () => void;
recipe: RecipeResponse;
onSaveConfiguration: (config: RecipeQualityConfiguration) => Promise<void>;
isLoading?: boolean;
}
const PROCESS_STAGE_LABELS = {
[ProcessStage.MIXING]: 'Mezclado',
[ProcessStage.PROOFING]: 'Fermentación',
[ProcessStage.SHAPING]: 'Formado',
[ProcessStage.BAKING]: 'Horneado',
[ProcessStage.COOLING]: 'Enfriado',
[ProcessStage.PACKAGING]: 'Empaquetado',
[ProcessStage.FINISHING]: 'Acabado'
};
export const QualityCheckConfigurationModal: React.FC<QualityCheckConfigurationModalProps> = ({
isOpen,
onClose,
recipe,
onSaveConfiguration,
isLoading = false
}) => {
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
// Initialize configuration from recipe or create default
const [configuration, setConfiguration] = useState<RecipeQualityConfiguration>(() => {
const existing = recipe.quality_check_configuration as RecipeQualityConfiguration;
return existing || {
stages: {},
global_parameters: {},
default_templates: []
};
});
// Fetch available templates
const {
data: templatesByStage,
isLoading: templatesLoading,
error: templatesError
} = useQualityTemplatesForRecipe(tenantId, recipe.id);
// Reset configuration when recipe changes
useEffect(() => {
const existing = recipe.quality_check_configuration as RecipeQualityConfiguration;
setConfiguration(existing || {
stages: {},
global_parameters: {},
default_templates: []
});
}, [recipe]);
const handleStageToggle = (stage: ProcessStage) => {
setConfiguration(prev => {
const newStages = { ...prev.stages };
if (newStages[stage]) {
// Remove stage configuration
delete newStages[stage];
} else {
// Add default stage configuration
newStages[stage] = {
stage,
template_ids: [],
custom_parameters: {},
is_required: true,
blocking: true
};
}
return {
...prev,
stages: newStages
};
});
};
const handleTemplateToggle = (stage: ProcessStage, templateId: string) => {
setConfiguration(prev => {
const stageConfig = prev.stages[stage];
if (!stageConfig) return prev;
const templateIds = stageConfig.template_ids.includes(templateId)
? stageConfig.template_ids.filter(id => id !== templateId)
: [...stageConfig.template_ids, templateId];
return {
...prev,
stages: {
...prev.stages,
[stage]: {
...stageConfig,
template_ids: templateIds
}
}
};
});
};
const handleStageSettingChange = (
stage: ProcessStage,
setting: keyof ProcessStageQualityConfig,
value: boolean
) => {
setConfiguration(prev => {
const stageConfig = prev.stages[stage];
if (!stageConfig) return prev;
return {
...prev,
stages: {
...prev.stages,
[stage]: {
...stageConfig,
[setting]: value
}
}
};
});
};
const handleSave = async () => {
try {
await onSaveConfiguration(configuration);
onClose();
} catch (error) {
console.error('Error saving quality configuration:', error);
}
};
const getConfiguredStagesCount = () => {
return Object.keys(configuration.stages).length;
};
const getTotalTemplatesCount = () => {
return Object.values(configuration.stages).reduce(
(total, stage) => total + stage.template_ids.length,
0
);
};
if (templatesLoading) {
return (
<Modal isOpen={isOpen} onClose={onClose} title="Configuración de Control de Calidad" size="xl">
<div className="flex items-center justify-center py-12">
<LoadingSpinner text="Cargando plantillas de calidad..." />
</div>
</Modal>
);
}
if (templatesError) {
return (
<Modal isOpen={isOpen} onClose={onClose} title="Configuración de Control de Calidad" size="xl">
<div className="text-center py-12">
<AlertTriangle className="mx-auto h-12 w-12 text-red-500 mb-4" />
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
Error al cargar plantillas
</h3>
<p className="text-[var(--text-secondary)]">
No se pudieron cargar las plantillas de control de calidad
</p>
</div>
</Modal>
);
}
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={`Control de Calidad - ${recipe.name}`}
size="xl"
>
<div className="space-y-6">
{/* Summary */}
<Card className="p-4 bg-[var(--bg-secondary)]">
<div className="flex items-center justify-between">
<div>
<h4 className="font-medium text-[var(--text-primary)]">
Resumen de Configuración
</h4>
<p className="text-sm text-[var(--text-secondary)] mt-1">
{getConfiguredStagesCount()} etapas configuradas {getTotalTemplatesCount()} controles asignados
</p>
</div>
<div className="flex gap-2">
<Badge variant="info">
{recipe.category || 'Sin categoría'}
</Badge>
<Badge variant={recipe.status === 'active' ? 'success' : 'warning'}>
{recipe.status}
</Badge>
</div>
</div>
</Card>
{/* Process Stages Configuration */}
<div className="space-y-4">
<h4 className="font-medium text-[var(--text-primary)] flex items-center gap-2">
<Settings className="w-5 h-5" />
Configuración por Etapas del Proceso
</h4>
{Object.values(ProcessStage).map(stage => {
const stageConfig = configuration.stages[stage];
const isConfigured = !!stageConfig;
const availableTemplates = templatesByStage?.[stage] || [];
return (
<Card key={stage} className="p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={isConfigured}
onChange={() => handleStageToggle(stage)}
className="rounded border-[var(--border-primary)]"
/>
<span className="font-medium">
{PROCESS_STAGE_LABELS[stage]}
</span>
</label>
{isConfigured && (
<Badge variant="success" size="sm">
{stageConfig.template_ids.length} controles
</Badge>
)}
</div>
{isConfigured && (
<div className="flex gap-4 text-sm">
<label className="flex items-center gap-1">
<input
type="checkbox"
checked={stageConfig.is_required}
onChange={(e) => handleStageSettingChange(
stage,
'is_required',
e.target.checked
)}
className="rounded border-[var(--border-primary)]"
/>
Requerido
</label>
<label className="flex items-center gap-1">
<input
type="checkbox"
checked={stageConfig.blocking}
onChange={(e) => handleStageSettingChange(
stage,
'blocking',
e.target.checked
)}
className="rounded border-[var(--border-primary)]"
/>
Bloqueante
</label>
</div>
)}
</div>
{isConfigured && (
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
Plantillas de Control de Calidad
</label>
{availableTemplates.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{availableTemplates.map(template => (
<label
key={template.id}
className="flex items-center gap-2 p-2 border rounded cursor-pointer hover:bg-[var(--bg-secondary)]"
>
<input
type="checkbox"
checked={stageConfig.template_ids.includes(template.id)}
onChange={() => handleTemplateToggle(stage, template.id)}
className="rounded border-[var(--border-primary)]"
/>
<div className="flex-1">
<div className="font-medium text-sm">
{template.name}
</div>
<div className="text-xs text-[var(--text-secondary)]">
{template.check_type} {template.category || 'Sin categoría'}
</div>
</div>
{template.is_critical && (
<Badge variant="error" size="sm">Crítico</Badge>
)}
{template.is_required && (
<Badge variant="warning" size="sm">Req.</Badge>
)}
</label>
))}
</div>
) : (
<p className="text-sm text-[var(--text-secondary)] py-4 text-center border border-dashed rounded">
No hay plantillas disponibles para esta etapa.
<br />
<a
href="/app/operations/production/quality-templates"
className="text-[var(--color-primary)] hover:underline"
>
Crear nueva plantilla
</a>
</p>
)}
</div>
</div>
)}
</Card>
);
})}
</div>
{/* Actions */}
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-primary)]">
<Button
variant="outline"
onClick={onClose}
disabled={isLoading}
>
Cancelar
</Button>
<Button
variant="primary"
onClick={handleSave}
disabled={isLoading}
isLoading={isLoading}
>
<Save className="w-4 h-4 mr-2" />
Guardar Configuración
</Button>
</div>
</div>
</Modal>
);
};

View File

@@ -1 +1,2 @@
export { CreateRecipeModal } from './CreateRecipeModal';
export { CreateRecipeModal } from './CreateRecipeModal';
export { QualityCheckConfigurationModal } from './QualityCheckConfigurationModal';