399 lines
15 KiB
TypeScript
399 lines
15 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
|||
|
|
import { useTranslation } from 'react-i18next';
|
|||
|
|
import { Plus, X, Clock, Flame, ChefHat } from 'lucide-react';
|
|||
|
|
import Button from '../../../ui/Button/Button';
|
|||
|
|
import Card from '../../../ui/Card/Card';
|
|||
|
|
import Input from '../../../ui/Input/Input';
|
|||
|
|
import Select from '../../../ui/Select/Select';
|
|||
|
|
|
|||
|
|
export interface ProductionProcess {
|
|||
|
|
id: string;
|
|||
|
|
name: string;
|
|||
|
|
sourceProduct: string;
|
|||
|
|
finishedProduct: string;
|
|||
|
|
processType: 'baking' | 'decorating' | 'finishing' | 'assembly';
|
|||
|
|
duration: number; // minutes
|
|||
|
|
temperature?: number; // celsius
|
|||
|
|
instructions?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface ProductionProcessesStepProps {
|
|||
|
|
onUpdate?: (data: { processes: ProductionProcess[] }) => void;
|
|||
|
|
onComplete?: () => void;
|
|||
|
|
initialData?: {
|
|||
|
|
processes?: ProductionProcess[];
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const PROCESS_TEMPLATES: Partial<ProductionProcess>[] = [
|
|||
|
|
{
|
|||
|
|
name: 'Horneado de Pan Pre-cocido',
|
|||
|
|
processType: 'baking',
|
|||
|
|
duration: 15,
|
|||
|
|
temperature: 200,
|
|||
|
|
instructions: 'Hornear a 200°C durante 15 minutos hasta dorar',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: 'Terminado de Croissant Congelado',
|
|||
|
|
processType: 'baking',
|
|||
|
|
duration: 20,
|
|||
|
|
temperature: 180,
|
|||
|
|
instructions: 'Descongelar 2h, hornear a 180°C por 20 min',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: 'Decoración de Pastel',
|
|||
|
|
processType: 'decorating',
|
|||
|
|
duration: 30,
|
|||
|
|
instructions: 'Aplicar crema, decorar y refrigerar',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: 'Montaje de Sándwich',
|
|||
|
|
processType: 'assembly',
|
|||
|
|
duration: 5,
|
|||
|
|
instructions: 'Ensamblar ingredientes según especificación',
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
export const ProductionProcessesStep: React.FC<ProductionProcessesStepProps> = ({
|
|||
|
|
onUpdate,
|
|||
|
|
onComplete,
|
|||
|
|
initialData,
|
|||
|
|
}) => {
|
|||
|
|
const { t } = useTranslation();
|
|||
|
|
const [processes, setProcesses] = useState<ProductionProcess[]>(
|
|||
|
|
initialData?.processes || []
|
|||
|
|
);
|
|||
|
|
const [isAddingNew, setIsAddingNew] = useState(false);
|
|||
|
|
const [showTemplates, setShowTemplates] = useState(true);
|
|||
|
|
const [newProcess, setNewProcess] = useState<Partial<ProductionProcess>>({
|
|||
|
|
name: '',
|
|||
|
|
sourceProduct: '',
|
|||
|
|
finishedProduct: '',
|
|||
|
|
processType: 'baking',
|
|||
|
|
duration: 15,
|
|||
|
|
temperature: 180,
|
|||
|
|
instructions: '',
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const processTypeOptions = [
|
|||
|
|
{ value: 'baking', label: t('onboarding:processes.type.baking', 'Horneado') },
|
|||
|
|
{ value: 'decorating', label: t('onboarding:processes.type.decorating', 'Decoración') },
|
|||
|
|
{ value: 'finishing', label: t('onboarding:processes.type.finishing', 'Terminado') },
|
|||
|
|
{ value: 'assembly', label: t('onboarding:processes.type.assembly', 'Montaje') },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const handleAddFromTemplate = (template: Partial<ProductionProcess>) => {
|
|||
|
|
const newProc: ProductionProcess = {
|
|||
|
|
id: `process-${Date.now()}`,
|
|||
|
|
name: template.name || '',
|
|||
|
|
sourceProduct: '',
|
|||
|
|
finishedProduct: '',
|
|||
|
|
processType: template.processType || 'baking',
|
|||
|
|
duration: template.duration || 15,
|
|||
|
|
temperature: template.temperature,
|
|||
|
|
instructions: template.instructions || '',
|
|||
|
|
};
|
|||
|
|
const updated = [...processes, newProc];
|
|||
|
|
setProcesses(updated);
|
|||
|
|
onUpdate?.({ processes: updated });
|
|||
|
|
setShowTemplates(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleAddNew = () => {
|
|||
|
|
if (!newProcess.name || !newProcess.sourceProduct || !newProcess.finishedProduct) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const process: ProductionProcess = {
|
|||
|
|
id: `process-${Date.now()}`,
|
|||
|
|
name: newProcess.name,
|
|||
|
|
sourceProduct: newProcess.sourceProduct,
|
|||
|
|
finishedProduct: newProcess.finishedProduct,
|
|||
|
|
processType: newProcess.processType || 'baking',
|
|||
|
|
duration: newProcess.duration || 15,
|
|||
|
|
temperature: newProcess.temperature,
|
|||
|
|
instructions: newProcess.instructions || '',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const updated = [...processes, process];
|
|||
|
|
setProcesses(updated);
|
|||
|
|
onUpdate?.({ processes: updated });
|
|||
|
|
|
|||
|
|
// Reset form
|
|||
|
|
setNewProcess({
|
|||
|
|
name: '',
|
|||
|
|
sourceProduct: '',
|
|||
|
|
finishedProduct: '',
|
|||
|
|
processType: 'baking',
|
|||
|
|
duration: 15,
|
|||
|
|
temperature: 180,
|
|||
|
|
instructions: '',
|
|||
|
|
});
|
|||
|
|
setIsAddingNew(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleRemove = (id: string) => {
|
|||
|
|
const updated = processes.filter(p => p.id !== id);
|
|||
|
|
setProcesses(updated);
|
|||
|
|
onUpdate?.({ processes: updated });
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleContinue = () => {
|
|||
|
|
onComplete?.();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getProcessIcon = (type: string) => {
|
|||
|
|
switch (type) {
|
|||
|
|
case 'baking':
|
|||
|
|
return <Flame className="w-5 h-5 text-orange-500" />;
|
|||
|
|
case 'decorating':
|
|||
|
|
return <ChefHat className="w-5 h-5 text-pink-500" />;
|
|||
|
|
case 'finishing':
|
|||
|
|
case 'assembly':
|
|||
|
|
return <Clock className="w-5 h-5 text-blue-500" />;
|
|||
|
|
default:
|
|||
|
|
return <Clock className="w-5 h-5 text-gray-500" />;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="max-w-4xl mx-auto p-6 space-y-6">
|
|||
|
|
{/* Header */}
|
|||
|
|
<div className="text-center space-y-2">
|
|||
|
|
<h1 className="text-2xl font-bold text-text-primary">
|
|||
|
|
{t('onboarding:processes.title', 'Procesos de Producción')}
|
|||
|
|
</h1>
|
|||
|
|
<p className="text-text-secondary">
|
|||
|
|
{t(
|
|||
|
|
'onboarding:processes.subtitle',
|
|||
|
|
'Define los procesos que usas para transformar productos pre-elaborados en productos terminados'
|
|||
|
|
)}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Templates Section */}
|
|||
|
|
{showTemplates && processes.length === 0 && (
|
|||
|
|
<Card className="p-6 space-y-4 bg-gradient-to-br from-blue-50 to-cyan-50">
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div className="space-y-1">
|
|||
|
|
<h3 className="font-semibold text-text-primary">
|
|||
|
|
{t('onboarding:processes.templates.title', '⚡ Comienza rápido con plantillas')}
|
|||
|
|
</h3>
|
|||
|
|
<p className="text-sm text-text-secondary">
|
|||
|
|
{t('onboarding:processes.templates.subtitle', 'Haz clic en una plantilla para agregarla')}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
<Button
|
|||
|
|
variant="ghost"
|
|||
|
|
size="sm"
|
|||
|
|
onClick={() => setShowTemplates(false)}
|
|||
|
|
>
|
|||
|
|
{t('onboarding:processes.templates.hide', 'Ocultar')}
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|||
|
|
{PROCESS_TEMPLATES.map((template, index) => (
|
|||
|
|
<button
|
|||
|
|
key={index}
|
|||
|
|
onClick={() => handleAddFromTemplate(template)}
|
|||
|
|
className="p-4 text-left bg-white border border-border-primary rounded-lg hover:shadow-md hover:border-primary-300 transition-all"
|
|||
|
|
>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
{getProcessIcon(template.processType || 'baking')}
|
|||
|
|
<span className="font-medium text-text-primary">{template.name}</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center gap-3 text-xs text-text-secondary">
|
|||
|
|
<span>⏱️ {template.duration} min</span>
|
|||
|
|
{template.temperature && <span>🌡️ {template.temperature}°C</span>}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</button>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</Card>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Existing Processes */}
|
|||
|
|
{processes.length > 0 && (
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
<h3 className="font-semibold text-text-primary">
|
|||
|
|
{t('onboarding:processes.your_processes', 'Tus Procesos')} ({processes.length})
|
|||
|
|
</h3>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{processes.map((process) => (
|
|||
|
|
<Card key={process.id} className="p-4">
|
|||
|
|
<div className="flex items-start justify-between gap-4">
|
|||
|
|
<div className="flex-1 space-y-2">
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
{getProcessIcon(process.processType)}
|
|||
|
|
<h4 className="font-semibold text-text-primary">{process.name}</h4>
|
|||
|
|
</div>
|
|||
|
|
<div className="text-sm text-text-secondary space-y-1">
|
|||
|
|
{process.sourceProduct && (
|
|||
|
|
<p>
|
|||
|
|
<span className="font-medium">
|
|||
|
|
{t('onboarding:processes.source', 'Desde')}:
|
|||
|
|
</span>{' '}
|
|||
|
|
{process.sourceProduct}
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
{process.finishedProduct && (
|
|||
|
|
<p>
|
|||
|
|
<span className="font-medium">
|
|||
|
|
{t('onboarding:processes.finished', 'Hasta')}:
|
|||
|
|
</span>{' '}
|
|||
|
|
{process.finishedProduct}
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
<div className="flex items-center gap-3 pt-1">
|
|||
|
|
<span>⏱️ {process.duration} min</span>
|
|||
|
|
{process.temperature && <span>🌡️ {process.temperature}°C</span>}
|
|||
|
|
</div>
|
|||
|
|
{process.instructions && (
|
|||
|
|
<p className="text-xs italic pt-1">{process.instructions}</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<button
|
|||
|
|
onClick={() => handleRemove(process.id)}
|
|||
|
|
className="text-text-secondary hover:text-red-600 transition-colors"
|
|||
|
|
>
|
|||
|
|
<X className="w-5 h-5" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</Card>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Add New Process Form */}
|
|||
|
|
{isAddingNew && (
|
|||
|
|
<Card className="p-6 space-y-4 border-2 border-primary-300">
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<h3 className="font-semibold text-text-primary">
|
|||
|
|
{t('onboarding:processes.add_new', 'Nuevo Proceso')}
|
|||
|
|
</h3>
|
|||
|
|
<button
|
|||
|
|
onClick={() => setIsAddingNew(false)}
|
|||
|
|
className="text-text-secondary hover:text-text-primary"
|
|||
|
|
>
|
|||
|
|
<X className="w-5 h-5" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|||
|
|
<div className="md:col-span-2">
|
|||
|
|
<Input
|
|||
|
|
label={t('onboarding:processes.form.name', 'Nombre del Proceso')}
|
|||
|
|
value={newProcess.name || ''}
|
|||
|
|
onChange={(e) => setNewProcess({ ...newProcess, name: e.target.value })}
|
|||
|
|
placeholder={t('onboarding:processes.form.name_placeholder', 'Ej: Horneado de pan')}
|
|||
|
|
required
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Input
|
|||
|
|
label={t('onboarding:processes.form.source', 'Producto Origen')}
|
|||
|
|
value={newProcess.sourceProduct || ''}
|
|||
|
|
onChange={(e) => setNewProcess({ ...newProcess, sourceProduct: e.target.value })}
|
|||
|
|
placeholder={t('onboarding:processes.form.source_placeholder', 'Ej: Pan pre-cocido')}
|
|||
|
|
required
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<Input
|
|||
|
|
label={t('onboarding:processes.form.finished', 'Producto Terminado')}
|
|||
|
|
value={newProcess.finishedProduct || ''}
|
|||
|
|
onChange={(e) => setNewProcess({ ...newProcess, finishedProduct: e.target.value })}
|
|||
|
|
placeholder={t('onboarding:processes.form.finished_placeholder', 'Ej: Pan fresco')}
|
|||
|
|
required
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<Select
|
|||
|
|
label={t('onboarding:processes.form.type', 'Tipo de Proceso')}
|
|||
|
|
value={newProcess.processType || 'baking'}
|
|||
|
|
onChange={(e) => setNewProcess({ ...newProcess, processType: e.target.value as any })}
|
|||
|
|
options={processTypeOptions}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<Input
|
|||
|
|
type="number"
|
|||
|
|
label={t('onboarding:processes.form.duration', 'Duración (minutos)')}
|
|||
|
|
value={newProcess.duration || 15}
|
|||
|
|
onChange={(e) => setNewProcess({ ...newProcess, duration: parseInt(e.target.value) })}
|
|||
|
|
min={1}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
{(newProcess.processType === 'baking' || newProcess.processType === 'finishing') && (
|
|||
|
|
<Input
|
|||
|
|
type="number"
|
|||
|
|
label={t('onboarding:processes.form.temperature', 'Temperatura (°C)')}
|
|||
|
|
value={newProcess.temperature || ''}
|
|||
|
|
onChange={(e) => setNewProcess({ ...newProcess, temperature: parseInt(e.target.value) || undefined })}
|
|||
|
|
placeholder="180"
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<div className="md:col-span-2">
|
|||
|
|
<label className="block text-sm font-medium text-text-primary mb-1">
|
|||
|
|
{t('onboarding:processes.form.instructions', 'Instrucciones (opcional)')}
|
|||
|
|
</label>
|
|||
|
|
<textarea
|
|||
|
|
value={newProcess.instructions || ''}
|
|||
|
|
onChange={(e) => setNewProcess({ ...newProcess, instructions: e.target.value })}
|
|||
|
|
placeholder={t('onboarding:processes.form.instructions_placeholder', 'Describe el proceso...')}
|
|||
|
|
rows={3}
|
|||
|
|
className="w-full px-3 py-2 border border-border-primary rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex gap-2 justify-end">
|
|||
|
|
<Button variant="outline" onClick={() => setIsAddingNew(false)}>
|
|||
|
|
{t('onboarding:processes.form.cancel', 'Cancelar')}
|
|||
|
|
</Button>
|
|||
|
|
<Button
|
|||
|
|
onClick={handleAddNew}
|
|||
|
|
disabled={!newProcess.name || !newProcess.sourceProduct || !newProcess.finishedProduct}
|
|||
|
|
>
|
|||
|
|
{t('onboarding:processes.form.add', 'Agregar Proceso')}
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
</Card>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Add Button */}
|
|||
|
|
{!isAddingNew && (
|
|||
|
|
<Button
|
|||
|
|
onClick={() => setIsAddingNew(true)}
|
|||
|
|
variant="outline"
|
|||
|
|
className="w-full border-dashed"
|
|||
|
|
>
|
|||
|
|
<Plus className="w-5 h-5 mr-2" />
|
|||
|
|
{t('onboarding:processes.add_button', 'Agregar Proceso')}
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Footer Actions */}
|
|||
|
|
<div className="flex items-center justify-between pt-6 border-t border-border-primary">
|
|||
|
|
<p className="text-sm text-text-secondary">
|
|||
|
|
{processes.length === 0
|
|||
|
|
? t('onboarding:processes.hint', '💡 Agrega al menos un proceso para continuar')
|
|||
|
|
: t('onboarding:processes.count', `${processes.length} proceso(s) configurado(s)`)}
|
|||
|
|
</p>
|
|||
|
|
<div className="flex gap-3">
|
|||
|
|
<Button variant="outline" onClick={handleContinue}>
|
|||
|
|
{t('onboarding:processes.skip', 'Omitir por ahora')}
|
|||
|
|
</Button>
|
|||
|
|
<Button onClick={handleContinue} disabled={processes.length === 0}>
|
|||
|
|
{t('onboarding:processes.continue', 'Continuar')}
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default ProductionProcessesStep;
|