Improve the UI add button

This commit is contained in:
Urtzi Alfaro
2025-11-16 22:13:52 +01:00
parent 54b7a5e080
commit d36f2ab9af
23 changed files with 2047 additions and 1740 deletions

View File

@@ -2,14 +2,14 @@ import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
Package, Package,
Building2, Building,
ChefHat, ChefHat,
Wrench, Wrench,
ClipboardCheck, ClipboardCheck,
ShoppingCart, ShoppingCart,
Users, Users,
UserPlus, UserPlus,
Euro, Euro as EuroIcon,
Sparkles, Sparkles,
} from 'lucide-react'; } from 'lucide-react';
@@ -39,7 +39,7 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
id: 'sales-entry', id: 'sales-entry',
title: 'Registro de Ventas', title: 'Registro de Ventas',
subtitle: 'Manual o carga masiva', subtitle: 'Manual o carga masiva',
icon: Euro, icon: EuroIcon,
badge: '⭐ Más Común', badge: '⭐ Más Común',
badgeColor: 'bg-gradient-to-r from-amber-100 to-orange-100 text-orange-800 font-semibold', badgeColor: 'bg-gradient-to-r from-amber-100 to-orange-100 text-orange-800 font-semibold',
isHighlighted: true, isHighlighted: true,
@@ -56,7 +56,7 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
id: 'supplier', id: 'supplier',
title: 'Proveedor', title: 'Proveedor',
subtitle: 'Relación comercial', subtitle: 'Relación comercial',
icon: Building2, icon: Building,
badge: 'Configuración', badge: 'Configuración',
badgeColor: 'bg-blue-100 text-blue-700', badgeColor: 'bg-blue-100 text-blue-700',
}, },
@@ -117,83 +117,6 @@ interface ItemTypeSelectorProps {
export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect }) => { export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect }) => {
const { t } = useTranslation('wizards'); const { t } = useTranslation('wizards');
// Generate item types from translations
const itemTypes: ItemTypeConfig[] = [
{
id: 'sales-entry',
title: t('itemTypeSelector.types.sales-entry.title'),
subtitle: t('itemTypeSelector.types.sales-entry.description'),
icon: Euro,
badge: '⭐ ' + t('itemTypeSelector.mostCommon', { defaultValue: 'Most Common' }),
badgeColor: 'bg-gradient-to-r from-amber-100 to-orange-100 text-orange-800 font-semibold',
isHighlighted: true,
},
{
id: 'inventory',
title: t('itemTypeSelector.types.inventory.title'),
subtitle: t('itemTypeSelector.types.inventory.description'),
icon: Package,
badge: t('itemTypeSelector.configuration', { defaultValue: 'Configuration' }),
badgeColor: 'bg-blue-100 text-blue-700',
},
{
id: 'supplier',
title: t('itemTypeSelector.types.supplier.title'),
subtitle: t('itemTypeSelector.types.supplier.description'),
icon: Building2,
badge: t('itemTypeSelector.configuration', { defaultValue: 'Configuration' }),
badgeColor: 'bg-blue-100 text-blue-700',
},
{
id: 'recipe',
title: t('itemTypeSelector.types.recipe.title'),
subtitle: t('itemTypeSelector.types.recipe.description'),
icon: ChefHat,
badge: t('itemTypeSelector.common', { defaultValue: 'Common' }),
badgeColor: 'bg-green-100 text-green-700',
},
{
id: 'equipment',
title: t('itemTypeSelector.types.equipment.title'),
subtitle: t('itemTypeSelector.types.equipment.description'),
icon: Wrench,
badge: t('itemTypeSelector.configuration', { defaultValue: 'Configuration' }),
badgeColor: 'bg-blue-100 text-blue-700',
},
{
id: 'quality-template',
title: t('itemTypeSelector.types.quality-template.title'),
subtitle: t('itemTypeSelector.types.quality-template.description'),
icon: ClipboardCheck,
badge: t('itemTypeSelector.configuration', { defaultValue: 'Configuration' }),
badgeColor: 'bg-blue-100 text-blue-700',
},
{
id: 'customer-order',
title: t('itemTypeSelector.types.customer-order.title'),
subtitle: t('itemTypeSelector.types.customer-order.description'),
icon: ShoppingCart,
badge: t('itemTypeSelector.daily', { defaultValue: 'Daily' }),
badgeColor: 'bg-amber-100 text-amber-700',
},
{
id: 'customer',
title: t('itemTypeSelector.types.customer.title'),
subtitle: t('itemTypeSelector.types.customer.description'),
icon: Users,
badge: t('itemTypeSelector.common', { defaultValue: 'Common' }),
badgeColor: 'bg-green-100 text-green-700',
},
{
id: 'team-member',
title: t('itemTypeSelector.types.team-member.title'),
subtitle: t('itemTypeSelector.types.team-member.description'),
icon: UserPlus,
badge: t('itemTypeSelector.configuration', { defaultValue: 'Configuration' }),
badgeColor: 'bg-blue-100 text-blue-700',
},
];
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
@@ -213,7 +136,7 @@ export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect })
{/* Item Type Grid */} {/* Item Type Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 md:gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 md:gap-4">
{itemTypes.map((itemType) => { {ITEM_TYPES.map((itemType) => {
const Icon = itemType.icon; const Icon = itemType.icon;
const isHighlighted = itemType.isHighlighted; const isHighlighted = itemType.isHighlighted;

View File

@@ -1,10 +1,11 @@
import React, { useState, useCallback, useMemo } from 'react'; import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
import { Sparkles } from 'lucide-react'; import { Sparkles } from 'lucide-react';
import { WizardModal, WizardStep } from '../../ui/WizardModal/WizardModal'; import { WizardModal, WizardStep } from '../../ui/WizardModal/WizardModal';
import { ItemTypeSelector, ItemType } from './ItemTypeSelector'; import { ItemTypeSelector, ItemType } from './ItemTypeSelector';
import { AnyWizardData } from './types';
// Import specific wizards // Import specific wizards
import { InventoryWizardSteps } from './wizards/InventoryWizard'; import { InventoryWizardSteps, ProductTypeStep, BasicInfoStep, StockConfigStep } from './wizards/InventoryWizard';
import { SupplierWizardSteps } from './wizards/SupplierWizard'; import { SupplierWizardSteps } from './wizards/SupplierWizard';
import { RecipeWizardSteps } from './wizards/RecipeWizard'; import { RecipeWizardSteps } from './wizards/RecipeWizard';
import { EquipmentWizardSteps } from './wizards/EquipmentWizard'; import { EquipmentWizardSteps } from './wizards/EquipmentWizard';
@@ -31,12 +32,22 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
const [selectedItemType, setSelectedItemType] = useState<ItemType | null>( const [selectedItemType, setSelectedItemType] = useState<ItemType | null>(
initialItemType || null initialItemType || null
); );
const [wizardData, setWizardData] = useState<Record<string, any>>({}); const [wizardData, setWizardData] = useState<AnyWizardData>({});
// Use a ref to store the current data - this allows step components
// to always access the latest data without causing the steps array to be recreated
const dataRef = useRef<AnyWizardData>({});
// Update ref whenever data changes
useEffect(() => {
dataRef.current = wizardData;
}, [wizardData]);
// Reset state when modal closes // Reset state when modal closes
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
setSelectedItemType(initialItemType || null); setSelectedItemType(initialItemType || null);
setWizardData({}); setWizardData({});
dataRef.current = {};
onClose(); onClose();
}, [onClose, initialItemType]); }, [onClose, initialItemType]);
@@ -45,11 +56,23 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
setSelectedItemType(itemType); setSelectedItemType(itemType);
}, []); }, []);
// CRITICAL FIX: Update both ref AND state, but wizardSteps won't recreate
// The step component needs to re-render to show typed text (controlled inputs)
// But wizardSteps useMemo ensures steps array doesn't recreate, so no component recreation
const handleDataChange = useCallback((newData: AnyWizardData) => {
// Update ref first for immediate access
dataRef.current = newData;
// Update state to trigger re-render (controlled inputs need this)
setWizardData(newData);
}, []);
// Handle wizard completion // Handle wizard completion
const handleWizardComplete = useCallback( const handleWizardComplete = useCallback(
(data?: any) => { (data?: any) => {
if (selectedItemType) { if (selectedItemType) {
onComplete?.(selectedItemType, data); // On completion, sync the ref to state for submission
setWizardData(dataRef.current);
onComplete?.(selectedItemType, dataRef.current);
} }
handleClose(); handleClose();
}, },
@@ -57,10 +80,10 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
); );
// Get wizard steps based on selected item type // Get wizard steps based on selected item type
// CRITICAL: Memoize the steps to prevent component recreation on every render // ARCHITECTURAL SOLUTION: We pass dataRef and setWizardData to wizard step functions.
// Without this, every keystroke causes the component to unmount/remount, losing focus // The wizard steps use these in their component wrappers, which creates a closure
// IMPORTANT: For dynamic wizards (like sales-entry), we need to include the entryMethod // that always accesses the CURRENT data from dataRef.current, without needing
// in the dependency array so steps update when the user selects manual vs upload // to recreate the steps array on every data change.
const wizardSteps = useMemo((): WizardStep[] => { const wizardSteps = useMemo((): WizardStep[] => {
if (!selectedItemType) { if (!selectedItemType) {
// Step 0: Item Type Selection // Step 0: Item Type Selection
@@ -76,30 +99,31 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
]; ];
} }
// Return specific wizard steps based on selected type // Pass dataRef and setWizardData - the wizard step functions will use
// dataRef.current to always access fresh data without recreating steps
switch (selectedItemType) { switch (selectedItemType) {
case 'inventory': case 'inventory':
return InventoryWizardSteps(wizardData, setWizardData); return InventoryWizardSteps(dataRef, setWizardData);
case 'supplier': case 'supplier':
return SupplierWizardSteps(wizardData, setWizardData); return SupplierWizardSteps(dataRef, setWizardData);
case 'recipe': case 'recipe':
return RecipeWizardSteps(wizardData, setWizardData); return RecipeWizardSteps(dataRef, setWizardData);
case 'equipment': case 'equipment':
return EquipmentWizardSteps(wizardData, setWizardData); return EquipmentWizardSteps(dataRef, setWizardData);
case 'quality-template': case 'quality-template':
return QualityTemplateWizardSteps(wizardData, setWizardData); return QualityTemplateWizardSteps(dataRef, setWizardData);
case 'customer-order': case 'customer-order':
return CustomerOrderWizardSteps(wizardData, setWizardData); return CustomerOrderWizardSteps(dataRef, setWizardData);
case 'customer': case 'customer':
return CustomerWizardSteps(wizardData, setWizardData); return CustomerWizardSteps(dataRef, setWizardData);
case 'team-member': case 'team-member':
return TeamMemberWizardSteps(wizardData, setWizardData); return TeamMemberWizardSteps(dataRef, setWizardData);
case 'sales-entry': case 'sales-entry':
return SalesEntryWizardSteps(wizardData, setWizardData); return SalesEntryWizardSteps(dataRef, setWizardData);
default: default:
return []; return [];
} }
}, [selectedItemType, handleItemTypeSelect, wizardData.entryMethod]); // Include only critical fields for dynamic step generation }, [selectedItemType, handleItemTypeSelect, wizardData.entryMethod]); // Add entryMethod for dynamic sales-entry steps
// Get wizard title based on selected item type // Get wizard title based on selected item type
const getWizardTitle = (): string => { const getWizardTitle = (): string => {
@@ -131,6 +155,8 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
steps={wizardSteps} steps={wizardSteps}
icon={<Sparkles className="w-6 h-6" />} icon={<Sparkles className="w-6 h-6" />}
size="xl" size="xl"
dataRef={dataRef}
onDataChange={handleDataChange}
/> />
); );
}; };

View File

@@ -0,0 +1,129 @@
/**
* AddressFields - Reusable address form fields
*
* Used by: SupplierWizard, CustomerWizard, CustomerOrderWizard
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
interface AddressFieldsProps {
data: {
address?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
};
onFieldChange: (field: string, value: string) => void;
required?: {
address?: boolean;
city?: boolean;
state?: boolean;
postalCode?: boolean;
country?: boolean;
};
labels?: {
address?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
};
fieldPrefix?: string; // For delivery addresses: 'delivery'
}
export const AddressFields: React.FC<AddressFieldsProps> = ({
data,
onFieldChange,
required = {},
labels = {},
fieldPrefix = '',
}) => {
const { t } = useTranslation('wizards');
const getFieldName = (field: string) => {
return fieldPrefix ? `${fieldPrefix}${field.charAt(0).toUpperCase()}${field.slice(1)}` : field;
};
return (
<div className="space-y-4">
{/* Address */}
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{labels.address || t('common.fields.address')}
{required.address && ' *'}
</label>
<input
type="text"
value={data.address || ''}
onChange={(e) => onFieldChange(getFieldName('address'), e.target.value)}
placeholder={t('common.fields.addressPlaceholder')}
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>
{/* City and State */}
<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">
{labels.city || t('common.fields.city')}
{required.city && ' *'}
</label>
<input
type="text"
value={data.city || ''}
onChange={(e) => onFieldChange(getFieldName('city'), e.target.value)}
placeholder={t('common.fields.cityPlaceholder')}
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">
{labels.state || t('common.fields.state')}
{required.state && ' *'}
</label>
<input
type="text"
value={data.state || ''}
onChange={(e) => onFieldChange(getFieldName('state'), e.target.value)}
placeholder={t('common.fields.statePlaceholder')}
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>
{/* Postal Code and Country */}
<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">
{labels.postalCode || t('common.fields.postalCode')}
{required.postalCode && ' *'}
</label>
<input
type="text"
value={data.postalCode || ''}
onChange={(e) => onFieldChange(getFieldName('postalCode'), e.target.value)}
placeholder={t('common.fields.postalCodePlaceholder')}
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">
{labels.country || t('common.fields.country')}
{required.country && ' *'}
</label>
<input
type="text"
value={data.country || ''}
onChange={(e) => onFieldChange(getFieldName('country'), e.target.value)}
placeholder={t('common.fields.countryPlaceholder')}
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>
);
};

View File

@@ -0,0 +1,89 @@
/**
* ContactInfoFields - Reusable contact information form fields
*
* Used by: SupplierWizard, CustomerWizard, TeamMemberWizard
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
interface ContactInfoFieldsProps {
data: {
email?: string;
phone?: string;
contactName?: string;
};
onFieldChange: (field: string, value: string) => void;
showContactName?: boolean;
required?: {
email?: boolean;
phone?: boolean;
contactName?: boolean;
};
labels?: {
email?: string;
phone?: string;
contactName?: string;
};
}
export const ContactInfoFields: React.FC<ContactInfoFieldsProps> = ({
data,
onFieldChange,
showContactName = false,
required = {},
labels = {},
}) => {
const { t } = useTranslation('wizards');
return (
<div className="space-y-4">
{/* Contact Name (optional, shown for suppliers) */}
{showContactName && (
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{labels.contactName || t('common.fields.contactName')}
{required.contactName && ' *'}
</label>
<input
type="text"
value={data.contactName || ''}
onChange={(e) => onFieldChange('contactName', e.target.value)}
placeholder={t('common.fields.contactNamePlaceholder')}
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>
)}
{/* Email */}
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{labels.email || t('common.fields.email')}
{required.email && ' *'}
</label>
<input
type="email"
value={data.email || ''}
onChange={(e) => onFieldChange('email', e.target.value)}
placeholder={t('common.fields.emailPlaceholder')}
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>
{/* Phone */}
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{labels.phone || t('common.fields.phone')}
{required.phone && ' *'}
</label>
<input
type="tel"
value={data.phone || ''}
onChange={(e) => onFieldChange('phone', e.target.value)}
placeholder={t('common.fields.phonePlaceholder')}
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>
);
};

View File

