Add improved production UI 3
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -1 +1,2 @@
|
||||
export { CreateRecipeModal } from './CreateRecipeModal';
|
||||
export { CreateRecipeModal } from './CreateRecipeModal';
|
||||
export { QualityCheckConfigurationModal } from './QualityCheckConfigurationModal';
|
||||
Reference in New Issue
Block a user