add traslations 2

This commit is contained in:
Urtzi Alfaro
2025-12-25 20:51:03 +01:00
parent 532b057f59
commit 6e3a6590d6
6 changed files with 1128 additions and 252 deletions

View File

@@ -42,119 +42,64 @@ export interface ItemTypeConfig {
keywords?: string[];
}
export const ITEM_TYPES: ItemTypeConfig[] = [
{
id: 'sales-entry',
title: 'Registro de Ventas',
subtitle: 'Manual o carga masiva',
// Base item types with icon and category only - titles/subtitles will be translated
export const ITEM_TYPE_CONFIGS: Record<ItemType, { icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>; category: 'daily' | 'common' | 'setup'; badgeColor: string }> = {
'sales-entry': {
icon: EuroIcon,
badge: '⭐ Más Común',
category: 'daily',
badgeColor: 'bg-gradient-to-r from-amber-100 to-orange-100 text-orange-800 font-semibold',
isHighlighted: true,
category: 'daily',
keywords: ['ventas', 'sales', 'ingresos', 'caja', 'revenue'],
},
{
id: 'inventory',
title: 'Inventario',
subtitle: 'Ingrediente o Producto',
'inventory': {
icon: Package,
badge: 'Configuración',
badgeColor: 'bg-blue-100 text-blue-700',
category: 'setup',
keywords: ['inventario', 'inventory', 'stock', 'ingredientes', 'productos'],
badgeColor: 'bg-blue-100 text-blue-700',
},
{
id: 'supplier',
title: 'Proveedor',
subtitle: 'Relación comercial',
'supplier': {
icon: Building,
badge: 'Configuración',
badgeColor: 'bg-blue-100 text-blue-700',
category: 'setup',
keywords: ['proveedor', 'supplier', 'vendor', 'distribuidor'],
badgeColor: 'bg-blue-100 text-blue-700',
},
{
id: 'recipe',
title: 'Receta',
subtitle: 'Fórmula de producción',
'recipe': {
icon: ChefHat,
badge: 'Común',
badgeColor: 'bg-green-100 text-green-700',
category: 'common',
keywords: ['receta', 'recipe', 'formula', 'producción'],
badgeColor: 'bg-green-100 text-green-700',
},
{
id: 'equipment',
title: 'Equipo',
subtitle: 'Activo de panadería',
'equipment': {
icon: Wrench,
badge: 'Configuración',
badgeColor: 'bg-blue-100 text-blue-700',
category: 'setup',
keywords: ['equipo', 'equipment', 'maquinaria', 'horno', 'mixer'],
badgeColor: 'bg-blue-100 text-blue-700',
},
{
id: 'quality-template',
title: 'Plantilla de Calidad',
subtitle: 'Estándares y controles',
'quality-template': {
icon: ClipboardCheck,
badge: 'Configuración',
badgeColor: 'bg-blue-100 text-blue-700',
category: 'setup',
keywords: ['calidad', 'quality', 'control', 'estándares', 'inspección'],
badgeColor: 'bg-blue-100 text-blue-700',
},
{
id: 'customer-order',
title: 'Pedido de Cliente',
subtitle: 'Nueva orden',
'customer-order': {
icon: ShoppingCart,
badge: 'Diario',
badgeColor: 'bg-amber-100 text-amber-700',
category: 'daily',
keywords: ['pedido', 'order', 'cliente', 'customer', 'orden'],
badgeColor: 'bg-amber-100 text-amber-700',
},
{
id: 'customer',
title: 'Cliente',
subtitle: 'Perfil de cliente',
'customer': {
icon: Users,
badge: 'Común',
badgeColor: 'bg-green-100 text-green-700',
category: 'common',
keywords: ['cliente', 'customer', 'comprador', 'contacto'],
badgeColor: 'bg-green-100 text-green-700',
},
{
id: 'team-member',
title: 'Miembro del Equipo',
subtitle: 'Empleado o colaborador',
'team-member': {
icon: UserPlus,
badge: 'Configuración',
badgeColor: 'bg-blue-100 text-blue-700',
category: 'setup',
keywords: ['empleado', 'employee', 'team', 'staff', 'usuario'],
badgeColor: 'bg-blue-100 text-blue-700',
},
{
id: 'purchase-order',
title: 'Orden de Compra',
subtitle: 'Compra a proveedor',
'purchase-order': {
icon: FileText,
badge: 'Diario',
badgeColor: 'bg-amber-100 text-amber-700',
category: 'daily',
keywords: ['compra', 'purchase', 'orden', 'proveedor', 'abastecimiento'],
badgeColor: 'bg-amber-100 text-amber-700',
},
{
id: 'production-batch',
title: 'Lote de Producción',
subtitle: 'Nueva orden de producción',
'production-batch': {
icon: Factory,
badge: 'Diario',
badgeColor: 'bg-amber-100 text-amber-700',
category: 'daily',
keywords: ['producción', 'production', 'lote', 'batch', 'fabricación'],
badgeColor: 'bg-amber-100 text-amber-700',
},
];
};
interface ItemTypeSelectorProps {
onSelect: (itemType: ItemType) => void;
@@ -165,9 +110,28 @@ export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect })
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<'all' | 'daily' | 'common' | 'setup'>('all');
// Build item types with translations
const itemTypes: ItemTypeConfig[] = useMemo(() => {
return Object.entries(ITEM_TYPE_CONFIGS).map(([id, config]) => ({
id: id as ItemType,
title: t(`itemTypeSelector.items.${id}.title`),
subtitle: t(`itemTypeSelector.items.${id}.subtitle`),
icon: config.icon,
badge: config.category === 'daily'
? t('itemTypeSelector.badges.daily')
: config.category === 'common'
? t('itemTypeSelector.badges.common')
: t('itemTypeSelector.badges.setup'),
badgeColor: config.badgeColor,
isHighlighted: id === 'sales-entry',
category: config.category,
keywords: t(`itemTypeSelector.items.${id}.keywords`, { returnObjects: true }) as string[],
}));
}, [t]);
// Filter items based on search and category
const filteredItems = useMemo(() => {
return ITEM_TYPES.filter(item => {
return itemTypes.filter(item => {
// Category filter
if (selectedCategory !== 'all' && item.category !== selectedCategory) {
return false;
@@ -184,13 +148,13 @@ export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect })
return true;
});
}, [searchQuery, selectedCategory]);
}, [itemTypes, searchQuery, selectedCategory]);
const categoryLabels = {
all: 'Todos',
daily: 'Diario',
common: 'Común',
setup: 'Configuración',
all: t('itemTypeSelector.categories.all'),
daily: t('itemTypeSelector.categories.daily'),
common: t('itemTypeSelector.categories.common'),
setup: t('itemTypeSelector.categories.setup'),
};
return (
@@ -219,7 +183,7 @@ export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect })
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Buscar por nombre o categoría..."
placeholder={t('itemTypeSelector.search.placeholder')}
className="w-full pl-10 pr-10 py-2.5 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)] placeholder-[var(--text-tertiary)]"
/>
{searchQuery && (
@@ -254,9 +218,9 @@ export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect })
{(searchQuery || selectedCategory !== 'all') && (
<div className="text-sm text-[var(--text-secondary)]">
{filteredItems.length === 0 ? (
<p className="text-center py-4">No se encontraron resultados</p>
<p className="text-center py-4">{t('itemTypeSelector.search.noResults')}</p>
) : (
<p>{filteredItems.length} {filteredItems.length === 1 ? 'resultado' : 'resultados'}</p>
<p>{filteredItems.length} {filteredItems.length === 1 ? t('itemTypeSelector.search.resultSingular') : t('itemTypeSelector.search.resultPlural')}</p>
)}
</div>
)}

View File

@@ -22,15 +22,18 @@ import { ProcessStage } from '../../../../api/types/qualityTemplates';
import { Badge } from '../../../ui';
import { Card } from '../../../ui';
// Stage labels for display
const STAGE_LABELS: Record<ProcessStage, string> = {
[ProcessStage.MIXING]: 'Mezclado',
[ProcessStage.PROOFING]: 'Fermentación',
[ProcessStage.SHAPING]: 'Formado',
[ProcessStage.BAKING]: 'Horneado',
[ProcessStage.COOLING]: 'Enfriado',
[ProcessStage.PACKAGING]: 'Empaquetado',
[ProcessStage.FINISHING]: 'Acabado',
// Helper to get stage label with translation
const getStageLabelKey = (stage: ProcessStage): string => {
const stageKeyMap: Record<ProcessStage, string> = {
[ProcessStage.MIXING]: 'mixing',
[ProcessStage.PROOFING]: 'proofing',
[ProcessStage.SHAPING]: 'shaping',
[ProcessStage.BAKING]: 'baking',
[ProcessStage.COOLING]: 'cooling',
[ProcessStage.PACKAGING]: 'packaging',
[ProcessStage.FINISHING]: 'finishing',
};
return stageKeyMap[stage];
};
// Step 1: Product & Recipe Selection
@@ -103,31 +106,31 @@ const ProductRecipeStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<Package 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">
Seleccionar Producto y Receta
{t('productionBatch.productRecipe.title')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
Elige el producto a producir y opcionalmente una receta
{t('productionBatch.productRecipe.description')}
</p>
</div>
{ingredientsLoading || recipesLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
<span className="ml-3 text-[var(--text-secondary)]">Cargando información...</span>
<span className="ml-3 text-[var(--text-secondary)]">{t('productionBatch.productRecipe.loading')}</span>
</div>
) : (
<>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Producto a Producir *
{t('productionBatch.productRecipe.productToProduce')} *
</label>
<select
value={data.product_id || ''}
onChange={(e) => handleProductChange(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="">Seleccionar producto...</option>
<option value="">{t('productionBatch.productRecipe.selectProduct')}</option>
{finishedProducts.map((product: any) => (
<option key={product.id} value={product.id}>
{product.name}
@@ -138,7 +141,7 @@ const ProductRecipeStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Receta a Utilizar (Opcional)
{t('productionBatch.productRecipe.recipeToUse')}
</label>
<select
value={data.recipe_id || ''}
@@ -146,7 +149,7 @@ const ProductRecipeStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
disabled={loadingRecipe}
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)] disabled:opacity-50"
>
<option value="">Sin receta específica</option>
<option value="">{t('productionBatch.productRecipe.noSpecificRecipe')}</option>
{recipes.map((recipe: any) => (
<option key={recipe.id} value={recipe.id}>
{recipe.name}
@@ -156,7 +159,7 @@ const ProductRecipeStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
{loadingRecipe && (
<p className="text-sm text-[var(--text-secondary)] mt-1 flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
Cargando detalles de la receta...
{t('productionBatch.productRecipe.loadingRecipeDetails')}
</p>
)}
</div>
@@ -167,7 +170,7 @@ const ProductRecipeStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
<Card className="mt-4 p-4 bg-blue-50 border-blue-200">
<h4 className="font-medium text-[var(--text-primary)] mb-3 flex items-center gap-2">
<ClipboardCheck className="w-5 h-5 text-blue-600" />
Controles de Calidad Requeridos
{t('productionBatch.productRecipe.qualityControlsRequired')}
</h4>
{selectedRecipe.quality_check_configuration && selectedRecipe.quality_check_configuration.stages ? (
<div className="space-y-3">
@@ -177,32 +180,31 @@ const ProductRecipeStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
return (
<div key={stage} className="flex items-center gap-2 text-sm">
<Badge variant="info">{STAGE_LABELS[stage as ProcessStage]}</Badge>
<Badge variant="info">{t(`productionBatch.additionalDetails.stages.${getStageLabelKey(stage as ProcessStage)}`)}</Badge>
<span className="text-[var(--text-secondary)]">
{config.template_ids.length} control{config.template_ids.length > 1 ? 'es' : ''}
{config.template_ids.length} {config.template_ids.length > 1 ? t('productionBatch.productRecipe.controls') : t('productionBatch.productRecipe.control')}
</span>
{config.blocking && <Badge variant="warning" size="sm">Bloqueante</Badge>}
{config.is_required && <Badge variant="error" size="sm">Requerido</Badge>}
{config.blocking && <Badge variant="warning" size="sm">{t('productionBatch.productRecipe.blocking')}</Badge>}
{config.is_required && <Badge variant="error" size="sm">{t('productionBatch.productRecipe.required')}</Badge>}
</div>
);
}
)}
<div className="mt-3 pt-3 border-t border-blue-200">
<p className="text-sm text-[var(--text-secondary)]">
<span className="font-medium">Umbral de calidad mínimo:</span>{' '}
<span className="font-medium">{t('productionBatch.productRecipe.minimumQualityThreshold')}:</span>{' '}
{selectedRecipe.quality_check_configuration.overall_quality_threshold || 7.0}/10
</p>
{selectedRecipe.quality_check_configuration.critical_stage_blocking && (
<p className="text-sm text-[var(--text-secondary)] mt-1">
<span className="font-medium text-orange-600"> Bloqueo crítico activado:</span> El lote no
puede avanzar si fallan checks críticos
<span className="font-medium text-orange-600"> {t('productionBatch.productRecipe.criticalBlockingActivated')}:</span> {t('productionBatch.productRecipe.batchCannotAdvance')}
</p>
)}
</div>
</div>
) : (
<p className="text-sm text-[var(--text-secondary)]">
Esta receta no tiene controles de calidad configurados.
{t('productionBatch.productRecipe.noQualityControls')}
</p>
)}
</Card>
@@ -257,13 +259,13 @@ const PlanningDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<Clock 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">Planificación de Producción</h3>
<p className="text-sm text-[var(--text-secondary)]">Define tiempos y cantidades de producción</p>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{t('productionBatch.planningDetails.title')}</h3>
<p className="text-sm text-[var(--text-secondary)]">{t('productionBatch.planningDetails.description')}</p>
</div>
<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">Inicio Planificado *</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('productionBatch.planningDetails.plannedStart')} *</label>
<input
type="datetime-local"
value={getValue('planned_start_time')}
@@ -274,7 +276,7 @@ const PlanningDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Duración (minutos) *
{t('productionBatch.planningDetails.duration')} *
</label>
<input
type="number"
@@ -287,7 +289,7 @@ const PlanningDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Fin Planificado (Calculado)
{t('productionBatch.planningDetails.plannedEnd')}
</label>
<input
type="datetime-local"
@@ -296,12 +298,12 @@ const PlanningDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
disabled
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)] text-[var(--text-tertiary)] cursor-not-allowed"
/>
<p className="text-xs text-[var(--text-tertiary)] mt-1">Se calcula automáticamente</p>
<p className="text-xs text-[var(--text-tertiary)] mt-1">{t('productionBatch.planningDetails.autoCalculated')}</p>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Cantidad Planificada *
{t('productionBatch.planningDetails.plannedQuantity')} *
</label>
<input
type="number"
@@ -319,14 +321,14 @@ const PlanningDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
<div className="flex items-start gap-2">
<Clock className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
<div className="text-sm">
<p className="font-medium text-blue-900">Resumen de Planificación:</p>
<p className="font-medium text-blue-900">{t('productionBatch.planningDetails.planningSummary')}:</p>
<p className="text-blue-700 mt-1">
Inicio: {new Date(getValue('planned_start_time')).toLocaleString('es-ES')}
{t('productionBatch.planningDetails.start')}: {new Date(getValue('planned_start_time')).toLocaleString('es-ES')}
</p>
<p className="text-blue-700">Fin: {new Date(getValue('planned_end_time')).toLocaleString('es-ES')}</p>
<p className="text-blue-700">{t('productionBatch.planningDetails.end')}: {new Date(getValue('planned_end_time')).toLocaleString('es-ES')}</p>
<p className="text-blue-700">
Duración: {getValue('planned_duration_minutes', 0)} minutos (
{(getValue('planned_duration_minutes', 0) / 60).toFixed(1)} horas)
{t('productionBatch.planningDetails.durationSummary')}: {getValue('planned_duration_minutes', 0)} {t('productionBatch.planningDetails.minutes')} (
{(getValue('planned_duration_minutes', 0) / 60).toFixed(1)} {t('productionBatch.planningDetails.hours')})
</p>
</div>
</div>
@@ -358,13 +360,13 @@ const PriorityResourcesStep: React.FC<WizardStepProps> = ({ dataRef, onDataChang
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<AlertCircle 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">Prioridad y Recursos</h3>
<p className="text-sm text-[var(--text-secondary)]">Configura la prioridad y asigna recursos</p>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{t('productionBatch.priorityResources.title')}</h3>
<p className="text-sm text-[var(--text-secondary)]">{t('productionBatch.priorityResources.description')}</p>
</div>
<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">Prioridad *</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('productionBatch.priorityResources.priority')} *</label>
<select
value={getValue('priority', ProductionPriorityEnum.MEDIUM)}
onChange={(e) => handleFieldChange({ priority: e.target.value })}
@@ -379,12 +381,12 @@ const PriorityResourcesStep: React.FC<WizardStepProps> = ({ dataRef, onDataChang
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Número de Lote</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('productionBatch.priorityResources.batchNumber')}</label>
<input
type="text"
value={getValue('batch_number')}
onChange={(e) => handleFieldChange({ batch_number: e.target.value })}
placeholder="Se generará automáticamente si se deja vacío"
placeholder={t('productionBatch.priorityResources.autoGeneratedPlaceholder')}
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>
@@ -396,7 +398,7 @@ const PriorityResourcesStep: React.FC<WizardStepProps> = ({ dataRef, onDataChang
onChange={(e) => handleFieldChange({ is_rush_order: e.target.checked })}
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
/>
<label className="text-sm font-medium text-[var(--text-primary)]">Orden Urgente</label>
<label className="text-sm font-medium text-[var(--text-primary)]">{t('productionBatch.priorityResources.rushOrder')}</label>
</div>
<div className="flex items-center gap-3 p-3 border border-[var(--border-secondary)] rounded-lg">
@@ -406,69 +408,69 @@ const PriorityResourcesStep: React.FC<WizardStepProps> = ({ dataRef, onDataChang
onChange={(e) => handleFieldChange({ is_special_recipe: e.target.checked })}
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
/>
<label className="text-sm font-medium text-[var(--text-primary)]">Receta Especial</label>
<label className="text-sm font-medium text-[var(--text-primary)]">{t('productionBatch.priorityResources.specialRecipe')}</label>
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
<Users className="w-4 h-4 inline mr-1.5" />
Personal Asignado (Opcional)
{t('productionBatch.priorityResources.assignedStaff')}
</label>
<input
type="text"
value={getValue('staff_assigned_string', '')}
onChange={(e) => handleFieldChange({ staff_assigned_string: e.target.value })}
placeholder="Separar nombres con comas (Ej: Juan Pérez, María García)"
placeholder={t('productionBatch.priorityResources.staffPlaceholder')}
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)]"
/>
<p className="text-xs text-[var(--text-tertiary)] mt-1">Lista de nombres separados por comas</p>
<p className="text-xs text-[var(--text-tertiary)] mt-1">{t('productionBatch.priorityResources.staffHint')}</p>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Notas de Producción (Opcional)
{t('productionBatch.priorityResources.productionNotes')}
</label>
<textarea
value={getValue('production_notes')}
onChange={(e) => handleFieldChange({ production_notes: e.target.value })}
placeholder="Instrucciones especiales, observaciones, etc."
placeholder={t('productionBatch.priorityResources.notesPlaceholder')}
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)]"
/>
</div>
<AdvancedOptionsSection title="Opciones Avanzadas" description="Información adicional de producción">
<AdvancedOptionsSection title={t('productionBatch.advancedOptions.title')} description={t('productionBatch.advancedOptions.description')}>
<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">ID de Estación</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('productionBatch.advancedOptions.stationId')}</label>
<input
type="text"
value={getValue('station_id')}
onChange={(e) => handleFieldChange({ station_id: e.target.value })}
placeholder="Ej: oven-1, mixer-2"
placeholder={t('productionBatch.advancedOptions.stationPlaceholder')}
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">ID de Pedido</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('productionBatch.advancedOptions.orderId')}</label>
<input
type="text"
value={getValue('order_id')}
onChange={(e) => handleFieldChange({ order_id: e.target.value })}
placeholder="ID del pedido asociado"
placeholder={t('productionBatch.advancedOptions.orderPlaceholder')}
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">ID de Pronóstico</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('productionBatch.advancedOptions.forecastId')}</label>
<input
type="text"
value={getValue('forecast_id')}
onChange={(e) => handleFieldChange({ forecast_id: e.target.value })}
placeholder="ID del pronóstico asociado"
placeholder={t('productionBatch.advancedOptions.forecastPlaceholder')}
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>
@@ -487,28 +489,28 @@ const ReviewStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<CheckCircle2 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">Revisar y Confirmar</h3>
<p className="text-sm text-[var(--text-secondary)]">Verifica los detalles antes de crear el lote</p>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{t('productionBatch.review.title')}</h3>
<p className="text-sm text-[var(--text-secondary)]">{t('productionBatch.review.description')}</p>
</div>
{/* Product Info */}
<div className="p-4 bg-[var(--bg-secondary)]/50 rounded-lg border border-[var(--border-secondary)]">
<h4 className="font-semibold text-[var(--text-primary)] mb-3 flex items-center gap-2">
<Package className="w-5 h-5" />
Información del Producto
{t('productionBatch.review.productInfo')}
</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Producto:</span>
<span className="text-[var(--text-secondary)]">{t('productionBatch.review.product')}:</span>
<span className="font-medium">{data.product_name || 'N/A'}</span>
</div>
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Receta:</span>
<span className="font-medium">{data.selectedRecipe?.name || 'Sin receta específica'}</span>
<span className="text-[var(--text-secondary)]">{t('productionBatch.review.recipe')}:</span>
<span className="font-medium">{data.selectedRecipe?.name || t('productionBatch.review.noSpecificRecipe')}</span>
</div>
{data.batch_number && (
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Número de Lote:</span>
<span className="text-[var(--text-secondary)]">{t('productionBatch.review.batchNumber')}:</span>
<span className="font-medium">{data.batch_number}</span>
</div>
)}
@@ -519,32 +521,32 @@ const ReviewStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
<div className="p-4 bg-[var(--bg-secondary)]/50 rounded-lg border border-[var(--border-secondary)]">
<h4 className="font-semibold text-[var(--text-primary)] mb-3 flex items-center gap-2">
<Clock className="w-5 h-5" />
Planificación de Producción
{t('productionBatch.review.productionPlanning')}
</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Inicio:</span>
<span className="text-[var(--text-secondary)]">{t('productionBatch.review.start')}:</span>
<span className="font-medium">
{data.planned_start_time
? new Date(data.planned_start_time).toLocaleString('es-ES')
: 'No especificado'}
: t('productionBatch.review.notSpecified')}
</span>
</div>
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Fin:</span>
<span className="text-[var(--text-secondary)]">{t('productionBatch.review.end')}:</span>
<span className="font-medium">
{data.planned_end_time ? new Date(data.planned_end_time).toLocaleString('es-ES') : 'No especificado'}
{data.planned_end_time ? new Date(data.planned_end_time).toLocaleString('es-ES') : t('productionBatch.review.notSpecified')}
</span>
</div>
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Duración:</span>
<span className="text-[var(--text-secondary)]">{t('productionBatch.review.duration')}:</span>
<span className="font-medium">
{data.planned_duration_minutes} minutos ({(data.planned_duration_minutes / 60).toFixed(1)} horas)
{data.planned_duration_minutes} {t('productionBatch.review.minutes')} ({(data.planned_duration_minutes / 60).toFixed(1)} {t('productionBatch.review.hours')})
</span>
</div>
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Cantidad:</span>
<span className="font-medium">{data.planned_quantity} unidades</span>
<span className="text-[var(--text-secondary)]">{t('productionBatch.review.quantity')}:</span>
<span className="font-medium">{data.planned_quantity} {t('productionBatch.review.units')}</span>
</div>
</div>
</div>
@@ -553,30 +555,30 @@ const ReviewStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
<div className="p-4 bg-[var(--bg-secondary)]/50 rounded-lg border border-[var(--border-secondary)]">
<h4 className="font-semibold text-[var(--text-primary)] mb-3 flex items-center gap-2">
<AlertCircle className="w-5 h-5" />
Prioridad y Recursos
{t('productionBatch.review.priorityAndResources')}
</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Prioridad:</span>
<span className="text-[var(--text-secondary)]">{t('productionBatch.review.priority')}:</span>
<span className="font-medium capitalize">{data.priority || ProductionPriorityEnum.MEDIUM}</span>
</div>
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Orden Urgente:</span>
<span className="font-medium">{data.is_rush_order ? 'Sí' : 'No'}</span>
<span className="text-[var(--text-secondary)]">{t('productionBatch.review.rushOrder')}:</span>
<span className="font-medium">{data.is_rush_order ? t('productionBatch.review.yes') : t('productionBatch.review.no')}</span>
</div>
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Receta Especial:</span>
<span className="font-medium">{data.is_special_recipe ? 'Sí' : 'No'}</span>
<span className="text-[var(--text-secondary)]">{t('productionBatch.review.specialRecipe')}:</span>
<span className="font-medium">{data.is_special_recipe ? t('productionBatch.review.yes') : t('productionBatch.review.no')}</span>
</div>
{data.staff_assigned_string && (
<div className="pt-2 border-t border-[var(--border-primary)]">
<span className="text-[var(--text-secondary)] block mb-1">Personal Asignado:</span>
<span className="text-[var(--text-secondary)] block mb-1">{t('productionBatch.review.assignedStaff')}:</span>
<span className="font-medium">{data.staff_assigned_string}</span>
</div>
)}
{data.production_notes && (
<div className="pt-2 border-t border-[var(--border-primary)]">
<span className="text-[var(--text-secondary)] block mb-1">Notas:</span>
<span className="text-[var(--text-secondary)] block mb-1">{t('productionBatch.review.notes')}:</span>
<span className="font-medium">{data.production_notes}</span>
</div>
)}
@@ -588,7 +590,7 @@ const ReviewStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
<Card className="p-4 bg-blue-50 border-blue-200">
<h4 className="font-medium text-[var(--text-primary)] mb-3 flex items-center gap-2">
<ClipboardCheck className="w-5 h-5 text-blue-600" />
Controles de Calidad Requeridos
{t('productionBatch.review.qualityControlsRequired')}
</h4>
<div className="space-y-2">
{Object.entries(data.selectedRecipe.quality_check_configuration.stages).map(
@@ -597,12 +599,12 @@ const ReviewStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
return (
<div key={stage} className="flex items-center gap-2 text-sm">
<Badge variant="info">{STAGE_LABELS[stage as ProcessStage]}</Badge>
<Badge variant="info">{t(`productionBatch.additionalDetails.stages.${getStageLabelKey(stage as ProcessStage)}`)}</Badge>
<span className="text-[var(--text-secondary)]">
{config.template_ids.length} control{config.template_ids.length > 1 ? 'es' : ''}
{config.template_ids.length} {config.template_ids.length > 1 ? t('productionBatch.review.controls') : t('productionBatch.review.control')}
</span>
{config.blocking && <Badge variant="warning" size="sm">Bloqueante</Badge>}
{config.is_required && <Badge variant="error" size="sm">Requerido</Badge>}
{config.blocking && <Badge variant="warning" size="sm">{t('productionBatch.review.blocking')}</Badge>}
{config.is_required && <Badge variant="error" size="sm">{t('productionBatch.review.required')}</Badge>}
</div>
);
}
@@ -616,7 +618,7 @@ const ReviewStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
export const ProductionBatchWizardSteps = (
dataRef: React.MutableRefObject<Record<string, any>>,
setData: (data: Record<string, any>) => void
_setData: (data: Record<string, any>) => void
): WizardStep[] => {
return [
{
@@ -626,7 +628,7 @@ export const ProductionBatchWizardSteps = (
validate: () => {
const data = dataRef.current;
if (!data.product_id) {
return 'Debes seleccionar un producto';
throw new Error('Debes seleccionar un producto');
}
return true;
},
@@ -638,16 +640,16 @@ export const ProductionBatchWizardSteps = (
validate: () => {
const data = dataRef.current;
if (!data.planned_start_time) {
return 'Debes especificar la fecha y hora de inicio';
throw new Error('Debes especificar la fecha y hora de inicio');
}
if (!data.planned_duration_minutes || data.planned_duration_minutes <= 0) {
return 'La duración debe ser mayor a 0 minutos';
throw new Error('La duración debe ser mayor a 0 minutos');
}
if (!data.planned_quantity || data.planned_quantity <= 0) {
return 'La cantidad debe ser mayor a 0';
throw new Error('La cantidad debe ser mayor a 0');
}
if (data.planned_end_time && new Date(data.planned_end_time) <= new Date(data.planned_start_time)) {
return 'La fecha de fin debe ser posterior a la fecha de inicio';
throw new Error('La fecha de fin debe ser posterior a la fecha de inicio');
}
return true;
},

View File

@@ -55,23 +55,23 @@ const SupplierSelectionStep: React.FC<WizardStepProps> = ({ dataRef, onDataChang
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<Building2 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">
Seleccionar Proveedor
{t('purchaseOrder.supplierSelection.title')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
Elige el proveedor para esta orden de compra
{t('purchaseOrder.supplierSelection.description')}
</p>
</div>
{isError && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
Error al cargar proveedores
{t('purchaseOrder.supplierSelection.errorLoading')}
</div>
)}
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
<span className="ml-3 text-[var(--text-secondary)]">Cargando proveedores...</span>
<span className="ml-3 text-[var(--text-secondary)]">{t('purchaseOrder.supplierSelection.loading')}</span>
</div>
) : (
<>
@@ -82,7 +82,7 @@ const SupplierSelectionStep: React.FC<WizardStepProps> = ({ dataRef, onDataChang
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Buscar proveedor por nombre o código..."
placeholder={t('purchaseOrder.supplierSelection.searchPlaceholder')}
className="w-full pl-10 pr-4 py-3 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>
@@ -92,8 +92,8 @@ const SupplierSelectionStep: React.FC<WizardStepProps> = ({ dataRef, onDataChang
{filteredSuppliers.length === 0 ? (
<div className="text-center py-8 border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
<Building2 className="w-12 h-12 mx-auto mb-3 text-[var(--text-tertiary)]" />
<p className="text-[var(--text-secondary)] mb-1">No se encontraron proveedores</p>
<p className="text-sm text-[var(--text-tertiary)]">Intenta con una búsqueda diferente</p>
<p className="text-[var(--text-secondary)] mb-1">{t('purchaseOrder.supplierSelection.noSuppliers')}</p>
<p className="text-sm text-[var(--text-tertiary)]">{t('purchaseOrder.supplierSelection.tryDifferentSearch')}</p>
</div>
) : (
filteredSuppliers.map((supplier: any) => (
@@ -250,36 +250,36 @@ const AddItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
};
const unitOptions = [
{ value: 'kg', label: 'Kilogramos' },
{ value: 'g', label: 'Gramos' },
{ value: 'l', label: 'Litros' },
{ value: 'ml', label: 'Mililitros' },
{ value: 'units', label: 'Unidades' },
{ value: 'boxes', label: 'Cajas' },
{ value: 'bags', label: 'Bolsas' },
{ value: 'kg', label: t('purchaseOrder.orderItems.units.kg') },
{ value: 'g', label: t('purchaseOrder.orderItems.units.g') },
{ value: 'l', label: t('purchaseOrder.orderItems.units.l') },
{ value: 'ml', label: t('purchaseOrder.orderItems.units.ml') },
{ value: 'units', label: t('purchaseOrder.orderItems.units.units') },
{ value: 'boxes', label: t('purchaseOrder.orderItems.units.boxes') },
{ value: 'bags', label: t('purchaseOrder.orderItems.units.bags') },
];
return (
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<Package 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">Productos a Comprar</h3>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{t('purchaseOrder.orderItems.titleHeader')}</h3>
<p className="text-sm text-[var(--text-secondary)]">
Proveedor: <span className="font-semibold">{data.supplier?.name || 'N/A'}</span>
{t('purchaseOrder.orderItems.supplier')}: <span className="font-semibold">{data.supplier?.name || 'N/A'}</span>
</p>
</div>
{isLoadingIngredients || isLoadingSupplierProducts ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
<span className="ml-3 text-[var(--text-secondary)]">Cargando productos...</span>
<span className="ml-3 text-[var(--text-secondary)]">{t('purchaseOrder.orderItems.loadingProducts')}</span>
</div>
) : (
<>
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium text-[var(--text-secondary)]">
Productos en la orden
{t('purchaseOrder.orderItems.productsInOrder')}
</label>
<button
onClick={handleAddItem}
@@ -287,7 +287,7 @@ const AddItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
className="px-3 py-1.5 text-sm bg-[var(--color-primary)] text-white rounded-md hover:bg-[var(--color-primary)]/90 transition-colors flex items-center gap-1 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Plus className="w-4 h-4" />
Agregar Producto
{t('purchaseOrder.orderItems.addProduct')}
</button>
</div>
@@ -295,7 +295,7 @@ const AddItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
<div className="p-4 bg-amber-50 border border-amber-200 rounded-lg text-amber-700 text-sm flex items-center gap-2">
<AlertCircle className="w-5 h-5 flex-shrink-0" />
<span>
Este proveedor no tiene ingredientes asignados. Configura la lista de precios del proveedor primero.
{t('purchaseOrder.orderItems.noIngredientsForSupplier')}
</span>
</div>
)}
@@ -303,8 +303,8 @@ const AddItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
{(data.items || []).length === 0 ? (
<div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-lg text-[var(--text-tertiary)]">
<Package className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p className="mb-2">No hay productos en la orden</p>
<p className="text-sm">Haz clic en "Agregar Producto" para comenzar</p>
<p className="mb-2">{t('purchaseOrder.orderItems.noProducts')}</p>
<p className="text-sm">{t('purchaseOrder.orderItems.clickToAdd')}</p>
</div>
) : (
<div className="space-y-3">
@@ -315,7 +315,7 @@ const AddItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
>
<div className="flex items-center justify-between">
<span className="text-sm font-semibold text-[var(--text-primary)]">
Producto #{index + 1}
{t('purchaseOrder.orderItems.productNumber', { number: index + 1 })}
</span>
<button
onClick={() => handleRemoveItem(index)}
@@ -328,14 +328,14 @@ const AddItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="md:col-span-2">
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Ingrediente *
{t('purchaseOrder.orderItems.ingredient')} *
</label>
<select
value={item.inventory_product_id}
onChange={(e) => handleUpdateItem(index, 'inventory_product_id', e.target.value)}
className="w-full px-3 py-2 text-sm 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="">Seleccionar ingrediente...</option>
<option value="">{t('purchaseOrder.orderItems.selectIngredient')}</option>
{ingredientsData.map((product: any) => (
<option key={product.id} value={product.id}>
{product.name} - {(product.last_purchase_price || product.average_cost || 0).toFixed(2)} /{' '}
@@ -347,7 +347,7 @@ const AddItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Cantidad *
{t('purchaseOrder.orderItems.quantity')} *
</label>
<input
type="number"
@@ -362,7 +362,7 @@ const AddItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
</div>
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">Unidad *</label>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">{t('purchaseOrder.orderItems.unit')} *</label>
<select
value={item.unit_of_measure}
onChange={(e) => handleUpdateItem(index, 'unit_of_measure', e.target.value)}
@@ -378,7 +378,7 @@ const AddItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
<div className="md:col-span-2">
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Precio Unitario () *
{t('purchaseOrder.orderItems.unitPrice')} *
</label>
<input
type="number"
@@ -393,7 +393,7 @@ const AddItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
<div className="pt-2 border-t border-[var(--border-primary)] text-sm">
<span className="font-semibold text-[var(--text-primary)]">
Subtotal: {item.subtotal.toFixed(2)}
{t('purchaseOrder.orderItems.subtotal')}: {item.subtotal.toFixed(2)}
</span>
</div>
</div>
@@ -404,7 +404,7 @@ const AddItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
{(data.items || []).length > 0 && (
<div className="p-4 bg-gradient-to-r from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 rounded-lg border-2 border-[var(--color-primary)]/20">
<div className="flex justify-between items-center">
<span className="text-lg font-semibold text-[var(--text-primary)]">Total:</span>
<span className="text-lg font-semibold text-[var(--text-primary)]">{t('purchaseOrder.orderItems.total')}:</span>
<span className="text-2xl font-bold text-[var(--color-primary)]">{calculateTotal().toFixed(2)}</span>
</div>
</div>
@@ -430,24 +430,24 @@ const OrderDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
};
const priorityOptions = [
{ value: 'low', label: 'Baja' },
{ value: 'normal', label: 'Normal' },
{ value: 'high', label: 'Alta' },
{ value: 'critical', label: 'Crítica' },
{ value: 'low', label: t('purchaseOrder.orderDetails.priorityOptions.low') },
{ value: 'normal', label: t('purchaseOrder.orderDetails.priorityOptions.normal') },
{ value: 'high', label: t('purchaseOrder.orderDetails.priorityOptions.high') },
{ value: 'critical', label: t('purchaseOrder.orderDetails.priorityOptions.critical') },
];
return (
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<Calendar 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">Detalles de la Orden</h3>
<p className="text-sm text-[var(--text-secondary)]">Configura fecha de entrega y prioridad</p>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{t('purchaseOrder.orderDetails.title')}</h3>
<p className="text-sm text-[var(--text-secondary)]">{t('purchaseOrder.orderDetails.description')}</p>
</div>
<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">
Fecha de Entrega Requerida *
{t('purchaseOrder.orderDetails.requiredDeliveryDate')} *
</label>
<input
type="date"
@@ -459,7 +459,7 @@ const OrderDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Prioridad *</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('purchaseOrder.orderDetails.priority')} *</label>
<select
value={getValue('priority', 'normal')}
onChange={(e) => handleFieldChange({ priority: e.target.value })}
@@ -475,12 +475,12 @@ const OrderDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
<div className="md:col-span-2">
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Notas (Opcional)
{t('purchaseOrder.orderDetails.notes')}
</label>
<textarea
value={getValue('notes')}
onChange={(e) => handleFieldChange({ notes: e.target.value })}
placeholder="Instrucciones especiales para el proveedor..."
placeholder={t('purchaseOrder.orderDetails.notesPlaceholder')}
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)]"
/>
@@ -488,12 +488,12 @@ const OrderDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
</div>
<AdvancedOptionsSection
title="Opciones Avanzadas"
description="Información financiera adicional"
title={t('purchaseOrder.advancedOptions.title')}
description={t('purchaseOrder.advancedOptions.description')}
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Impuestos ()</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('purchaseOrder.advancedOptions.tax')}</label>
<input
type="number"
value={getValue('tax_amount', 0)}
@@ -505,7 +505,7 @@ const OrderDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Costo de Envío ()</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('purchaseOrder.advancedOptions.shippingCost')}</label>
<input
type="number"
value={getValue('shipping_cost', 0)}
@@ -517,7 +517,7 @@ const OrderDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Descuento ()</label>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('purchaseOrder.advancedOptions.discount')}</label>
<input
type="number"
value={getValue('discount_amount', 0)}
@@ -554,28 +554,28 @@ const ReviewSubmitStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<CheckCircle2 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">Revisar y Confirmar</h3>
<p className="text-sm text-[var(--text-secondary)]">Verifica los detalles antes de crear la orden</p>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{t('purchaseOrder.review.title')}</h3>
<p className="text-sm text-[var(--text-secondary)]">{t('purchaseOrder.review.description')}</p>
</div>
{/* Supplier Info */}
<div className="p-4 bg-[var(--bg-secondary)]/50 rounded-lg border border-[var(--border-secondary)]">
<h4 className="font-semibold text-[var(--text-primary)] mb-3 flex items-center gap-2">
<Building2 className="w-5 h-5" />
Información del Proveedor
{t('purchaseOrder.review.supplierInfo')}
</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Proveedor:</span>
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.supplier')}:</span>
<span className="font-medium">{data.supplier?.name || 'N/A'}</span>
</div>
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Código:</span>
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.code')}:</span>
<span className="font-medium">{data.supplier?.supplier_code || 'N/A'}</span>
</div>
{data.supplier?.email && (
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Email:</span>
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.email')}:</span>
<span className="font-medium">{data.supplier.email}</span>
</div>
)}
@@ -586,24 +586,24 @@ const ReviewSubmitStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
<div className="p-4 bg-[var(--bg-secondary)]/50 rounded-lg border border-[var(--border-secondary)]">
<h4 className="font-semibold text-[var(--text-primary)] mb-3 flex items-center gap-2">
<Calendar className="w-5 h-5" />
Detalles de la Orden
{t('purchaseOrder.review.orderDetails')}
</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Fecha de Entrega:</span>
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.deliveryDate')}:</span>
<span className="font-medium">
{data.required_delivery_date
? new Date(data.required_delivery_date).toLocaleDateString('es-ES')
: 'No especificada'}
: t('purchaseOrder.review.notSpecified')}
</span>
</div>
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Prioridad:</span>
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.priority')}:</span>
<span className="font-medium capitalize">{data.priority || 'normal'}</span>
</div>
{data.notes && (
<div className="pt-2 border-t border-[var(--border-primary)]">
<span className="text-[var(--text-secondary)] block mb-1">Notas:</span>
<span className="text-[var(--text-secondary)] block mb-1">{t('purchaseOrder.review.notes')}:</span>
<span className="font-medium">{data.notes}</span>
</div>
)}
@@ -614,7 +614,7 @@ const ReviewSubmitStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
<div className="p-4 bg-[var(--bg-secondary)]/50 rounded-lg border border-[var(--border-secondary)]">
<h4 className="font-semibold text-[var(--text-primary)] mb-3 flex items-center gap-2">
<Package className="w-5 h-5" />
Productos ({(data.items || []).length})
{t('purchaseOrder.review.products')} ({(data.items || []).length})
</h4>
<div className="space-y-2">
{(data.items || []).map((item: any, index: number) => (
@@ -623,7 +623,7 @@ const ReviewSubmitStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
className="flex justify-between items-start p-3 bg-[var(--bg-primary)] rounded-lg border border-[var(--border-secondary)]"
>
<div className="flex-1">
<p className="font-medium text-[var(--text-primary)]">{item.product_name || 'Producto sin nombre'}</p>
<p className="font-medium text-[var(--text-primary)]">{item.product_name || t('purchaseOrder.review.productNoName')}</p>
<p className="text-sm text-[var(--text-secondary)]">
{item.ordered_quantity} {item.unit_of_measure} × {item.unit_price.toFixed(2)}
</p>
@@ -640,33 +640,33 @@ const ReviewSubmitStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
<div className="p-4 bg-gradient-to-r from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 rounded-lg border-2 border-[var(--color-primary)]/20">
<h4 className="font-semibold text-[var(--text-primary)] mb-3 flex items-center gap-2">
<TrendingUp className="w-5 h-5" />
Resumen Financiero
{t('purchaseOrder.review.financialSummary')}
</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Subtotal:</span>
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.subtotal')}:</span>
<span className="font-medium">{calculateSubtotal().toFixed(2)}</span>
</div>
{(data.tax_amount || 0) > 0 && (
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Impuestos:</span>
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.taxes')}:</span>
<span className="font-medium">{(data.tax_amount || 0).toFixed(2)}</span>
</div>
)}
{(data.shipping_cost || 0) > 0 && (
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Costo de Envío:</span>
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.shipping')}:</span>
<span className="font-medium">{(data.shipping_cost || 0).toFixed(2)}</span>
</div>
)}
{(data.discount_amount || 0) > 0 && (
<div className="flex justify-between">
<span className="text-[var(--text-secondary)]">Descuento:</span>
<span className="text-[var(--text-secondary)]">{t('purchaseOrder.review.discount')}:</span>
<span className="font-medium text-green-600">-{(data.discount_amount || 0).toFixed(2)}</span>
</div>
)}
<div className="pt-2 border-t-2 border-[var(--color-primary)]/30 flex justify-between">
<span className="text-lg font-semibold text-[var(--text-primary)]">Total:</span>
<span className="text-lg font-semibold text-[var(--text-primary)]">{t('purchaseOrder.review.total')}:</span>
<span className="text-2xl font-bold text-[var(--color-primary)]">{calculateTotal().toFixed(2)}</span>
</div>
</div>
@@ -677,17 +677,17 @@ const ReviewSubmitStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
export const PurchaseOrderWizardSteps = (
dataRef: React.MutableRefObject<Record<string, any>>,
setData: (data: Record<string, any>) => void
_setData: (data: Record<string, any>) => void
): WizardStep[] => {
return [
{
id: 'supplier-selection',
title: 'Seleccionar Proveedor',
title: 'Seleccionar Proveedor', // Will be translated in UnifiedAddWizard
component: SupplierSelectionStep,
validate: () => {
const data = dataRef.current;
if (!data.supplier_id) {
return 'Debes seleccionar un proveedor';
throw new Error('Debes seleccionar un proveedor');
}
return true;
},
@@ -699,13 +699,13 @@ export const PurchaseOrderWizardSteps = (
validate: () => {
const data = dataRef.current;
if (!data.items || data.items.length === 0) {
return 'Debes agregar al menos un producto';
throw new Error('Debes agregar al menos un producto');
}
const invalidItems = data.items.some(
(item: any) => !item.inventory_product_id || item.ordered_quantity < 0.01 || item.unit_price < 0.01
);
if (invalidItems) {
return 'Todos los productos deben tener ingrediente, cantidad mayor a 0 y precio mayor a 0';
throw new Error('Todos los productos deben tener ingrediente, cantidad mayor a 0 y precio mayor a 0');
}
return true;
},
@@ -717,7 +717,7 @@ export const PurchaseOrderWizardSteps = (
validate: () => {
const data = dataRef.current;
if (!data.required_delivery_date) {
return 'Debes especificar una fecha de entrega';
throw new Error('Debes especificar una fecha de entrega');
}
return true;
},