@@ -0,0 +1,158 @@
/**
* JsonEditor - Better UX for JSONB fields
*
* Provides syntax highlighting, validation, and error messages
* instead of raw textarea for JSON editing.
*
* Used by: QualityTemplateWizard, CustomerOrderWizard
*/
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { AlertCircle, CheckCircle } from 'lucide-react';
interface JsonEditorProps {
value: any;
onChange: (value: any) => void;
label?: string;
placeholder?: string;
required?: boolean;
rows?: number;
}
export const JsonEditor: React.FC<JsonEditorProps> = ({
value,
onChange,
label,
placeholder,
required = false,
rows = 6,
}) => {
const { t } = useTranslation('wizards');
const [jsonString, setJsonString] = useState('');
const [error, setError] = useState<string | null>(null);
const [isValid, setIsValid] = useState(true);
// Initialize from value
useEffect(() => {
try {
if (value === null || value === undefined || value === '') {
setJsonString('');
} else if (typeof value === 'string') {
// Try to parse to validate
JSON.parse(value);
setJsonString(value);
} else {
setJsonString(JSON.stringify(value, null, 2));
}
setIsValid(true);
setError(null);
} catch (e) {
setJsonString(typeof value === 'string' ? value : '');
setIsValid(false);
}
}, []);
const handleChange = (newValue: string) => {
setJsonString(newValue);
// Validate JSON
if (newValue.trim() === '') {
setError(null);
setIsValid(true);
onChange(null);
return;
}
try {
const parsed = JSON.parse(newValue);
setError(null);
setIsValid(true);
onChange(parsed);
} catch (e) {
setError(e instanceof Error ? e.message : 'Invalid JSON');
setIsValid(false);
// Don't update parent with invalid JSON
}
};
const formatJson = () => {
try {
const parsed = JSON.parse(jsonString);
const formatted = JSON.stringify(parsed, null, 2);
setJsonString(formatted);
setError(null);
setIsValid(true);
onChange(parsed);
} catch (e) {
setError(e instanceof Error ? e.message : 'Invalid JSON');
setIsValid(false);
}
};
return (
<div>
{label && (
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{label}
{required && ' *'}
</label>
)}
<div className="relative">
<textarea
value={jsonString}
onChange={(e) => handleChange(e.target.value)}
placeholder={placeholder || '{\n "key": "value"\n}'}
rows={rows}
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 font-mono text-sm ${
isValid
? 'border-[var(--border-secondary)] focus:ring-[var(--color-primary)]'
: 'border-red-500 focus:ring-red-500'
} bg-[var(--bg-primary)] text-[var(--text-primary)]`}
/>
{/* Validation indicator */}
<div className="absolute top-2 right-2">
{jsonString && isValid && (
<CheckCircle className="w-5 h-5 text-green-500" />
)}
{jsonString && !isValid && (
<AlertCircle className="w-5 h-5 text-red-500" />
)}
</div>
</div>
{/* Error message */}
{error && (
<div className="mt-2 flex items-start gap-2 text-sm text-red-600">
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
<span>{error}</span>
</div>
)}
{/* Format button */}
{jsonString && (
<div className="mt-2">
<button
type="button"
onClick={formatJson}
className="text-sm px-3 py-1 rounded transition-colors"
style={{
backgroundColor: 'var(--bg-tertiary)',
color: 'var(--text-secondary)',
border: '1px solid var(--border-secondary)',
}}
>
{t('common.actions.formatJson', 'Format JSON')}
</button>
</div>
)}
{/* Help text */}
<p className="mt-2 text-xs" style={{ color: 'var(--text-tertiary)' }}>
{t('common.hints.jsonEditor', 'Enter valid JSON. Use the format button to auto-format.')}
</p>
</div>
);
};

View File

@@ -0,0 +1,10 @@
/**
* Shared Wizard Components - Central Export
*
* Reusable components and hooks for all wizard types
*/
export { ContactInfoFields } from './ContactInfoFields';
export { AddressFields } from './AddressFields';
export { JsonEditor } from './JsonEditor';
export { useWizardSubmit } from './useWizardSubmit';

View File

@@ -0,0 +1,61 @@
/**
* useWizardSubmit - Standardized hook for wizard submission
*
* Provides consistent loading states, error handling, and success callbacks
* across all wizard types.
*/
import { useState, useCallback } from 'react';
interface UseWizardSubmitOptions<T> {
onSubmit: (data: T) => Promise<void>;
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
}
interface UseWizardSubmitReturn<T> {
submit: (data: T) => Promise<void>;
loading: boolean;
error: string | null;
clearError: () => void;
}
export function useWizardSubmit<T>({
onSubmit,
onSuccess,
onError,
}: UseWizardSubmitOptions<T>): UseWizardSubmitReturn<T> {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const submit = useCallback(
async (data: T) => {
setLoading(true);
setError(null);
try {
await onSubmit(data);
onSuccess?.(data);
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : 'An error occurred during submission';
setError(errorMessage);
onError?.(err instanceof Error ? err : new Error(errorMessage));
} finally {
setLoading(false);
}
},
[onSubmit, onSuccess, onError]
);
const clearError = useCallback(() => {
setError(null);
}, []);
return {
submit,
loading,
error,
clearError,
};
}

View File

@@ -0,0 +1,9 @@
/**
* Unified Wizard Types - Central Export
*
* This file exports all TypeScript interfaces
* for the Unified Wizard system.
*/
// Export all TypeScript types and interfaces
export * from './wizard-data.types';

View File

@@ -0,0 +1,441 @@
/**
* TypeScript interfaces for all Unified Wizard data structures
*
* This file provides type safety for wizard form data across all sub-wizards.
* Each interface represents the complete data structure for a specific wizard type.
*/
// ============================================================================
// Common Types
// ============================================================================
export type MeasurementUnit = 'kg' | 'g' | 'l' | 'ml' | 'units' | 'dozen' | 'lb' | 'oz';
export type IngredientCategory =
| 'flour'
| 'dairy'
| 'eggs'
| 'fats'
| 'sweeteners'
| 'additives'
| 'fruits'
| 'nuts'
| 'spices'
| 'leavening';
export type ProductCategory = 'bread' | 'pastry' | 'cake' | 'cookies' | 'specialty';
export type ProductType = 'ingredient' | 'finished_product';
export type SupplierType = 'local' | 'national' | 'international' | 'distributor' | 'manufacturer';
export type SupplierStatus = 'active' | 'inactive' | 'pending';
export type EquipmentType = 'oven' | 'mixer' | 'proofer' | 'refrigerator' | 'other';
export type QualityCheckType =
| 'visual'
| 'weight'
| 'temperature'
| 'texture'
| 'taste'
| 'moisture'
| 'shelf-life';
export type CustomerType = 'individual' | 'business' | 'central_bakery';
export type CustomerSegment = 'vip' | 'regular' | 'wholesale';
export type PriorityLevel = 'low' | 'medium' | 'high';
export type DeliveryMethod = 'pickup' | 'delivery' | 'shipping';
export type OrderStatus =
| 'draft'
| 'confirmed'
| 'in_production'
| 'ready'
| 'delivered'
| 'cancelled';
export type PaymentStatus = 'pending' | 'partial' | 'paid' | 'overdue';
export type EmploymentType = 'full-time' | 'part-time' | 'contractor';
export type Position = 'baker' | 'pastry-chef' | 'manager' | 'sales' | 'delivery';
export type SalesEntryMethod = 'manual' | 'upload';
export type PaymentMethod = 'cash' | 'card' | 'transfer' | 'check';
// ============================================================================
// Inventory Wizard Data
// ============================================================================
export interface InventoryWizardData {
// Step 1: Product Type
productType?: ProductType;
sku?: string;
barcode?: string;
brand?: string;
// Step 2: Basic Info
name?: string;
ingredientCategory?: IngredientCategory;
productCategory?: ProductCategory;
description?: string;
packageSize?: string;
isPerishable?: boolean;
shelfLifeDays?: number;
allergenInfo?: string;
// Step 3: Stock Configuration
unitOfMeasure?: MeasurementUnit;
standardCost?: number;
lowStockThreshold?: number;
reorderPoint?: number;
reorderQuantity?: number;
maxStockLevel?: number;
leadTimeDays?: number;
}
// ============================================================================
// Supplier Wizard Data
// ============================================================================
export interface SupplierWizardData {
// Basic Information
name?: string;
supplierCode?: string;
supplierType?: SupplierType;
status?: SupplierStatus;
// Contact Information
contactName?: string;
email?: string;
phone?: string;
// Payment & Terms
paymentTerms?: string;
currency?: string;
standardLeadTime?: number;
// Advanced Options
address?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
website?: string;
taxId?: string;
certifications?: string; // Comma-separated
specializations?: string; // Comma-separated
notes?: string;
isPreferred?: boolean;
autoApproveOrders?: boolean;
}
// ============================================================================
// Recipe Wizard Data
// ============================================================================
export interface RecipeIngredient {
id: string;
ingredientId: string;
quantity: number;
unit: MeasurementUnit;
notes: string;
order: number;
}
export interface RecipeWizardData {
// Step 1: Recipe Details
name?: string;
recipeCode?: string;
category?: ProductCategory;
description?: string;
finishedProductId?: string;
yieldQuantity?: number;
yieldUnit?: MeasurementUnit;
prepTime?: number;
bakingTime?: number;
coolingTime?: number;
isSeasonal?: boolean;
seasonalStartMonth?: number;
seasonalEndMonth?: number;
isSignature?: boolean;
targetEnvironmentTemp?: number;
targetEnvironmentHumidity?: number;
targetMargin?: number;
// Step 2: Ingredients
ingredients?: RecipeIngredient[];
// Step 3: Quality Templates (Optional)
qualityCheckId?: string;
enableQualityChecks?: boolean;
qualityConfig?: any; // JSONB
}
// ============================================================================
// Equipment Wizard Data
// ============================================================================
export interface EquipmentWizardData {
name?: string;
equipmentType?: EquipmentType;
brand?: string;
model?: string;
location?: string;
purchaseDate?: string; // ISO date string
maintenanceIntervalDays?: number;
notes?: string;
}
// ============================================================================
// Quality Template Wizard Data
// ============================================================================
export interface QualityTemplateWizardData {
name?: string;
checkType?: QualityCheckType;
weight?: number; // 0-10
scoringMethod?: string;
passThreshold?: number;
frequencyDays?: number;
checkPoints?: any; // JSON array
parameters?: any; // JSONB
thresholds?: any; // JSONB
scoringCriteria?: any; // JSONB
acceptanceCriteria?: any; // JSONB
responsibleRole?: string;
requiredEquipment?: string;
requiresPhoto?: boolean;
isCriticalControlPoint?: boolean;
notifyOnFailure?: boolean;
description?: string;
}
// ============================================================================
// Customer Order Wizard Data
// ============================================================================
export interface OrderItem {
productId: string;
productName: string;
quantity: number;
unitPrice: number;
notes?: string;
}
export interface CustomerOrderWizardData {
// Step 1: Customer Selection
customer?: any; // Customer object from DB
showNewCustomerForm?: boolean;
newCustomerName?: string;
newCustomerEmail?: string;
newCustomerPhone?: string;
newCustomerType?: CustomerType;
// Step 2: Order Items
orderItems?: OrderItem[];
// Step 3: Delivery & Payment
requestedDeliveryDate?: string; // ISO date string
deliveryMethod?: DeliveryMethod;
deliveryAddress?: string;
deliveryCity?: string;
deliveryState?: string;
deliveryPostalCode?: string;
deliveryInstructions?: string;
// Advanced Options - Pricing
discountAmount?: number;
deliveryFee?: number;
taxRate?: number;
// Advanced Options - Production
productionScheduledDate?: string;
productionNotes?: string;
// Advanced Options - Fulfillment
fulfillmentStatus?: string;
fulfillmentNotes?: string;
// Advanced Options - Source & Channel
orderSource?: string;
orderChannel?: string;
// Advanced Options - Notes & Communication
internalNotes?: string;
customerNotes?: string;
packagingInstructions?: string;
specialRequests?: string;
allergyWarnings?: string;
// Advanced Options - Notifications
sendEmailConfirmation?: boolean;
sendSMSNotifications?: boolean;
customerEmail?: string;
customerPhone?: string;
// Advanced Options - Quality
qualityRequirements?: string;
// Advanced Options - Recurring
isRecurring?: boolean;
recurringFrequency?: string;
// Advanced Options - Metadata
tags?: string; // JSON array
customFields?: any; // JSONB
}
// ============================================================================
// Customer Wizard Data
// ============================================================================
export interface CustomerWizardData {
// Basic Information
name?: string;
customerCode?: string;
customerType?: CustomerType;
email?: string;
phone?: string;
// Business Details
businessName?: string;
taxId?: string;
businessLicense?: string;
// Payment & Credit
paymentTerms?: string;
creditLimit?: number;
// Address
address?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
// Customer Segmentation
segment?: CustomerSegment;
priority?: PriorityLevel;
// Preferences
preferredDeliveryMethod?: DeliveryMethod;
specialInstructions?: string;
notes?: string;
}
// ============================================================================
// Team Member Wizard Data
// ============================================================================
export interface TeamMemberWizardData {
// Step 1: Member Details
name?: string;
email?: string;
phone?: string;
position?: Position;
employmentType?: EmploymentType;
hireDate?: string; // ISO date string
hourlyRate?: number;
notes?: string;
// Step 2: Permissions
role?: string;
canManageInventory?: boolean;
canViewRecipes?: boolean;
canCreateOrders?: boolean;
canViewFinancial?: boolean;
}
// ============================================================================
// Sales Entry Wizard Data
// ============================================================================
export interface SalesItem {
productId: string;
productName: string;
quantity: number;
unitPrice: number;
}
export interface SalesEntryWizardData {
// Step 1: Entry Method
entryMethod?: SalesEntryMethod;
// Step 2a: Manual Entry
saleDate?: string; // ISO date string
paymentMethod?: PaymentMethod;
salesItems?: SalesItem[];
notes?: string;
// Step 2b: File Upload
uploadedFile?: File;
validationResults?: {
totalRows: number;
validRows: number;
errors: string[];
};
// Step 3: Review
confirmedForSubmission?: boolean;
}
// ============================================================================
// Union Type for All Wizard Data
// ============================================================================
export type AnyWizardData =
| InventoryWizardData
| SupplierWizardData
| RecipeWizardData
| EquipmentWizardData
| QualityTemplateWizardData
| CustomerOrderWizardData
| CustomerWizardData
| TeamMemberWizardData
| SalesEntryWizardData;
// ============================================================================
// Type Guards
// ============================================================================
export function isInventoryWizardData(data: any): data is InventoryWizardData {
return data && ('productType' in data || 'unitOfMeasure' in data);
}
export function isSupplierWizardData(data: any): data is SupplierWizardData {
return data && ('supplierType' in data || 'supplierCode' in data);
}
export function isRecipeWizardData(data: any): data is RecipeWizardData {
return data && ('ingredients' in data || 'finishedProductId' in data);
}
export function isEquipmentWizardData(data: any): data is EquipmentWizardData {
return data && 'equipmentType' in data;
}
export function isQualityTemplateWizardData(data: any): data is QualityTemplateWizardData {
return data && 'checkType' in data;
}
export function isCustomerOrderWizardData(data: any): data is CustomerOrderWizardData {
return data && ('orderItems' in data || 'deliveryMethod' in data);
}
export function isCustomerWizardData(data: any): data is CustomerWizardData {
return data && ('customerCode' in data || 'customerType' in data);
}
export function isTeamMemberWizardData(data: any): data is TeamMemberWizardData {
return data && ('position' in data || 'employmentType' in data);
}
export function isSalesEntryWizardData(data: any): data is SalesEntryWizardData {
return data && 'entryMethod' in data;
}

View File

@@ -21,13 +21,9 @@ import OrdersService from '../../../../api/services/orders';
import { inventoryService } from '../../../../api/services/inventory'; import { inventoryService } from '../../../../api/services/inventory';
import { ProductType } from '../../../../api/types/inventory'; import { ProductType } from '../../../../api/types/inventory';
interface WizardDataProps extends WizardStepProps {
data: Record<string, any>;
onDataChange: (data: Record<string, any>) => void;
}
// Step 1: Customer Selection // Step 1: Customer Selection
const CustomerSelectionStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => { const CustomerSelectionStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards'); const { t } = useTranslation('wizards');
const { currentTenant } = useTenant(); const { currentTenant } = useTenant();
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
@@ -52,7 +48,7 @@ const CustomerSelectionStep: React.FC<WizardDataProps> = ({ data, onDataChange }
const handleCustomerChange = (newCustomer: any, newShowForm: boolean) => { const handleCustomerChange = (newCustomer: any, newShowForm: boolean) => {
setSelectedCustomer(newCustomer); setSelectedCustomer(newCustomer);
setShowNewCustomerForm(newShowForm); setShowNewCustomerForm(newShowForm);
onDataChange({ onDataChange?.({
...data, ...data,
customer: newCustomer, customer: newCustomer,
showNewCustomerForm: newShowForm, showNewCustomerForm: newShowForm,
@@ -63,7 +59,7 @@ const CustomerSelectionStep: React.FC<WizardDataProps> = ({ data, onDataChange }
const handleNewCustomerChange = (updates: any) => { const handleNewCustomerChange = (updates: any) => {
const updated = { ...newCustomer, ...updates }; const updated = { ...newCustomer, ...updates };
setNewCustomer(updated); setNewCustomer(updated);
onDataChange({ onDataChange?.({
...data, ...data,
showNewCustomerForm, showNewCustomerForm,
newCustomerName: updated.name, newCustomerName: updated.name,
@@ -288,10 +284,10 @@ const CustomerSelectionStep: React.FC<WizardDataProps> = ({ data, onDataChange }
}; };
// Step 2: Order Items // Step 2: Order Items
const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => { const OrderItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards'); const { t } = useTranslation('wizards');
const { currentTenant } = useTenant(); const { currentTenant } = useTenant();
const [orderItems, setOrderItems] = useState(data.orderItems || []);
const [products, setProducts] = useState<any[]>([]); const [products, setProducts] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -302,9 +298,8 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
// Update parent whenever order items change // Update parent whenever order items change
const updateOrderItems = (newItems: any[]) => { const updateOrderItems = (newItems: any[]) => {
setOrderItems(newItems);
const totalAmount = newItems.reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0); const totalAmount = newItems.reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0);
onDataChange({ ...data, orderItems: newItems, totalAmount }); onDataChange?.({ ...data, orderItems: newItems, totalAmount });
}; };
const fetchProducts = async () => { const fetchProducts = async () => {
@@ -329,7 +324,7 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
const handleAddItem = () => { const handleAddItem = () => {
updateOrderItems([ updateOrderItems([
...orderItems, ...(data.orderItems || []),
{ {
id: Date.now(), id: Date.now(),
productId: '', productId: '',
@@ -344,7 +339,7 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
}; };
const handleUpdateItem = (index: number, field: string, value: any) => { const handleUpdateItem = (index: number, field: string, value: any) => {
const updated = orderItems.map((item: any, i: number) => { const updated = (data.orderItems || []).map((item: any, i: number) => {
if (i === index) { if (i === index) {
const newItem = { ...item, [field]: value }; const newItem = { ...item, [field]: value };
@@ -368,11 +363,11 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
}; };
const handleRemoveItem = (index: number) => { const handleRemoveItem = (index: number) => {
updateOrderItems(orderItems.filter((_: any, i: number) => i !== index)); updateOrderItems((data.orderItems || []).filter((_: any, i: number) => i !== index));
}; };
const calculateTotal = () => { const calculateTotal = () => {
return orderItems.reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0); return (data.orderItems || []).reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0);
}; };
return ( return (
@@ -414,7 +409,7 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
</button> </button>
</div> </div>
{orderItems.length === 0 ? ( {(data.orderItems || []).length === 0 ? (
<div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-lg text-[var(--text-tertiary)]"> <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" /> <Package className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p className="mb-2">{t('customerOrder.messages.noProductsInOrder')}</p> <p className="mb-2">{t('customerOrder.messages.noProductsInOrder')}</p>
@@ -422,7 +417,7 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{orderItems.map((item: any, index: number) => ( {(data.orderItems || []).map((item: any, index: number) => (
<div <div
key={item.id} key={item.id}
className="p-4 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]/30 space-y-3" className="p-4 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]/30 space-y-3"
@@ -510,7 +505,7 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
</div> </div>
)} )}
{orderItems.length > 0 && ( {(data.orderItems || []).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="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"> <div className="flex justify-between items-center">
<span className="text-lg font-semibold text-[var(--text-primary)]">{t('customerOrder.messages.orderTotal')}:</span> <span className="text-lg font-semibold text-[var(--text-primary)]">{t('customerOrder.messages.orderTotal')}:</span>
@@ -528,88 +523,18 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
}; };
// Step 3: Delivery & Payment with ALL fields // Step 3: Delivery & Payment with ALL fields
const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => { const DeliveryPaymentStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards'); const { t } = useTranslation('wizards');
const [orderData, setOrderData] = useState({
// Required fields
requestedDeliveryDate: data.requestedDeliveryDate || '',
orderNumber: data.orderNumber || '',
// Basic order info // Helper to get field value with defaults
orderType: data.orderType || 'standard', const getValue = (field: string, defaultValue: any = '') => {
priority: data.priority || 'normal', return data[field] ?? defaultValue;
status: data.status || 'pending', };
// Delivery fields
deliveryMethod: data.deliveryMethod || 'pickup',
deliveryAddress: data.deliveryAddress || '',
deliveryInstructions: data.deliveryInstructions || '',
deliveryContactName: data.deliveryContactName || '',
deliveryContactPhone: data.deliveryContactPhone || '',
deliveryTimeWindow: data.deliveryTimeWindow || '',
deliveryFee: data.deliveryFee || '',
// Payment fields
paymentMethod: data.paymentMethod || 'invoice',
paymentTerms: data.paymentTerms || 'net_30',
paymentStatus: data.paymentStatus || 'pending',
paymentDueDate: data.paymentDueDate || '',
// Pricing fields
subtotalAmount: data.subtotalAmount || '',
taxAmount: data.taxAmount || '',
discountPercentage: data.discountPercentage || '',
discountAmount: data.discountAmount || '',
shippingCost: data.shippingCost || '',
// Production & scheduling
productionStartDate: data.productionStartDate || '',
productionDueDate: data.productionDueDate || '',
productionBatchNumber: data.productionBatchNumber || '',
productionNotes: data.productionNotes || '',
// Fulfillment
actualDeliveryDate: data.actualDeliveryDate || '',
pickupLocation: data.pickupLocation || '',
shippingTrackingNumber: data.shippingTrackingNumber || '',
shippingCarrier: data.shippingCarrier || '',
// Source & channel
orderSource: data.orderSource || 'manual',
salesChannel: data.salesChannel || 'direct',
salesRepId: data.salesRepId || '',
// Communication
customerPurchaseOrder: data.customerPurchaseOrder || '',
internalNotes: data.internalNotes || '',
customerNotes: data.customerNotes || '',
specialInstructions: data.specialInstructions || '',
// Notifications
notifyCustomerOnStatusChange: data.notifyCustomerOnStatusChange ?? true,
notifyCustomerOnDelivery: data.notifyCustomerOnDelivery ?? true,
customerNotificationEmail: data.customerNotificationEmail || '',
customerNotificationPhone: data.customerNotificationPhone || '',
// Quality & requirements
qualityCheckRequired: data.qualityCheckRequired ?? false,
qualityCheckStatus: data.qualityCheckStatus || '',
packagingInstructions: data.packagingInstructions || '',
labelingRequirements: data.labelingRequirements || '',
// Advanced options
isRecurring: data.isRecurring ?? false,
recurringSchedule: data.recurringSchedule || '',
parentOrderId: data.parentOrderId || '',
relatedOrderIds: data.relatedOrderIds || '',
tags: data.tags || '',
metadata: data.metadata || '',
});
// Update parent whenever order data changes // Update parent whenever order data changes
const handleOrderDataChange = (newOrderData: any) => { const handleOrderDataChange = (updates: Record<string, any>) => {
setOrderData(newOrderData); onDataChange?.({ ...data, ...updates });
onDataChange({ ...data, ...newOrderData });
}; };
return ( return (
@@ -633,8 +558,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="date" type="date"
value={orderData.requestedDeliveryDate} value={getValue('requestedDeliveryDate')}
onChange={(e) => handleOrderDataChange({ ...orderData, requestedDeliveryDate: e.target.value })} onChange={(e) => handleOrderDataChange({ requestedDeliveryDate: e.target.value })}
min={new Date().toISOString().split('T')[0]} min={new Date().toISOString().split('T')[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)]" 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)]"
/> />
@@ -649,7 +574,7 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="text" type="text"
value={orderData.orderNumber || t('customerOrder.deliveryPayment.autoGeneratedLabel')} value={getValue('orderNumber') || t('customerOrder.deliveryPayment.autoGeneratedLabel')}
readOnly readOnly
disabled disabled
placeholder={t('customerOrder.deliveryPayment.autoGeneratedPlaceholder')} placeholder={t('customerOrder.deliveryPayment.autoGeneratedPlaceholder')}
@@ -667,8 +592,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
{t('customerOrder.deliveryPayment.orderType')} {t('customerOrder.deliveryPayment.orderType')}
</label> </label>
<select <select
value={orderData.orderType} value={getValue('orderType', 'standard')}
onChange={(e) => handleOrderDataChange({ ...orderData, orderType: e.target.value })} onChange={(e) => handleOrderDataChange({orderType: 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)]" 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="standard">{t('customerOrder.orderTypes.standard')}</option> <option value="standard">{t('customerOrder.orderTypes.standard')}</option>
@@ -683,8 +608,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
{t('customerOrder.deliveryPayment.priority')} {t('customerOrder.deliveryPayment.priority')}
</label> </label>
<select <select
value={orderData.priority} value={getValue('priority', 'normal')}
onChange={(e) => handleOrderDataChange({ ...orderData, priority: e.target.value })} onChange={(e) => handleOrderDataChange({priority: 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)]" 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="low">{t('customerOrder.priorities.low')}</option> <option value="low">{t('customerOrder.priorities.low')}</option>
@@ -699,8 +624,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
{t('customerOrder.deliveryPayment.status')} {t('customerOrder.deliveryPayment.status')}
</label> </label>
<select <select
value={orderData.status} value={getValue('status')}
onChange={(e) => handleOrderDataChange({ ...orderData, status: e.target.value })} onChange={(e) => handleOrderDataChange({status: 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)]" 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="pending">{t('customerOrder.statuses.pending')}</option> <option value="pending">{t('customerOrder.statuses.pending')}</option>
@@ -725,9 +650,9 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
<div className="grid grid-cols-1 md:grid-cols-3 gap-3"> <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<button <button
type="button" type="button"
onClick={() => handleOrderDataChange({ ...orderData, deliveryMethod: 'pickup' })} onClick={() => handleOrderDataChange({deliveryMethod: 'pickup' })}
className={`p-3 rounded-lg border-2 transition-all ${ className={`p-3 rounded-lg border-2 transition-all ${
orderData.deliveryMethod === 'pickup' getValue('deliveryMethod', 'pickup') === 'pickup'
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5' ? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
: 'border-[var(--border-secondary)]' : 'border-[var(--border-secondary)]'
}`} }`}
@@ -737,9 +662,9 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</button> </button>
<button <button
type="button" type="button"
onClick={() => handleOrderDataChange({ ...orderData, deliveryMethod: 'delivery' })} onClick={() => handleOrderDataChange({deliveryMethod: 'delivery' })}
className={`p-3 rounded-lg border-2 transition-all ${ className={`p-3 rounded-lg border-2 transition-all ${
orderData.deliveryMethod === 'delivery' getValue('deliveryMethod', 'pickup') === 'delivery'
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5' ? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
: 'border-[var(--border-secondary)]' : 'border-[var(--border-secondary)]'
}`} }`}
@@ -749,9 +674,9 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</button> </button>
<button <button
type="button" type="button"
onClick={() => handleOrderDataChange({ ...orderData, deliveryMethod: 'shipping' })} onClick={() => handleOrderDataChange({deliveryMethod: 'shipping' })}
className={`p-3 rounded-lg border-2 transition-all ${ className={`p-3 rounded-lg border-2 transition-all ${
orderData.deliveryMethod === 'shipping' getValue('deliveryMethod', 'pickup') === 'shipping'
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5' ? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
: 'border-[var(--border-secondary)]' : 'border-[var(--border-secondary)]'
}`} }`}
@@ -762,15 +687,15 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</div> </div>
</div> </div>
{(orderData.deliveryMethod === 'delivery' || orderData.deliveryMethod === 'shipping') && ( {(getValue('deliveryMethod', 'pickup') === 'delivery' || getValue('deliveryMethod', 'pickup') === 'shipping') && (
<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">
<MapPin className="w-4 h-4 inline mr-1.5" /> <MapPin className="w-4 h-4 inline mr-1.5" />
{t('customerOrder.messages.deliveryAddress')} * {t('customerOrder.messages.deliveryAddress')} *
</label> </label>
<textarea <textarea
value={orderData.deliveryAddress} value={getValue('deliveryAddress')}
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryAddress: e.target.value })} onChange={(e) => handleOrderDataChange({deliveryAddress: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.deliveryAddressPlaceholder')} placeholder={t('customerOrder.deliveryPayment.deliveryAddressPlaceholder')}
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)]"
@@ -785,8 +710,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="text" type="text"
value={orderData.deliveryContactName} value={getValue('deliveryContactName')}
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryContactName: e.target.value })} onChange={(e) => handleOrderDataChange({deliveryContactName: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.deliveryContactNamePlaceholder')} placeholder={t('customerOrder.deliveryPayment.deliveryContactNamePlaceholder')}
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)]"
/> />
@@ -798,8 +723,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="tel" type="tel"
value={orderData.deliveryContactPhone} value={getValue('deliveryContactPhone')}
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryContactPhone: e.target.value })} onChange={(e) => handleOrderDataChange({deliveryContactPhone: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.phoneNumberPlaceholder')} placeholder={t('customerOrder.deliveryPayment.phoneNumberPlaceholder')}
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)]"
/> />
@@ -818,8 +743,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
{t('customerOrder.messages.paymentMethod')} {t('customerOrder.messages.paymentMethod')}
</label> </label>
<select <select
value={orderData.paymentMethod} value={getValue('paymentMethod')}
onChange={(e) => handleOrderDataChange({ ...orderData, paymentMethod: e.target.value })} onChange={(e) => handleOrderDataChange({paymentMethod: 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)]" 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="cash">{t('customerOrder.paymentMethods.cash')}</option> <option value="cash">{t('customerOrder.paymentMethods.cash')}</option>
@@ -835,8 +760,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
{t('customerOrder.deliveryPayment.paymentTerms')} {t('customerOrder.deliveryPayment.paymentTerms')}
</label> </label>
<select <select
value={orderData.paymentTerms} value={getValue('paymentTerms')}
onChange={(e) => handleOrderDataChange({ ...orderData, paymentTerms: e.target.value })} onChange={(e) => handleOrderDataChange({paymentTerms: 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)]" 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="immediate">{t('customerOrder.paymentTerms.immediate')}</option> <option value="immediate">{t('customerOrder.paymentTerms.immediate')}</option>
@@ -850,8 +775,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
{t('customerOrder.deliveryPayment.paymentStatus')} {t('customerOrder.deliveryPayment.paymentStatus')}
</label> </label>
<select <select
value={orderData.paymentStatus} value={getValue('paymentStatus')}
onChange={(e) => handleOrderDataChange({ ...orderData, paymentStatus: e.target.value })} onChange={(e) => handleOrderDataChange({paymentStatus: 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)]" 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="pending">{t('customerOrder.paymentStatuses.pending')}</option> <option value="pending">{t('customerOrder.paymentStatuses.pending')}</option>
@@ -867,8 +792,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="date" type="date"
value={orderData.paymentDueDate} value={getValue('paymentDueDate')}
onChange={(e) => handleOrderDataChange({ ...orderData, paymentDueDate: e.target.value })} onChange={(e) => handleOrderDataChange({paymentDueDate: 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)]" 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>
@@ -913,8 +838,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="number" type="number"
value={orderData.discountPercentage} value={getValue('discountPercentage')}
onChange={(e) => handleOrderDataChange({ ...orderData, discountPercentage: e.target.value })} onChange={(e) => handleOrderDataChange({discountPercentage: e.target.value })}
placeholder="0" placeholder="0"
step="0.01" step="0.01"
min="0" min="0"
@@ -929,8 +854,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="number" type="number"
value={orderData.deliveryFee} value={getValue('deliveryFee')}
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryFee: e.target.value })} onChange={(e) => handleOrderDataChange({deliveryFee: e.target.value })}
placeholder="0.00" placeholder="0.00"
step="0.01" step="0.01"
min="0" min="0"
@@ -952,8 +877,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="date" type="date"
value={orderData.productionStartDate} value={getValue('productionStartDate')}
onChange={(e) => handleOrderDataChange({ ...orderData, productionStartDate: e.target.value })} onChange={(e) => handleOrderDataChange({productionStartDate: 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)]" 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>
@@ -964,8 +889,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="date" type="date"
value={orderData.productionDueDate} value={getValue('productionDueDate')}
onChange={(e) => handleOrderDataChange({ ...orderData, productionDueDate: e.target.value })} onChange={(e) => handleOrderDataChange({productionDueDate: 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)]" 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>
@@ -976,8 +901,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="text" type="text"
value={orderData.productionBatchNumber} value={getValue('productionBatchNumber')}
onChange={(e) => handleOrderDataChange({ ...orderData, productionBatchNumber: e.target.value })} onChange={(e) => handleOrderDataChange({productionBatchNumber: e.target.value })}
placeholder={t('customerOrder.messages.productionBatchNumberPlaceholder')} placeholder={t('customerOrder.messages.productionBatchNumberPlaceholder')}
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)]"
/> />
@@ -989,8 +914,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="text" type="text"
value={orderData.deliveryTimeWindow} value={getValue('deliveryTimeWindow')}
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryTimeWindow: e.target.value })} onChange={(e) => handleOrderDataChange({deliveryTimeWindow: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.deliveryTimeWindowPlaceholder')} placeholder={t('customerOrder.deliveryPayment.deliveryTimeWindowPlaceholder')}
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)]"
/> />
@@ -1001,8 +926,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
{t('customerOrder.deliveryPayment.productionNotes')} {t('customerOrder.deliveryPayment.productionNotes')}
</label> </label>
<textarea <textarea
value={orderData.productionNotes} value={getValue('productionNotes')}
onChange={(e) => handleOrderDataChange({ ...orderData, productionNotes: e.target.value })} onChange={(e) => handleOrderDataChange({productionNotes: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.productionNotesPlaceholder')} placeholder={t('customerOrder.deliveryPayment.productionNotesPlaceholder')}
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)]"
@@ -1023,8 +948,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="text" type="text"
value={orderData.shippingTrackingNumber} value={getValue('shippingTrackingNumber')}
onChange={(e) => handleOrderDataChange({ ...orderData, shippingTrackingNumber: e.target.value })} onChange={(e) => handleOrderDataChange({shippingTrackingNumber: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.shippingTrackingNumberPlaceholder')} placeholder={t('customerOrder.deliveryPayment.shippingTrackingNumberPlaceholder')}
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)]"
/> />
@@ -1036,8 +961,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="text" type="text"
value={orderData.shippingCarrier} value={getValue('shippingCarrier')}
onChange={(e) => handleOrderDataChange({ ...orderData, shippingCarrier: e.target.value })} onChange={(e) => handleOrderDataChange({shippingCarrier: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.shippingCarrierPlaceholder')} placeholder={t('customerOrder.deliveryPayment.shippingCarrierPlaceholder')}
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)]"
/> />
@@ -1049,8 +974,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="text" type="text"
value={orderData.pickupLocation} value={getValue('pickupLocation')}
onChange={(e) => handleOrderDataChange({ ...orderData, pickupLocation: e.target.value })} onChange={(e) => handleOrderDataChange({pickupLocation: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.pickupLocationPlaceholder')} placeholder={t('customerOrder.deliveryPayment.pickupLocationPlaceholder')}
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)]"
/> />
@@ -1062,8 +987,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="date" type="date"
value={orderData.actualDeliveryDate} value={getValue('actualDeliveryDate')}
onChange={(e) => handleOrderDataChange({ ...orderData, actualDeliveryDate: e.target.value })} onChange={(e) => handleOrderDataChange({actualDeliveryDate: 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)]" 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>
@@ -1081,8 +1006,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
{t('customerOrder.deliveryPayment.orderSource')} {t('customerOrder.deliveryPayment.orderSource')}
</label> </label>
<select <select
value={orderData.orderSource} value={getValue('orderSource')}
onChange={(e) => handleOrderDataChange({ ...orderData, orderSource: e.target.value })} onChange={(e) => handleOrderDataChange({orderSource: 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)]" 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="manual">{t('customerOrder.orderSources.manual')}</option> <option value="manual">{t('customerOrder.orderSources.manual')}</option>
@@ -1098,8 +1023,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
{t('customerOrder.deliveryPayment.salesChannel')} {t('customerOrder.deliveryPayment.salesChannel')}
</label> </label>
<select <select
value={orderData.salesChannel} value={getValue('salesChannel')}
onChange={(e) => handleOrderDataChange({ ...orderData, salesChannel: e.target.value })} onChange={(e) => handleOrderDataChange({salesChannel: 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)]" 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="direct">{t('customerOrder.salesChannels.direct')}</option> <option value="direct">{t('customerOrder.salesChannels.direct')}</option>
@@ -1115,8 +1040,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="text" type="text"
value={orderData.salesRepId} value={getValue('salesRepId')}
onChange={(e) => handleOrderDataChange({ ...orderData, salesRepId: e.target.value })} onChange={(e) => handleOrderDataChange({salesRepId: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.salesRepIdPlaceholder')} placeholder={t('customerOrder.deliveryPayment.salesRepIdPlaceholder')}
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)]"
/> />
@@ -1136,8 +1061,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="text" type="text"
value={orderData.customerPurchaseOrder} value={getValue('customerPurchaseOrder')}
onChange={(e) => handleOrderDataChange({ ...orderData, customerPurchaseOrder: e.target.value })} onChange={(e) => handleOrderDataChange({customerPurchaseOrder: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.customerPurchaseOrderPlaceholder')} placeholder={t('customerOrder.deliveryPayment.customerPurchaseOrderPlaceholder')}
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)]"
/> />
@@ -1148,8 +1073,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
{t('customerOrder.deliveryPayment.deliveryInstructions')} {t('customerOrder.deliveryPayment.deliveryInstructions')}
</label> </label>
<textarea <textarea
value={orderData.deliveryInstructions} value={getValue('deliveryInstructions')}
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryInstructions: e.target.value })} onChange={(e) => handleOrderDataChange({deliveryInstructions: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.deliveryInstructionsPlaceholder')} placeholder={t('customerOrder.deliveryPayment.deliveryInstructionsPlaceholder')}
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)]"
@@ -1161,8 +1086,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
{t('customerOrder.deliveryPayment.specialInstructions')} {t('customerOrder.deliveryPayment.specialInstructions')}
</label> </label>
<textarea <textarea
value={orderData.specialInstructions} value={getValue('specialInstructions')}
onChange={(e) => handleOrderDataChange({ ...orderData, specialInstructions: e.target.value })} onChange={(e) => handleOrderDataChange({specialInstructions: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.specialInstructionsPlaceholder')} placeholder={t('customerOrder.deliveryPayment.specialInstructionsPlaceholder')}
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)]"
@@ -1174,8 +1099,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
{t('customerOrder.deliveryPayment.internalNotes')} {t('customerOrder.deliveryPayment.internalNotes')}
</label> </label>
<textarea <textarea
value={orderData.internalNotes} value={getValue('internalNotes')}
onChange={(e) => handleOrderDataChange({ ...orderData, internalNotes: e.target.value })} onChange={(e) => handleOrderDataChange({internalNotes: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.internalNotesPlaceholder')} placeholder={t('customerOrder.deliveryPayment.internalNotesPlaceholder')}
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)]"
@@ -1187,8 +1112,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
{t('customerOrder.deliveryPayment.customerNotes')} {t('customerOrder.deliveryPayment.customerNotes')}
</label> </label>
<textarea <textarea
value={orderData.customerNotes} value={getValue('customerNotes')}
onChange={(e) => handleOrderDataChange({ ...orderData, customerNotes: e.target.value })} onChange={(e) => handleOrderDataChange({customerNotes: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.customerNotesPlaceholder')} placeholder={t('customerOrder.deliveryPayment.customerNotesPlaceholder')}
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)]"
@@ -1206,8 +1131,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
type="checkbox" type="checkbox"
checked={orderData.notifyCustomerOnStatusChange} checked={getValue('notifyCustomerOnStatusChange')}
onChange={(e) => handleOrderDataChange({ ...orderData, notifyCustomerOnStatusChange: e.target.checked })} onChange={(e) => handleOrderDataChange({notifyCustomerOnStatusChange: e.target.checked })}
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]" className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
/> />
<label className="text-sm text-[var(--text-secondary)]"> <label className="text-sm text-[var(--text-secondary)]">
@@ -1218,8 +1143,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
type="checkbox" type="checkbox"
checked={orderData.notifyCustomerOnDelivery} checked={getValue('notifyCustomerOnDelivery')}
onChange={(e) => handleOrderDataChange({ ...orderData, notifyCustomerOnDelivery: e.target.checked })} onChange={(e) => handleOrderDataChange({notifyCustomerOnDelivery: e.target.checked })}
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]" className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
/> />
<label className="text-sm text-[var(--text-secondary)]"> <label className="text-sm text-[var(--text-secondary)]">
@@ -1233,8 +1158,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="email" type="email"
value={orderData.customerNotificationEmail} value={getValue('customerNotificationEmail')}
onChange={(e) => handleOrderDataChange({ ...orderData, customerNotificationEmail: e.target.value })} onChange={(e) => handleOrderDataChange({customerNotificationEmail: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.notificationEmailPlaceholder')} placeholder={t('customerOrder.deliveryPayment.notificationEmailPlaceholder')}
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)]"
/> />
@@ -1246,8 +1171,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="tel" type="tel"
value={orderData.customerNotificationPhone} value={getValue('customerNotificationPhone')}
onChange={(e) => handleOrderDataChange({ ...orderData, customerNotificationPhone: e.target.value })} onChange={(e) => handleOrderDataChange({customerNotificationPhone: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.phoneNumberPlaceholder')} placeholder={t('customerOrder.deliveryPayment.phoneNumberPlaceholder')}
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)]"
/> />
@@ -1264,8 +1189,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
type="checkbox" type="checkbox"
checked={orderData.qualityCheckRequired} checked={getValue('qualityCheckRequired')}
onChange={(e) => handleOrderDataChange({ ...orderData, qualityCheckRequired: e.target.checked })} onChange={(e) => handleOrderDataChange({qualityCheckRequired: e.target.checked })}
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]" className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
/> />
<label className="text-sm text-[var(--text-secondary)]"> <label className="text-sm text-[var(--text-secondary)]">
@@ -1278,8 +1203,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
{t('customerOrder.deliveryPayment.qualityCheckStatus')} {t('customerOrder.deliveryPayment.qualityCheckStatus')}
</label> </label>
<select <select
value={orderData.qualityCheckStatus} value={getValue('qualityCheckStatus')}
onChange={(e) => handleOrderDataChange({ ...orderData, qualityCheckStatus: e.target.value })} onChange={(e) => handleOrderDataChange({qualityCheckStatus: 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)]" 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="">{t('customerOrder.qualityCheckStatuses.not_started')}</option> <option value="">{t('customerOrder.qualityCheckStatuses.not_started')}</option>
@@ -1295,8 +1220,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="text" type="text"
value={orderData.packagingInstructions} value={getValue('packagingInstructions')}
onChange={(e) => handleOrderDataChange({ ...orderData, packagingInstructions: e.target.value })} onChange={(e) => handleOrderDataChange({packagingInstructions: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.packagingInstructionsPlaceholder')} placeholder={t('customerOrder.deliveryPayment.packagingInstructionsPlaceholder')}
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)]"
/> />
@@ -1308,8 +1233,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="text" type="text"
value={orderData.labelingRequirements} value={getValue('labelingRequirements')}
onChange={(e) => handleOrderDataChange({ ...orderData, labelingRequirements: e.target.value })} onChange={(e) => handleOrderDataChange({labelingRequirements: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.labelingRequirementsPlaceholder')} placeholder={t('customerOrder.deliveryPayment.labelingRequirementsPlaceholder')}
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)]"
/> />
@@ -1326,8 +1251,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
type="checkbox" type="checkbox"
checked={orderData.isRecurring} checked={getValue('isRecurring')}
onChange={(e) => handleOrderDataChange({ ...orderData, isRecurring: e.target.checked })} onChange={(e) => handleOrderDataChange({isRecurring: e.target.checked })}
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]" className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
/> />
<label className="text-sm text-[var(--text-secondary)]"> <label className="text-sm text-[var(--text-secondary)]">
@@ -1335,15 +1260,15 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
</div> </div>
{orderData.isRecurring && ( {getValue('isRecurring', false) && (
<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">
{t('customerOrder.deliveryPayment.recurringSchedule')} {t('customerOrder.deliveryPayment.recurringSchedule')}
</label> </label>
<input <input
type="text" type="text"
value={orderData.recurringSchedule} value={getValue('recurringSchedule')}
onChange={(e) => handleOrderDataChange({ ...orderData, recurringSchedule: e.target.value })} onChange={(e) => handleOrderDataChange({recurringSchedule: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.recurringSchedulePlaceholder')} placeholder={t('customerOrder.deliveryPayment.recurringSchedulePlaceholder')}
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)]"
/> />
@@ -1359,8 +1284,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</label> </label>
<input <input
type="text" type="text"
value={orderData.tags} value={getValue('tags')}
onChange={(e) => handleOrderDataChange({ ...orderData, tags: e.target.value })} onChange={(e) => handleOrderDataChange({tags: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.tagsPlaceholder')} placeholder={t('customerOrder.deliveryPayment.tagsPlaceholder')}
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)]"
/> />
@@ -1374,8 +1299,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
</Tooltip> </Tooltip>
</label> </label>
<textarea <textarea
value={orderData.metadata} value={getValue('metadata')}
onChange={(e) => handleOrderDataChange({ ...orderData, metadata: e.target.value })} onChange={(e) => handleOrderDataChange({metadata: e.target.value })}
placeholder={t('customerOrder.deliveryPayment.metadataPlaceholder')} placeholder={t('customerOrder.deliveryPayment.metadataPlaceholder')}
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)] font-mono text-xs" 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"
@@ -1389,34 +1314,26 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
}; };
export const CustomerOrderWizardSteps = ( export const CustomerOrderWizardSteps = (
data: Record<string, any>, dataRef: React.MutableRefObject<Record<string, any>>,
setData: (data: Record<string, any>) => void setData: (data: Record<string, any>) => void
): WizardStep[] => [ ): WizardStep[] => {
{ // New architecture: return direct component references instead of arrow functions
id: 'customer-selection', // dataRef and onDataChange are now passed through WizardModal props
title: 'customerOrder.customerSelection.title', return [
component: (props) => <CustomerSelectionStep {...props} data={data} onDataChange={setData} />, {
validate: () => { id: 'customer-selection',
return !!(data.customer || (data.showNewCustomerForm && data.newCustomerName && data.newCustomerPhone)); title: 'customerOrder.customerSelection.title',
component: CustomerSelectionStep,
}, },
}, {
{ id: 'order-items',
id: 'order-items', title: 'customerOrder.orderItems.title',
title: 'customerOrder.orderItems.title', component: OrderItemsStep,
component: (props) => <OrderItemsStep {...props} data={data} onDataChange={setData} />,
validate: () => {
return !!(data.orderItems && data.orderItems.length > 0);
}, },
}, {
{ id: 'delivery-payment',
id: 'delivery-payment', title: 'customerOrder.deliveryPayment.title',
title: 'customerOrder.deliveryPayment.title', component: DeliveryPaymentStep,
component: (props) => <DeliveryPaymentStep {...props} data={data} onDataChange={setData} />,
validate: () => {
return !!(
data.requestedDeliveryDate &&
(data.deliveryMethod === 'pickup' || data.deliveryAddress)
);
}, },
}, ];
]; };

View File

@@ -1,110 +1,23 @@
import React, { useState, useEffect } from 'react'; import React, { useEffect } from 'react';
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal'; import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import { Users, CheckCircle2, Loader2 } from 'lucide-react'; import { Users } from 'lucide-react';
import { useTenant } from '../../../../stores/tenant.store';
import OrdersService from '../../../../api/services/orders';
import { showToast } from '../../../../utils/toast';
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection'; import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
import Tooltip from '../../../ui/Tooltip/Tooltip'; import Tooltip from '../../../ui/Tooltip/Tooltip';
interface WizardDataProps extends WizardStepProps { const CustomerDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
data: Record<string, any>; const data = dataRef?.current || {};
onDataChange: (data: Record<string, any>) => void;
}
const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => { const handleFieldChange = (field: string, value: any) => {
const { currentTenant } = useTenant(); onDataChange?.({ ...data, [field]: value });
const [customerData, setCustomerData] = useState({
// Required fields
name: data.name || '',
customerCode: data.customerCode || '',
customerType: data.customerType || 'individual',
country: data.country || 'US',
// Basic optional fields
businessName: data.businessName || '',
email: data.email || '',
phone: data.phone || '',
// Advanced optional fields
addressLine1: data.addressLine1 || '',
addressLine2: data.addressLine2 || '',
city: data.city || '',
state: data.state || '',
postalCode: data.postalCode || '',
taxId: data.taxId || '',
businessLicense: data.businessLicense || '',
paymentTerms: data.paymentTerms || 'immediate',
creditLimit: data.creditLimit || '',
discountPercentage: data.discountPercentage || 0,
customerSegment: data.customerSegment || 'regular',
priorityLevel: data.priorityLevel || 'normal',
preferredDeliveryMethod: data.preferredDeliveryMethod || 'delivery',
specialInstructions: data.specialInstructions || '',
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!customerData.customerCode && customerData.name) {
const code = `CUST-${customerData.name.substring(0, 3).toUpperCase()}-${Date.now().toString().slice(-4)}`;
setCustomerData(prev => ({ ...prev, customerCode: code }));
}
}, [customerData.name]);
useEffect(() => {
onDataChange({ ...data, ...customerData });
}, [customerData]);
const handleCreateCustomer = async () => {
if (!currentTenant?.id) {
setError('Could not obtain tenant information');
return;
}
setLoading(true);
setError(null);
try {
const payload = {
name: customerData.name,
customer_code: customerData.customerCode,
customer_type: customerData.customerType,
country: customerData.country,
business_name: customerData.businessName || undefined,
email: customerData.email || undefined,
phone: customerData.phone || undefined,
address_line1: customerData.addressLine1 || undefined,
address_line2: customerData.addressLine2 || undefined,
city: customerData.city || undefined,
state: customerData.state || undefined,
postal_code: customerData.postalCode || undefined,
tax_id: customerData.taxId || undefined,
business_license: customerData.businessLicense || undefined,
payment_terms: customerData.paymentTerms,
credit_limit: customerData.creditLimit ? parseFloat(customerData.creditLimit) : undefined,
discount_percentage: customerData.discountPercentage,
customer_segment: customerData.customerSegment,
priority_level: customerData.priorityLevel,
preferred_delivery_method: customerData.preferredDeliveryMethod,
special_instructions: customerData.specialInstructions || undefined,
is_active: true,
};
await OrdersService.createCustomer(currentTenant.id, payload);
showToast.success('Customer created successfully');
onComplete();
} catch (err: any) {
console.error('Error creating customer:', err);
const errorMessage = err.response?.data?.detail || 'Error creating customer';
setError(errorMessage);
showToast.error(errorMessage);
} finally {
setLoading(false);
}
}; };
useEffect(() => {
if (!data.customerCode && data.name) {
const code = `CUST-${data.name.substring(0, 3).toUpperCase()}-${Date.now().toString().slice(-4)}`;
onDataChange?.({ ...data, customerCode: code });
}
}, [data.name]);
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)]">
@@ -113,12 +26,6 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
<p className="text-sm text-[var(--text-secondary)]">Essential customer information</p> <p className="text-sm text-[var(--text-secondary)]">Essential customer information</p>
</div> </div>
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{error}
</div>
)}
{/* Required Fields */} {/* Required Fields */}
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -128,8 +35,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={customerData.name} value={data.name || ''}
onChange={(e) => setCustomerData({ ...customerData, name: e.target.value })} onChange={(e) => handleFieldChange('name', e.target.value)}
placeholder="e.g., Restaurant El Molino" placeholder="e.g., Restaurant El Molino"
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)]"
/> />
@@ -144,8 +51,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={customerData.customerCode} value={data.customerCode}
onChange={(e) => setCustomerData({ ...customerData, customerCode: e.target.value })} onChange={(e) => handleFieldChange('customerCode', e.target.value)}
placeholder="CUST-001" placeholder="CUST-001"
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)]"
/> />
@@ -158,8 +65,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
Customer Type * Customer Type *
</label> </label>
<select <select
value={customerData.customerType} value={data.customerType}
onChange={(e) => setCustomerData({ ...customerData, customerType: e.target.value })} onChange={(e) => handleFieldChange('customerType', 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)]" 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="individual">Individual</option> <option value="individual">Individual</option>
@@ -174,8 +81,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={customerData.country} value={data.country}
onChange={(e) => setCustomerData({ ...customerData, country: e.target.value })} onChange={(e) => handleFieldChange('country', e.target.value)}
placeholder="US" placeholder="US"
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)]"
/> />
@@ -188,8 +95,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={customerData.businessName} value={data.businessName}
onChange={(e) => setCustomerData({ ...customerData, businessName: e.target.value })} onChange={(e) => handleFieldChange('businessName', e.target.value)}
placeholder="Legal business name" placeholder="Legal business name"
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)]"
/> />
@@ -202,8 +109,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="email" type="email"
value={customerData.email} value={data.email}
onChange={(e) => setCustomerData({ ...customerData, email: e.target.value })} onChange={(e) => handleFieldChange('email', e.target.value)}
placeholder="contact@company.com" placeholder="contact@company.com"
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)]"
/> />
@@ -215,8 +122,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="tel" type="tel"
value={customerData.phone} value={data.phone}
onChange={(e) => setCustomerData({ ...customerData, phone: e.target.value })} onChange={(e) => handleFieldChange('phone', e.target.value)}
placeholder="+1 234 567 8900" placeholder="+1 234 567 8900"
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)]"
/> />
@@ -237,8 +144,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={customerData.addressLine1} value={data.addressLine1}
onChange={(e) => setCustomerData({ ...customerData, addressLine1: e.target.value })} onChange={(e) => handleFieldChange('addressLine1', e.target.value)}
placeholder="Street address" placeholder="Street address"
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)]"
/> />
@@ -250,8 +157,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={customerData.addressLine2} value={data.addressLine2}
onChange={(e) => setCustomerData({ ...customerData, addressLine2: e.target.value })} onChange={(e) => handleFieldChange('addressLine2', e.target.value)}
placeholder="Apartment, suite, etc." placeholder="Apartment, suite, etc."
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)]"
/> />
@@ -263,8 +170,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={customerData.city} value={data.city}
onChange={(e) => setCustomerData({ ...customerData, city: e.target.value })} onChange={(e) => handleFieldChange('city', e.target.value)}
placeholder="City" placeholder="City"
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)]"
/> />
@@ -276,8 +183,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={customerData.state} value={data.state}
onChange={(e) => setCustomerData({ ...customerData, state: e.target.value })} onChange={(e) => handleFieldChange('state', e.target.value)}
placeholder="State" placeholder="State"
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)]"
/> />
@@ -289,8 +196,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={customerData.postalCode} value={data.postalCode}
onChange={(e) => setCustomerData({ ...customerData, postalCode: e.target.value })} onChange={(e) => handleFieldChange('postalCode', e.target.value)}
placeholder="12345" placeholder="12345"
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)]"
/> />
@@ -302,8 +209,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={customerData.taxId} value={data.taxId}
onChange={(e) => setCustomerData({ ...customerData, taxId: e.target.value })} onChange={(e) => handleFieldChange('taxId', e.target.value)}
placeholder="Tax identification number" placeholder="Tax identification number"
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)]"
/> />
@@ -315,8 +222,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={customerData.businessLicense} value={data.businessLicense}
onChange={(e) => setCustomerData({ ...customerData, businessLicense: e.target.value })} onChange={(e) => handleFieldChange('businessLicense', e.target.value)}
placeholder="Business license number" placeholder="Business license number"
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)]"
/> />
@@ -327,8 +234,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
Payment Terms Payment Terms
</label> </label>
<select <select
value={customerData.paymentTerms} value={data.paymentTerms}
onChange={(e) => setCustomerData({ ...customerData, paymentTerms: e.target.value })} onChange={(e) => handleFieldChange('paymentTerms', 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)]" 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="immediate">Immediate</option> <option value="immediate">Immediate</option>
@@ -343,8 +250,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="number" type="number"
value={customerData.creditLimit} value={data.creditLimit}
onChange={(e) => setCustomerData({ ...customerData, creditLimit: e.target.value })} onChange={(e) => handleFieldChange('creditLimit', e.target.value)}
placeholder="5000.00" placeholder="5000.00"
min="0" min="0"
step="0.01" step="0.01"
@@ -358,8 +265,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="number" type="number"
value={customerData.discountPercentage} value={data.discountPercentage}
onChange={(e) => setCustomerData({ ...customerData, discountPercentage: parseFloat(e.target.value) || 0 })} onChange={(e) => handleFieldChange('discountPercentage', parseFloat(e.target.value) || 0)}
placeholder="10" placeholder="10"
min="0" min="0"
max="100" max="100"
@@ -373,8 +280,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
Customer Segment Customer Segment
</label> </label>
<select <select
value={customerData.customerSegment} value={data.customerSegment}
onChange={(e) => setCustomerData({ ...customerData, customerSegment: e.target.value })} onChange={(e) => handleFieldChange('customerSegment', 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)]" 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="vip">VIP</option> <option value="vip">VIP</option>
@@ -388,8 +295,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
Priority Level Priority Level
</label> </label>
<select <select
value={customerData.priorityLevel} value={data.priorityLevel}
onChange={(e) => setCustomerData({ ...customerData, priorityLevel: e.target.value })} onChange={(e) => handleFieldChange('priorityLevel', 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)]" 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="high">High</option> <option value="high">High</option>
@@ -403,8 +310,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
Preferred Delivery Method Preferred Delivery Method
</label> </label>
<select <select
value={customerData.preferredDeliveryMethod} value={data.preferredDeliveryMethod}
onChange={(e) => setCustomerData({ ...customerData, preferredDeliveryMethod: e.target.value })} onChange={(e) => handleFieldChange('preferredDeliveryMethod', 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)]" 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="delivery">Delivery</option> <option value="delivery">Delivery</option>
@@ -418,8 +325,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
Special Instructions Special Instructions
</label> </label>
<textarea <textarea
value={customerData.specialInstructions} value={data.specialInstructions}
onChange={(e) => setCustomerData({ ...customerData, specialInstructions: e.target.value })} onChange={(e) => handleFieldChange('specialInstructions', e.target.value)}
placeholder="Any special notes or instructions for this customer..." placeholder="Any special notes or instructions for this customer..."
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)]"
rows={3} rows={3}
@@ -427,42 +334,72 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</div> </div>
</div> </div>
</AdvancedOptionsSection> </AdvancedOptionsSection>
<div className="flex justify-center pt-4 border-t border-[var(--border-primary)]">
<button
type="button"
onClick={handleCreateCustomer}
disabled={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" />
Creating customer...
</>
) : (
<>
<CheckCircle2 className="w-5 h-5" />
Create Customer
</>
)}
</button>
</div>
</div> </div>
); );
}; };
export const CustomerWizardSteps = ( export const CustomerWizardSteps = (
data: Record<string, any>, dataRef: React.MutableRefObject<Record<string, any>>,
setData: (data: Record<string, any>) => void setData: (data: Record<string, any>) => void
): WizardStep[] => [ ): WizardStep[] => {
{ // New architecture: return direct component references instead of arrow functions
id: 'customer-details', // dataRef and onDataChange are now passed through WizardModal props
title: 'Customer Details', return [
description: 'Contact and business information', {
component: (props) => <CustomerDetailsStep {...props} data={data} onDataChange={setData} />, id: 'customer-details',
validate: () => { title: 'Customer Details',
return !!(data.name && data.customerCode && data.customerType && data.country); description: 'Contact and business information',
component: CustomerDetailsStep,
validate: async () => {
// Import these at the top level of this file would be better, but for now do it inline
const { useTenant } = await import('../../../../stores/tenant.store');
const OrdersService = (await import('../../../../api/services/orders')).default;
const { showToast } = await import('../../../../utils/toast');
const data = dataRef.current;
const { currentTenant } = useTenant.getState();
if (!currentTenant?.id) {
showToast.error('Could not obtain tenant information');
return false;
}
try {
const payload = {
name: data.name || '',
customer_code: data.customerCode || '',
customer_type: data.customerType || 'individual',
country: data.country || 'US',
business_name: data.businessName || undefined,
email: data.email || undefined,
phone: data.phone || undefined,
address_line1: data.addressLine1 || undefined,
address_line2: data.addressLine2 || undefined,
city: data.city || undefined,
state: data.state || undefined,
postal_code: data.postalCode || undefined,
tax_id: data.taxId || undefined,
business_license: data.businessLicense || undefined,
payment_terms: data.paymentTerms || 'immediate',
credit_limit: data.creditLimit ? parseFloat(data.creditLimit) : undefined,
discount_percentage: data.discountPercentage || 0,
customer_segment: data.customerSegment || 'regular',
priority_level: data.priorityLevel || 'normal',
preferred_delivery_method: data.preferredDeliveryMethod || 'delivery',
special_instructions: data.specialInstructions || undefined,
is_active: true,
};
await OrdersService.createCustomer(currentTenant.id, payload);
showToast.success('Customer created successfully');
return true;
} catch (err: any) {
console.error('Error creating customer:', err);
const errorMessage = err.response?.data?.detail || 'Error creating customer';
showToast.error(errorMessage);
return false;
}
},
}, },
}, ];
]; };

View File

@@ -1,65 +1,12 @@
import React, { useState } from 'react'; import React from 'react';
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal'; import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import { Wrench, CheckCircle2, Loader2 } from 'lucide-react'; import { Wrench } from 'lucide-react';
import { useTenant } from '../../../../stores/tenant.store';
import { equipmentService } from '../../../../api/services/equipment';
import { showToast } from '../../../../utils/toast';
interface WizardDataProps extends WizardStepProps { const EquipmentDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
data: Record<string, any>; const data = dataRef?.current || {};
onDataChange: (data: Record<string, any>) => void;
}
const EquipmentDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => { const handleFieldChange = (field: string, value: any) => {
const { currentTenant } = useTenant(); onDataChange?.({ ...data, [field]: value });
const [equipmentData, setEquipmentData] = useState({
type: data.type || 'oven',
brand: data.brand || '',
model: data.model || '',
location: data.location || '',
purchaseDate: data.purchaseDate || '',
status: data.status || 'active',
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSave = async () => {
if (!currentTenant?.id) {
setError('No se pudo obtener información del tenant');
return;
}
setLoading(true);
setError(null);
try {
const equipmentCreateData: any = {
name: `${equipmentData.type} - ${equipmentData.brand || 'Sin marca'}`,
type: equipmentData.type,
model: equipmentData.brand,
serialNumber: equipmentData.model,
location: equipmentData.location,
status: equipmentData.status,
installDate: equipmentData.purchaseDate || new Date().toISOString().split('T')[0],
lastMaintenance: new Date().toISOString().split('T')[0],
nextMaintenance: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
maintenanceInterval: 30,
is_active: true
};
await equipmentService.createEquipment(currentTenant.id, equipmentCreateData);
showToast.success('Equipo creado exitosamente');
onDataChange({ ...data, ...equipmentData });
onComplete();
} catch (err: any) {
console.error('Error creating equipment:', err);
const errorMessage = err.response?.data?.detail || 'Error al crear el equipo';
setError(errorMessage);
showToast.error(errorMessage);
} finally {
setLoading(false);
}
}; };
return ( return (
@@ -69,18 +16,12 @@ const EquipmentDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">Equipo de Panadería</h3> <h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">Equipo de Panadería</h3>
</div> </div>
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{error}
</div>
)}
<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">Tipo de Equipo *</label> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Tipo de Equipo *</label>
<select <select
value={equipmentData.type} value={data.type || 'oven'}
onChange={(e) => setEquipmentData({ ...equipmentData, type: e.target.value })} onChange={(e) => handleFieldChange('type', 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)]" 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="oven">Horno</option> <option value="oven">Horno</option>
@@ -94,8 +35,8 @@ const EquipmentDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Marca/Modelo</label> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Marca/Modelo</label>
<input <input
type="text" type="text"
value={equipmentData.brand} value={data.brand || ''}
onChange={(e) => setEquipmentData({ ...equipmentData, brand: e.target.value })} onChange={(e) => handleFieldChange('brand', e.target.value)}
placeholder="Ej: Rational SCC 101" placeholder="Ej: Rational SCC 101"
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)]"
/> />
@@ -104,8 +45,8 @@ const EquipmentDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Ubicación</label> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Ubicación</label>
<input <input
type="text" type="text"
value={equipmentData.location} value={data.location || ''}
onChange={(e) => setEquipmentData({ ...equipmentData, location: e.target.value })} onChange={(e) => handleFieldChange('location', e.target.value)}
placeholder="Ej: Cocina principal" placeholder="Ej: Cocina principal"
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)]"
/> />
@@ -114,36 +55,63 @@ const EquipmentDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Fecha de Compra</label> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Fecha de Compra</label>
<input <input
type="date" type="date"
value={equipmentData.purchaseDate} value={data.purchaseDate || ''}
onChange={(e) => setEquipmentData({ ...equipmentData, purchaseDate: e.target.value })} onChange={(e) => handleFieldChange('purchaseDate', 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)]" 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 className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
<button
onClick={handleSave}
disabled={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" />
Agregar Equipo
</>
)}
</button>
</div>
</div> </div>
); );
}; };
export const EquipmentWizardSteps = (data: Record<string, any>, setData: (data: Record<string, any>) => void): WizardStep[] => [ export const EquipmentWizardSteps = (dataRef: React.MutableRefObject<Record<string, any>>, setData: (data: Record<string, any>) => void): WizardStep[] => {
{ id: 'equipment-details', title: 'Detalles del Equipo', description: 'Tipo, modelo, ubicación', component: (props) => <EquipmentDetailsStep {...props} data={data} onDataChange={setData} /> }, // New architecture: return direct component references instead of arrow functions
]; // dataRef and onDataChange are now passed through WizardModal props
return [
{
id: 'equipment-details',
title: 'Detalles del Equipo',
description: 'Tipo, modelo, ubicación',
component: EquipmentDetailsStep,
validate: async () => {
const { useTenant } = await import('../../../../stores/tenant.store');
const { equipmentService } = await import('../../../../api/services/equipment');
const { showToast } = await import('../../../../utils/toast');
const data = dataRef.current;
const { currentTenant } = useTenant.getState();
if (!currentTenant?.id) {
showToast.error('No se pudo obtener información del tenant');
return false;
}
try {
const equipmentCreateData: any = {
name: `${data.type || 'oven'} - ${data.brand || 'Sin marca'}`,
type: data.type || 'oven',
model: data.brand || '',
serialNumber: data.model || '',
location: data.location || '',
status: data.status || 'active',
installDate: data.purchaseDate || new Date().toISOString().split('T')[0],
lastMaintenance: new Date().toISOString().split('T')[0],
nextMaintenance: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
maintenanceInterval: 30,
is_active: true
};
await equipmentService.createEquipment(currentTenant.id, equipmentCreateData);
showToast.success('Equipo creado exitosamente');
return true;
} catch (err: any) {
console.error('Error creating equipment:', err);
const errorMessage = err.response?.data?.detail || 'Error al crear el equipo';
showToast.error(errorMessage);
return false;
}
},
},
];
};

View File

@@ -5,21 +5,18 @@ import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
import Tooltip from '../../../ui/Tooltip/Tooltip'; import Tooltip from '../../../ui/Tooltip/Tooltip';
import { Info, Package, ShoppingBag } from 'lucide-react'; import { Info, Package, ShoppingBag } from 'lucide-react';
interface WizardDataProps extends WizardStepProps {
data: Record<string, any>;
onDataChange: (data: Record<string, any>) => void;
}
// STEP 1: Product Type Selection with advanced fields // STEP 1: Product Type Selection with advanced fields
const ProductTypeStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => { const ProductTypeStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
// New architecture: access data from dataRef.current
const data = dataRef?.current || {};
const { t } = useTranslation('wizards'); const { t } = useTranslation('wizards');
const handleFieldChange = (field: string, value: any) => { const handleFieldChange = (field: string, value: any) => {
onDataChange({ ...data, [field]: value }); onDataChange?.({ ...data, [field]: value });
}; };
const handleTypeSelect = (type: string) => { const handleTypeSelect = (type: string) => {
onDataChange({ ...data, productType: type }); onDataChange?.({ ...data, productType: type });
}; };
return ( return (
@@ -151,11 +148,12 @@ const ProductTypeStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
}; };
// STEP 2: Basic Information with advanced fields // STEP 2: Basic Information with advanced fields
const BasicInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => { const BasicInfoStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards'); const { t } = useTranslation('wizards');
const handleFieldChange = (field: string, value: any) => { const handleFieldChange = (field: string, value: any) => {
onDataChange({ ...data, [field]: value }); onDataChange?.({ ...data, [field]: value });
}; };
return ( return (
@@ -308,208 +306,283 @@ const BasicInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
); );
}; };
// STEP 3: Stock Configuration with advanced fields // STEP 3: Initial Stock/Lots Configuration
const StockConfigStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => { const StockConfigStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards'); const { t } = useTranslation('wizards');
const [lots, setLots] = useState<any[]>(data.initialLots || []);
const handleFieldChange = (field: string, value: any) => { const handleFieldChange = (field: string, value: any) => {
onDataChange({ ...data, [field]: value }); onDataChange?.({ ...data, [field]: value });
}; };
const handleAddLot = () => {
const newLot = {
id: Date.now(),
quantity: '',
unitCost: '',
expirationDate: '',
lotNumber: `LOT-${Date.now()}`,
location: '',
};
const updatedLots = [...lots, newLot];
setLots(updatedLots);
onDataChange?.({ ...data, initialLots: updatedLots });
};
const handleRemoveLot = (lotId: number) => {
const updatedLots = lots.filter(lot => lot.id !== lotId);
setLots(updatedLots);
onDataChange?.({ ...data, initialLots: updatedLots });
};
const handleLotChange = (lotId: number, field: string, value: any) => {
const updatedLots = lots.map(lot =>
lot.id === lotId ? { ...lot, [field]: value } : lot
);
setLots(updatedLots);
onDataChange?.({ ...data, initialLots: updatedLots });
};
const totalQuantity = lots.reduce((sum, lot) => sum + (parseFloat(lot.quantity) || 0), 0);
const totalValue = lots.reduce((sum, lot) => {
const qty = parseFloat(lot.quantity) || 0;
const cost = parseFloat(lot.unitCost) || 0;
return sum + (qty * cost);
}, 0);
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Summary Card */} {/* Header */}
<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">
{t('inventory.steps.initialStock', 'Stock Inicial')}
</h3>
<p className="text-sm text-[var(--text-secondary)]">
{t('inventory.initialStockDescription', 'Agrega uno o más lotes para registrar el inventario inicial')}
</p>
</div>
{/* Product Summary */}
<div className="bg-gradient-to-br from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 border border-[var(--color-primary)]/20 rounded-lg p-4"> <div className="bg-gradient-to-br from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 border border-[var(--color-primary)]/20 rounded-lg p-4">
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-3 flex items-center gap-2"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<Package className="w-4 h-4" /> <div>
{t('inventory.summary', 'Summary')} <span className="text-[var(--text-tertiary)] block mb-1">Producto</span>
</h4> <span className="font-medium text-[var(--text-primary)]">{data.name || 'N/A'}</span>
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2">
<span className="text-[var(--text-tertiary)]">{t('inventory.fields.productType')}:</span>
<span className="font-medium text-[var(--text-primary)]">
{data.productType === 'ingredient'
? t('inventory.productTypes.ingredient')
: t('inventory.productTypes.finished_product')}
</span>
</div> </div>
{data.name && ( <div>
<div className="flex items-center gap-2"> <span className="text-[var(--text-tertiary)] block mb-1">Unidad</span>
<span className="text-[var(--text-tertiary)]">{t('inventory.fields.name')}:</span> <span className="font-medium text-[var(--text-primary)]">{data.unitOfMeasure || 'N/A'}</span>
<span className="font-medium text-[var(--text-primary)]">{data.name}</span> </div>
<div>
<span className="text-[var(--text-tertiary)] block mb-1">Cantidad Total</span>
<span className="font-medium text-[var(--color-primary)]">{totalQuantity.toFixed(2)}</span>
</div>
<div>
<span className="text-[var(--text-tertiary)] block mb-1">Valor Total</span>
<span className="font-medium text-green-600">${totalValue.toFixed(2)}</span>
</div>
</div>
</div>
{/* Unit of Measure - Required (moved from previous step conceptually but kept here) */}
{!data.unitOfMeasure && (
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{t('inventory.fields.unitOfMeasure')} *
</label>
<select
value={data.unitOfMeasure || ''}
onChange={(e) => handleFieldChange('unitOfMeasure', 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="">{t('inventory.units.select')}</option>
<option value="kg">{t('inventory.units.kg')}</option>
<option value="g">{t('inventory.units.g')}</option>
<option value="l">{t('inventory.units.l')}</option>
<option value="ml">{t('inventory.units.ml')}</option>
<option value="units">{t('inventory.units.units')}</option>
<option value="dozen">{t('inventory.units.dozen')}</option>
<option value="lb">{t('inventory.units.lb')}</option>
<option value="oz">{t('inventory.units.oz')}</option>
</select>
</div>
)}
{/* Lots List */}
{lots.length > 0 && (
<div className="space-y-4">
<h4 className="text-sm font-semibold text-[var(--text-primary)] flex items-center gap-2">
<ShoppingBag className="w-4 h-4" />
Lotes Registrados ({lots.length})
</h4>
{lots.map((lot, index) => (
<div
key={lot.id}
className="border border-[var(--border-secondary)] rounded-lg p-4 bg-[var(--bg-secondary)]/30 space-y-3"
>
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-semibold text-[var(--text-primary)]">
Lote #{index + 1}
</span>
<button
onClick={() => handleRemoveLot(lot.id)}
className="text-red-500 hover:text-red-700 transition-colors p-1"
>
<span className="text-xs">Eliminar</span>
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{/* Quantity */}
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Cantidad *
</label>
<input
type="number"
value={lot.quantity}
onChange={(e) => handleLotChange(lot.id, 'quantity', e.target.value)}
placeholder="100"
step="0.01"
min="0"
className="w-full px-2 py-1.5 text-sm border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
{/* Unit Cost */}
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Costo Unitario ($)
</label>
<input
type="number"
value={lot.unitCost}
onChange={(e) => handleLotChange(lot.id, 'unitCost', e.target.value)}
placeholder="0.00"
step="0.01"
min="0"
className="w-full px-2 py-1.5 text-sm border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
{/* Lot Number */}
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Número de Lote
</label>
<input
type="text"
value={lot.lotNumber}
onChange={(e) => handleLotChange(lot.id, 'lotNumber', e.target.value)}
placeholder="LOT-001"
className="w-full px-2 py-1.5 text-sm border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
{/* Expiration Date */}
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Fecha de Expiración
</label>
<input
type="date"
value={lot.expirationDate}
onChange={(e) => handleLotChange(lot.id, 'expirationDate', e.target.value)}
className="w-full px-2 py-1.5 text-sm border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
{/* Location */}
<div className="md:col-span-2">
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
Ubicación
</label>
<input
type="text"
value={lot.location}
onChange={(e) => handleLotChange(lot.id, 'location', e.target.value)}
placeholder="Ej: Almacén A, Estante 3"
className="w-full px-2 py-1.5 text-sm border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
/>
</div>
</div>
{/* Lot Total */}
{lot.quantity && lot.unitCost && (
<div className="text-xs text-[var(--text-tertiary)] pt-2 border-t border-[var(--border-secondary)]">
Valor del lote: <span className="font-semibold text-green-600">
${(parseFloat(lot.quantity) * parseFloat(lot.unitCost)).toFixed(2)}
</span>
</div>
)}
</div> </div>
)} ))}
</div> </div>
</div> )}
{/* Unit of Measure - Required */} {/* Add Lot Button */}
<div> <button
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2"> onClick={handleAddLot}
{t('inventory.fields.unitOfMeasure')} * className="w-full py-3 border-2 border-dashed border-[var(--color-primary)] text-[var(--color-primary)] rounded-lg hover:bg-[var(--color-primary)]/5 transition-colors font-medium inline-flex items-center justify-center gap-2"
</label>
<select
value={data.unitOfMeasure || ''}
onChange={(e) => handleFieldChange('unitOfMeasure', 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="">{t('inventory.units.select')}</option>
<option value="kg">{t('inventory.units.kg')}</option>
<option value="g">{t('inventory.units.g')}</option>
<option value="l">{t('inventory.units.l')}</option>
<option value="ml">{t('inventory.units.ml')}</option>
<option value="units">{t('inventory.units.units')}</option>
<option value="dozen">{t('inventory.units.dozen')}</option>
<option value="lb">{t('inventory.units.lb')}</option>
<option value="oz">{t('inventory.units.oz')}</option>
</select>
</div>
{/* Standard Cost - Visible but optional */}
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{t('inventory.fields.standardCost')}
<Tooltip content={t('tooltips.standardCost')}>
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
</Tooltip>
</label>
<input
type="number"
value={data.standardCost || ''}
onChange={(e) => handleFieldChange('standardCost', e.target.value)}
placeholder="0.00"
step="0.01"
min="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>
{/* Advanced Fields Section */}
<AdvancedOptionsSection
title={t('inventory.sections.advancedStockSettings')}
description={t('inventory.sections.advancedStockSettingsDescription', 'Configure inventory thresholds and reorder points')}
> >
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <Package className="w-5 h-5" />
{/* Low Stock Threshold */} {lots.length === 0 ? 'Agregar Lote Inicial' : 'Agregar Otro Lote'}
<div> </button>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{t('inventory.fields.lowStockThreshold')}
<Tooltip content={t('tooltips.lowStockThreshold')}>
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
</Tooltip>
</label>
<input
type="number"
value={data.lowStockThreshold || ''}
onChange={(e) => handleFieldChange('lowStockThreshold', e.target.value)}
placeholder="10"
min="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>
{/* Reorder Point */} {/* Skip Option */}
<div> {lots.length === 0 && (
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2"> <p className="text-xs text-center text-[var(--text-tertiary)] italic">
{t('inventory.fields.reorderPoint')} Puedes saltar este paso si prefieres agregar el stock inicial más tarde
<Tooltip content={t('tooltips.reorderPoint')}> </p>
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" /> )}
</Tooltip>
</label>
<input
type="number"
value={data.reorderPoint || ''}
onChange={(e) => handleFieldChange('reorderPoint', e.target.value)}
placeholder="20"
min="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>
{/* Reorder Quantity */}
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{t('inventory.fields.reorderQuantity')}
<Tooltip content={t('tooltips.reorderQuantity')}>
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
</Tooltip>
</label>
<input
type="number"
value={data.reorderQuantity || ''}
onChange={(e) => handleFieldChange('reorderQuantity', e.target.value)}
placeholder="100"
min="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>
{/* Max Stock Level */}
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{t('inventory.fields.maxStockLevel')}
</label>
<input
type="number"
value={data.maxStockLevel || ''}
onChange={(e) => handleFieldChange('maxStockLevel', e.target.value)}
placeholder="500"
min="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>
{/* Lead Time Days */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{t('inventory.fields.leadTimeDays')}
<Tooltip content={t('tooltips.leadTime')}>
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
</Tooltip>
</label>
<input
type="number"
value={data.leadTimeDays || ''}
onChange={(e) => handleFieldChange('leadTimeDays', e.target.value)}
placeholder="7"
min="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>
</AdvancedOptionsSection>
</div> </div>
); );
}; };
export const InventoryWizardSteps = ( export const InventoryWizardSteps = (
data: Record<string, any>, dataRef: React.MutableRefObject<Record<string, any>>,
setData: (data: Record<string, any>) => void setData: (data: Record<string, any>) => void
): WizardStep[] => { ): WizardStep[] => {
// New architecture: return direct component references instead of arrow functions
// dataRef and onDataChange are now passed through WizardModal props
return [ return [
{ {
id: 'product-type', id: 'product-type',
title: 'inventory.steps.productType', title: 'inventory.steps.productType',
component: (props) => <ProductTypeStep {...props} data={data} onDataChange={setData} />, component: ProductTypeStep,
validate: () => { validate: () => {
return !!data.productType; return !!dataRef.current.productType;
}, },
}, },
{ {
id: 'basic-info', id: 'basic-info',
title: 'inventory.steps.basicInfo', title: 'inventory.steps.basicInfo',
component: (props) => <BasicInfoStep {...props} data={data} onDataChange={setData} />, component: BasicInfoStep,
validate: () => { validate: () => {
const category = const category =
data.productType === 'ingredient' ? data.ingredientCategory : data.productCategory; dataRef.current.productType === 'ingredient' ? dataRef.current.ingredientCategory : dataRef.current.productCategory;
return !!(data.name && data.name.trim().length >= 2 && category); return !!(dataRef.current.name && dataRef.current.name.trim().length >= 2 && category);
}, },
}, },
{ {
id: 'stock-config', id: 'stock-config',
title: 'inventory.steps.stockConfig', title: 'inventory.steps.stockConfig',
component: (props) => <StockConfigStep {...props} data={data} onDataChange={setData} />, component: StockConfigStep,
validate: () => { validate: () => {
return !!data.unitOfMeasure; // Step is optional - user can skip if not adding initial lots
// Only validate that if lots are added, they have required fields
const lots = dataRef.current.initialLots || [];
if (lots.length === 0) {
return true; // No lots, can proceed
}
// If lots exist, validate they have quantity and unit cost
return lots.every((lot: any) => {
const hasQuantity = lot.quantity && parseFloat(lot.quantity) > 0;
const hasUnitCost = lot.unitCost && parseFloat(lot.unitCost) >= 0;
return hasQuantity && hasUnitCost;
});
}, },
}, },
]; ];

View File

@@ -5,57 +5,13 @@ import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
import Tooltip from '../../../ui/Tooltip/Tooltip'; import Tooltip from '../../../ui/Tooltip/Tooltip';
import { Info } from 'lucide-react'; import { Info } from 'lucide-react';
interface WizardDataProps extends WizardStepProps {
data: Record<string, any>;
onDataChange: (data: Record<string, any>) => void;
}
// Single comprehensive step with all fields // Single comprehensive step with all fields
const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => { const QualityTemplateDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const data = dataRef?.current || {};
const { t } = useTranslation('wizards'); const { t } = useTranslation('wizards');
const [templateData, setTemplateData] = useState({
// Required fields
name: data.name || '',
checkType: data.checkType || 'product_quality',
weight: data.weight || '5.0',
// Basic fields const handleFieldChange = (field: string, value: any) => {
templateCode: data.templateCode || '', onDataChange?.({ ...data, [field]: value });
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 || '',
requiredEquipment: data.requiredEquipment || '',
acceptanceCriteria: data.acceptanceCriteria || '',
specificConditions: data.specificConditions || '',
});
// Update parent whenever local state changes
const handleDataChange = (newTemplateData: any) => {
setTemplateData(newTemplateData);
onDataChange({ ...data, ...newTemplateData });
}; };
return ( return (
@@ -77,8 +33,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
</label> </label>
<input <input
type="text" type="text"
value={templateData.name} value={data.name}
onChange={(e) => handleDataChange({ ...templateData, name: e.target.value })} onChange={(e) => handleFieldChange('name', e.target.value)}
placeholder={t('qualityTemplate.fields.namePlaceholder')} placeholder={t('qualityTemplate.fields.namePlaceholder')}
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)]"
/> />
@@ -89,8 +45,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
{t('qualityTemplate.fields.checkType')} * {t('qualityTemplate.fields.checkType')} *
</label> </label>
<select <select
value={templateData.checkType} value={data.checkType}
onChange={(e) => handleDataChange({ ...templateData, checkType: e.target.value })} onChange={(e) => handleFieldChange('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)]" 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">{t('qualityTemplate.checkTypes.product_quality')}</option> <option value="product_quality">{t('qualityTemplate.checkTypes.product_quality')}</option>
@@ -112,8 +68,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
</label> </label>
<input <input
type="number" type="number"
value={templateData.weight} value={data.weight}
onChange={(e) => handleDataChange({ ...templateData, weight: e.target.value })} onChange={(e) => handleFieldChange('weight', e.target.value)}
placeholder="5.0" placeholder="5.0"
step="0.1" step="0.1"
min="0" min="0"
@@ -136,8 +92,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
</label> </label>
<input <input
type="text" type="text"
value={templateData.templateCode} value={data.templateCode}
onChange={(e) => handleDataChange({ ...templateData, templateCode: e.target.value })} onChange={(e) => handleFieldChange('templateCode', e.target.value)}
placeholder={t('qualityTemplate.fields.templateCodePlaceholder')} placeholder={t('qualityTemplate.fields.templateCodePlaceholder')}
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)]"
/> />
@@ -149,8 +105,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
</label> </label>
<input <input
type="text" type="text"
value={templateData.version} value={data.version}
onChange={(e) => handleDataChange({ ...templateData, version: e.target.value })} onChange={(e) => handleFieldChange('version', e.target.value)}
placeholder="1.0" 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)]" 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)]"
/> />
@@ -161,8 +117,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
{t('qualityTemplate.fields.description')} {t('qualityTemplate.fields.description')}
</label> </label>
<textarea <textarea
value={templateData.description} value={data.description}
onChange={(e) => handleDataChange({ ...templateData, description: e.target.value })} onChange={(e) => handleFieldChange('description', e.target.value)}
placeholder={t('qualityTemplate.fields.descriptionPlaceholder')} placeholder={t('qualityTemplate.fields.descriptionPlaceholder')}
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)]"
@@ -178,8 +134,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
</label> </label>
<input <input
type="text" type="text"
value={templateData.applicableStages} value={data.applicableStages}
onChange={(e) => handleDataChange({ ...templateData, applicableStages: e.target.value })} onChange={(e) => handleFieldChange('applicableStages', e.target.value)}
placeholder={t('qualityTemplate.fields.applicablePlaceholder')} placeholder={t('qualityTemplate.fields.applicablePlaceholder')}
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)]"
/> />
@@ -196,8 +152,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
{t('qualityTemplate.scoringMethods.scoringMethod')} {t('qualityTemplate.scoringMethods.scoringMethod')}
</label> </label>
<select <select
value={templateData.scoringMethod} value={data.scoringMethod}
onChange={(e) => handleDataChange({ ...templateData, scoringMethod: e.target.value })} onChange={(e) => handleFieldChange('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)]" 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">{t('qualityTemplate.scoringMethods.weightedAverage')}</option> <option value="weighted_average">{t('qualityTemplate.scoringMethods.weightedAverage')}</option>
@@ -216,8 +172,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
</label> </label>
<input <input
type="number" type="number"
value={templateData.passThreshold} value={data.passThreshold}
onChange={(e) => handleDataChange({ ...templateData, passThreshold: e.target.value })} onChange={(e) => handleFieldChange('passThreshold', e.target.value)}
placeholder="70.0" placeholder="70.0"
step="0.1" step="0.1"
min="0" min="0"
@@ -235,8 +191,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
</label> </label>
<input <input
type="number" type="number"
value={templateData.frequencyDays} value={data.frequencyDays}
onChange={(e) => handleDataChange({ ...templateData, frequencyDays: e.target.value })} onChange={(e) => handleFieldChange('frequencyDays', e.target.value)}
placeholder={t('qualityTemplate.advancedFields.frequencyPlaceholder')} placeholder={t('qualityTemplate.advancedFields.frequencyPlaceholder')}
min="1" 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)]" 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)]"
@@ -246,8 +202,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
type="checkbox" type="checkbox"
checked={templateData.isRequired} checked={data.isRequired}
onChange={(e) => handleDataChange({ ...templateData, isRequired: e.target.checked })} onChange={(e) => handleFieldChange('isRequired', e.target.checked)}
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]" className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
/> />
<label className="text-sm text-[var(--text-secondary)]"> <label className="text-sm text-[var(--text-secondary)]">
@@ -276,8 +232,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
</Tooltip> </Tooltip>
</label> </label>
<textarea <textarea
value={templateData.checkPoints} value={data.checkPoints}
onChange={(e) => handleDataChange({ ...templateData, checkPoints: e.target.value })} onChange={(e) => handleFieldChange('checkPoints', e.target.value)}
placeholder={t('qualityTemplate.advancedFields.checkPointsPlaceholder')} placeholder={t('qualityTemplate.advancedFields.checkPointsPlaceholder')}
rows={4} 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" 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"
@@ -289,8 +245,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
{t('qualityTemplate.advancedFields.acceptanceCriteria')} {t('qualityTemplate.advancedFields.acceptanceCriteria')}
</label> </label>
<textarea <textarea
value={templateData.acceptanceCriteria} value={data.acceptanceCriteria}
onChange={(e) => handleDataChange({ ...templateData, acceptanceCriteria: e.target.value })} onChange={(e) => handleFieldChange('acceptanceCriteria', e.target.value)}
placeholder={t('qualityTemplate.advancedFields.acceptanceCriteriaPlaceholder')} placeholder={t('qualityTemplate.advancedFields.acceptanceCriteriaPlaceholder')}
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)]"
@@ -313,8 +269,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
</Tooltip> </Tooltip>
</label> </label>
<textarea <textarea
value={templateData.parameters} value={data.parameters}
onChange={(e) => handleDataChange({ ...templateData, parameters: e.target.value })} onChange={(e) => handleFieldChange('parameters', e.target.value)}
placeholder={t('qualityTemplate.advancedFields.parametersPlaceholder')} placeholder={t('qualityTemplate.advancedFields.parametersPlaceholder')}
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)] font-mono text-xs" 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"
@@ -329,8 +285,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
</Tooltip> </Tooltip>
</label> </label>
<textarea <textarea
value={templateData.thresholds} value={data.thresholds}
onChange={(e) => handleDataChange({ ...templateData, thresholds: e.target.value })} onChange={(e) => handleFieldChange('thresholds', e.target.value)}
placeholder={t('qualityTemplate.advancedFields.thresholdsPlaceholder')} placeholder={t('qualityTemplate.advancedFields.thresholdsPlaceholder')}
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)] font-mono text-xs" 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"
@@ -345,8 +301,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
</Tooltip> </Tooltip>
</label> </label>
<textarea <textarea
value={templateData.scoringCriteria} value={data.scoringCriteria}
onChange={(e) => handleDataChange({ ...templateData, scoringCriteria: e.target.value })} onChange={(e) => handleFieldChange('scoringCriteria', e.target.value)}
placeholder={t('qualityTemplate.advancedFields.scoringCriteriaPlaceholder')} placeholder={t('qualityTemplate.advancedFields.scoringCriteriaPlaceholder')}
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)] font-mono text-xs" 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"
@@ -367,8 +323,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
</label> </label>
<input <input
type="text" type="text"
value={templateData.responsibleRole} value={data.responsibleRole}
onChange={(e) => handleDataChange({ ...templateData, responsibleRole: e.target.value })} onChange={(e) => handleFieldChange('responsibleRole', e.target.value)}
placeholder={t('qualityTemplate.advancedFields.responsibleRolePlaceholder')} placeholder={t('qualityTemplate.advancedFields.responsibleRolePlaceholder')}
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)]"
/> />
@@ -380,8 +336,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
</label> </label>
<input <input
type="text" type="text"
value={templateData.requiredEquipment} value={data.requiredEquipment}
onChange={(e) => handleDataChange({ ...templateData, requiredEquipment: e.target.value })} onChange={(e) => handleFieldChange('requiredEquipment', e.target.value)}
placeholder={t('qualityTemplate.advancedFields.requiredEquipmentPlaceholder')} placeholder={t('qualityTemplate.advancedFields.requiredEquipmentPlaceholder')}
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)]"
/> />
@@ -392,8 +348,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
{t('qualityTemplate.advancedFields.specificConditions')} {t('qualityTemplate.advancedFields.specificConditions')}
</label> </label>
<textarea <textarea
value={templateData.specificConditions} value={data.specificConditions}
onChange={(e) => handleDataChange({ ...templateData, specificConditions: e.target.value })} onChange={(e) => handleFieldChange('specificConditions', e.target.value)}
placeholder={t('qualityTemplate.advancedFields.specificConditionsPlaceholder')} placeholder={t('qualityTemplate.advancedFields.specificConditionsPlaceholder')}
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)]"
@@ -411,8 +367,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
type="checkbox" type="checkbox"
checked={templateData.isActive} checked={data.isActive}
onChange={(e) => handleDataChange({ ...templateData, isActive: e.target.checked })} onChange={(e) => handleFieldChange('isActive', e.target.checked)}
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]" className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
/> />
<label className="text-sm text-[var(--text-secondary)]"> <label className="text-sm text-[var(--text-secondary)]">
@@ -423,8 +379,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
type="checkbox" type="checkbox"
checked={templateData.requiresPhoto} checked={data.requiresPhoto}
onChange={(e) => handleDataChange({ ...templateData, requiresPhoto: e.target.checked })} onChange={(e) => handleFieldChange('requiresPhoto', e.target.checked)}
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]" className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
/> />
<label className="text-sm text-[var(--text-secondary)]"> <label className="text-sm text-[var(--text-secondary)]">
@@ -435,8 +391,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
type="checkbox" type="checkbox"
checked={templateData.criticalControlPoint} checked={data.criticalControlPoint}
onChange={(e) => handleDataChange({ ...templateData, criticalControlPoint: e.target.checked })} onChange={(e) => handleFieldChange('criticalControlPoint', e.target.checked)}
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]" className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
/> />
<label className="text-sm text-[var(--text-secondary)]"> <label className="text-sm text-[var(--text-secondary)]">
@@ -447,8 +403,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
type="checkbox" type="checkbox"
checked={templateData.notifyOnFail} checked={data.notifyOnFail}
onChange={(e) => handleDataChange({ ...templateData, notifyOnFail: e.target.checked })} onChange={(e) => handleFieldChange('notifyOnFail', e.target.checked)}
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]" className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
/> />
<label className="text-sm text-[var(--text-secondary)]"> <label className="text-sm text-[var(--text-secondary)]">
@@ -463,15 +419,16 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
}; };
export const QualityTemplateWizardSteps = ( export const QualityTemplateWizardSteps = (
data: Record<string, any>, dataRef: React.MutableRefObject<Record<string, any>>,
setData: (data: Record<string, any>) => void setData: (data: Record<string, any>) => void
): WizardStep[] => [ ): WizardStep[] => {
{ // New architecture: return direct component references instead of arrow functions
id: 'template-details', // dataRef and onDataChange are now passed through WizardModal props
title: 'qualityTemplate.advancedFields.templateDetailsTitle', return [
component: (props) => <QualityTemplateDetailsStep {...props} data={data} onDataChange={setData} />, {
validate: () => { id: 'template-details',
return !!(data.name && data.checkType && data.weight); title: 'qualityTemplate.advancedFields.templateDetailsTitle',
component: QualityTemplateDetailsStep,
}, },
}, ];
]; };

View File

@@ -12,60 +12,20 @@ import { showToast } from '../../../../utils/toast';
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection'; import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
import Tooltip from '../../../ui/Tooltip/Tooltip'; import Tooltip from '../../../ui/Tooltip/Tooltip';
interface WizardDataProps extends WizardStepProps { const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
data: Record<string, any>; const data = dataRef?.current || {};
onDataChange: (data: Record<string, any>) => void;
}
const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
const { currentTenant } = useTenant(); const { currentTenant } = useTenant();
const [recipeData, setRecipeData] = useState({
// Required fields
name: data.name || '',
finishedProductId: data.finishedProductId || '',
yieldQuantity: data.yieldQuantity || '',
yieldUnit: data.yieldUnit || 'units',
// Optional basic fields
category: data.category || 'bread',
prepTime: data.prepTime || '',
instructions: data.instructions || '',
// Advanced optional fields
recipeCode: data.recipeCode || '',
version: data.version || '1.0',
difficultyLevel: data.difficultyLevel || 3,
cookTime: data.cookTime || '',
restTime: data.restTime || '',
totalTime: data.totalTime || '',
description: data.description || '',
preparationNotes: data.preparationNotes || '',
storageInstructions: data.storageInstructions || '',
servesCount: data.servesCount || '',
batchSizeMultiplier: data.batchSizeMultiplier || 1.0,
minBatchSize: data.minBatchSize || '',
maxBatchSize: data.maxBatchSize || '',
optimalProductionTemp: data.optimalProductionTemp || '',
optimalHumidity: data.optimalHumidity || '',
isSeasonal: data.isSeasonal || false,
isSignatureItem: data.isSignatureItem || false,
seasonStartMonth: data.seasonStartMonth || '',
seasonEndMonth: data.seasonEndMonth || '',
allergens: data.allergens || '',
dietaryTags: data.dietaryTags || '',
targetMargin: data.targetMargin || '',
});
const [finishedProducts, setFinishedProducts] = useState<IngredientResponse[]>([]); const [finishedProducts, setFinishedProducts] = useState<IngredientResponse[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const handleFieldChange = (field: string, value: any) => {
onDataChange?.({ ...data, [field]: value });
};
useEffect(() => { useEffect(() => {
fetchFinishedProducts(); fetchFinishedProducts();
}, []); }, []);
useEffect(() => {
onDataChange({ ...data, ...recipeData });
}, [recipeData]);
const fetchFinishedProducts = async () => { const fetchFinishedProducts = async () => {
if (!currentTenant?.id) return; if (!currentTenant?.id) return;
setLoading(true); setLoading(true);
@@ -97,8 +57,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</label> </label>
<input <input
type="text" type="text"
value={recipeData.name} value={data.name}
onChange={(e) => setRecipeData({ ...recipeData, name: e.target.value })} onChange={(e) => handleFieldChange('name', e.target.value)}
placeholder="e.g., Traditional Baguette" placeholder="e.g., Traditional Baguette"
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)]"
/> />
@@ -110,8 +70,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
Category * Category *
</label> </label>
<select <select
value={recipeData.category} value={data.category}
onChange={(e) => setRecipeData({ ...recipeData, category: e.target.value })} onChange={(e) => handleFieldChange('category', 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)]" 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="bread">Bread</option> <option value="bread">Bread</option>
@@ -133,8 +93,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</Tooltip> </Tooltip>
</label> </label>
<select <select
value={recipeData.finishedProductId} value={data.finishedProductId}
onChange={(e) => setRecipeData({ ...recipeData, finishedProductId: e.target.value })} onChange={(e) => handleFieldChange('finishedProductId', 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)]" 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={loading} disabled={loading}
> >
@@ -153,8 +113,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</label> </label>
<input <input
type="number" type="number"
value={recipeData.yieldQuantity} value={data.yieldQuantity}
onChange={(e) => setRecipeData({ ...recipeData, yieldQuantity: e.target.value })} onChange={(e) => handleFieldChange('yieldQuantity', e.target.value)}
placeholder="12" placeholder="12"
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)]"
min="0.01" min="0.01"
@@ -167,8 +127,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
Yield Unit * Yield Unit *
</label> </label>
<select <select
value={recipeData.yieldUnit} value={data.yieldUnit}
onChange={(e) => setRecipeData({ ...recipeData, yieldUnit: e.target.value })} onChange={(e) => handleFieldChange('yieldUnit', 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)]" 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="units">Units</option> <option value="units">Units</option>
@@ -187,8 +147,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</label> </label>
<input <input
type="number" type="number"
value={recipeData.prepTime} value={data.prepTime}
onChange={(e) => setRecipeData({ ...recipeData, prepTime: e.target.value })} onChange={(e) => handleFieldChange('prepTime', e.target.value)}
placeholder="60" placeholder="60"
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)]"
min="0" min="0"
@@ -200,8 +160,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
Instructions Instructions
</label> </label>
<textarea <textarea
value={recipeData.instructions} value={data.instructions}
onChange={(e) => setRecipeData({ ...recipeData, instructions: e.target.value })} onChange={(e) => handleFieldChange('instructions', e.target.value)}
placeholder="Step-by-step preparation instructions..." placeholder="Step-by-step preparation instructions..."
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)]"
rows={4} rows={4}
@@ -221,8 +181,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</label> </label>
<input <input
type="text" type="text"
value={recipeData.recipeCode} value={data.recipeCode}
onChange={(e) => setRecipeData({ ...recipeData, recipeCode: e.target.value })} onChange={(e) => handleFieldChange('recipeCode', e.target.value)}
placeholder="RCP-001" placeholder="RCP-001"
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)]"
/> />
@@ -234,8 +194,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</label> </label>
<input <input
type="text" type="text"
value={recipeData.version} value={data.version}
onChange={(e) => setRecipeData({ ...recipeData, version: e.target.value })} onChange={(e) => handleFieldChange('version', e.target.value)}
placeholder="1.0" 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)]" 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)]"
/> />
@@ -250,8 +210,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</label> </label>
<input <input
type="number" type="number"
value={recipeData.difficultyLevel} value={data.difficultyLevel}
onChange={(e) => setRecipeData({ ...recipeData, difficultyLevel: parseInt(e.target.value) || 1 })} onChange={(e) => handleFieldChange('difficultyLevel', parseInt(e.target.value) || 1)}
min="1" min="1"
max="5" max="5"
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)]"
@@ -264,8 +224,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</label> </label>
<input <input
type="number" type="number"
value={recipeData.cookTime} value={data.cookTime}
onChange={(e) => setRecipeData({ ...recipeData, cookTime: e.target.value })} onChange={(e) => handleFieldChange('cookTime', e.target.value)}
placeholder="30" placeholder="30"
min="0" min="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)]" 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)]"
@@ -281,8 +241,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</label> </label>
<input <input
type="number" type="number"
value={recipeData.restTime} value={data.restTime}
onChange={(e) => setRecipeData({ ...recipeData, restTime: e.target.value })} onChange={(e) => handleFieldChange('restTime', e.target.value)}
placeholder="60" placeholder="60"
min="0" min="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)]" 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)]"
@@ -295,8 +255,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</label> </label>
<input <input
type="number" type="number"
value={recipeData.totalTime} value={data.totalTime}
onChange={(e) => setRecipeData({ ...recipeData, totalTime: e.target.value })} onChange={(e) => handleFieldChange('totalTime', e.target.value)}
placeholder="90" placeholder="90"
min="0" min="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)]" 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)]"
@@ -309,8 +269,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</label> </label>
<input <input
type="number" type="number"
value={recipeData.servesCount} value={data.servesCount}
onChange={(e) => setRecipeData({ ...recipeData, servesCount: e.target.value })} onChange={(e) => handleFieldChange('servesCount', e.target.value)}
placeholder="8" placeholder="8"
min="0" min="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)]" 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)]"
@@ -326,8 +286,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</label> </label>
<input <input
type="number" type="number"
value={recipeData.batchSizeMultiplier} value={data.batchSizeMultiplier}
onChange={(e) => setRecipeData({ ...recipeData, batchSizeMultiplier: parseFloat(e.target.value) || 1 })} onChange={(e) => handleFieldChange('batchSizeMultiplier', parseFloat(e.target.value) || 1)}
placeholder="1.0" placeholder="1.0"
min="0.1" min="0.1"
step="0.1" step="0.1"
@@ -341,8 +301,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</label> </label>
<input <input
type="number" type="number"
value={recipeData.minBatchSize} value={data.minBatchSize}
onChange={(e) => setRecipeData({ ...recipeData, minBatchSize: e.target.value })} onChange={(e) => handleFieldChange('minBatchSize', e.target.value)}
placeholder="5" placeholder="5"
min="0" min="0"
step="0.1" step="0.1"
@@ -356,8 +316,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</label> </label>
<input <input
type="number" type="number"
value={recipeData.maxBatchSize} value={data.maxBatchSize}
onChange={(e) => setRecipeData({ ...recipeData, maxBatchSize: e.target.value })} onChange={(e) => handleFieldChange('maxBatchSize', e.target.value)}
placeholder="100" placeholder="100"
min="0" min="0"
step="0.1" step="0.1"
@@ -371,8 +331,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</label> </label>
<input <input
type="number" type="number"
value={recipeData.optimalProductionTemp} value={data.optimalProductionTemp}
onChange={(e) => setRecipeData({ ...recipeData, optimalProductionTemp: e.target.value })} onChange={(e) => handleFieldChange('optimalProductionTemp', e.target.value)}
placeholder="24" placeholder="24"
step="0.1" step="0.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)]" 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)]"
@@ -385,8 +345,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</label> </label>
<input <input
type="number" type="number"
value={recipeData.optimalHumidity} value={data.optimalHumidity}
onChange={(e) => setRecipeData({ ...recipeData, optimalHumidity: e.target.value })} onChange={(e) => handleFieldChange('optimalHumidity', e.target.value)}
placeholder="65" placeholder="65"
min="0" min="0"
max="100" max="100"
@@ -401,8 +361,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</label> </label>
<input <input
type="number" type="number"
value={recipeData.targetMargin} value={data.targetMargin}
onChange={(e) => setRecipeData({ ...recipeData, targetMargin: e.target.value })} onChange={(e) => handleFieldChange('targetMargin', e.target.value)}
placeholder="30" placeholder="30"
min="0" min="0"
max="100" max="100"
@@ -417,8 +377,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
<input <input
type="checkbox" type="checkbox"
id="isSeasonal" id="isSeasonal"
checked={recipeData.isSeasonal} checked={data.isSeasonal}
onChange={(e) => setRecipeData({ ...recipeData, isSeasonal: e.target.checked })} onChange={(e) => handleFieldChange('isSeasonal', e.target.checked)}
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]" className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
/> />
<label htmlFor="isSeasonal" className="text-sm font-medium text-[var(--text-secondary)]"> <label htmlFor="isSeasonal" className="text-sm font-medium text-[var(--text-secondary)]">
@@ -430,8 +390,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
<input <input
type="checkbox" type="checkbox"
id="isSignatureItem" id="isSignatureItem"
checked={recipeData.isSignatureItem} checked={data.isSignatureItem}
onChange={(e) => setRecipeData({ ...recipeData, isSignatureItem: e.target.checked })} onChange={(e) => handleFieldChange('isSignatureItem', e.target.checked)}
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]" className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
/> />
<label htmlFor="isSignatureItem" className="text-sm font-medium text-[var(--text-secondary)]"> <label htmlFor="isSignatureItem" className="text-sm font-medium text-[var(--text-secondary)]">
@@ -440,15 +400,15 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</div> </div>
</div> </div>
{recipeData.isSeasonal && ( {data.isSeasonal && (
<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">
Season Start Month Season Start Month
</label> </label>
<select <select
value={recipeData.seasonStartMonth} value={data.seasonStartMonth}
onChange={(e) => setRecipeData({ ...recipeData, seasonStartMonth: e.target.value })} onChange={(e) => handleFieldChange('seasonStartMonth', 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)]" 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="">Select month...</option> <option value="">Select month...</option>
@@ -465,8 +425,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
Season End Month Season End Month
</label> </label>
<select <select
value={recipeData.seasonEndMonth} value={data.seasonEndMonth}
onChange={(e) => setRecipeData({ ...recipeData, seasonEndMonth: e.target.value })} onChange={(e) => handleFieldChange('seasonEndMonth', 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)]" 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="">Select month...</option> <option value="">Select month...</option>
@@ -485,8 +445,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
Description Description
</label> </label>
<textarea <textarea
value={recipeData.description} value={data.description}
onChange={(e) => setRecipeData({ ...recipeData, description: e.target.value })} onChange={(e) => handleFieldChange('description', e.target.value)}
placeholder="Detailed description of the recipe..." placeholder="Detailed description of the recipe..."
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)]"
rows={3} rows={3}
@@ -498,8 +458,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
Preparation Notes Preparation Notes
</label> </label>
<textarea <textarea
value={recipeData.preparationNotes} value={data.preparationNotes}
onChange={(e) => setRecipeData({ ...recipeData, preparationNotes: e.target.value })} onChange={(e) => handleFieldChange('preparationNotes', e.target.value)}
placeholder="Tips and notes for preparation..." placeholder="Tips and notes for preparation..."
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)]"
rows={3} rows={3}
@@ -511,8 +471,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
Storage Instructions Storage Instructions
</label> </label>
<textarea <textarea
value={recipeData.storageInstructions} value={data.storageInstructions}
onChange={(e) => setRecipeData({ ...recipeData, storageInstructions: e.target.value })} onChange={(e) => handleFieldChange('storageInstructions', e.target.value)}
placeholder="How to store the finished product..." placeholder="How to store the finished product..."
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)]"
rows={3} rows={3}
@@ -525,8 +485,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</label> </label>
<input <input
type="text" type="text"
value={recipeData.allergens} value={data.allergens}
onChange={(e) => setRecipeData({ ...recipeData, allergens: e.target.value })} onChange={(e) => handleFieldChange('allergens', e.target.value)}
placeholder="e.g., gluten, dairy, eggs (comma-separated)" placeholder="e.g., gluten, dairy, eggs (comma-separated)"
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)]"
/> />
@@ -538,8 +498,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
</label> </label>
<input <input
type="text" type="text"
value={recipeData.dietaryTags} value={data.dietaryTags}
onChange={(e) => setRecipeData({ ...recipeData, dietaryTags: e.target.value })} onChange={(e) => handleFieldChange('dietaryTags', e.target.value)}
placeholder="e.g., vegan, gluten-free, organic (comma-separated)" placeholder="e.g., vegan, gluten-free, organic (comma-separated)"
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)]"
/> />
@@ -558,10 +518,10 @@ interface SelectedIngredient {
order: number; order: number;
} }
const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => { const IngredientsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const data = dataRef?.current || {};
const { currentTenant } = useTenant(); const { currentTenant } = useTenant();
const [ingredients, setIngredients] = useState<IngredientResponse[]>([]); const [ingredients, setIngredients] = useState<IngredientResponse[]>([]);
const [selectedIngredients, setSelectedIngredients] = useState<SelectedIngredient[]>(data.ingredients || []);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
@@ -570,10 +530,6 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
fetchIngredients(); fetchIngredients();
}, []); }, []);
useEffect(() => {
onDataChange({ ...data, ingredients: selectedIngredients });
}, [selectedIngredients]);
const fetchIngredients = async () => { const fetchIngredients = async () => {
if (!currentTenant?.id) return; if (!currentTenant?.id) return;
setLoading(true); setLoading(true);
@@ -596,21 +552,25 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
quantity: 0, quantity: 0,
unit: MeasurementUnit.GRAMS, unit: MeasurementUnit.GRAMS,
notes: '', notes: '',
order: selectedIngredients.length + 1, order: (data.ingredients || []).length + 1,
}; };
setSelectedIngredients([...selectedIngredients, newIngredient]); onDataChange?.({ ...data, ingredients: [...(data.ingredients || []), newIngredient] });
}; };
const handleUpdateIngredient = (id: string, field: keyof SelectedIngredient, value: any) => { const handleUpdateIngredient = (id: string, field: keyof SelectedIngredient, value: any) => {
setSelectedIngredients( onDataChange?.({
selectedIngredients.map(ing => ...data,
ingredients: (data.ingredients || []).map(ing =>
ing.id === id ? { ...ing, [field]: value } : ing ing.id === id ? { ...ing, [field]: value } : ing
) )
); });
}; };
const handleRemoveIngredient = (id: string) => { const handleRemoveIngredient = (id: string) => {
setSelectedIngredients(selectedIngredients.filter(ing => ing.id !== id)); onDataChange?.({
...data,
ingredients: (data.ingredients || []).filter(ing => ing.id !== id)
});
}; };
const filteredIngredients = ingredients.filter(ing => const filteredIngredients = ingredients.filter(ing =>
@@ -639,14 +599,14 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
) : ( ) : (
<> <>
<div className="space-y-4"> <div className="space-y-4">
{selectedIngredients.length === 0 ? ( {(data.ingredients || []).length === 0 ? (
<div className="text-center py-8 border-2 border-dashed border-[var(--border-secondary)] rounded-lg"> <div className="text-center py-8 border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
<Package className="w-12 h-12 mx-auto mb-3 text-[var(--text-tertiary)]" /> <Package className="w-12 h-12 mx-auto mb-3 text-[var(--text-tertiary)]" />
<p className="text-[var(--text-secondary)] mb-2">No ingredients added</p> <p className="text-[var(--text-secondary)] mb-2">No ingredients added</p>
<p className="text-sm text-[var(--text-tertiary)]">Click "Add Ingredient" to begin</p> <p className="text-sm text-[var(--text-tertiary)]">Click "Add Ingredient" to begin</p>
</div> </div>
) : ( ) : (
selectedIngredients.map((selectedIng) => ( (data.ingredients || []).map((selectedIng) => (
<div key={selectedIng.id} className="p-4 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]"> <div key={selectedIng.id} className="p-4 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]">
<div className="grid grid-cols-1 md:grid-cols-12 gap-3 items-start"> <div className="grid grid-cols-1 md:grid-cols-12 gap-3 items-start">
<div className="md:col-span-5"> <div className="md:col-span-5">
@@ -737,10 +697,10 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
); );
}; };
const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => { const QualityTemplatesStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onComplete }) => {
const data = dataRef?.current || {};
const { currentTenant } = useTenant(); const { currentTenant } = useTenant();
const [templates, setTemplates] = useState<QualityCheckTemplateResponse[]>([]); const [templates, setTemplates] = useState<QualityCheckTemplateResponse[]>([]);
const [selectedTemplateIds, setSelectedTemplateIds] = useState<string[]>(data.selectedTemplates || []);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -749,10 +709,6 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
fetchTemplates(); fetchTemplates();
}, []); }, []);
useEffect(() => {
onDataChange({ ...data, selectedTemplates: selectedTemplateIds });
}, [selectedTemplateIds]);
const fetchTemplates = async () => { const fetchTemplates = async () => {
if (!currentTenant?.id) return; if (!currentTenant?.id) return;
setLoading(true); setLoading(true);
@@ -768,11 +724,11 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
}; };
const toggleTemplate = (templateId: string) => { const toggleTemplate = (templateId: string) => {
setSelectedTemplateIds(prev => const currentTemplates = data.selectedTemplates || [];
prev.includes(templateId) const newTemplates = currentTemplates.includes(templateId)
? prev.filter(id => id !== templateId) ? currentTemplates.filter(id => id !== templateId)
: [...prev, templateId] : [...currentTemplates, templateId];
); onDataChange?.({ ...data, selectedTemplates: newTemplates });
}; };
const handleCreateRecipe = async () => { const handleCreateRecipe = async () => {
@@ -795,11 +751,11 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
})); }));
let qualityConfig: RecipeQualityConfiguration | undefined; let qualityConfig: RecipeQualityConfiguration | undefined;
if (selectedTemplateIds.length > 0) { if ((data.selectedTemplates || []).length > 0) {
qualityConfig = { qualityConfig = {
stages: { stages: {
production: { production: {
template_ids: selectedTemplateIds, template_ids: data.selectedTemplates || [],
required_checks: [], required_checks: [],
optional_checks: [], optional_checks: [],
blocking_on_failure: true, blocking_on_failure: true,
@@ -901,7 +857,7 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
type="button" type="button"
onClick={() => toggleTemplate(template.id)} onClick={() => toggleTemplate(template.id)}
className={`w-full p-4 rounded-lg border-2 transition-all text-left ${ className={`w-full p-4 rounded-lg border-2 transition-all text-left ${
selectedTemplateIds.includes(template.id) (data.selectedTemplates || []).includes(template.id)
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5' ? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50' : 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50'
}`} }`}
@@ -928,7 +884,7 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
)} )}
</div> </div>
</div> </div>
{selectedTemplateIds.includes(template.id) && ( {(data.selectedTemplates || []).includes(template.id) && (
<CheckCircle2 className="w-5 h-5 text-[var(--color-primary)] flex-shrink-0 ml-3" /> <CheckCircle2 className="w-5 h-5 text-[var(--color-primary)] flex-shrink-0 ml-3" />
)} )}
</div> </div>
@@ -937,10 +893,10 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
</div> </div>
)} )}
{selectedTemplateIds.length > 0 && ( {(data.selectedTemplates || []).length > 0 && (
<div className="p-4 bg-[var(--color-primary)]/5 rounded-lg border border-[var(--color-primary)]/20"> <div className="p-4 bg-[var(--color-primary)]/5 rounded-lg border border-[var(--color-primary)]/20">
<p className="text-sm text-[var(--text-primary)]"> <p className="text-sm text-[var(--text-primary)]">
<strong>{selectedTemplateIds.length}</strong> template(s) selected <strong>{(data.selectedTemplates || []).length}</strong> template(s) selected
</p> </p>
</div> </div>
)} )}
@@ -971,36 +927,28 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
); );
}; };
export const RecipeWizardSteps = (data: Record<string, any>, setData: (data: Record<string, any>) => void): WizardStep[] => [ export const RecipeWizardSteps = (dataRef: React.MutableRefObject<Record<string, any>>, setData: (data: Record<string, any>) => void): WizardStep[] => {
{ // New architecture: return direct component references instead of arrow functions
id: 'recipe-details', // dataRef and onDataChange are now passed through WizardModal props
title: 'Recipe Details', return [
description: 'Name, category, yield', {
component: (props) => <RecipeDetailsStep {...props} data={data} onDataChange={setData} />, id: 'recipe-details',
validate: () => { title: 'Recipe Details',
return !!(data.name && data.finishedProductId && data.yieldQuantity); description: 'Name, category, yield',
component: RecipeDetailsStep,
}, },
}, {
{ id: 'recipe-ingredients',
id: 'recipe-ingredients', title: 'Ingredients',
title: 'Ingredients', description: 'Selection and quantities',
description: 'Selection and quantities', component: IngredientsStep,
component: (props) => <IngredientsStep {...props} data={data} onDataChange={setData} />,
validate: () => {
if (!data.ingredients || data.ingredients.length === 0) {
return false;
}
const invalidIngredients = data.ingredients.filter(
(ing: any) => !ing.ingredientId || ing.quantity <= 0
);
return invalidIngredients.length === 0;
}, },
}, {
{ id: 'recipe-quality-templates',
id: 'recipe-quality-templates', title: 'Quality Templates',
title: 'Quality Templates', description: 'Applicable quality controls',
description: 'Applicable quality controls', component: QualityTemplatesStep,
component: (props) => <QualityTemplatesStep {...props} data={data} onDataChange={setData} />, isOptional: true,
isOptional: true, },
}, ];
]; };

View File

@@ -4,7 +4,6 @@ import {
Edit3, Edit3,
Upload, Upload,
CheckCircle2, CheckCircle2,
AlertCircle,
Download, Download,
FileSpreadsheet, FileSpreadsheet,
Calendar, Calendar,
@@ -23,24 +22,18 @@ import { showToast } from '../../../../utils/toast';
// STEP 1: Entry Method Selection // STEP 1: Entry Method Selection
// ======================================== // ========================================
interface EntryMethodStepProps extends WizardStepProps { const EntryMethodStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNext }) => {
data: Record<string, any>; const data = dataRef?.current || {};
onDataChange: (data: Record<string, any>) => void;
}
const EntryMethodStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, onNext }) => {
const [selectedMethod, setSelectedMethod] = useState<'manual' | 'upload'>( const [selectedMethod, setSelectedMethod] = useState<'manual' | 'upload'>(
data.entryMethod || 'manual' data.entryMethod || 'manual'
); );
const handleSelect = (method: 'manual' | 'upload') => { const handleSelect = (method: 'manual' | 'upload') => {
setSelectedMethod(method); setSelectedMethod(method);
onDataChange({ ...data, entryMethod: method }); const newData = { ...data, entryMethod: method };
}; onDataChange?.(newData);
// Automatically advance to next step after selection
const handleContinue = () => { setTimeout(() => onNext?.(), 100);
onDataChange({ ...data, entryMethod: selectedMethod });
onNext();
}; };
return ( return (
@@ -166,17 +159,6 @@ const EntryMethodStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
</div> </div>
</button> </button>
</div> </div>
{/* Continue Button */}
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
<button
onClick={handleContinue}
className="px-6 py-3 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 transition-colors font-medium inline-flex items-center gap-2"
>
Continuar
<CheckCircle2 className="w-5 h-5" />
</button>
</div>
</div> </div>
); );
}; };
@@ -185,14 +167,9 @@ const EntryMethodStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
// STEP 2a: Manual Entry Form // STEP 2a: Manual Entry Form
// ======================================== // ========================================
const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, onNext }) => { const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNext }) => {
const data = dataRef?.current || {};
const { currentTenant } = useTenant(); const { currentTenant } = useTenant();
const [salesItems, setSalesItems] = useState(data.salesItems || []);
const [saleDate, setSaleDate] = useState(
data.saleDate || new Date().toISOString().split('T')[0]
);
const [paymentMethod, setPaymentMethod] = useState(data.paymentMethod || 'cash');
const [notes, setNotes] = useState(data.notes || '');
const [products, setProducts] = useState<any[]>([]); const [products, setProducts] = useState<any[]>([]);
const [loadingProducts, setLoadingProducts] = useState(true); const [loadingProducts, setLoadingProducts] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -219,14 +196,15 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
}; };
const handleAddItem = () => { const handleAddItem = () => {
setSalesItems([ const newItems = [
...salesItems, ...(data.salesItems || []),
{ id: Date.now(), productId: '', product: '', quantity: 1, unitPrice: 0, subtotal: 0 }, { id: Date.now(), productId: '', product: '', quantity: 1, unitPrice: 0, subtotal: 0 },
]); ];
onDataChange?.({ ...data, salesItems: newItems });
}; };
const handleUpdateItem = (index: number, field: string, value: any) => { const handleUpdateItem = (index: number, field: string, value: any) => {
const updated = salesItems.map((item: any, i: number) => { const updated = (data.salesItems || []).map((item: any, i: number) => {
if (i === index) { if (i === index) {
const newItem = { ...item, [field]: value }; const newItem = { ...item, [field]: value };
@@ -247,28 +225,25 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
} }
return item; return item;
}); });
setSalesItems(updated); onDataChange?.({ ...data, salesItems: updated });
}; };
const handleRemoveItem = (index: number) => { const handleRemoveItem = (index: number) => {
setSalesItems(salesItems.filter((_: any, i: number) => i !== index)); const newItems = (data.salesItems || []).filter((_: any, i: number) => i !== index);
onDataChange?.({ ...data, salesItems: newItems });
}; };
const calculateTotal = () => { const calculateTotal = () => {
return salesItems.reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0); return (data.salesItems || []).reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0);
}; };
const handleSave = () => { // Auto-save totalAmount when items change
onDataChange({ useEffect(() => {
onDataChange?.({
...data, ...data,
salesItems,
saleDate,
paymentMethod,
notes,
totalAmount: calculateTotal(), totalAmount: calculateTotal(),
}); });
onNext(); }, [data.salesItems]);
};
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -290,8 +265,8 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
</label> </label>
<input <input
type="date" type="date"
value={saleDate} value={data.saleDate || new Date().toISOString().split('T')[0]}
onChange={(e) => setSaleDate(e.target.value)} onChange={(e) => onDataChange?.({ ...data, saleDate: 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)]" 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>
@@ -302,8 +277,8 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
Método de Pago * Método de Pago *
</label> </label>
<select <select
value={paymentMethod} value={data.paymentMethod || 'cash'}
onChange={(e) => setPaymentMethod(e.target.value)} onChange={(e) => onDataChange?.({ ...data, paymentMethod: 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)]" 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="cash">Efectivo</option> <option value="cash">Efectivo</option>
@@ -349,7 +324,7 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
<p>No hay productos terminados disponibles</p> <p>No hay productos terminados disponibles</p>
<p className="text-sm">Agrega productos al inventario primero</p> <p className="text-sm">Agrega productos al inventario primero</p>
</div> </div>
) : salesItems.length === 0 ? ( ) : (data.salesItems || []).length === 0 ? (
<div className="text-center py-8 border-2 border-dashed border-[var(--border-secondary)] rounded-lg text-[var(--text-tertiary)]"> <div className="text-center py-8 border-2 border-dashed border-[var(--border-secondary)] rounded-lg text-[var(--text-tertiary)]">
<Package className="w-8 h-8 mx-auto mb-2 opacity-50" /> <Package className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p>No hay productos agregados</p> <p>No hay productos agregados</p>
@@ -357,7 +332,7 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{salesItems.map((item: any, index: number) => ( {(data.salesItems || []).map((item: any, index: number) => (
<div <div
key={item.id} key={item.id}
className="p-3 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]/30" className="p-3 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]/30"
@@ -421,7 +396,7 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
)} )}
{/* Total */} {/* Total */}
{salesItems.length > 0 && ( {(data.salesItems || []).length > 0 && (
<div className="pt-3 border-t border-[var(--border-primary)] text-right"> <div className="pt-3 border-t border-[var(--border-primary)] text-right">
<span className="text-lg font-bold text-[var(--text-primary)]"> <span className="text-lg font-bold text-[var(--text-primary)]">
Total: {calculateTotal().toFixed(2)} Total: {calculateTotal().toFixed(2)}
@@ -436,24 +411,13 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
Notas (Opcional) Notas (Opcional)
</label> </label>
<textarea <textarea
value={notes} value={data.notes || ''}
onChange={(e) => setNotes(e.target.value)} onChange={(e) => onDataChange?.({ ...data, notes: e.target.value })}
placeholder="Información adicional sobre esta venta..." placeholder="Información adicional sobre esta venta..."
rows={3} 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)] text-sm" 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)] text-sm"
/> />
</div> </div>
{/* Save Button */}
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
<button
onClick={handleSave}
disabled={salesItems.length === 0}
className="px-6 py-3 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
>
Guardar y Continuar
</button>
</div>
</div> </div>
); );
}; };
@@ -462,9 +426,9 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
// STEP 2b: File Upload // STEP 2b: File Upload
// ======================================== // ========================================
const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, onNext }) => { const FileUploadStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNext }) => {
const data = dataRef?.current || {};
const { currentTenant } = useTenant(); const { currentTenant } = useTenant();
const [file, setFile] = useState<File | null>(data.uploadedFile || null);
const [validating, setValidating] = useState(false); const [validating, setValidating] = useState(false);
const [importing, setImporting] = useState(false); const [importing, setImporting] = useState(false);
const [validationResult, setValidationResult] = useState<any>(null); const [validationResult, setValidationResult] = useState<any>(null);
@@ -474,26 +438,26 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0]; const selectedFile = e.target.files?.[0];
if (selectedFile) { if (selectedFile) {
setFile(selectedFile); onDataChange?.({ ...data, uploadedFile: selectedFile });
setValidationResult(null); setValidationResult(null);
setError(null); setError(null);
} }
}; };
const handleRemoveFile = () => { const handleRemoveFile = () => {
setFile(null); onDataChange?.({ ...data, uploadedFile: null });
setValidationResult(null); setValidationResult(null);
setError(null); setError(null);
}; };
const handleValidate = async () => { const handleValidate = async () => {
if (!file || !currentTenant?.id) return; if (!data.uploadedFile || !currentTenant?.id) return;
setValidating(true); setValidating(true);
setError(null); setError(null);
try { try {
const result = await salesService.validateImportFile(currentTenant.id, file); const result = await salesService.validateImportFile(currentTenant.id, data.uploadedFile);
setValidationResult(result); setValidationResult(result);
} catch (err: any) { } catch (err: any) {
console.error('Error validating file:', err); console.error('Error validating file:', err);
@@ -504,15 +468,15 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
}; };
const handleImport = async () => { const handleImport = async () => {
if (!file || !currentTenant?.id) return; if (!data.uploadedFile || !currentTenant?.id) return;
setImporting(true); setImporting(true);
setError(null); setError(null);
try { try {
const result = await salesService.importSalesData(currentTenant.id, file, false); const result = await salesService.importSalesData(currentTenant.id, data.uploadedFile, false);
onDataChange({ ...data, uploadedFile: file, importResult: result }); onDataChange?.({ ...data, importResult: result });
onNext(); onNext?.();
} catch (err: any) { } catch (err: any) {
console.error('Error importing file:', err); console.error('Error importing file:', err);
setError(err.response?.data?.detail || 'Error al importar el archivo'); setError(err.response?.data?.detail || 'Error al importar el archivo');
@@ -583,7 +547,7 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
</div> </div>
{/* File Upload Area */} {/* File Upload Area */}
{!file ? ( {!data.uploadedFile ? (
<div className="border-2 border-dashed border-[var(--border-secondary)] rounded-xl p-8 text-center bg-[var(--bg-secondary)]/30"> <div className="border-2 border-dashed border-[var(--border-secondary)] rounded-xl p-8 text-center bg-[var(--bg-secondary)]/30">
<FileSpreadsheet className="w-16 h-16 mx-auto mb-4 text-[var(--color-primary)]/50" /> <FileSpreadsheet className="w-16 h-16 mx-auto mb-4 text-[var(--color-primary)]/50" />
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2"> <h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
@@ -613,9 +577,9 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<FileSpreadsheet className="w-8 h-8 text-[var(--color-primary)]" /> <FileSpreadsheet className="w-8 h-8 text-[var(--color-primary)]" />
<div> <div>
<p className="font-medium text-[var(--text-primary)]">{file.name}</p> <p className="font-medium text-[var(--text-primary)]">{data.uploadedFile.name}</p>
<p className="text-sm text-[var(--text-secondary)]"> <p className="text-sm text-[var(--text-secondary)]">
{(file.size / 1024).toFixed(2)} KB {(data.uploadedFile.size / 1024).toFixed(2)} KB
</p> </p>
</div> </div>
</div> </div>
@@ -701,53 +665,8 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
// STEP 3: Review & Confirm // STEP 3: Review & Confirm
// ======================================== // ========================================
const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => { const ReviewStep: React.FC<WizardStepProps> = ({ dataRef }) => {
const { currentTenant } = useTenant(); const data = dataRef?.current || {};
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleConfirm = async () => {
if (!currentTenant?.id) {
setError('No se pudo obtener información del tenant');
return;
}
setLoading(true);
setError(null);
try {
if (data.entryMethod === 'manual' && data.salesItems) {
// Create individual sales records for each item
for (const item of data.salesItems) {
const salesData = {
inventory_product_id: item.productId || null, // Include inventory product ID for stock tracking
product_name: item.product,
product_category: 'general', // Could be enhanced with category selection
quantity_sold: item.quantity,
unit_price: item.unitPrice,
total_amount: item.subtotal,
sale_date: data.saleDate,
sales_channel: 'retail',
source: 'manual',
payment_method: data.paymentMethod,
notes: data.notes,
};
await salesService.createSalesRecord(currentTenant.id, salesData);
}
}
showToast.success('Registro de ventas guardado exitosamente');
onComplete();
} catch (err: any) {
console.error('Error saving sales data:', err);
const errorMessage = err.response?.data?.detail || 'Error al guardar los datos de ventas';
setError(errorMessage);
showToast.error(errorMessage);
} finally {
setLoading(false);
}
};
const isManual = data.entryMethod === 'manual'; const isManual = data.entryMethod === 'manual';
const isUpload = data.entryMethod === 'upload'; const isUpload = data.entryMethod === 'upload';
@@ -768,13 +687,6 @@ const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
</p> </p>
</div> </div>
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm flex items-start gap-2">
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
<span>{error}</span>
</div>
)}
{isManual && data.salesItems && ( {isManual && data.salesItems && (
<div className="space-y-4"> <div className="space-y-4">
{/* Summary */} {/* Summary */}
@@ -796,10 +708,10 @@ const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
{/* Items */} {/* Items */}
<div> <div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-2"> <h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-2">
Productos ({data.salesItems.length}) Productos ({(data.salesItems || []).length})
</h4> </h4>
<div className="space-y-2"> <div className="space-y-2">
{data.salesItems.map((item: any) => ( {(data.salesItems || []).map((item: any) => (
<div <div
key={item.id} key={item.id}
className="p-3 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-primary)] flex justify-between items-center" className="p-3 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-primary)] flex justify-between items-center"
@@ -853,27 +765,6 @@ const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
</div> </div>
</div> </div>
)} )}
{/* Confirm Button */}
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-primary)]">
<button
onClick={handleConfirm}
disabled={loading || (isUpload && !data.importResult)}
className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors 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" />
Confirmar y Guardar
</>
)}
</button>
</div>
</div> </div>
); );
}; };
@@ -883,20 +774,19 @@ const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
// ======================================== // ========================================
export const SalesEntryWizardSteps = ( export const SalesEntryWizardSteps = (
data: Record<string, any>, dataRef: React.MutableRefObject<Record<string, any>>,
setData: (data: Record<string, any>) => void setData: (data: Record<string, any>) => void
): WizardStep[] => { ): WizardStep[] => {
const entryMethod = data.entryMethod; const entryMethod = dataRef.current.entryMethod;
// Dynamic steps based on entry method // New architecture: return direct component references instead of arrow functions
// dataRef and onDataChange are now passed through WizardModal props
const steps: WizardStep[] = [ const steps: WizardStep[] = [
{ {
id: 'entry-method', id: 'entry-method',
title: 'Método de Entrada', title: 'Método de Entrada',
description: 'Elige cómo registrar las ventas', description: 'Elige cómo registrar las ventas',
component: (props) => ( component: EntryMethodStep,
<EntryMethodStep {...props} data={data} onDataChange={setData} />
),
}, },
]; ];
@@ -905,14 +795,18 @@ export const SalesEntryWizardSteps = (
id: 'manual-entry', id: 'manual-entry',
title: 'Ingresar Datos', title: 'Ingresar Datos',
description: 'Registra los detalles de la venta', description: 'Registra los detalles de la venta',
component: (props) => <ManualEntryStep {...props} data={data} onDataChange={setData} />, component: ManualEntryStep,
validate: () => {
const data = dataRef.current;
return (data.salesItems || []).length > 0;
},
}); });
} else if (entryMethod === 'upload') { } else if (entryMethod === 'upload') {
steps.push({ steps.push({
id: 'file-upload', id: 'file-upload',
title: 'Cargar Archivo', title: 'Cargar Archivo',
description: 'Importa ventas desde archivo', description: 'Importa ventas desde archivo',
component: (props) => <FileUploadStep {...props} data={data} onDataChange={setData} />, component: FileUploadStep,
}); });
} }
@@ -920,7 +814,51 @@ export const SalesEntryWizardSteps = (
id: 'review', id: 'review',
title: 'Revisar', title: 'Revisar',
description: 'Confirma los datos antes de guardar', description: 'Confirma los datos antes de guardar',
component: (props) => <ReviewStep {...props} data={data} onDataChange={setData} />, component: ReviewStep,
validate: async () => {
const { useTenant } = await import('../../../../stores/tenant.store');
const { salesService } = await import('../../../../api/services/sales');
const { showToast } = await import('../../../../utils/toast');
const data = dataRef.current;
const { currentTenant } = useTenant.getState();
if (!currentTenant?.id) {
showToast.error('No se pudo obtener información del tenant');
return false;
}
try {
if (data.entryMethod === 'manual' && data.salesItems) {
// Create individual sales records for each item
for (const item of data.salesItems) {
const salesData = {
inventory_product_id: item.productId || null,
product_name: item.product,
product_category: 'general',
quantity_sold: item.quantity,
unit_price: item.unitPrice,
total_amount: item.subtotal,
sale_date: data.saleDate,
sales_channel: 'retail',
source: 'manual',
payment_method: data.paymentMethod,
notes: data.notes,
};
await salesService.createSalesRecord(currentTenant.id, salesData);
}
}
showToast.success('Registro de ventas guardado exitosamente');
return true;
} catch (err: any) {
console.error('Error saving sales data:', err);
const errorMessage = err.response?.data?.detail || 'Error al guardar los datos de ventas';
showToast.error(errorMessage);
return false;
}
},
}); });
return steps; return steps;

View File

@@ -7,62 +7,23 @@ import { showToast } from '../../../../utils/toast';
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection'; import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
import Tooltip from '../../../ui/Tooltip/Tooltip'; import Tooltip from '../../../ui/Tooltip/Tooltip';
interface WizardDataProps extends WizardStepProps { const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onComplete }) => {
data: Record<string, any>; // New architecture: access data from dataRef.current
onDataChange: (data: Record<string, any>) => void; const data = dataRef?.current || {};
}
const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
const { currentTenant } = useTenant(); const { currentTenant } = useTenant();
const [supplierData, setSupplierData] = useState({
// Required fields
name: data.name || '',
supplierType: data.supplierType || 'ingredients',
status: data.status || 'pending_approval',
paymentTerms: data.paymentTerms || 'net_30',
currency: data.currency || 'EUR',
standardLeadTime: data.standardLeadTime || 3,
// Basic optional fields
contactPerson: data.contactPerson || '',
email: data.email || '',
phone: data.phone || '',
// Advanced optional fields
supplierCode: data.supplierCode || '',
taxId: data.taxId || '',
registrationNumber: data.registrationNumber || '',
mobile: data.mobile || '',
website: data.website || '',
addressLine1: data.addressLine1 || '',
addressLine2: data.addressLine2 || '',
city: data.city || '',
stateProvince: data.stateProvince || '',
postalCode: data.postalCode || '',
country: data.country || '',
creditLimit: data.creditLimit || '',
minimumOrderAmount: data.minimumOrderAmount || '',
deliveryArea: data.deliveryArea || '',
isPreferredSupplier: data.isPreferredSupplier || false,
autoApproveEnabled: data.autoApproveEnabled || false,
notes: data.notes || '',
certifications: data.certifications || '',
specializations: data.specializations || '',
});
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { const handleFieldChange = (field: string, value: any) => {
if (!supplierData.supplierCode && supplierData.name) { onDataChange?.({ ...data, [field]: value });
const code = `SUP-${supplierData.name.substring(0, 3).toUpperCase()}-${Date.now().toString().slice(-4)}`; };
setSupplierData(prev => ({ ...prev, supplierCode: code }));
}
}, [supplierData.name]);
useEffect(() => { useEffect(() => {
onDataChange({ ...data, ...supplierData }); if (!data.supplierCode && data.name) {
}, [supplierData]); const code = `SUP-${data.name.substring(0, 3).toUpperCase()}-${Date.now().toString().slice(-4)}`;
onDataChange?.({ ...data, supplierCode: code });
}
}, [data.name]);
const handleCreateSupplier = async () => { const handleCreateSupplier = async () => {
if (!currentTenant?.id) { if (!currentTenant?.id) {
@@ -75,41 +36,41 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
try { try {
const payload = { const payload = {
name: supplierData.name, name: data.name,
supplier_type: supplierData.supplierType, supplier_type: data.supplierType,
status: supplierData.status, status: data.status,
payment_terms: supplierData.paymentTerms, payment_terms: data.paymentTerms,
currency: supplierData.currency, currency: data.currency,
standard_lead_time: supplierData.standardLeadTime, standard_lead_time: data.standardLeadTime,
supplier_code: supplierData.supplierCode || undefined, supplier_code: data.supplierCode || undefined,
tax_id: supplierData.taxId || undefined, tax_id: data.taxId || undefined,
registration_number: supplierData.registrationNumber || undefined, registration_number: data.registrationNumber || undefined,
contact_person: supplierData.contactPerson || undefined, contact_person: data.contactPerson || undefined,
email: supplierData.email || undefined, email: data.email || undefined,
phone: supplierData.phone || undefined, phone: data.phone || undefined,
mobile: supplierData.mobile || undefined, mobile: data.mobile || undefined,
website: supplierData.website || undefined, website: data.website || undefined,
address_line1: supplierData.addressLine1 || undefined, address_line1: data.addressLine1 || undefined,
address_line2: supplierData.addressLine2 || undefined, address_line2: data.addressLine2 || undefined,
city: supplierData.city || undefined, city: data.city || undefined,
state_province: supplierData.stateProvince || undefined, state_province: data.stateProvince || undefined,
postal_code: supplierData.postalCode || undefined, postal_code: data.postalCode || undefined,
country: supplierData.country || undefined, country: data.country || undefined,
credit_limit: supplierData.creditLimit ? parseFloat(supplierData.creditLimit) : undefined, credit_limit: data.creditLimit ? parseFloat(data.creditLimit) : undefined,
minimum_order_amount: supplierData.minimumOrderAmount ? parseFloat(supplierData.minimumOrderAmount) : undefined, minimum_order_amount: data.minimumOrderAmount ? parseFloat(data.minimumOrderAmount) : undefined,
delivery_area: supplierData.deliveryArea || undefined, delivery_area: data.deliveryArea || undefined,
is_preferred_supplier: supplierData.isPreferredSupplier, is_preferred_supplier: data.isPreferredSupplier,
auto_approve_enabled: supplierData.autoApproveEnabled, auto_approve_enabled: data.autoApproveEnabled,
notes: supplierData.notes || undefined, notes: data.notes || undefined,
certifications: supplierData.certifications ? JSON.parse(`{"items": ${JSON.stringify(supplierData.certifications.split(',').map(c => c.trim()))}}`) : undefined, certifications: data.certifications ? JSON.parse(`{"items": ${JSON.stringify(data.certifications.split(',').map(c => c.trim()))}}`) : undefined,
specializations: supplierData.specializations ? JSON.parse(`{"items": ${JSON.stringify(supplierData.specializations.split(',').map(s => s.trim()))}}`) : undefined, specializations: data.specializations ? JSON.parse(`{"items": ${JSON.stringify(data.specializations.split(',').map(s => s.trim()))}}`) : undefined,
created_by: currentTenant.id, created_by: currentTenant.id,
updated_by: currentTenant.id, updated_by: currentTenant.id,
}; };
await suppliersService.createSupplier(currentTenant.id, payload); await suppliersService.createSupplier(currentTenant.id, payload);
showToast.success('Supplier created successfully'); showToast.success('Supplier created successfully');
onComplete(); // Let the wizard handle completion via the Next/Complete button
} catch (err: any) { } catch (err: any) {
console.error('Error creating supplier:', err); console.error('Error creating supplier:', err);
const errorMessage = err.response?.data?.detail || 'Error creating supplier'; const errorMessage = err.response?.data?.detail || 'Error creating supplier';
@@ -143,8 +104,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={supplierData.name} value={data.name}
onChange={(e) => setSupplierData({ ...supplierData, name: e.target.value })} onChange={(e) => handleFieldChange('name', e.target.value)}
placeholder="e.g., Premium Flour Suppliers Ltd." placeholder="e.g., Premium Flour Suppliers Ltd."
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)]"
/> />
@@ -158,8 +119,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</Tooltip> </Tooltip>
</label> </label>
<select <select
value={supplierData.supplierType} value={data.supplierType}
onChange={(e) => setSupplierData({ ...supplierData, supplierType: e.target.value })} onChange={(e) => handleFieldChange('supplierType', 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)]" 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="ingredients">Ingredients</option> <option value="ingredients">Ingredients</option>
@@ -176,8 +137,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
Status * Status *
</label> </label>
<select <select
value={supplierData.status} value={data.status}
onChange={(e) => setSupplierData({ ...supplierData, status: e.target.value })} onChange={(e) => handleFieldChange('status', 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)]" 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="active">Active</option> <option value="active">Active</option>
@@ -193,8 +154,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
Payment Terms * Payment Terms *
</label> </label>
<select <select
value={supplierData.paymentTerms} value={data.paymentTerms}
onChange={(e) => setSupplierData({ ...supplierData, paymentTerms: e.target.value })} onChange={(e) => handleFieldChange('paymentTerms', 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)]" 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="cod">COD (Cash on Delivery)</option> <option value="cod">COD (Cash on Delivery)</option>
@@ -213,8 +174,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={supplierData.currency} value={data.currency}
onChange={(e) => setSupplierData({ ...supplierData, currency: e.target.value })} onChange={(e) => handleFieldChange('currency', e.target.value)}
placeholder="EUR" placeholder="EUR"
maxLength={3} maxLength={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)]" 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)]"
@@ -230,8 +191,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="number" type="number"
value={supplierData.standardLeadTime} value={data.standardLeadTime}
onChange={(e) => setSupplierData({ ...supplierData, standardLeadTime: parseInt(e.target.value) || 0 })} onChange={(e) => handleFieldChange('standardLeadTime', parseInt(e.target.value) || 0)}
placeholder="3" placeholder="3"
min="0" min="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)]" 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)]"
@@ -246,8 +207,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={supplierData.contactPerson} value={data.contactPerson}
onChange={(e) => setSupplierData({ ...supplierData, contactPerson: e.target.value })} onChange={(e) => handleFieldChange('contactPerson', e.target.value)}
placeholder="John Doe" placeholder="John Doe"
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)]"
/> />
@@ -259,8 +220,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="email" type="email"
value={supplierData.email} value={data.email}
onChange={(e) => setSupplierData({ ...supplierData, email: e.target.value })} onChange={(e) => handleFieldChange('email', e.target.value)}
placeholder="contact@supplier.com" placeholder="contact@supplier.com"
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)]"
/> />
@@ -272,8 +233,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="tel" type="tel"
value={supplierData.phone} value={data.phone}
onChange={(e) => setSupplierData({ ...supplierData, phone: e.target.value })} onChange={(e) => handleFieldChange('phone', e.target.value)}
placeholder="+1 234 567 8900" placeholder="+1 234 567 8900"
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)]"
/> />
@@ -294,8 +255,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={supplierData.supplierCode} value={data.supplierCode}
onChange={(e) => setSupplierData({ ...supplierData, supplierCode: e.target.value })} onChange={(e) => handleFieldChange('supplierCode', e.target.value)}
placeholder="SUP-001" placeholder="SUP-001"
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)]"
/> />
@@ -307,8 +268,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="tel" type="tel"
value={supplierData.mobile} value={data.mobile}
onChange={(e) => setSupplierData({ ...supplierData, mobile: e.target.value })} onChange={(e) => handleFieldChange('mobile', e.target.value)}
placeholder="+1 234 567 8900" placeholder="+1 234 567 8900"
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)]"
/> />
@@ -320,8 +281,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={supplierData.taxId} value={data.taxId}
onChange={(e) => setSupplierData({ ...supplierData, taxId: e.target.value })} onChange={(e) => handleFieldChange('taxId', e.target.value)}
placeholder="VAT/Tax ID" placeholder="VAT/Tax ID"
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)]"
/> />
@@ -333,8 +294,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={supplierData.registrationNumber} value={data.registrationNumber}
onChange={(e) => setSupplierData({ ...supplierData, registrationNumber: e.target.value })} onChange={(e) => handleFieldChange('registrationNumber', e.target.value)}
placeholder="Business registration number" placeholder="Business registration number"
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)]"
/> />
@@ -346,8 +307,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="url" type="url"
value={supplierData.website} value={data.website}
onChange={(e) => setSupplierData({ ...supplierData, website: e.target.value })} onChange={(e) => handleFieldChange('website', e.target.value)}
placeholder="https://www.supplier.com" placeholder="https://www.supplier.com"
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)]"
/> />
@@ -359,8 +320,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={supplierData.addressLine1} value={data.addressLine1}
onChange={(e) => setSupplierData({ ...supplierData, addressLine1: e.target.value })} onChange={(e) => handleFieldChange('addressLine1', e.target.value)}
placeholder="Street address" placeholder="Street address"
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)]"
/> />
@@ -372,8 +333,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={supplierData.addressLine2} value={data.addressLine2}
onChange={(e) => setSupplierData({ ...supplierData, addressLine2: e.target.value })} onChange={(e) => handleFieldChange('addressLine2', e.target.value)}
placeholder="Suite, building, etc." placeholder="Suite, building, etc."
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)]"
/> />
@@ -385,8 +346,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={supplierData.city} value={data.city}
onChange={(e) => setSupplierData({ ...supplierData, city: e.target.value })} onChange={(e) => handleFieldChange('city', e.target.value)}
placeholder="City" placeholder="City"
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)]"
/> />
@@ -398,8 +359,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={supplierData.stateProvince} value={data.stateProvince}
onChange={(e) => setSupplierData({ ...supplierData, stateProvince: e.target.value })} onChange={(e) => handleFieldChange('stateProvince', e.target.value)}
placeholder="State" placeholder="State"
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)]"
/> />
@@ -411,8 +372,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={supplierData.postalCode} value={data.postalCode}
onChange={(e) => setSupplierData({ ...supplierData, postalCode: e.target.value })} onChange={(e) => handleFieldChange('postalCode', e.target.value)}
placeholder="12345" placeholder="12345"
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)]"
/> />
@@ -424,8 +385,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={supplierData.country} value={data.country}
onChange={(e) => setSupplierData({ ...supplierData, country: e.target.value })} onChange={(e) => handleFieldChange('country', e.target.value)}
placeholder="Country" placeholder="Country"
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)]"
/> />
@@ -437,8 +398,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="number" type="number"
value={supplierData.creditLimit} value={data.creditLimit}
onChange={(e) => setSupplierData({ ...supplierData, creditLimit: e.target.value })} onChange={(e) => handleFieldChange('creditLimit', e.target.value)}
placeholder="10000.00" placeholder="10000.00"
min="0" min="0"
step="0.01" step="0.01"
@@ -452,8 +413,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="number" type="number"
value={supplierData.minimumOrderAmount} value={data.minimumOrderAmount}
onChange={(e) => setSupplierData({ ...supplierData, minimumOrderAmount: e.target.value })} onChange={(e) => handleFieldChange('minimumOrderAmount', e.target.value)}
placeholder="100.00" placeholder="100.00"
min="0" min="0"
step="0.01" step="0.01"
@@ -467,8 +428,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={supplierData.deliveryArea} value={data.deliveryArea}
onChange={(e) => setSupplierData({ ...supplierData, deliveryArea: e.target.value })} onChange={(e) => handleFieldChange('deliveryArea', e.target.value)}
placeholder="e.g., New York Metro Area" placeholder="e.g., New York Metro Area"
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)]"
/> />
@@ -480,8 +441,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
<input <input
type="checkbox" type="checkbox"
id="isPreferredSupplier" id="isPreferredSupplier"
checked={supplierData.isPreferredSupplier} checked={data.isPreferredSupplier}
onChange={(e) => setSupplierData({ ...supplierData, isPreferredSupplier: e.target.checked })} onChange={(e) => handleFieldChange('isPreferredSupplier', e.target.checked)}
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]" className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
/> />
<label htmlFor="isPreferredSupplier" className="text-sm font-medium text-[var(--text-secondary)]"> <label htmlFor="isPreferredSupplier" className="text-sm font-medium text-[var(--text-secondary)]">
@@ -493,8 +454,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
<input <input
type="checkbox" type="checkbox"
id="autoApproveEnabled" id="autoApproveEnabled"
checked={supplierData.autoApproveEnabled} checked={data.autoApproveEnabled}
onChange={(e) => setSupplierData({ ...supplierData, autoApproveEnabled: e.target.checked })} onChange={(e) => handleFieldChange('autoApproveEnabled', e.target.checked)}
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]" className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
/> />
<label htmlFor="autoApproveEnabled" className="text-sm font-medium text-[var(--text-secondary)]"> <label htmlFor="autoApproveEnabled" className="text-sm font-medium text-[var(--text-secondary)]">
@@ -509,8 +470,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={supplierData.certifications} value={data.certifications}
onChange={(e) => setSupplierData({ ...supplierData, certifications: e.target.value })} onChange={(e) => handleFieldChange('certifications', e.target.value)}
placeholder="e.g., ISO 9001, HACCP, Organic (comma-separated)" placeholder="e.g., ISO 9001, HACCP, Organic (comma-separated)"
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)]"
/> />
@@ -522,8 +483,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</label> </label>
<input <input
type="text" type="text"
value={supplierData.specializations} value={data.specializations}
onChange={(e) => setSupplierData({ ...supplierData, specializations: e.target.value })} onChange={(e) => handleFieldChange('specializations', e.target.value)}
placeholder="e.g., Organic flours, Gluten-free products (comma-separated)" placeholder="e.g., Organic flours, Gluten-free products (comma-separated)"
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)]"
/> />
@@ -534,8 +495,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
Notes Notes
</label> </label>
<textarea <textarea
value={supplierData.notes} value={data.notes}
onChange={(e) => setSupplierData({ ...supplierData, notes: e.target.value })} onChange={(e) => handleFieldChange('notes', e.target.value)}
placeholder="Additional notes about this supplier..." placeholder="Additional notes about this supplier..."
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)]"
rows={3} rows={3}
@@ -544,48 +505,22 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
</div> </div>
</AdvancedOptionsSection> </AdvancedOptionsSection>
<div className="flex justify-center pt-4 border-t border-[var(--border-primary)]">
<button
type="button"
onClick={handleCreateSupplier}
disabled={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" />
Creating supplier...
</>
) : (
<>
<CheckCircle2 className="w-5 h-5" />
Create Supplier
</>
)}
</button>
</div>
</div> </div>
); );
}; };
export const SupplierWizardSteps = ( export const SupplierWizardSteps = (
data: Record<string, any>, dataRef: React.MutableRefObject<Record<string, any>>,
setData: (data: Record<string, any>) => void setData: (data: Record<string, any>) => void
): WizardStep[] => [ ): WizardStep[] => {
{ // New architecture: return direct component reference instead of arrow function
id: 'supplier-details', // dataRef and onDataChange are now passed through WizardModal props
title: 'Supplier Details', return [
description: 'Essential supplier information', {
component: (props) => <SupplierDetailsStep {...props} data={data} onDataChange={setData} />, id: 'supplier-details',
validate: () => { title: 'Supplier Details',
return !!( description: 'Essential supplier information',
data.name && component: SupplierDetailsStep,
data.supplierType &&
data.status &&
data.paymentTerms &&
data.currency &&
data.standardLeadTime >= 0
);
}, },
}, ];
]; };

View File

@@ -1,22 +1,12 @@
import React, { useState } from 'react'; import React from 'react';
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal'; import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import { UserPlus, Shield, CheckCircle2, Mail, Phone, Loader2 } from 'lucide-react'; import { UserPlus, Shield, Mail, Phone } from 'lucide-react';
import { useTenant } from '../../../../stores/tenant.store';
import { authService } from '../../../../api/services/auth';
interface WizardDataProps extends WizardStepProps { const MemberDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
data: Record<string, any>; const data = dataRef?.current || {};
onDataChange: (data: Record<string, any>) => void; const handleFieldChange = (field: string, value: any) => {
} onDataChange?.({ ...data, [field]: value });
};
const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNext }) => {
const [memberData, setMemberData] = useState({
fullName: data.fullName || '',
email: data.email || '',
phone: data.phone || '',
position: data.position || 'baker',
employmentType: data.employmentType || 'full-time',
});
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -29,8 +19,8 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Nombre Completo *</label> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Nombre Completo *</label>
<input <input
type="text" type="text"
value={memberData.fullName} value={data.fullName || ''}
onChange={(e) => setMemberData({ ...memberData, fullName: e.target.value })} onChange={(e) => handleFieldChange('fullName', e.target.value)}
placeholder="Ej: Juan García" placeholder="Ej: Juan García"
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)]"
/> />
@@ -42,8 +32,8 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
</label> </label>
<input <input
type="email" type="email"
value={memberData.email} value={data.email || ''}
onChange={(e) => setMemberData({ ...memberData, email: e.target.value })} onChange={(e) => handleFieldChange('email', e.target.value)}
placeholder="juan@panaderia.com" placeholder="juan@panaderia.com"
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)]"
/> />
@@ -55,8 +45,8 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
</label> </label>
<input <input
type="tel" type="tel"
value={memberData.phone} value={data.phone || ''}
onChange={(e) => setMemberData({ ...memberData, phone: e.target.value })} onChange={(e) => handleFieldChange('phone', e.target.value)}
placeholder="+34 123 456 789" placeholder="+34 123 456 789"
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)]"
/> />
@@ -64,8 +54,8 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
<div> <div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Posición *</label> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Posición *</label>
<select <select
value={memberData.position} value={data.position || 'baker'}
onChange={(e) => setMemberData({ ...memberData, position: e.target.value })} onChange={(e) => handleFieldChange('position', 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)]" 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="baker">Panadero</option> <option value="baker">Panadero</option>
@@ -78,8 +68,8 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
<div> <div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Tipo de Empleo</label> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Tipo de Empleo</label>
<select <select
value={memberData.employmentType} value={data.employmentType || 'full-time'}
onChange={(e) => setMemberData({ ...memberData, employmentType: e.target.value })} onChange={(e) => handleFieldChange('employmentType', 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)]" 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="full-time">Tiempo Completo</option> <option value="full-time">Tiempo Completo</option>
@@ -88,72 +78,15 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
</select> </select>
</div> </div>
</div> </div>
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
<button
onClick={() => {
onDataChange({ ...data, ...memberData });
onNext();
}}
disabled={!memberData.fullName || !memberData.email}
className="px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 disabled:opacity-50 disabled:cursor-not-allowed"
>
Continuar
</button>
</div>
</div> </div>
); );
}; };
const PermissionsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => { const PermissionsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
const { currentTenant } = useTenant(); const data = dataRef?.current || {};
const [permissions, setPermissions] = useState({
role: data.role || 'staff',
canManageInventory: data.canManageInventory || false,
canViewRecipes: data.canViewRecipes || true,
canCreateOrders: data.canCreateOrders || false,
canViewFinancial: data.canViewFinancial || false,
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSave = async () => { const handleFieldChange = (field: string, value: any) => {
if (!currentTenant?.id) { onDataChange?.({ ...data, [field]: value });
setError('No se pudo obtener información del tenant');
return;
}
setLoading(true);
setError(null);
try {
// Generate a temporary password (in production, this should be sent via email)
const tempPassword = `Temp${Math.random().toString(36).substring(2, 10)}!`;
// Register the new team member
const registrationData = {
email: data.email,
password: tempPassword,
full_name: data.fullName,
phone_number: data.phone || undefined,
tenant_id: currentTenant.id,
role: permissions.role,
};
await authService.register(registrationData);
// In a real implementation, you would:
// 1. Send email with temporary password
// 2. Store permissions in a separate permissions table
// 3. Link user to tenant with specific role
onDataChange({ ...data, ...permissions, tempPassword });
onComplete();
} catch (err: any) {
console.error('Error creating team member:', err);
setError(err.response?.data?.detail || 'Error al crear el miembro del equipo');
} finally {
setLoading(false);
}
}; };
return ( return (
@@ -164,17 +97,11 @@ const PermissionsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComp
<p className="text-sm text-[var(--text-secondary)]">{data.fullName}</p> <p className="text-sm text-[var(--text-secondary)]">{data.fullName}</p>
</div> </div>
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{error}
</div>
)}
<div> <div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-3">Rol del Sistema</label> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-3">Rol del Sistema</label>
<select <select
value={permissions.role} value={data.role}
onChange={(e) => setPermissions({ ...permissions, role: e.target.value })} onChange={(e) => handleFieldChange('role', 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)]" 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="admin">Administrador</option> <option value="admin">Administrador</option>
@@ -198,8 +125,8 @@ const PermissionsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComp
> >
<input <input
type="checkbox" type="checkbox"
checked={permissions[key as keyof typeof permissions] as boolean} checked={data[key] ?? false}
onChange={(e) => setPermissions({ ...permissions, [key]: e.target.checked })} onChange={(e) => handleFieldChange(key, e.target.checked)}
className="w-4 h-4 text-[var(--color-primary)] rounded focus:ring-[var(--color-primary)]" className="w-4 h-4 text-[var(--color-primary)] rounded focus:ring-[var(--color-primary)]"
/> />
<span className="text-sm text-[var(--text-primary)]">{label}</span> <span className="text-sm text-[var(--text-primary)]">{label}</span>
@@ -207,30 +134,68 @@ const PermissionsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComp
))} ))}
</div> </div>
</div> </div>
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-primary)]">
<button
onClick={handleSave}
disabled={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" />
Agregar Miembro
</>
)}
</button>
</div>
</div> </div>
); );
}; };
export const TeamMemberWizardSteps = (data: Record<string, any>, setData: (data: Record<string, any>) => void): WizardStep[] => [ export const TeamMemberWizardSteps = (dataRef: React.MutableRefObject<Record<string, any>>, setData: (data: Record<string, any>) => void): WizardStep[] => {
{ id: 'member-details', title: 'Datos Personales', description: 'Nombre, contacto, posición', component: (props) => <MemberDetailsStep {...props} data={data} onDataChange={setData} /> }, // New architecture: return direct component references instead of arrow functions
{ id: 'member-permissions', title: 'Rol y Permisos', description: 'Accesos al sistema', component: (props) => <PermissionsStep {...props} data={data} onDataChange={setData} /> }, // dataRef and onDataChange are now passed through WizardModal props
]; return [
{
id: 'member-details',
title: 'Datos Personales',
description: 'Nombre, contacto, posición',
component: MemberDetailsStep,
},
{
id: 'member-permissions',
title: 'Rol y Permisos',
description: 'Accesos al sistema',
component: PermissionsStep,
validate: async () => {
const { useTenant } = await import('../../../../stores/tenant.store');
const { authService } = await import('../../../../api/services/auth');
const { showToast } = await import('../../../../utils/toast');
const data = dataRef.current;
const { currentTenant } = useTenant.getState();
if (!currentTenant?.id) {
showToast.error('No se pudo obtener información del tenant');
return false;
}
try {
// Generate a temporary password (in production, this should be sent via email)
const tempPassword = `Temp${Math.random().toString(36).substring(2, 10)}!`;
// Register the new team member
const registrationData = {
email: data.email,
password: tempPassword,
full_name: data.fullName,
phone_number: data.phone || undefined,
tenant_id: currentTenant.id,
role: data.role,
};
await authService.register(registrationData);
// In a real implementation, you would:
// 1. Send email with temporary password
// 2. Store permissions in a separate permissions table
// 3. Link user to tenant with specific role
showToast.success('Miembro del equipo agregado exitosamente');
return true;
} catch (err: any) {
console.error('Error creating team member:', err);
const errorMessage = err.response?.data?.detail || 'Error al crear el miembro del equipo';
showToast.error(errorMessage);
return false;
}
},
},
];
};

View File

@@ -1,102 +0,0 @@
import React from 'react';
import { Clock, X, FileText, Trash2 } from 'lucide-react';
import { formatTimeAgo } from '../../../hooks/useWizardDraft';
interface DraftRecoveryPromptProps {
isOpen: boolean;
lastSaved: Date;
onRestore: () => void;
onDiscard: () => void;
onClose: () => void;
wizardName: string;
}
export const DraftRecoveryPrompt: React.FC<DraftRecoveryPromptProps> = ({
isOpen,
lastSaved,
onRestore,
onDiscard,
onClose,
wizardName
}) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-[60] flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal */}
<div className="relative w-full max-w-md bg-[var(--bg-primary)] rounded-xl shadow-2xl overflow-hidden">
{/* Header */}
<div className="px-6 py-4 border-b border-[var(--border-secondary)] bg-gradient-to-r from-[var(--color-warning)]/10 to-[var(--color-warning)]/5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-[var(--color-warning)]/20 flex items-center justify-center">
<FileText className="w-5 h-5 text-[var(--color-warning)]" />
</div>
<div>
<h2 className="text-lg font-semibold text-[var(--text-primary)]">
Borrador Detectado
</h2>
<p className="text-sm text-[var(--text-secondary)]">
{wizardName}
</p>
</div>
</div>
<button
onClick={onClose}
className="p-1.5 hover:bg-[var(--bg-secondary)] rounded-lg transition-colors"
>
<X className="w-4 h-4 text-[var(--text-secondary)]" />
</button>
</div>
</div>
{/* Content */}
<div className="p-6 space-y-4">
{/* Info Card */}
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-secondary)]">
<div className="flex items-start gap-3">
<Clock className="w-5 h-5 text-[var(--text-tertiary)] mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="text-sm text-[var(--text-primary)] font-medium mb-1">
Progreso guardado automáticamente
</p>
<p className="text-sm text-[var(--text-secondary)]">
Guardado {formatTimeAgo(lastSaved)}
</p>
</div>
</div>
</div>
{/* Description */}
<p className="text-sm text-[var(--text-secondary)]">
Encontramos un borrador de este formulario. ¿Deseas continuar desde donde lo dejaste o empezar de nuevo?
</p>
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-[var(--border-secondary)] bg-[var(--bg-secondary)] flex items-center gap-3">
<button
onClick={onDiscard}
className="flex-1 px-4 py-2.5 bg-[var(--bg-primary)] border border-[var(--border-secondary)] text-[var(--text-primary)] rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors flex items-center justify-center gap-2 text-sm font-medium"
>
<Trash2 className="w-4 h-4" />
Descartar y Empezar de Nuevo
</button>
<button
onClick={onRestore}
className="flex-1 px-4 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] transition-colors flex items-center justify-center gap-2 text-sm font-medium"
>
<FileText className="w-4 h-4" />
Restaurar Borrador
</button>
</div>
</div>
</div>
);
};

View File

@@ -1 +0,0 @@
export { DraftRecoveryPrompt } from './DraftRecoveryPrompt';

View File

@@ -19,6 +19,9 @@ export interface WizardStepProps {
currentStepIndex: number; currentStepIndex: number;
totalSteps: number; totalSteps: number;
goToStep: (index: number) => void; goToStep: (index: number) => void;
// New architecture: dataRef and onDataChange passed from UnifiedAddWizard
dataRef?: React.MutableRefObject<any>;
onDataChange?: (data: any) => void;
} }
interface WizardModalProps { interface WizardModalProps {
@@ -29,6 +32,9 @@ interface WizardModalProps {
steps: WizardStep[]; steps: WizardStep[];
icon?: React.ReactNode; icon?: React.ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl'; size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
// New architecture: optional dataRef and onDataChange for wizards that use them
dataRef?: React.MutableRefObject<any>;
onDataChange?: (data: any) => void;
} }
export const WizardModal: React.FC<WizardModalProps> = ({ export const WizardModal: React.FC<WizardModalProps> = ({
@@ -38,7 +44,9 @@ export const WizardModal: React.FC<WizardModalProps> = ({
title, title,
steps, steps,
icon, icon,
size = 'xl' size = 'xl',
dataRef,
onDataChange
}) => { }) => {
const [currentStepIndex, setCurrentStepIndex] = useState(0); const [currentStepIndex, setCurrentStepIndex] = useState(0);
const [isValidating, setIsValidating] = useState(false); const [isValidating, setIsValidating] = useState(false);
@@ -55,12 +63,36 @@ export const WizardModal: React.FC<WizardModalProps> = ({
'2xl': 'max-w-7xl' '2xl': 'max-w-7xl'
}; };
const handleClose = useCallback(() => {
setCurrentStepIndex(0);
onClose();
}, [onClose]);
const handleComplete = useCallback(() => {
onComplete();
handleClose();
}, [onComplete, handleClose]);
const goToStep = useCallback((index: number) => {
if (index >= 0 && index < steps.length) {
setCurrentStepIndex(index);
}
}, [steps.length]);
const handleBack = useCallback(() => {
setCurrentStepIndex(prev => Math.max(prev - 1, 0));
}, []);
const handleNext = useCallback(async () => { const handleNext = useCallback(async () => {
// Access current step inside callback to avoid re-creating function on every render
const step = steps[currentStepIndex];
const lastStep = currentStepIndex === steps.length - 1;
// Validate current step if validator exists // Validate current step if validator exists
if (currentStep.validate) { if (step.validate) {
setIsValidating(true); setIsValidating(true);
try { try {
const isValid = await currentStep.validate(); const isValid = await step.validate();
if (!isValid) { if (!isValid) {
setIsValidating(false); setIsValidating(false);
return; return;
@@ -73,32 +105,12 @@ export const WizardModal: React.FC<WizardModalProps> = ({
setIsValidating(false); setIsValidating(false);
} }
if (isLastStep) { if (lastStep) {
handleComplete(); handleComplete();
} else { } else {
setCurrentStepIndex(prev => Math.min(prev + 1, steps.length - 1)); setCurrentStepIndex(prev => Math.min(prev + 1, steps.length - 1));
} }
}, [currentStep, isLastStep, steps.length]); }, [steps, currentStepIndex, handleComplete]);
const handleBack = useCallback(() => {
setCurrentStepIndex(prev => Math.max(prev - 1, 0));
}, []);
const handleComplete = useCallback(() => {
onComplete();
handleClose();
}, [onComplete]);
const handleClose = useCallback(() => {
setCurrentStepIndex(0);
onClose();
}, [onClose]);
const goToStep = useCallback((index: number) => {
if (index >= 0 && index < steps.length) {
setCurrentStepIndex(index);
}
}, [steps.length]);
if (!isOpen) return null; if (!isOpen) return null;
@@ -184,6 +196,8 @@ export const WizardModal: React.FC<WizardModalProps> = ({
currentStepIndex={currentStepIndex} currentStepIndex={currentStepIndex}
totalSteps={steps.length} totalSteps={steps.length}
goToStep={goToStep} goToStep={goToStep}
dataRef={dataRef}
onDataChange={onDataChange}
/> />
</div> </div>

View File

@@ -1,113 +0,0 @@
import { useState, useEffect, useCallback } from 'react';
interface WizardDraft<T> {
data: T;
timestamp: number;
currentStep: number;
}
interface UseWizardDraftOptions {
key: string; // Unique key for this wizard type
ttl?: number; // Time to live in milliseconds (default: 7 days)
autoSaveInterval?: number; // Auto-save interval in milliseconds (default: 30 seconds)
}
export function useWizardDraft<T>(options: UseWizardDraftOptions) {
const { key, ttl = 7 * 24 * 60 * 60 * 1000, autoSaveInterval = 30000 } = options;
const storageKey = `wizard_draft_${key}`;
const [draftData, setDraftData] = useState<T | null>(null);
const [draftStep, setDraftStep] = useState<number>(0);
const [hasDraft, setHasDraft] = useState(false);
const [lastSaved, setLastSaved] = useState<Date | null>(null);
// Load draft on mount
useEffect(() => {
try {
const stored = localStorage.getItem(storageKey);
if (stored) {
const draft: WizardDraft<T> = JSON.parse(stored);
// Check if draft is still valid (not expired)
const now = Date.now();
if (now - draft.timestamp < ttl) {
setDraftData(draft.data);
setDraftStep(draft.currentStep);
setHasDraft(true);
setLastSaved(new Date(draft.timestamp));
} else {
// Draft expired, clear it
localStorage.removeItem(storageKey);
}
}
} catch (error) {
console.error('Error loading wizard draft:', error);
localStorage.removeItem(storageKey);
}
}, [storageKey, ttl]);
// Save draft
const saveDraft = useCallback(
(data: T, currentStep: number) => {
try {
const draft: WizardDraft<T> = {
data,
timestamp: Date.now(),
currentStep
};
localStorage.setItem(storageKey, JSON.stringify(draft));
setLastSaved(new Date());
setHasDraft(true);
} catch (error) {
console.error('Error saving wizard draft:', error);
}
},
[storageKey]
);
// Clear draft
const clearDraft = useCallback(() => {
try {
localStorage.removeItem(storageKey);
setDraftData(null);
setDraftStep(0);
setHasDraft(false);
setLastSaved(null);
} catch (error) {
console.error('Error clearing wizard draft:', error);
}
}, [storageKey]);
// Load draft data
const loadDraft = useCallback(() => {
return { data: draftData, step: draftStep };
}, [draftData, draftStep]);
// Dismiss draft (clear without loading)
const dismissDraft = useCallback(() => {
clearDraft();
}, [clearDraft]);
return {
// State
hasDraft,
lastSaved,
// Actions
saveDraft,
loadDraft,
clearDraft,
dismissDraft
};
}
// Format time ago
export function formatTimeAgo(date: Date): string {
const now = new Date();
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
if (seconds < 60) return 'hace un momento';
if (seconds < 3600) return `hace ${Math.floor(seconds / 60)} minutos`;
if (seconds < 86400) return `hace ${Math.floor(seconds / 3600)} horas`;
return `hace ${Math.floor(seconds / 86400)} días`;
}

View File

@@ -162,6 +162,20 @@ export function NewDashboardPage() {
handleRefreshAll(); handleRefreshAll();
}; };
// Keyboard shortcut for Quick Add (Cmd/Ctrl + K)
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Check for Cmd+K (Mac) or Ctrl+K (Windows/Linux)
if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
event.preventDefault();
setIsAddWizardOpen(true);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
// Demo tour auto-start logic // Demo tour auto-start logic
useEffect(() => { useEffect(() => {
console.log('[Dashboard] Demo mode:', isDemoMode); console.log('[Dashboard] Demo mode:', isDemoMode);
@@ -221,18 +235,29 @@ export function NewDashboardPage() {
<span className="hidden sm:inline">{t('common:actions.refresh')}</span> <span className="hidden sm:inline">{t('common:actions.refresh')}</span>
</button> </button>
{/* Unified Add Button */} {/* Unified Add Button with Keyboard Shortcut */}
<button <button
onClick={() => setIsAddWizardOpen(true)} onClick={() => setIsAddWizardOpen(true)}
className="flex items-center gap-2 px-6 py-2.5 rounded-lg font-semibold transition-all duration-200 shadow-lg hover:shadow-xl hover:-translate-y-0.5 active:translate-y-0" className="group relative flex items-center gap-2 px-6 py-2.5 rounded-lg font-semibold transition-all duration-200 shadow-lg hover:shadow-xl hover:-translate-y-0.5 active:translate-y-0"
style={{ style={{
background: 'linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%)', background: 'linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%)',
color: 'white' color: 'white'
}} }}
title={`Quick Add (${navigator.platform.includes('Mac') ? 'Cmd' : 'Ctrl'}+K)`}
> >
<Plus className="w-5 h-5" /> <Plus className="w-5 h-5" />
<span className="hidden sm:inline">{t('common:actions.add')}</span> <span className="hidden sm:inline">{t('common:actions.add')}</span>
<Sparkles className="w-4 h-4 opacity-80" /> <Sparkles className="w-4 h-4 opacity-80" />
{/* Keyboard shortcut badge - shown on hover */}
<span className="hidden lg:flex absolute -bottom-8 left-1/2 -translate-x-1/2 items-center gap-1 px-2 py-1 rounded text-xs font-mono opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap pointer-events-none" style={{ backgroundColor: 'var(--bg-primary)', color: 'var(--text-secondary)', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
<kbd className="px-1.5 py-0.5 rounded text-xs font-semibold" style={{ backgroundColor: 'var(--bg-tertiary)', border: '1px solid var(--border-secondary)' }}>
{navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'}
</kbd>
<span>+</span>
<kbd className="px-1.5 py-0.5 rounded text-xs font-semibold" style={{ backgroundColor: 'var(--bg-tertiary)', border: '1px solid var(--border-secondary)' }}>
K
</kbd>
</span>
</button> </button>
</div> </div>
</div> </div>