Fix some UI issues 2
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import {
|
||||
Modal,
|
||||
@@ -9,18 +9,47 @@ import {
|
||||
Badge,
|
||||
Card
|
||||
} from '../../ui';
|
||||
import { LoadingSpinner } from '../../shared';
|
||||
import {
|
||||
QualityCheckType,
|
||||
ProcessStage,
|
||||
type QualityCheckTemplateCreate
|
||||
} from '../../../api/types/qualityTemplates';
|
||||
import { useCurrentTenant } from '../../../stores/tenant.store';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { recipesService } from '../../../api/services/recipes';
|
||||
import type { RecipeResponse } from '../../../api/types/recipes';
|
||||
import {
|
||||
Plus,
|
||||
X,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
AlertTriangle,
|
||||
Info,
|
||||
Settings,
|
||||
Link as LinkIcon
|
||||
} from 'lucide-react';
|
||||
|
||||
interface CreateQualityTemplateModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onCreateTemplate: (templateData: QualityCheckTemplateCreate) => Promise<void>;
|
||||
onCreateTemplate: (templateData: QualityCheckTemplateCreate, recipeAssociations?: RecipeAssociation[]) => Promise<void>;
|
||||
isLoading?: boolean;
|
||||
initialRecipe?: RecipeResponse; // Pre-select recipe if coming from recipe page
|
||||
}
|
||||
|
||||
interface RecipeAssociation {
|
||||
recipeId: string;
|
||||
recipeName: string;
|
||||
stages: ProcessStage[];
|
||||
}
|
||||
|
||||
interface StageConfiguration {
|
||||
stage: ProcessStage;
|
||||
isRequired: boolean;
|
||||
isOptional: boolean;
|
||||
blockingOnFailure: boolean;
|
||||
minQualityScore?: number;
|
||||
}
|
||||
|
||||
const QUALITY_CHECK_TYPE_OPTIONS = [
|
||||
@@ -58,10 +87,22 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
|
||||
isOpen,
|
||||
onClose,
|
||||
onCreateTemplate,
|
||||
isLoading = false
|
||||
isLoading = false,
|
||||
initialRecipe
|
||||
}) => {
|
||||
const currentTenant = useCurrentTenant();
|
||||
const [selectedStages, setSelectedStages] = useState<ProcessStage[]>([]);
|
||||
const [recipeAssociations, setRecipeAssociations] = useState<RecipeAssociation[]>([]);
|
||||
const [stageConfigurations, setStageConfigurations] = useState<StageConfiguration[]>([]);
|
||||
const [showRecipeAssociation, setShowRecipeAssociation] = useState(false);
|
||||
const [showAdvancedConfig, setShowAdvancedConfig] = useState(false);
|
||||
|
||||
// Fetch available recipes for association
|
||||
const { data: recipes, isLoading: recipesLoading } = useQuery({
|
||||
queryKey: ['recipes', currentTenant?.id],
|
||||
queryFn: () => recipesService.searchRecipes(currentTenant?.id || '', { limit: 1000 }),
|
||||
enabled: isOpen && !!currentTenant?.id && showRecipeAssociation
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -94,6 +135,33 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
|
||||
QualityCheckType.WEIGHT
|
||||
].includes(checkType);
|
||||
|
||||
// Initialize with recipe if provided
|
||||
useEffect(() => {
|
||||
if (initialRecipe && isOpen) {
|
||||
setRecipeAssociations([{
|
||||
recipeId: initialRecipe.id,
|
||||
recipeName: initialRecipe.name,
|
||||
stages: []
|
||||
}]);
|
||||
setShowRecipeAssociation(true);
|
||||
}
|
||||
}, [initialRecipe, isOpen]);
|
||||
|
||||
// Update stage configurations when selected stages change
|
||||
useEffect(() => {
|
||||
const newConfigs = selectedStages.map(stage => {
|
||||
const existing = stageConfigurations.find(c => c.stage === stage);
|
||||
return existing || {
|
||||
stage,
|
||||
isRequired: false,
|
||||
isOptional: true,
|
||||
blockingOnFailure: true,
|
||||
minQualityScore: undefined
|
||||
};
|
||||
});
|
||||
setStageConfigurations(newConfigs);
|
||||
}, [selectedStages]);
|
||||
|
||||
const handleStageToggle = (stage: ProcessStage) => {
|
||||
const newStages = selectedStages.includes(stage)
|
||||
? selectedStages.filter(s => s !== stage)
|
||||
@@ -102,17 +170,78 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
|
||||
setSelectedStages(newStages);
|
||||
};
|
||||
|
||||
const handleAddRecipeAssociation = () => {
|
||||
const newAssociation: RecipeAssociation = {
|
||||
recipeId: '',
|
||||
recipeName: '',
|
||||
stages: []
|
||||
};
|
||||
setRecipeAssociations([...recipeAssociations, newAssociation]);
|
||||
};
|
||||
|
||||
const handleRemoveRecipeAssociation = (index: number) => {
|
||||
setRecipeAssociations(recipeAssociations.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleRecipeAssociationChange = (index: number, field: keyof RecipeAssociation, value: any) => {
|
||||
const updated = [...recipeAssociations];
|
||||
if (field === 'recipeId' && recipes) {
|
||||
const recipe = recipes.find(r => r.id === value);
|
||||
if (recipe) {
|
||||
updated[index] = {
|
||||
...updated[index],
|
||||
recipeId: value,
|
||||
recipeName: recipe.name
|
||||
};
|
||||
}
|
||||
} else {
|
||||
(updated[index] as any)[field] = value;
|
||||
}
|
||||
setRecipeAssociations(updated);
|
||||
};
|
||||
|
||||
const handleStageConfigChange = (stage: ProcessStage, field: keyof StageConfiguration, value: any) => {
|
||||
setStageConfigurations(configs =>
|
||||
configs.map(config =>
|
||||
config.stage === stage ? { ...config, [field]: value } : config
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: QualityCheckTemplateCreate) => {
|
||||
try {
|
||||
// Validate recipe associations if any
|
||||
const validRecipeAssociations = recipeAssociations
|
||||
.filter(association => association.recipeId && association.stages.length > 0)
|
||||
.map(association => ({
|
||||
...association,
|
||||
stageConfigurations: stageConfigurations
|
||||
.filter(config => association.stages.includes(config.stage))
|
||||
.reduce((acc, config) => {
|
||||
acc[config.stage] = {
|
||||
template_ids: [], // Will be populated by backend
|
||||
required_checks: config.isRequired ? [data.name] : [],
|
||||
optional_checks: !config.isRequired ? [data.name] : [],
|
||||
blocking_on_failure: config.blockingOnFailure,
|
||||
min_quality_score: config.minQualityScore || null
|
||||
};
|
||||
return acc;
|
||||
}, {} as Record<string, any>)
|
||||
}));
|
||||
|
||||
await onCreateTemplate({
|
||||
...data,
|
||||
applicable_stages: selectedStages.length > 0 ? selectedStages : undefined,
|
||||
created_by: currentTenant?.id || ''
|
||||
});
|
||||
}, validRecipeAssociations.length > 0 ? validRecipeAssociations : undefined);
|
||||
|
||||
// Reset form
|
||||
reset();
|
||||
setSelectedStages([]);
|
||||
setRecipeAssociations([]);
|
||||
setStageConfigurations([]);
|
||||
setShowRecipeAssociation(false);
|
||||
setShowAdvancedConfig(false);
|
||||
} catch (error) {
|
||||
console.error('Error creating template:', error);
|
||||
}
|
||||
@@ -121,6 +250,10 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
|
||||
const handleClose = () => {
|
||||
reset();
|
||||
setSelectedStages([]);
|
||||
setRecipeAssociations([]);
|
||||
setStageConfigurations([]);
|
||||
setShowRecipeAssociation(false);
|
||||
setShowAdvancedConfig(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
@@ -296,16 +429,171 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Recipe Association */}
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<LinkIcon className="h-5 w-5 text-[var(--text-secondary)]" />
|
||||
<h4 className="font-medium text-[var(--text-primary)]">
|
||||
Asociación con Recetas
|
||||
</h4>
|
||||
<Badge variant="outline" size="sm">
|
||||
Nuevo
|
||||
</Badge>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowRecipeAssociation(!showRecipeAssociation)}
|
||||
>
|
||||
{showRecipeAssociation ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{showRecipeAssociation && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start space-x-2 p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<Info className="h-4 w-4 text-blue-500 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-[var(--text-secondary)]">
|
||||
<p className="font-medium text-[var(--text-primary)] mb-1">Asociación Automática con Recetas</p>
|
||||
<p>
|
||||
Al asociar esta plantilla con recetas, se aplicará automáticamente a los lotes de producción
|
||||
creados a partir de esas recetas. Esto asegura consistencia en los controles de calidad.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{recipeAssociations.map((association, index) => {
|
||||
const isComplete = association.recipeId && association.stages.length > 0;
|
||||
return (
|
||||
<div key={index} className={`p-4 border rounded-lg space-y-3 ${
|
||||
isComplete ? 'border-green-200 bg-green-50/50' : 'border-[var(--border-primary)]'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h5 className="font-medium text-[var(--text-primary)]">
|
||||
Receta {index + 1}
|
||||
</h5>
|
||||
{isComplete && (
|
||||
<Badge variant="success" size="sm">Configurada</Badge>
|
||||
)}
|
||||
</div>
|
||||
{recipeAssociations.length > 1 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveRecipeAssociation(index)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Seleccionar Receta
|
||||
</label>
|
||||
{recipesLoading ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<LoadingSpinner size="sm" />
|
||||
<span className="text-sm text-[var(--text-secondary)]">Cargando recetas...</span>
|
||||
</div>
|
||||
) : (
|
||||
<Select
|
||||
value={association.recipeId}
|
||||
onChange={(e) => handleRecipeAssociationChange(index, 'recipeId', e.target.value)}
|
||||
>
|
||||
<option value="">Seleccionar receta</option>
|
||||
{recipes?.map(recipe => (
|
||||
<option key={recipe.id} value={recipe.id}>
|
||||
{recipe.name} {recipe.category && `(${recipe.category})`}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{association.recipeId && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||
Etapas donde aplicar en esta receta
|
||||
</label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||
{PROCESS_STAGE_OPTIONS.map(stage => (
|
||||
<label
|
||||
key={stage.value}
|
||||
className="flex items-center space-x-2 p-2 border rounded cursor-pointer hover:bg-[var(--bg-secondary)]"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={association.stages.includes(stage.value)}
|
||||
onChange={(e) => {
|
||||
const newStages = e.target.checked
|
||||
? [...association.stages, stage.value]
|
||||
: association.stages.filter(s => s !== stage.value);
|
||||
handleRecipeAssociationChange(index, 'stages', newStages);
|
||||
}}
|
||||
className="rounded border-[var(--border-primary)]"
|
||||
/>
|
||||
<span className="text-sm">{stage.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{association.recipeId && association.stages.length === 0 && (
|
||||
<div className="flex items-start space-x-2 p-2 bg-yellow-50/50 border border-yellow-200 rounded">
|
||||
<AlertTriangle className="h-4 w-4 text-yellow-500 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-sm text-yellow-700">
|
||||
Selecciona al menos una etapa para completar la configuración de esta receta.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddRecipeAssociation}
|
||||
disabled={recipesLoading}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Agregar Receta
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Process Stages */}
|
||||
<Card className="p-4">
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-4">
|
||||
Etapas del Proceso Aplicables
|
||||
</h4>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="font-medium text-[var(--text-primary)]">
|
||||
Etapas del Proceso Aplicables
|
||||
</h4>
|
||||
{selectedStages.length > 0 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowAdvancedConfig(!showAdvancedConfig)}
|
||||
>
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
{showAdvancedConfig ? 'Ocultar' : 'Configurar'} Avanzado
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
||||
Selecciona las etapas donde se debe aplicar este control. Si no seleccionas ninguna, se aplicará a todas las etapas.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-4">
|
||||
{PROCESS_STAGE_OPTIONS.map(stage => (
|
||||
<label
|
||||
key={stage.value}
|
||||
@@ -321,6 +609,81 @@ export const CreateQualityTemplateModal: React.FC<CreateQualityTemplateModalProp
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Advanced Stage Configuration */}
|
||||
{showAdvancedConfig && selectedStages.length > 0 && (
|
||||
<div className="mt-6 p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<div className="flex items-start space-x-2 mb-4">
|
||||
<AlertTriangle className="h-4 w-4 text-orange-500 mt-0.5" />
|
||||
<div className="text-sm text-[var(--text-secondary)]">
|
||||
<p className="font-medium text-[var(--text-primary)] mb-1">Configuración Avanzada por Etapa</p>
|
||||
<p>
|
||||
Configura comportamientos específicos para cada etapa seleccionada. Esta configuración se aplicará
|
||||
cuando se use la plantilla en recetas asociadas.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{stageConfigurations.map(config => (
|
||||
<div key={config.stage} className="p-3 bg-white rounded-lg border">
|
||||
<h5 className="font-medium text-[var(--text-primary)] mb-3">
|
||||
{PROCESS_STAGE_OPTIONS.find(s => s.value === config.stage)?.label}
|
||||
</h5>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.isRequired}
|
||||
onChange={(e) => handleStageConfigChange(config.stage, 'isRequired', e.target.checked)}
|
||||
className="rounded border-[var(--border-primary)]"
|
||||
/>
|
||||
<span className="text-sm">Control requerido</span>
|
||||
<Badge variant="info" size="sm">Obligatorio</Badge>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.blockingOnFailure}
|
||||
onChange={(e) => handleStageConfigChange(config.stage, 'blockingOnFailure', e.target.checked)}
|
||||
className="rounded border-[var(--border-primary)]"
|
||||
/>
|
||||
<span className="text-sm">Bloquear si falla</span>
|
||||
<Badge variant="error" size="sm">Crítico</Badge>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
||||
Puntuación Mínima (opcional)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="10"
|
||||
step="0.1"
|
||||
value={config.minQualityScore || ''}
|
||||
onChange={(e) => handleStageConfigChange(
|
||||
config.stage,
|
||||
'minQualityScore',
|
||||
e.target.value ? parseFloat(e.target.value) : undefined
|
||||
)}
|
||||
placeholder="ej: 7.0"
|
||||
className="w-24"
|
||||
/>
|
||||
<p className="text-xs text-[var(--text-tertiary)] mt-1">
|
||||
Puntuación mínima requerida (0-10)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Settings */}
|
||||
|
||||
Reference in New Issue
Block a user