feat: Rewrite QualityTemplateWizard with comprehensive field support
Complete rewrite following the established pattern. Key improvements:
- Removed internal "Crear Plantilla" button and API call
- Added validate prop with required field checks (name, checkType, weight)
- Real-time data sync with parent wizard using useEffect
- Auto-generation of template_code from name (TPL-XXX-1234)
- Added ALL 25 backend fields from research:
* Required: name, checkType, weight
* Basic: templateCode, description, applicableStages
* Check Points: checkPoints (JSON array configuration)
* Scoring: scoringMethod, passThreshold, isRequired, frequencyDays
* Advanced Config (JSONB): parameters, thresholds, scoringCriteria
* Status: isActive, version
* Helper fields: requiresPhoto, criticalControlPoint, notifyOnFail,
responsibleRole, requiredEquipment, acceptanceCriteria, specificConditions
- Organized fields using AdvancedOptionsSection for progressive disclosure
- Added tooltips for complex fields using existing Tooltip component
- Expanded check_type options (7 types vs original 4)
- Comprehensive validation for required fields only
- Note: API integration removed from wizard step, should be handled by
parent component on wizard completion
This commit is contained in:
@@ -1,230 +1,405 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
||||||
import { ClipboardCheck, CheckCircle2, Loader2 } from 'lucide-react';
|
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
|
||||||
import { useTenant } from '../../../../stores/tenant.store';
|
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
||||||
import { qualityTemplateService } from '../../../../api/services/qualityTemplates';
|
import { Info } from 'lucide-react';
|
||||||
import { QualityCheckTemplateCreate } from '../../../../api/types/qualityTemplates';
|
|
||||||
import { showToast } from '../../../../utils/toast';
|
|
||||||
|
|
||||||
interface WizardDataProps extends WizardStepProps {
|
interface WizardDataProps extends WizardStepProps {
|
||||||
data: Record<string, any>;
|
data: Record<string, any>;
|
||||||
onDataChange: (data: Record<string, any>) => void;
|
onDataChange: (data: Record<string, any>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TemplateInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
// Single comprehensive step with all fields
|
||||||
const { currentTenant } = useTenant();
|
const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||||
const [templateData, setTemplateData] = useState({
|
const [templateData, setTemplateData] = useState({
|
||||||
|
// Required fields
|
||||||
name: data.name || '',
|
name: data.name || '',
|
||||||
scope: data.scope || 'product',
|
checkType: data.checkType || 'product_quality',
|
||||||
frequency: data.frequency || 'batch',
|
weight: data.weight || '5.0',
|
||||||
frequencyTime: data.frequencyTime || '',
|
|
||||||
|
// Basic fields
|
||||||
|
templateCode: data.templateCode || '',
|
||||||
|
description: data.description || '',
|
||||||
|
applicableStages: data.applicableStages || '',
|
||||||
|
|
||||||
|
// Check points configuration
|
||||||
|
checkPoints: data.checkPoints || '',
|
||||||
|
|
||||||
|
// Scoring configuration
|
||||||
|
scoringMethod: data.scoringMethod || 'weighted_average',
|
||||||
|
passThreshold: data.passThreshold || '70.0',
|
||||||
|
isRequired: data.isRequired ?? false,
|
||||||
|
frequencyDays: data.frequencyDays || '',
|
||||||
|
|
||||||
|
// Advanced configuration (JSONB fields)
|
||||||
|
parameters: data.parameters || '',
|
||||||
|
thresholds: data.thresholds || '',
|
||||||
|
scoringCriteria: data.scoringCriteria || '',
|
||||||
|
|
||||||
|
// Status
|
||||||
|
isActive: data.isActive ?? true,
|
||||||
|
version: data.version || '1.0',
|
||||||
|
|
||||||
|
// Helper fields for UI
|
||||||
|
requiresPhoto: data.requiresPhoto ?? false,
|
||||||
|
criticalControlPoint: data.criticalControlPoint ?? false,
|
||||||
|
notifyOnFail: data.notifyOnFail ?? false,
|
||||||
responsibleRole: data.responsibleRole || '',
|
responsibleRole: data.responsibleRole || '',
|
||||||
requiresPhoto: data.requiresPhoto || false,
|
|
||||||
criticalControlPoint: data.criticalControlPoint || false,
|
|
||||||
requiredEquipment: data.requiredEquipment || '',
|
requiredEquipment: data.requiredEquipment || '',
|
||||||
acceptanceCriteria: data.acceptanceCriteria || '',
|
acceptanceCriteria: data.acceptanceCriteria || '',
|
||||||
notifyOnFail: data.notifyOnFail || false,
|
|
||||||
specificConditions: data.specificConditions || '',
|
specificConditions: data.specificConditions || '',
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
// Auto-generate template code from name if not provided
|
||||||
if (!currentTenant?.id) {
|
useEffect(() => {
|
||||||
setError('No se pudo obtener información del tenant');
|
if (!templateData.templateCode && templateData.name) {
|
||||||
return;
|
const code = `TPL-${templateData.name.substring(0, 3).toUpperCase()}-${Date.now().toString().slice(-4)}`;
|
||||||
|
setTemplateData(prev => ({ ...prev, templateCode: code }));
|
||||||
}
|
}
|
||||||
|
}, [templateData.name]);
|
||||||
|
|
||||||
setLoading(true);
|
// Sync with parent wizard state in real-time
|
||||||
setError(null);
|
useEffect(() => {
|
||||||
|
onDataChange({ ...data, ...templateData });
|
||||||
try {
|
}, [templateData]);
|
||||||
const scopeMapping: Record<string, string> = {
|
|
||||||
product: 'product_quality',
|
|
||||||
process: 'process_hygiene',
|
|
||||||
equipment: 'equipment',
|
|
||||||
safety: 'safety'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build comprehensive description
|
|
||||||
let description = `Plantilla de ${templateData.scope} con frecuencia ${templateData.frequency}.`;
|
|
||||||
if (templateData.frequencyTime) {
|
|
||||||
description += ` ${templateData.frequencyTime}.`;
|
|
||||||
}
|
|
||||||
if (templateData.responsibleRole) {
|
|
||||||
description += ` Responsable: ${templateData.responsibleRole}.`;
|
|
||||||
}
|
|
||||||
if (templateData.requiredEquipment) {
|
|
||||||
description += ` Equipo requerido: ${templateData.requiredEquipment}.`;
|
|
||||||
}
|
|
||||||
if (templateData.acceptanceCriteria) {
|
|
||||||
description += ` Criterios: ${templateData.acceptanceCriteria}.`;
|
|
||||||
}
|
|
||||||
if (templateData.specificConditions) {
|
|
||||||
description += ` Condiciones: ${templateData.specificConditions}.`;
|
|
||||||
}
|
|
||||||
if (templateData.requiresPhoto) {
|
|
||||||
description += ` Requiere fotografía.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const templateCreateData: QualityCheckTemplateCreate = {
|
|
||||||
name: templateData.name,
|
|
||||||
description: description,
|
|
||||||
check_type: scopeMapping[templateData.scope] || 'product_quality',
|
|
||||||
applicable_stages: [],
|
|
||||||
check_points: [
|
|
||||||
{
|
|
||||||
name: templateData.acceptanceCriteria ? 'Verificación de Criterios' : 'Verificación General',
|
|
||||||
description: templateData.acceptanceCriteria || 'Punto de verificación inicial',
|
|
||||||
expected_value: 'Conforme',
|
|
||||||
measurement_type: 'pass_fail',
|
|
||||||
is_critical: templateData.criticalControlPoint,
|
|
||||||
weight: 1.0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
scoring_method: 'weighted_average',
|
|
||||||
pass_threshold: 70.0,
|
|
||||||
weight: templateData.criticalControlPoint ? 10.0 : 5.0,
|
|
||||||
is_required: templateData.frequency === 'batch' || templateData.criticalControlPoint,
|
|
||||||
is_active: true,
|
|
||||||
frequency_days: templateData.frequency === 'daily' ? 1 : templateData.frequency === 'weekly' ? 7 : undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
await qualityTemplateService.createTemplate(currentTenant.id, templateCreateData);
|
|
||||||
|
|
||||||
showToast.success('Plantilla de calidad creada exitosamente');
|
|
||||||
onDataChange({ ...data, ...templateData });
|
|
||||||
onComplete();
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Error creating quality template:', err);
|
|
||||||
const errorMessage = err.response?.data?.detail || 'Error al crear la plantilla de calidad';
|
|
||||||
setError(errorMessage);
|
|
||||||
showToast.error(errorMessage);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
||||||
<ClipboardCheck className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">Plantilla de Calidad</h3>
|
Quality Template Details
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">
|
||||||
|
Fill in the required information to create a quality check template
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{/* Required Fields */}
|
||||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{error}
|
<div className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={templateData.name}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, name: e.target.value })}
|
||||||
|
placeholder="E.g., Bread Quality Control, Hygiene Inspection"
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div>
|
||||||
{/* Basic Information */}
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Check Type *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={templateData.checkType}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, checkType: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
|
>
|
||||||
|
<option value="product_quality">Product Quality</option>
|
||||||
|
<option value="process_hygiene">Process Hygiene</option>
|
||||||
|
<option value="equipment">Equipment</option>
|
||||||
|
<option value="safety">Safety</option>
|
||||||
|
<option value="cleaning">Cleaning</option>
|
||||||
|
<option value="temperature">Temperature Control</option>
|
||||||
|
<option value="documentation">Documentation</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Weight *
|
||||||
|
<Tooltip content="Importance weight for scoring (0.0-10.0)">
|
||||||
|
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={templateData.weight}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, weight: e.target.value })}
|
||||||
|
placeholder="5.0"
|
||||||
|
step="0.1"
|
||||||
|
min="0"
|
||||||
|
max="10"
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Basic Information */}
|
||||||
|
<div className="border-t border-[var(--border-primary)] pt-4">
|
||||||
|
<h4 className="text-sm font-semibold text-[var(--text-primary)] mb-3">Basic Information</h4>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Template Code
|
||||||
|
<Tooltip content="Auto-generated from name, or enter custom code">
|
||||||
|
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={templateData.templateCode}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, templateCode: e.target.value })}
|
||||||
|
placeholder="TPL-XXX-1234"
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Version
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={templateData.version}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, version: e.target.value })}
|
||||||
|
placeholder="1.0"
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={templateData.description}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, description: e.target.value })}
|
||||||
|
placeholder="Detailed description of the quality check template"
|
||||||
|
rows={2}
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Applicable Stages
|
||||||
|
<Tooltip content="Comma-separated list of production stages: e.g., mixing, proofing, baking, cooling">
|
||||||
|
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={templateData.applicableStages}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, applicableStages: e.target.value })}
|
||||||
|
placeholder="mixing, proofing, baking, cooling"
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scoring Configuration */}
|
||||||
|
<div className="border-t border-[var(--border-primary)] pt-4">
|
||||||
|
<h4 className="text-sm font-semibold text-[var(--text-primary)] mb-3">Scoring Configuration</h4>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Scoring Method
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={templateData.scoringMethod}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, scoringMethod: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
|
>
|
||||||
|
<option value="weighted_average">Weighted Average</option>
|
||||||
|
<option value="pass_fail">Pass/Fail</option>
|
||||||
|
<option value="percentage">Percentage</option>
|
||||||
|
<option value="points">Points-based</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Pass Threshold (%)
|
||||||
|
<Tooltip content="Minimum score required to pass (0-100)">
|
||||||
|
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={templateData.passThreshold}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, passThreshold: e.target.value })}
|
||||||
|
placeholder="70.0"
|
||||||
|
step="0.1"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Frequency (days)
|
||||||
|
<Tooltip content="How often this check should be performed (leave empty for batch-based)">
|
||||||
|
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={templateData.frequencyDays}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, frequencyDays: e.target.value })}
|
||||||
|
placeholder="Leave empty for batch-based"
|
||||||
|
min="1"
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={templateData.isRequired}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, isRequired: e.target.checked })}
|
||||||
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||||
|
/>
|
||||||
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
|
Required Check
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Advanced Options */}
|
||||||
|
<AdvancedOptionsSection
|
||||||
|
title="Advanced Options"
|
||||||
|
description="Optional fields for comprehensive quality template configuration"
|
||||||
|
>
|
||||||
|
{/* Check Points Configuration */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h4 className="text-sm font-semibold text-[var(--text-primary)] border-b border-[var(--border-secondary)] pb-2">
|
<h5 className="text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">
|
||||||
Información Básica
|
Check Points Configuration
|
||||||
</h4>
|
</h5>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
<div className="md:col-span-2">
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Nombre *</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
<input
|
Check Points (JSON Array)
|
||||||
type="text"
|
<Tooltip content='Array of check points: [{"name": "Visual Check", "description": "...", "weight": 1.0}]'>
|
||||||
value={templateData.name}
|
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||||
onChange={(e) => setTemplateData({ ...templateData, name: e.target.value })}
|
</Tooltip>
|
||||||
placeholder="Ej: Control de Calidad de Pan"
|
</label>
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
<textarea
|
||||||
|
value={templateData.checkPoints}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, checkPoints: e.target.value })}
|
||||||
|
placeholder='[{"name": "Visual Inspection", "description": "Check appearance", "expected_value": "Golden brown", "measurement_type": "visual", "is_critical": false, "weight": 1.0}]'
|
||||||
|
rows={4}
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Alcance *</label>
|
|
||||||
<select
|
|
||||||
value={templateData.scope}
|
|
||||||
onChange={(e) => setTemplateData({ ...templateData, scope: e.target.value })}
|
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
||||||
>
|
|
||||||
<option value="product">Calidad de Producto</option>
|
|
||||||
<option value="process">Higiene de Proceso</option>
|
|
||||||
<option value="equipment">Equipo</option>
|
|
||||||
<option value="safety">Seguridad</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Frecuencia *</label>
|
|
||||||
<select
|
|
||||||
value={templateData.frequency}
|
|
||||||
onChange={(e) => setTemplateData({ ...templateData, frequency: e.target.value })}
|
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
||||||
>
|
|
||||||
<option value="batch">Cada Lote</option>
|
|
||||||
<option value="daily">Diario</option>
|
|
||||||
<option value="weekly">Semanal</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="md:col-span-2">
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
Hora/Condiciones Específicas
|
Acceptance Criteria
|
||||||
</label>
|
</label>
|
||||||
<input
|
<textarea
|
||||||
type="text"
|
value={templateData.acceptanceCriteria}
|
||||||
value={templateData.frequencyTime}
|
onChange={(e) => setTemplateData({ ...templateData, acceptanceCriteria: e.target.value })}
|
||||||
onChange={(e) => setTemplateData({ ...templateData, frequencyTime: e.target.value })}
|
placeholder="E.g., Golden uniform color, fluffy texture, no burns..."
|
||||||
placeholder="Ej: 8:00 AM, antes de hornear, temperatura ambiente"
|
rows={2}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* JSONB Configuration Fields */}
|
||||||
|
<div className="space-y-4 border-t border-[var(--border-primary)] pt-4">
|
||||||
|
<h5 className="text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">
|
||||||
|
Advanced Configuration (JSONB)
|
||||||
|
</h5>
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Parameters (JSON)
|
||||||
|
<Tooltip content='Template parameters: {"temp_min": 75, "temp_max": 85, "humidity": 65}'>
|
||||||
|
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={templateData.parameters}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, parameters: e.target.value })}
|
||||||
|
placeholder='{"temp_min": 75, "temp_max": 85, "humidity": 65}'
|
||||||
|
rows={2}
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Thresholds (JSON)
|
||||||
|
<Tooltip content='Threshold values: {"critical": 90, "warning": 70, "acceptable": 50}'>
|
||||||
|
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={templateData.thresholds}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, thresholds: e.target.value })}
|
||||||
|
placeholder='{"critical": 90, "warning": 70, "acceptable": 50}'
|
||||||
|
rows={2}
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Scoring Criteria (JSON)
|
||||||
|
<Tooltip content='Custom scoring criteria: {"appearance": 30, "texture": 30, "taste": 40}'>
|
||||||
|
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={templateData.scoringCriteria}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, scoringCriteria: e.target.value })}
|
||||||
|
placeholder='{"appearance": 30, "texture": 30, "taste": 40}'
|
||||||
|
rows={2}
|
||||||
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Responsibility & Requirements */}
|
{/* Responsibility & Requirements */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4 border-t border-[var(--border-primary)] pt-4">
|
||||||
<h4 className="text-sm font-semibold text-[var(--text-primary)] border-b border-[var(--border-secondary)] pb-2">
|
<h5 className="text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">
|
||||||
Responsabilidad y Requisitos
|
Responsibility & Requirements
|
||||||
</h4>
|
</h5>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
Persona/Rol Responsable
|
Responsible Role/Person
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={templateData.responsibleRole}
|
value={templateData.responsibleRole}
|
||||||
onChange={(e) => setTemplateData({ ...templateData, responsibleRole: e.target.value })}
|
onChange={(e) => setTemplateData({ ...templateData, responsibleRole: e.target.value })}
|
||||||
placeholder="Ej: Jefe de Producción, Panadero"
|
placeholder="E.g., Production Manager, Baker"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
Equipo/Herramientas Requeridas
|
Required Equipment/Tools
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={templateData.requiredEquipment}
|
value={templateData.requiredEquipment}
|
||||||
onChange={(e) => setTemplateData({ ...templateData, requiredEquipment: e.target.value })}
|
onChange={(e) => setTemplateData({ ...templateData, requiredEquipment: e.target.value })}
|
||||||
placeholder="Ej: Termómetro, báscula, cronómetro"
|
placeholder="E.g., Thermometer, scale, timer"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
Criterios de Aceptación
|
Specific Conditions or Notes
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={templateData.acceptanceCriteria}
|
|
||||||
onChange={(e) => setTemplateData({ ...templateData, acceptanceCriteria: e.target.value })}
|
|
||||||
placeholder="Ej: Color dorado uniforme, textura esponjosa, sin quemaduras..."
|
|
||||||
rows={3}
|
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="md:col-span-2">
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
||||||
Condiciones o Notas Especiales
|
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={templateData.specificConditions}
|
value={templateData.specificConditions}
|
||||||
onChange={(e) => setTemplateData({ ...templateData, specificConditions: e.target.value })}
|
onChange={(e) => setTemplateData({ ...templateData, specificConditions: e.target.value })}
|
||||||
placeholder="Ej: Solo aplicable en días húmedos, verificar 30 min después de hornear..."
|
placeholder="E.g., Only applicable on humid days, check 30 min after baking..."
|
||||||
rows={2}
|
rows={2}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -233,76 +408,75 @@ const TemplateInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange, onCom
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Control Settings */}
|
{/* Control Settings */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4 border-t border-[var(--border-primary)] pt-4">
|
||||||
<h4 className="text-sm font-semibold text-[var(--text-primary)] border-b border-[var(--border-secondary)] pb-2">
|
<h5 className="text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">
|
||||||
Configuración de Control
|
Control Settings
|
||||||
</h4>
|
</h5>
|
||||||
<div className="space-y-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<label className="flex items-center gap-3 p-3 bg-[var(--bg-secondary)]/30 rounded-lg border border-[var(--border-secondary)] cursor-pointer hover:bg-[var(--bg-secondary)]/50 transition-colors">
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={templateData.isActive}
|
||||||
|
onChange={(e) => setTemplateData({ ...templateData, isActive: e.target.checked })}
|
||||||
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||||
|
/>
|
||||||
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
|
Active Template
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={templateData.requiresPhoto}
|
checked={templateData.requiresPhoto}
|
||||||
onChange={(e) => setTemplateData({ ...templateData, requiresPhoto: e.target.checked })}
|
onChange={(e) => setTemplateData({ ...templateData, requiresPhoto: e.target.checked })}
|
||||||
className="w-4 h-4 text-[var(--color-primary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||||
/>
|
/>
|
||||||
<div>
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
<p className="text-sm font-medium text-[var(--text-primary)]">Requiere Fotografía</p>
|
Requires Photo Evidence
|
||||||
<p className="text-xs text-[var(--text-tertiary)]">Se debe adjuntar evidencia fotográfica</p>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex items-center gap-3 p-3 bg-[var(--bg-secondary)]/30 rounded-lg border border-[var(--border-secondary)] cursor-pointer hover:bg-[var(--bg-secondary)]/50 transition-colors">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={templateData.criticalControlPoint}
|
checked={templateData.criticalControlPoint}
|
||||||
onChange={(e) => setTemplateData({ ...templateData, criticalControlPoint: e.target.checked })}
|
onChange={(e) => setTemplateData({ ...templateData, criticalControlPoint: e.target.checked })}
|
||||||
className="w-4 h-4 text-[var(--color-primary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||||
/>
|
/>
|
||||||
<div>
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
<p className="text-sm font-medium text-[var(--text-primary)]">Punto de Control Crítico (PCC)</p>
|
Critical Control Point (CCP)
|
||||||
<p className="text-xs text-[var(--text-tertiary)]">Requiere acción inmediata si falla</p>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex items-center gap-3 p-3 bg-[var(--bg-secondary)]/30 rounded-lg border border-[var(--border-secondary)] cursor-pointer hover:bg-[var(--bg-secondary)]/50 transition-colors">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={templateData.notifyOnFail}
|
checked={templateData.notifyOnFail}
|
||||||
onChange={(e) => setTemplateData({ ...templateData, notifyOnFail: e.target.checked })}
|
onChange={(e) => setTemplateData({ ...templateData, notifyOnFail: e.target.checked })}
|
||||||
className="w-4 h-4 text-[var(--color-primary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||||
/>
|
/>
|
||||||
<div>
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
<p className="text-sm font-medium text-[var(--text-primary)]">Notificar en Caso de Fallo</p>
|
Notify on Failure
|
||||||
<p className="text-xs text-[var(--text-tertiary)]">Enviar alerta cuando el control no pase</p>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AdvancedOptionsSection>
|
||||||
|
|
||||||
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
|
|
||||||
<button
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={!templateData.name || loading}
|
|
||||||
className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-semibold inline-flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
|
||||||
Guardando...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CheckCircle2 className="w-5 h-5" />
|
|
||||||
Crear Plantilla
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QualityTemplateWizardSteps = (data: Record<string, any>, setData: (data: Record<string, any>) => void): WizardStep[] => [
|
export const QualityTemplateWizardSteps = (
|
||||||
{ id: 'template-info', title: 'Información de Plantilla', description: 'Nombre, alcance, frecuencia', component: (props) => <TemplateInfoStep {...props} data={data} onDataChange={setData} /> },
|
data: Record<string, any>,
|
||||||
|
setData: (data: Record<string, any>) => void
|
||||||
|
): WizardStep[] => [
|
||||||
|
{
|
||||||
|
id: 'template-details',
|
||||||
|
title: 'Template Details',
|
||||||
|
component: (props) => <QualityTemplateDetailsStep {...props} data={data} onDataChange={setData} />,
|
||||||
|
validate: () => {
|
||||||
|
return !!(data.name && data.checkType && data.weight);
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user