Improve the UI add button
This commit is contained in:
@@ -2,14 +2,14 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Package,
|
||||
Building2,
|
||||
Building,
|
||||
ChefHat,
|
||||
Wrench,
|
||||
ClipboardCheck,
|
||||
ShoppingCart,
|
||||
Users,
|
||||
UserPlus,
|
||||
Euro,
|
||||
Euro as EuroIcon,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
|
||||
@@ -39,7 +39,7 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
|
||||
id: 'sales-entry',
|
||||
title: 'Registro de Ventas',
|
||||
subtitle: 'Manual o carga masiva',
|
||||
icon: Euro,
|
||||
icon: EuroIcon,
|
||||
badge: '⭐ Más Común',
|
||||
badgeColor: 'bg-gradient-to-r from-amber-100 to-orange-100 text-orange-800 font-semibold',
|
||||
isHighlighted: true,
|
||||
@@ -56,7 +56,7 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
|
||||
id: 'supplier',
|
||||
title: 'Proveedor',
|
||||
subtitle: 'Relación comercial',
|
||||
icon: Building2,
|
||||
icon: Building,
|
||||
badge: 'Configuración',
|
||||
badgeColor: 'bg-blue-100 text-blue-700',
|
||||
},
|
||||
@@ -117,83 +117,6 @@ interface ItemTypeSelectorProps {
|
||||
export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect }) => {
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
@@ -213,7 +136,7 @@ export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect })
|
||||
|
||||
{/* Item Type Grid */}
|
||||
<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 isHighlighted = itemType.isHighlighted;
|
||||
|
||||
|
||||
@@ -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 { WizardModal, WizardStep } from '../../ui/WizardModal/WizardModal';
|
||||
import { ItemTypeSelector, ItemType } from './ItemTypeSelector';
|
||||
import { AnyWizardData } from './types';
|
||||
|
||||
// Import specific wizards
|
||||
import { InventoryWizardSteps } from './wizards/InventoryWizard';
|
||||
import { InventoryWizardSteps, ProductTypeStep, BasicInfoStep, StockConfigStep } from './wizards/InventoryWizard';
|
||||
import { SupplierWizardSteps } from './wizards/SupplierWizard';
|
||||
import { RecipeWizardSteps } from './wizards/RecipeWizard';
|
||||
import { EquipmentWizardSteps } from './wizards/EquipmentWizard';
|
||||
@@ -31,12 +32,22 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
||||
const [selectedItemType, setSelectedItemType] = useState<ItemType | 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
|
||||
const handleClose = useCallback(() => {
|
||||
setSelectedItemType(initialItemType || null);
|
||||
setWizardData({});
|
||||
dataRef.current = {};
|
||||
onClose();
|
||||
}, [onClose, initialItemType]);
|
||||
|
||||
@@ -45,11 +56,23 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
||||
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
|
||||
const handleWizardComplete = useCallback(
|
||||
(data?: any) => {
|
||||
if (selectedItemType) {
|
||||
onComplete?.(selectedItemType, data);
|
||||
// On completion, sync the ref to state for submission
|
||||
setWizardData(dataRef.current);
|
||||
onComplete?.(selectedItemType, dataRef.current);
|
||||
}
|
||||
handleClose();
|
||||
},
|
||||
@@ -57,10 +80,10 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
||||
);
|
||||
|
||||
// Get wizard steps based on selected item type
|
||||
// CRITICAL: Memoize the steps to prevent component recreation on every render
|
||||
// Without this, every keystroke causes the component to unmount/remount, losing focus
|
||||
// IMPORTANT: For dynamic wizards (like sales-entry), we need to include the entryMethod
|
||||
// in the dependency array so steps update when the user selects manual vs upload
|
||||
// ARCHITECTURAL SOLUTION: We pass dataRef and setWizardData to wizard step functions.
|
||||
// The wizard steps use these in their component wrappers, which creates a closure
|
||||
// that always accesses the CURRENT data from dataRef.current, without needing
|
||||
// to recreate the steps array on every data change.
|
||||
const wizardSteps = useMemo((): WizardStep[] => {
|
||||
if (!selectedItemType) {
|
||||
// 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) {
|
||||
case 'inventory':
|
||||
return InventoryWizardSteps(wizardData, setWizardData);
|
||||
return InventoryWizardSteps(dataRef, setWizardData);
|
||||
case 'supplier':
|
||||
return SupplierWizardSteps(wizardData, setWizardData);
|
||||
return SupplierWizardSteps(dataRef, setWizardData);
|
||||
case 'recipe':
|
||||
return RecipeWizardSteps(wizardData, setWizardData);
|
||||
return RecipeWizardSteps(dataRef, setWizardData);
|
||||
case 'equipment':
|
||||
return EquipmentWizardSteps(wizardData, setWizardData);
|
||||
return EquipmentWizardSteps(dataRef, setWizardData);
|
||||
case 'quality-template':
|
||||
return QualityTemplateWizardSteps(wizardData, setWizardData);
|
||||
return QualityTemplateWizardSteps(dataRef, setWizardData);
|
||||
case 'customer-order':
|
||||
return CustomerOrderWizardSteps(wizardData, setWizardData);
|
||||
return CustomerOrderWizardSteps(dataRef, setWizardData);
|
||||
case 'customer':
|
||||
return CustomerWizardSteps(wizardData, setWizardData);
|
||||
return CustomerWizardSteps(dataRef, setWizardData);
|
||||
case 'team-member':
|
||||
return TeamMemberWizardSteps(wizardData, setWizardData);
|
||||
return TeamMemberWizardSteps(dataRef, setWizardData);
|
||||
case 'sales-entry':
|
||||
return SalesEntryWizardSteps(wizardData, setWizardData);
|
||||
return SalesEntryWizardSteps(dataRef, setWizardData);
|
||||
default:
|
||||
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
|
||||
const getWizardTitle = (): string => {
|
||||
@@ -131,6 +155,8 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
||||
steps={wizardSteps}
|
||||
icon={<Sparkles className="w-6 h-6" />}
|
||||
size="xl"
|
||||
dataRef={dataRef}
|
||||
onDataChange={handleDataChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -21,13 +21,9 @@ import OrdersService from '../../../../api/services/orders';
|
||||
import { inventoryService } from '../../../../api/services/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
|
||||
const CustomerSelectionStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||
const CustomerSelectionStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||
const data = dataRef?.current || {};
|
||||
const { t } = useTranslation('wizards');
|
||||
const { currentTenant } = useTenant();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -52,7 +48,7 @@ const CustomerSelectionStep: React.FC<WizardDataProps> = ({ data, onDataChange }
|
||||
const handleCustomerChange = (newCustomer: any, newShowForm: boolean) => {
|
||||
setSelectedCustomer(newCustomer);
|
||||
setShowNewCustomerForm(newShowForm);
|
||||
onDataChange({
|
||||
onDataChange?.({
|
||||
...data,
|
||||
customer: newCustomer,
|
||||
showNewCustomerForm: newShowForm,
|
||||
@@ -63,7 +59,7 @@ const CustomerSelectionStep: React.FC<WizardDataProps> = ({ data, onDataChange }
|
||||
const handleNewCustomerChange = (updates: any) => {
|
||||
const updated = { ...newCustomer, ...updates };
|
||||
setNewCustomer(updated);
|
||||
onDataChange({
|
||||
onDataChange?.({
|
||||
...data,
|
||||
showNewCustomerForm,
|
||||
newCustomerName: updated.name,
|
||||
@@ -288,10 +284,10 @@ const CustomerSelectionStep: React.FC<WizardDataProps> = ({ data, onDataChange }
|
||||
};
|
||||
|
||||
// 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 { currentTenant } = useTenant();
|
||||
const [orderItems, setOrderItems] = useState(data.orderItems || []);
|
||||
const [products, setProducts] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -302,9 +298,8 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||
|
||||
// Update parent whenever order items change
|
||||
const updateOrderItems = (newItems: any[]) => {
|
||||
setOrderItems(newItems);
|
||||
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 () => {
|
||||
@@ -329,7 +324,7 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||
|
||||
const handleAddItem = () => {
|
||||
updateOrderItems([
|
||||
...orderItems,
|
||||
...(data.orderItems || []),
|
||||
{
|
||||
id: Date.now(),
|
||||
productId: '',
|
||||
@@ -344,7 +339,7 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||
};
|
||||
|
||||
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) {
|
||||
const newItem = { ...item, [field]: value };
|
||||
|
||||
@@ -368,11 +363,11 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||
};
|
||||
|
||||
const handleRemoveItem = (index: number) => {
|
||||
updateOrderItems(orderItems.filter((_: any, i: number) => i !== index));
|
||||
updateOrderItems((data.orderItems || []).filter((_: any, i: number) => i !== index));
|
||||
};
|
||||
|
||||
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 (
|
||||
@@ -414,7 +409,7 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||
</button>
|
||||
</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)]">
|
||||
<Package className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||
<p className="mb-2">{t('customerOrder.messages.noProductsInOrder')}</p>
|
||||
@@ -422,7 +417,7 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{orderItems.map((item: any, index: number) => (
|
||||
{(data.orderItems || []).map((item: any, index: number) => (
|
||||
<div
|
||||
key={item.id}
|
||||
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>
|
||||
)}
|
||||
|
||||
{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="flex justify-between items-center">
|
||||
<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
|
||||
const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||
const DeliveryPaymentStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||
const data = dataRef?.current || {};
|
||||
const { t } = useTranslation('wizards');
|
||||
const [orderData, setOrderData] = useState({
|
||||
// Required fields
|
||||
requestedDeliveryDate: data.requestedDeliveryDate || '',
|
||||
orderNumber: data.orderNumber || '',
|
||||
|
||||
// Basic order info
|
||||
orderType: data.orderType || 'standard',
|
||||
priority: data.priority || 'normal',
|
||||
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 || '',
|
||||
});
|
||||
// Helper to get field value with defaults
|
||||
const getValue = (field: string, defaultValue: any = '') => {
|
||||
return data[field] ?? defaultValue;
|
||||
};
|
||||
|
||||
// Update parent whenever order data changes
|
||||
const handleOrderDataChange = (newOrderData: any) => {
|
||||
setOrderData(newOrderData);
|
||||
onDataChange({ ...data, ...newOrderData });
|
||||
const handleOrderDataChange = (updates: Record<string, any>) => {
|
||||
onDataChange?.({ ...data, ...updates });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -633,8 +558,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={orderData.requestedDeliveryDate}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, requestedDeliveryDate: e.target.value })}
|
||||
value={getValue('requestedDeliveryDate')}
|
||||
onChange={(e) => handleOrderDataChange({ requestedDeliveryDate: e.target.value })}
|
||||
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)]"
|
||||
/>
|
||||
@@ -649,7 +574,7 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={orderData.orderNumber || t('customerOrder.deliveryPayment.autoGeneratedLabel')}
|
||||
value={getValue('orderNumber') || t('customerOrder.deliveryPayment.autoGeneratedLabel')}
|
||||
readOnly
|
||||
disabled
|
||||
placeholder={t('customerOrder.deliveryPayment.autoGeneratedPlaceholder')}
|
||||
@@ -667,8 +592,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
{t('customerOrder.deliveryPayment.orderType')}
|
||||
</label>
|
||||
<select
|
||||
value={orderData.orderType}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, orderType: e.target.value })}
|
||||
value={getValue('orderType', 'standard')}
|
||||
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)]"
|
||||
>
|
||||
<option value="standard">{t('customerOrder.orderTypes.standard')}</option>
|
||||
@@ -683,8 +608,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
{t('customerOrder.deliveryPayment.priority')}
|
||||
</label>
|
||||
<select
|
||||
value={orderData.priority}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, priority: e.target.value })}
|
||||
value={getValue('priority', 'normal')}
|
||||
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)]"
|
||||
>
|
||||
<option value="low">{t('customerOrder.priorities.low')}</option>
|
||||
@@ -699,8 +624,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
{t('customerOrder.deliveryPayment.status')}
|
||||
</label>
|
||||
<select
|
||||
value={orderData.status}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, status: e.target.value })}
|
||||
value={getValue('status')}
|
||||
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)]"
|
||||
>
|
||||
<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">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleOrderDataChange({ ...orderData, deliveryMethod: 'pickup' })}
|
||||
onClick={() => handleOrderDataChange({deliveryMethod: 'pickup' })}
|
||||
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(--border-secondary)]'
|
||||
}`}
|
||||
@@ -737,9 +662,9 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleOrderDataChange({ ...orderData, deliveryMethod: 'delivery' })}
|
||||
onClick={() => handleOrderDataChange({deliveryMethod: 'delivery' })}
|
||||
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(--border-secondary)]'
|
||||
}`}
|
||||
@@ -749,9 +674,9 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleOrderDataChange({ ...orderData, deliveryMethod: 'shipping' })}
|
||||
onClick={() => handleOrderDataChange({deliveryMethod: 'shipping' })}
|
||||
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(--border-secondary)]'
|
||||
}`}
|
||||
@@ -762,15 +687,15 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(orderData.deliveryMethod === 'delivery' || orderData.deliveryMethod === 'shipping') && (
|
||||
{(getValue('deliveryMethod', 'pickup') === 'delivery' || getValue('deliveryMethod', 'pickup') === 'shipping') && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
<MapPin className="w-4 h-4 inline mr-1.5" />
|
||||
{t('customerOrder.messages.deliveryAddress')} *
|
||||
</label>
|
||||
<textarea
|
||||
value={orderData.deliveryAddress}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryAddress: e.target.value })}
|
||||
value={getValue('deliveryAddress')}
|
||||
onChange={(e) => handleOrderDataChange({deliveryAddress: e.target.value })}
|
||||
placeholder={t('customerOrder.deliveryPayment.deliveryAddressPlaceholder')}
|
||||
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)]"
|
||||
@@ -785,8 +710,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={orderData.deliveryContactName}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryContactName: e.target.value })}
|
||||
value={getValue('deliveryContactName')}
|
||||
onChange={(e) => handleOrderDataChange({deliveryContactName: e.target.value })}
|
||||
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)]"
|
||||
/>
|
||||
@@ -798,8 +723,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={orderData.deliveryContactPhone}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryContactPhone: e.target.value })}
|
||||
value={getValue('deliveryContactPhone')}
|
||||
onChange={(e) => handleOrderDataChange({deliveryContactPhone: e.target.value })}
|
||||
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)]"
|
||||
/>
|
||||
@@ -818,8 +743,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
{t('customerOrder.messages.paymentMethod')}
|
||||
</label>
|
||||
<select
|
||||
value={orderData.paymentMethod}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, paymentMethod: e.target.value })}
|
||||
value={getValue('paymentMethod')}
|
||||
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)]"
|
||||
>
|
||||
<option value="cash">{t('customerOrder.paymentMethods.cash')}</option>
|
||||
@@ -835,8 +760,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
{t('customerOrder.deliveryPayment.paymentTerms')}
|
||||
</label>
|
||||
<select
|
||||
value={orderData.paymentTerms}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, paymentTerms: e.target.value })}
|
||||
value={getValue('paymentTerms')}
|
||||
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)]"
|
||||
>
|
||||
<option value="immediate">{t('customerOrder.paymentTerms.immediate')}</option>
|
||||
@@ -850,8 +775,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
{t('customerOrder.deliveryPayment.paymentStatus')}
|
||||
</label>
|
||||
<select
|
||||
value={orderData.paymentStatus}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, paymentStatus: e.target.value })}
|
||||
value={getValue('paymentStatus')}
|
||||
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)]"
|
||||
>
|
||||
<option value="pending">{t('customerOrder.paymentStatuses.pending')}</option>
|
||||
@@ -867,8 +792,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={orderData.paymentDueDate}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, paymentDueDate: e.target.value })}
|
||||
value={getValue('paymentDueDate')}
|
||||
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)]"
|
||||
/>
|
||||
</div>
|
||||
@@ -913,8 +838,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={orderData.discountPercentage}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, discountPercentage: e.target.value })}
|
||||
value={getValue('discountPercentage')}
|
||||
onChange={(e) => handleOrderDataChange({discountPercentage: e.target.value })}
|
||||
placeholder="0"
|
||||
step="0.01"
|
||||
min="0"
|
||||
@@ -929,8 +854,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={orderData.deliveryFee}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryFee: e.target.value })}
|
||||
value={getValue('deliveryFee')}
|
||||
onChange={(e) => handleOrderDataChange({deliveryFee: e.target.value })}
|
||||
placeholder="0.00"
|
||||
step="0.01"
|
||||
min="0"
|
||||
@@ -952,8 +877,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={orderData.productionStartDate}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, productionStartDate: e.target.value })}
|
||||
value={getValue('productionStartDate')}
|
||||
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)]"
|
||||
/>
|
||||
</div>
|
||||
@@ -964,8 +889,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={orderData.productionDueDate}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, productionDueDate: e.target.value })}
|
||||
value={getValue('productionDueDate')}
|
||||
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)]"
|
||||
/>
|
||||
</div>
|
||||
@@ -976,8 +901,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={orderData.productionBatchNumber}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, productionBatchNumber: e.target.value })}
|
||||
value={getValue('productionBatchNumber')}
|
||||
onChange={(e) => handleOrderDataChange({productionBatchNumber: e.target.value })}
|
||||
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)]"
|
||||
/>
|
||||
@@ -989,8 +914,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={orderData.deliveryTimeWindow}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryTimeWindow: e.target.value })}
|
||||
value={getValue('deliveryTimeWindow')}
|
||||
onChange={(e) => handleOrderDataChange({deliveryTimeWindow: e.target.value })}
|
||||
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)]"
|
||||
/>
|
||||
@@ -1001,8 +926,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
{t('customerOrder.deliveryPayment.productionNotes')}
|
||||
</label>
|
||||
<textarea
|
||||
value={orderData.productionNotes}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, productionNotes: e.target.value })}
|
||||
value={getValue('productionNotes')}
|
||||
onChange={(e) => handleOrderDataChange({productionNotes: e.target.value })}
|
||||
placeholder={t('customerOrder.deliveryPayment.productionNotesPlaceholder')}
|
||||
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)]"
|
||||
@@ -1023,8 +948,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={orderData.shippingTrackingNumber}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, shippingTrackingNumber: e.target.value })}
|
||||
value={getValue('shippingTrackingNumber')}
|
||||
onChange={(e) => handleOrderDataChange({shippingTrackingNumber: e.target.value })}
|
||||
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)]"
|
||||
/>
|
||||
@@ -1036,8 +961,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={orderData.shippingCarrier}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, shippingCarrier: e.target.value })}
|
||||
value={getValue('shippingCarrier')}
|
||||
onChange={(e) => handleOrderDataChange({shippingCarrier: e.target.value })}
|
||||
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)]"
|
||||
/>
|
||||
@@ -1049,8 +974,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={orderData.pickupLocation}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, pickupLocation: e.target.value })}
|
||||
value={getValue('pickupLocation')}
|
||||
onChange={(e) => handleOrderDataChange({pickupLocation: e.target.value })}
|
||||
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)]"
|
||||
/>
|
||||
@@ -1062,8 +987,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={orderData.actualDeliveryDate}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, actualDeliveryDate: e.target.value })}
|
||||
value={getValue('actualDeliveryDate')}
|
||||
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)]"
|
||||
/>
|
||||
</div>
|
||||
@@ -1081,8 +1006,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
{t('customerOrder.deliveryPayment.orderSource')}
|
||||
</label>
|
||||
<select
|
||||
value={orderData.orderSource}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, orderSource: e.target.value })}
|
||||
value={getValue('orderSource')}
|
||||
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)]"
|
||||
>
|
||||
<option value="manual">{t('customerOrder.orderSources.manual')}</option>
|
||||
@@ -1098,8 +1023,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
{t('customerOrder.deliveryPayment.salesChannel')}
|
||||
</label>
|
||||
<select
|
||||
value={orderData.salesChannel}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, salesChannel: e.target.value })}
|
||||
value={getValue('salesChannel')}
|
||||
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)]"
|
||||
>
|
||||
<option value="direct">{t('customerOrder.salesChannels.direct')}</option>
|
||||
@@ -1115,8 +1040,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={orderData.salesRepId}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, salesRepId: e.target.value })}
|
||||
value={getValue('salesRepId')}
|
||||
onChange={(e) => handleOrderDataChange({salesRepId: e.target.value })}
|
||||
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)]"
|
||||
/>
|
||||
@@ -1136,8 +1061,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={orderData.customerPurchaseOrder}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, customerPurchaseOrder: e.target.value })}
|
||||
value={getValue('customerPurchaseOrder')}
|
||||
onChange={(e) => handleOrderDataChange({customerPurchaseOrder: e.target.value })}
|
||||
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)]"
|
||||
/>
|
||||
@@ -1148,8 +1073,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
{t('customerOrder.deliveryPayment.deliveryInstructions')}
|
||||
</label>
|
||||
<textarea
|
||||
value={orderData.deliveryInstructions}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryInstructions: e.target.value })}
|
||||
value={getValue('deliveryInstructions')}
|
||||
onChange={(e) => handleOrderDataChange({deliveryInstructions: e.target.value })}
|
||||
placeholder={t('customerOrder.deliveryPayment.deliveryInstructionsPlaceholder')}
|
||||
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)]"
|
||||
@@ -1161,8 +1086,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
{t('customerOrder.deliveryPayment.specialInstructions')}
|
||||
</label>
|
||||
<textarea
|
||||
value={orderData.specialInstructions}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, specialInstructions: e.target.value })}
|
||||
value={getValue('specialInstructions')}
|
||||
onChange={(e) => handleOrderDataChange({specialInstructions: e.target.value })}
|
||||
placeholder={t('customerOrder.deliveryPayment.specialInstructionsPlaceholder')}
|
||||
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)]"
|
||||
@@ -1174,8 +1099,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
{t('customerOrder.deliveryPayment.internalNotes')}
|
||||
</label>
|
||||
<textarea
|
||||
value={orderData.internalNotes}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, internalNotes: e.target.value })}
|
||||
value={getValue('internalNotes')}
|
||||
onChange={(e) => handleOrderDataChange({internalNotes: e.target.value })}
|
||||
placeholder={t('customerOrder.deliveryPayment.internalNotesPlaceholder')}
|
||||
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)]"
|
||||
@@ -1187,8 +1112,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
{t('customerOrder.deliveryPayment.customerNotes')}
|
||||
</label>
|
||||
<textarea
|
||||
value={orderData.customerNotes}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, customerNotes: e.target.value })}
|
||||
value={getValue('customerNotes')}
|
||||
onChange={(e) => handleOrderDataChange({customerNotes: e.target.value })}
|
||||
placeholder={t('customerOrder.deliveryPayment.customerNotesPlaceholder')}
|
||||
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)]"
|
||||
@@ -1206,8 +1131,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={orderData.notifyCustomerOnStatusChange}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, notifyCustomerOnStatusChange: e.target.checked })}
|
||||
checked={getValue('notifyCustomerOnStatusChange')}
|
||||
onChange={(e) => handleOrderDataChange({notifyCustomerOnStatusChange: e.target.checked })}
|
||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||
/>
|
||||
<label className="text-sm text-[var(--text-secondary)]">
|
||||
@@ -1218,8 +1143,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={orderData.notifyCustomerOnDelivery}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, notifyCustomerOnDelivery: e.target.checked })}
|
||||
checked={getValue('notifyCustomerOnDelivery')}
|
||||
onChange={(e) => handleOrderDataChange({notifyCustomerOnDelivery: e.target.checked })}
|
||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||
/>
|
||||
<label className="text-sm text-[var(--text-secondary)]">
|
||||
@@ -1233,8 +1158,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={orderData.customerNotificationEmail}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, customerNotificationEmail: e.target.value })}
|
||||
value={getValue('customerNotificationEmail')}
|
||||
onChange={(e) => handleOrderDataChange({customerNotificationEmail: e.target.value })}
|
||||
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)]"
|
||||
/>
|
||||
@@ -1246,8 +1171,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={orderData.customerNotificationPhone}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, customerNotificationPhone: e.target.value })}
|
||||
value={getValue('customerNotificationPhone')}
|
||||
onChange={(e) => handleOrderDataChange({customerNotificationPhone: e.target.value })}
|
||||
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)]"
|
||||
/>
|
||||
@@ -1264,8 +1189,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={orderData.qualityCheckRequired}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, qualityCheckRequired: e.target.checked })}
|
||||
checked={getValue('qualityCheckRequired')}
|
||||
onChange={(e) => handleOrderDataChange({qualityCheckRequired: e.target.checked })}
|
||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||
/>
|
||||
<label className="text-sm text-[var(--text-secondary)]">
|
||||
@@ -1278,8 +1203,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
{t('customerOrder.deliveryPayment.qualityCheckStatus')}
|
||||
</label>
|
||||
<select
|
||||
value={orderData.qualityCheckStatus}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, qualityCheckStatus: e.target.value })}
|
||||
value={getValue('qualityCheckStatus')}
|
||||
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)]"
|
||||
>
|
||||
<option value="">{t('customerOrder.qualityCheckStatuses.not_started')}</option>
|
||||
@@ -1295,8 +1220,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={orderData.packagingInstructions}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, packagingInstructions: e.target.value })}
|
||||
value={getValue('packagingInstructions')}
|
||||
onChange={(e) => handleOrderDataChange({packagingInstructions: e.target.value })}
|
||||
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)]"
|
||||
/>
|
||||
@@ -1308,8 +1233,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={orderData.labelingRequirements}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, labelingRequirements: e.target.value })}
|
||||
value={getValue('labelingRequirements')}
|
||||
onChange={(e) => handleOrderDataChange({labelingRequirements: e.target.value })}
|
||||
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)]"
|
||||
/>
|
||||
@@ -1326,8 +1251,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={orderData.isRecurring}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, isRecurring: e.target.checked })}
|
||||
checked={getValue('isRecurring')}
|
||||
onChange={(e) => handleOrderDataChange({isRecurring: e.target.checked })}
|
||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||
/>
|
||||
<label className="text-sm text-[var(--text-secondary)]">
|
||||
@@ -1335,15 +1260,15 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{orderData.isRecurring && (
|
||||
{getValue('isRecurring', false) && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('customerOrder.deliveryPayment.recurringSchedule')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={orderData.recurringSchedule}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, recurringSchedule: e.target.value })}
|
||||
value={getValue('recurringSchedule')}
|
||||
onChange={(e) => handleOrderDataChange({recurringSchedule: e.target.value })}
|
||||
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)]"
|
||||
/>
|
||||
@@ -1359,8 +1284,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={orderData.tags}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, tags: e.target.value })}
|
||||
value={getValue('tags')}
|
||||
onChange={(e) => handleOrderDataChange({tags: e.target.value })}
|
||||
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)]"
|
||||
/>
|
||||
@@ -1374,8 +1299,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
</Tooltip>
|
||||
</label>
|
||||
<textarea
|
||||
value={orderData.metadata}
|
||||
onChange={(e) => handleOrderDataChange({ ...orderData, metadata: e.target.value })}
|
||||
value={getValue('metadata')}
|
||||
onChange={(e) => handleOrderDataChange({metadata: e.target.value })}
|
||||
placeholder={t('customerOrder.deliveryPayment.metadataPlaceholder')}
|
||||
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"
|
||||
@@ -1389,34 +1314,26 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
||||
};
|
||||
|
||||
export const CustomerOrderWizardSteps = (
|
||||
data: Record<string, any>,
|
||||
dataRef: React.MutableRefObject<Record<string, any>>,
|
||||
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 [
|
||||
{
|
||||
id: 'customer-selection',
|
||||
title: 'customerOrder.customerSelection.title',
|
||||
component: (props) => <CustomerSelectionStep {...props} data={data} onDataChange={setData} />,
|
||||
validate: () => {
|
||||
return !!(data.customer || (data.showNewCustomerForm && data.newCustomerName && data.newCustomerPhone));
|
||||
},
|
||||
component: CustomerSelectionStep,
|
||||
},
|
||||
{
|
||||
id: 'order-items',
|
||||
title: 'customerOrder.orderItems.title',
|
||||
component: (props) => <OrderItemsStep {...props} data={data} onDataChange={setData} />,
|
||||
validate: () => {
|
||||
return !!(data.orderItems && data.orderItems.length > 0);
|
||||
},
|
||||
component: OrderItemsStep,
|
||||
},
|
||||
{
|
||||
id: 'delivery-payment',
|
||||
title: 'customerOrder.deliveryPayment.title',
|
||||
component: (props) => <DeliveryPaymentStep {...props} data={data} onDataChange={setData} />,
|
||||
validate: () => {
|
||||
return !!(
|
||||
data.requestedDeliveryDate &&
|
||||
(data.deliveryMethod === 'pickup' || data.deliveryAddress)
|
||||
);
|
||||
},
|
||||
component: DeliveryPaymentStep,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1,109 +1,22 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
||||
import { Users, CheckCircle2, Loader2 } from 'lucide-react';
|
||||
import { useTenant } from '../../../../stores/tenant.store';
|
||||
import OrdersService from '../../../../api/services/orders';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { Users } from 'lucide-react';
|
||||
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
|
||||
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
||||
|
||||
interface WizardDataProps extends WizardStepProps {
|
||||
data: Record<string, any>;
|
||||
onDataChange: (data: Record<string, any>) => void;
|
||||
}
|
||||
const CustomerDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||
const data = dataRef?.current || {};
|
||||
|
||||
const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
||||
const { currentTenant } = useTenant();
|
||||
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,
|
||||
const handleFieldChange = (field: string, value: any) => {
|
||||
onDataChange?.({ ...data, [field]: value });
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
@@ -113,12 +26,6 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
<p className="text-sm text-[var(--text-secondary)]">Essential customer information</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Required Fields */}
|
||||
<div className="space-y-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>
|
||||
<input
|
||||
type="text"
|
||||
value={customerData.name}
|
||||
onChange={(e) => setCustomerData({ ...customerData, name: e.target.value })}
|
||||
value={data.name || ''}
|
||||
onChange={(e) => handleFieldChange('name', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -144,8 +51,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customerData.customerCode}
|
||||
onChange={(e) => setCustomerData({ ...customerData, customerCode: e.target.value })}
|
||||
value={data.customerCode}
|
||||
onChange={(e) => handleFieldChange('customerCode', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -158,8 +65,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
Customer Type *
|
||||
</label>
|
||||
<select
|
||||
value={customerData.customerType}
|
||||
onChange={(e) => setCustomerData({ ...customerData, customerType: e.target.value })}
|
||||
value={data.customerType}
|
||||
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)]"
|
||||
>
|
||||
<option value="individual">Individual</option>
|
||||
@@ -174,8 +81,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customerData.country}
|
||||
onChange={(e) => setCustomerData({ ...customerData, country: e.target.value })}
|
||||
value={data.country}
|
||||
onChange={(e) => handleFieldChange('country', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -188,8 +95,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customerData.businessName}
|
||||
onChange={(e) => setCustomerData({ ...customerData, businessName: e.target.value })}
|
||||
value={data.businessName}
|
||||
onChange={(e) => handleFieldChange('businessName', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -202,8 +109,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={customerData.email}
|
||||
onChange={(e) => setCustomerData({ ...customerData, email: e.target.value })}
|
||||
value={data.email}
|
||||
onChange={(e) => handleFieldChange('email', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -215,8 +122,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={customerData.phone}
|
||||
onChange={(e) => setCustomerData({ ...customerData, phone: e.target.value })}
|
||||
value={data.phone}
|
||||
onChange={(e) => handleFieldChange('phone', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -237,8 +144,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customerData.addressLine1}
|
||||
onChange={(e) => setCustomerData({ ...customerData, addressLine1: e.target.value })}
|
||||
value={data.addressLine1}
|
||||
onChange={(e) => handleFieldChange('addressLine1', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -250,8 +157,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customerData.addressLine2}
|
||||
onChange={(e) => setCustomerData({ ...customerData, addressLine2: e.target.value })}
|
||||
value={data.addressLine2}
|
||||
onChange={(e) => handleFieldChange('addressLine2', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -263,8 +170,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customerData.city}
|
||||
onChange={(e) => setCustomerData({ ...customerData, city: e.target.value })}
|
||||
value={data.city}
|
||||
onChange={(e) => handleFieldChange('city', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -276,8 +183,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customerData.state}
|
||||
onChange={(e) => setCustomerData({ ...customerData, state: e.target.value })}
|
||||
value={data.state}
|
||||
onChange={(e) => handleFieldChange('state', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -289,8 +196,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customerData.postalCode}
|
||||
onChange={(e) => setCustomerData({ ...customerData, postalCode: e.target.value })}
|
||||
value={data.postalCode}
|
||||
onChange={(e) => handleFieldChange('postalCode', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -302,8 +209,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customerData.taxId}
|
||||
onChange={(e) => setCustomerData({ ...customerData, taxId: e.target.value })}
|
||||
value={data.taxId}
|
||||
onChange={(e) => handleFieldChange('taxId', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -315,8 +222,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customerData.businessLicense}
|
||||
onChange={(e) => setCustomerData({ ...customerData, businessLicense: e.target.value })}
|
||||
value={data.businessLicense}
|
||||
onChange={(e) => handleFieldChange('businessLicense', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -327,8 +234,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
Payment Terms
|
||||
</label>
|
||||
<select
|
||||
value={customerData.paymentTerms}
|
||||
onChange={(e) => setCustomerData({ ...customerData, paymentTerms: e.target.value })}
|
||||
value={data.paymentTerms}
|
||||
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)]"
|
||||
>
|
||||
<option value="immediate">Immediate</option>
|
||||
@@ -343,8 +250,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={customerData.creditLimit}
|
||||
onChange={(e) => setCustomerData({ ...customerData, creditLimit: e.target.value })}
|
||||
value={data.creditLimit}
|
||||
onChange={(e) => handleFieldChange('creditLimit', e.target.value)}
|
||||
placeholder="5000.00"
|
||||
min="0"
|
||||
step="0.01"
|
||||
@@ -358,8 +265,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={customerData.discountPercentage}
|
||||
onChange={(e) => setCustomerData({ ...customerData, discountPercentage: parseFloat(e.target.value) || 0 })}
|
||||
value={data.discountPercentage}
|
||||
onChange={(e) => handleFieldChange('discountPercentage', parseFloat(e.target.value) || 0)}
|
||||
placeholder="10"
|
||||
min="0"
|
||||
max="100"
|
||||
@@ -373,8 +280,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
Customer Segment
|
||||
</label>
|
||||
<select
|
||||
value={customerData.customerSegment}
|
||||
onChange={(e) => setCustomerData({ ...customerData, customerSegment: e.target.value })}
|
||||
value={data.customerSegment}
|
||||
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)]"
|
||||
>
|
||||
<option value="vip">VIP</option>
|
||||
@@ -388,8 +295,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
Priority Level
|
||||
</label>
|
||||
<select
|
||||
value={customerData.priorityLevel}
|
||||
onChange={(e) => setCustomerData({ ...customerData, priorityLevel: e.target.value })}
|
||||
value={data.priorityLevel}
|
||||
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)]"
|
||||
>
|
||||
<option value="high">High</option>
|
||||
@@ -403,8 +310,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
Preferred Delivery Method
|
||||
</label>
|
||||
<select
|
||||
value={customerData.preferredDeliveryMethod}
|
||||
onChange={(e) => setCustomerData({ ...customerData, preferredDeliveryMethod: e.target.value })}
|
||||
value={data.preferredDeliveryMethod}
|
||||
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)]"
|
||||
>
|
||||
<option value="delivery">Delivery</option>
|
||||
@@ -418,8 +325,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
Special Instructions
|
||||
</label>
|
||||
<textarea
|
||||
value={customerData.specialInstructions}
|
||||
onChange={(e) => setCustomerData({ ...customerData, specialInstructions: e.target.value })}
|
||||
value={data.specialInstructions}
|
||||
onChange={(e) => handleFieldChange('specialInstructions', e.target.value)}
|
||||
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)]"
|
||||
rows={3}
|
||||
@@ -427,42 +334,72 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
export const CustomerWizardSteps = (
|
||||
data: Record<string, any>,
|
||||
dataRef: React.MutableRefObject<Record<string, any>>,
|
||||
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 [
|
||||
{
|
||||
id: 'customer-details',
|
||||
title: 'Customer Details',
|
||||
description: 'Contact and business information',
|
||||
component: (props) => <CustomerDetailsStep {...props} data={data} onDataChange={setData} />,
|
||||
validate: () => {
|
||||
return !!(data.name && data.customerCode && data.customerType && data.country);
|
||||
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;
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1,65 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
||||
import { Wrench, CheckCircle2, Loader2 } from 'lucide-react';
|
||||
import { useTenant } from '../../../../stores/tenant.store';
|
||||
import { equipmentService } from '../../../../api/services/equipment';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { Wrench } from 'lucide-react';
|
||||
|
||||
interface WizardDataProps extends WizardStepProps {
|
||||
data: Record<string, any>;
|
||||
onDataChange: (data: Record<string, any>) => void;
|
||||
}
|
||||
const EquipmentDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||
const data = dataRef?.current || {};
|
||||
|
||||
const EquipmentDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
||||
const { currentTenant } = useTenant();
|
||||
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);
|
||||
}
|
||||
const handleFieldChange = (field: string, value: any) => {
|
||||
onDataChange?.({ ...data, [field]: value });
|
||||
};
|
||||
|
||||
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>
|
||||
</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>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Tipo de Equipo *</label>
|
||||
<select
|
||||
value={equipmentData.type}
|
||||
onChange={(e) => setEquipmentData({ ...equipmentData, type: e.target.value })}
|
||||
value={data.type || 'oven'}
|
||||
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)]"
|
||||
>
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
value={equipmentData.brand}
|
||||
onChange={(e) => setEquipmentData({ ...equipmentData, brand: e.target.value })}
|
||||
value={data.brand || ''}
|
||||
onChange={(e) => handleFieldChange('brand', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -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>
|
||||
<input
|
||||
type="text"
|
||||
value={equipmentData.location}
|
||||
onChange={(e) => setEquipmentData({ ...equipmentData, location: e.target.value })}
|
||||
value={data.location || ''}
|
||||
onChange={(e) => handleFieldChange('location', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -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>
|
||||
<input
|
||||
type="date"
|
||||
value={equipmentData.purchaseDate}
|
||||
onChange={(e) => setEquipmentData({ ...equipmentData, purchaseDate: e.target.value })}
|
||||
value={data.purchaseDate || ''}
|
||||
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)]"
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
export const EquipmentWizardSteps = (data: 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} /> },
|
||||
export const EquipmentWizardSteps = (dataRef: React.MutableRefObject<Record<string, any>>, setData: (data: Record<string, any>) => void): WizardStep[] => {
|
||||
// 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;
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -5,21 +5,18 @@ import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
|
||||
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
||||
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
|
||||
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 handleFieldChange = (field: string, value: any) => {
|
||||
onDataChange({ ...data, [field]: value });
|
||||
onDataChange?.({ ...data, [field]: value });
|
||||
};
|
||||
|
||||
const handleTypeSelect = (type: string) => {
|
||||
onDataChange({ ...data, productType: type });
|
||||
onDataChange?.({ ...data, productType: type });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -151,11 +148,12 @@ const ProductTypeStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||
};
|
||||
|
||||
// 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 handleFieldChange = (field: string, value: any) => {
|
||||
onDataChange({ ...data, [field]: value });
|
||||
onDataChange?.({ ...data, [field]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -308,41 +306,88 @@ const BasicInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// STEP 3: Stock Configuration with advanced fields
|
||||
const StockConfigStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||
// STEP 3: Initial Stock/Lots Configuration
|
||||
const StockConfigStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||
const data = dataRef?.current || {};
|
||||
const { t } = useTranslation('wizards');
|
||||
const [lots, setLots] = useState<any[]>(data.initialLots || []);
|
||||
|
||||
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 (
|
||||
<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">
|
||||
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-3 flex items-center gap-2">
|
||||
<Package className="w-4 h-4" />
|
||||
{t('inventory.summary', 'Summary')}
|
||||
</h4>
|
||||
<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 className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-[var(--text-tertiary)] block mb-1">Producto</span>
|
||||
<span className="font-medium text-[var(--text-primary)]">{data.name || 'N/A'}</span>
|
||||
</div>
|
||||
{data.name && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[var(--text-tertiary)]">{t('inventory.fields.name')}:</span>
|
||||
<span className="font-medium text-[var(--text-primary)]">{data.name}</span>
|
||||
<div>
|
||||
<span className="text-[var(--text-tertiary)] block mb-1">Unidad</span>
|
||||
<span className="font-medium text-[var(--text-primary)]">{data.unitOfMeasure || 'N/A'}</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 */}
|
||||
{/* 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')} *
|
||||
@@ -363,153 +408,181 @@ const StockConfigStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||
<option value="oz">{t('inventory.units.oz')}</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Standard Cost - Visible but optional */}
|
||||
{/* 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-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 className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
||||
Cantidad *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={data.standardCost || ''}
|
||||
onChange={(e) => handleFieldChange('standardCost', e.target.value)}
|
||||
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-3 py-2 border border-[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-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>
|
||||
|
||||
{/* 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">
|
||||
{/* Low Stock Threshold */}
|
||||
{/* Lot Number */}
|
||||
<div>
|
||||
<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 className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
||||
Número de Lote
|
||||
</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)]"
|
||||
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>
|
||||
|
||||
{/* Reorder Point */}
|
||||
{/* Expiration Date */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||
{t('inventory.fields.reorderPoint')}
|
||||
<Tooltip content={t('tooltips.reorderPoint')}>
|
||||
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
||||
</Tooltip>
|
||||
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
||||
Fecha de Expiración
|
||||
</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)]"
|
||||
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>
|
||||
|
||||
{/* 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 */}
|
||||
{/* Location */}
|
||||
<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 className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
||||
Ubicación
|
||||
</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)]"
|
||||
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>
|
||||
</AdvancedOptionsSection>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* Add Lot Button */}
|
||||
<button
|
||||
onClick={handleAddLot}
|
||||
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"
|
||||
>
|
||||
<Package className="w-5 h-5" />
|
||||
{lots.length === 0 ? 'Agregar Lote Inicial' : 'Agregar Otro Lote'}
|
||||
</button>
|
||||
|
||||
{/* Skip Option */}
|
||||
{lots.length === 0 && (
|
||||
<p className="text-xs text-center text-[var(--text-tertiary)] italic">
|
||||
Puedes saltar este paso si prefieres agregar el stock inicial más tarde
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InventoryWizardSteps = (
|
||||
data: Record<string, any>,
|
||||
dataRef: React.MutableRefObject<Record<string, any>>,
|
||||
setData: (data: Record<string, any>) => void
|
||||
): WizardStep[] => {
|
||||
// New architecture: return direct component references instead of arrow functions
|
||||
// dataRef and onDataChange are now passed through WizardModal props
|
||||
return [
|
||||
{
|
||||
id: 'product-type',
|
||||
title: 'inventory.steps.productType',
|
||||
component: (props) => <ProductTypeStep {...props} data={data} onDataChange={setData} />,
|
||||
component: ProductTypeStep,
|
||||
validate: () => {
|
||||
return !!data.productType;
|
||||
return !!dataRef.current.productType;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'basic-info',
|
||||
title: 'inventory.steps.basicInfo',
|
||||
component: (props) => <BasicInfoStep {...props} data={data} onDataChange={setData} />,
|
||||
component: BasicInfoStep,
|
||||
validate: () => {
|
||||
const category =
|
||||
data.productType === 'ingredient' ? data.ingredientCategory : data.productCategory;
|
||||
return !!(data.name && data.name.trim().length >= 2 && category);
|
||||
dataRef.current.productType === 'ingredient' ? dataRef.current.ingredientCategory : dataRef.current.productCategory;
|
||||
return !!(dataRef.current.name && dataRef.current.name.trim().length >= 2 && category);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'stock-config',
|
||||
title: 'inventory.steps.stockConfig',
|
||||
component: (props) => <StockConfigStep {...props} data={data} onDataChange={setData} />,
|
||||
component: StockConfigStep,
|
||||
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;
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -5,57 +5,13 @@ import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
|
||||
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
||||
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
|
||||
const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||
const QualityTemplateDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||
const data = dataRef?.current || {};
|
||||
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
|
||||
templateCode: data.templateCode || '',
|
||||
description: data.description || '',
|
||||
applicableStages: data.applicableStages || '',
|
||||
|
||||
// Check points configuration
|
||||
checkPoints: data.checkPoints || '',
|
||||
|
||||
// Scoring configuration
|
||||
scoringMethod: data.scoringMethod || 'weighted_average',
|
||||
passThreshold: data.passThreshold || '70.0',
|
||||
isRequired: data.isRequired ?? false,
|
||||
frequencyDays: data.frequencyDays || '',
|
||||
|
||||
// Advanced configuration (JSONB fields)
|
||||
parameters: data.parameters || '',
|
||||
thresholds: data.thresholds || '',
|
||||
scoringCriteria: data.scoringCriteria || '',
|
||||
|
||||
// Status
|
||||
isActive: data.isActive ?? true,
|
||||
version: data.version || '1.0',
|
||||
|
||||
// Helper fields for UI
|
||||
requiresPhoto: data.requiresPhoto ?? false,
|
||||
criticalControlPoint: data.criticalControlPoint ?? false,
|
||||
notifyOnFail: data.notifyOnFail ?? false,
|
||||
responsibleRole: data.responsibleRole || '',
|
||||
requiredEquipment: data.requiredEquipment || '',
|
||||
acceptanceCriteria: data.acceptanceCriteria || '',
|
||||
specificConditions: data.specificConditions || '',
|
||||
});
|
||||
|
||||
// Update parent whenever local state changes
|
||||
const handleDataChange = (newTemplateData: any) => {
|
||||
setTemplateData(newTemplateData);
|
||||
onDataChange({ ...data, ...newTemplateData });
|
||||
const handleFieldChange = (field: string, value: any) => {
|
||||
onDataChange?.({ ...data, [field]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -77,8 +33,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={templateData.name}
|
||||
onChange={(e) => handleDataChange({ ...templateData, name: e.target.value })}
|
||||
value={data.name}
|
||||
onChange={(e) => handleFieldChange('name', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -89,8 +45,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
{t('qualityTemplate.fields.checkType')} *
|
||||
</label>
|
||||
<select
|
||||
value={templateData.checkType}
|
||||
onChange={(e) => handleDataChange({ ...templateData, checkType: e.target.value })}
|
||||
value={data.checkType}
|
||||
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)]"
|
||||
>
|
||||
<option value="product_quality">{t('qualityTemplate.checkTypes.product_quality')}</option>
|
||||
@@ -112,8 +68,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={templateData.weight}
|
||||
onChange={(e) => handleDataChange({ ...templateData, weight: e.target.value })}
|
||||
value={data.weight}
|
||||
onChange={(e) => handleFieldChange('weight', e.target.value)}
|
||||
placeholder="5.0"
|
||||
step="0.1"
|
||||
min="0"
|
||||
@@ -136,8 +92,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={templateData.templateCode}
|
||||
onChange={(e) => handleDataChange({ ...templateData, templateCode: e.target.value })}
|
||||
value={data.templateCode}
|
||||
onChange={(e) => handleFieldChange('templateCode', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -149,8 +105,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={templateData.version}
|
||||
onChange={(e) => handleDataChange({ ...templateData, version: e.target.value })}
|
||||
value={data.version}
|
||||
onChange={(e) => handleFieldChange('version', e.target.value)}
|
||||
placeholder="1.0"
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
@@ -161,8 +117,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
{t('qualityTemplate.fields.description')}
|
||||
</label>
|
||||
<textarea
|
||||
value={templateData.description}
|
||||
onChange={(e) => handleDataChange({ ...templateData, description: e.target.value })}
|
||||
value={data.description}
|
||||
onChange={(e) => handleFieldChange('description', e.target.value)}
|
||||
placeholder={t('qualityTemplate.fields.descriptionPlaceholder')}
|
||||
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)]"
|
||||
@@ -178,8 +134,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={templateData.applicableStages}
|
||||
onChange={(e) => handleDataChange({ ...templateData, applicableStages: e.target.value })}
|
||||
value={data.applicableStages}
|
||||
onChange={(e) => handleFieldChange('applicableStages', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -196,8 +152,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
{t('qualityTemplate.scoringMethods.scoringMethod')}
|
||||
</label>
|
||||
<select
|
||||
value={templateData.scoringMethod}
|
||||
onChange={(e) => handleDataChange({ ...templateData, scoringMethod: e.target.value })}
|
||||
value={data.scoringMethod}
|
||||
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)]"
|
||||
>
|
||||
<option value="weighted_average">{t('qualityTemplate.scoringMethods.weightedAverage')}</option>
|
||||
@@ -216,8 +172,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={templateData.passThreshold}
|
||||
onChange={(e) => handleDataChange({ ...templateData, passThreshold: e.target.value })}
|
||||
value={data.passThreshold}
|
||||
onChange={(e) => handleFieldChange('passThreshold', e.target.value)}
|
||||
placeholder="70.0"
|
||||
step="0.1"
|
||||
min="0"
|
||||
@@ -235,8 +191,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={templateData.frequencyDays}
|
||||
onChange={(e) => handleDataChange({ ...templateData, frequencyDays: e.target.value })}
|
||||
value={data.frequencyDays}
|
||||
onChange={(e) => handleFieldChange('frequencyDays', e.target.value)}
|
||||
placeholder={t('qualityTemplate.advancedFields.frequencyPlaceholder')}
|
||||
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)]"
|
||||
@@ -246,8 +202,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={templateData.isRequired}
|
||||
onChange={(e) => handleDataChange({ ...templateData, isRequired: e.target.checked })}
|
||||
checked={data.isRequired}
|
||||
onChange={(e) => handleFieldChange('isRequired', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||
/>
|
||||
<label className="text-sm text-[var(--text-secondary)]">
|
||||
@@ -276,8 +232,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
</Tooltip>
|
||||
</label>
|
||||
<textarea
|
||||
value={templateData.checkPoints}
|
||||
onChange={(e) => handleDataChange({ ...templateData, checkPoints: e.target.value })}
|
||||
value={data.checkPoints}
|
||||
onChange={(e) => handleFieldChange('checkPoints', e.target.value)}
|
||||
placeholder={t('qualityTemplate.advancedFields.checkPointsPlaceholder')}
|
||||
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"
|
||||
@@ -289,8 +245,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
{t('qualityTemplate.advancedFields.acceptanceCriteria')}
|
||||
</label>
|
||||
<textarea
|
||||
value={templateData.acceptanceCriteria}
|
||||
onChange={(e) => handleDataChange({ ...templateData, acceptanceCriteria: e.target.value })}
|
||||
value={data.acceptanceCriteria}
|
||||
onChange={(e) => handleFieldChange('acceptanceCriteria', e.target.value)}
|
||||
placeholder={t('qualityTemplate.advancedFields.acceptanceCriteriaPlaceholder')}
|
||||
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)]"
|
||||
@@ -313,8 +269,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
</Tooltip>
|
||||
</label>
|
||||
<textarea
|
||||
value={templateData.parameters}
|
||||
onChange={(e) => handleDataChange({ ...templateData, parameters: e.target.value })}
|
||||
value={data.parameters}
|
||||
onChange={(e) => handleFieldChange('parameters', e.target.value)}
|
||||
placeholder={t('qualityTemplate.advancedFields.parametersPlaceholder')}
|
||||
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"
|
||||
@@ -329,8 +285,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
</Tooltip>
|
||||
</label>
|
||||
<textarea
|
||||
value={templateData.thresholds}
|
||||
onChange={(e) => handleDataChange({ ...templateData, thresholds: e.target.value })}
|
||||
value={data.thresholds}
|
||||
onChange={(e) => handleFieldChange('thresholds', e.target.value)}
|
||||
placeholder={t('qualityTemplate.advancedFields.thresholdsPlaceholder')}
|
||||
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"
|
||||
@@ -345,8 +301,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
</Tooltip>
|
||||
</label>
|
||||
<textarea
|
||||
value={templateData.scoringCriteria}
|
||||
onChange={(e) => handleDataChange({ ...templateData, scoringCriteria: e.target.value })}
|
||||
value={data.scoringCriteria}
|
||||
onChange={(e) => handleFieldChange('scoringCriteria', e.target.value)}
|
||||
placeholder={t('qualityTemplate.advancedFields.scoringCriteriaPlaceholder')}
|
||||
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"
|
||||
@@ -367,8 +323,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={templateData.responsibleRole}
|
||||
onChange={(e) => handleDataChange({ ...templateData, responsibleRole: e.target.value })}
|
||||
value={data.responsibleRole}
|
||||
onChange={(e) => handleFieldChange('responsibleRole', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -380,8 +336,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={templateData.requiredEquipment}
|
||||
onChange={(e) => handleDataChange({ ...templateData, requiredEquipment: e.target.value })}
|
||||
value={data.requiredEquipment}
|
||||
onChange={(e) => handleFieldChange('requiredEquipment', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -392,8 +348,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
{t('qualityTemplate.advancedFields.specificConditions')}
|
||||
</label>
|
||||
<textarea
|
||||
value={templateData.specificConditions}
|
||||
onChange={(e) => handleDataChange({ ...templateData, specificConditions: e.target.value })}
|
||||
value={data.specificConditions}
|
||||
onChange={(e) => handleFieldChange('specificConditions', e.target.value)}
|
||||
placeholder={t('qualityTemplate.advancedFields.specificConditionsPlaceholder')}
|
||||
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)]"
|
||||
@@ -411,8 +367,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={templateData.isActive}
|
||||
onChange={(e) => handleDataChange({ ...templateData, isActive: e.target.checked })}
|
||||
checked={data.isActive}
|
||||
onChange={(e) => handleFieldChange('isActive', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||
/>
|
||||
<label className="text-sm text-[var(--text-secondary)]">
|
||||
@@ -423,8 +379,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={templateData.requiresPhoto}
|
||||
onChange={(e) => handleDataChange({ ...templateData, requiresPhoto: e.target.checked })}
|
||||
checked={data.requiresPhoto}
|
||||
onChange={(e) => handleFieldChange('requiresPhoto', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||
/>
|
||||
<label className="text-sm text-[var(--text-secondary)]">
|
||||
@@ -435,8 +391,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={templateData.criticalControlPoint}
|
||||
onChange={(e) => handleDataChange({ ...templateData, criticalControlPoint: e.target.checked })}
|
||||
checked={data.criticalControlPoint}
|
||||
onChange={(e) => handleFieldChange('criticalControlPoint', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||
/>
|
||||
<label className="text-sm text-[var(--text-secondary)]">
|
||||
@@ -447,8 +403,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={templateData.notifyOnFail}
|
||||
onChange={(e) => handleDataChange({ ...templateData, notifyOnFail: e.target.checked })}
|
||||
checked={data.notifyOnFail}
|
||||
onChange={(e) => handleFieldChange('notifyOnFail', e.target.checked)}
|
||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||
/>
|
||||
<label className="text-sm text-[var(--text-secondary)]">
|
||||
@@ -463,15 +419,16 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
||||
};
|
||||
|
||||
export const QualityTemplateWizardSteps = (
|
||||
data: Record<string, any>,
|
||||
dataRef: React.MutableRefObject<Record<string, any>>,
|
||||
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 [
|
||||
{
|
||||
id: 'template-details',
|
||||
title: 'qualityTemplate.advancedFields.templateDetailsTitle',
|
||||
component: (props) => <QualityTemplateDetailsStep {...props} data={data} onDataChange={setData} />,
|
||||
validate: () => {
|
||||
return !!(data.name && data.checkType && data.weight);
|
||||
},
|
||||
component: QualityTemplateDetailsStep,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -12,60 +12,20 @@ import { showToast } from '../../../../utils/toast';
|
||||
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
|
||||
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
||||
|
||||
interface WizardDataProps extends WizardStepProps {
|
||||
data: Record<string, any>;
|
||||
onDataChange: (data: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||
const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||
const data = dataRef?.current || {};
|
||||
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 [loading, setLoading] = useState(false);
|
||||
|
||||
const handleFieldChange = (field: string, value: any) => {
|
||||
onDataChange?.({ ...data, [field]: value });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchFinishedProducts();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onDataChange({ ...data, ...recipeData });
|
||||
}, [recipeData]);
|
||||
|
||||
const fetchFinishedProducts = async () => {
|
||||
if (!currentTenant?.id) return;
|
||||
setLoading(true);
|
||||
@@ -97,8 +57,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={recipeData.name}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, name: e.target.value })}
|
||||
value={data.name}
|
||||
onChange={(e) => handleFieldChange('name', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -110,8 +70,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
Category *
|
||||
</label>
|
||||
<select
|
||||
value={recipeData.category}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, category: e.target.value })}
|
||||
value={data.category}
|
||||
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)]"
|
||||
>
|
||||
<option value="bread">Bread</option>
|
||||
@@ -133,8 +93,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<select
|
||||
value={recipeData.finishedProductId}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, finishedProductId: e.target.value })}
|
||||
value={data.finishedProductId}
|
||||
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)]"
|
||||
disabled={loading}
|
||||
>
|
||||
@@ -153,8 +113,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={recipeData.yieldQuantity}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, yieldQuantity: e.target.value })}
|
||||
value={data.yieldQuantity}
|
||||
onChange={(e) => handleFieldChange('yieldQuantity', e.target.value)}
|
||||
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)]"
|
||||
min="0.01"
|
||||
@@ -167,8 +127,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
Yield Unit *
|
||||
</label>
|
||||
<select
|
||||
value={recipeData.yieldUnit}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, yieldUnit: e.target.value })}
|
||||
value={data.yieldUnit}
|
||||
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)]"
|
||||
>
|
||||
<option value="units">Units</option>
|
||||
@@ -187,8 +147,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={recipeData.prepTime}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, prepTime: e.target.value })}
|
||||
value={data.prepTime}
|
||||
onChange={(e) => handleFieldChange('prepTime', e.target.value)}
|
||||
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)]"
|
||||
min="0"
|
||||
@@ -200,8 +160,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
Instructions
|
||||
</label>
|
||||
<textarea
|
||||
value={recipeData.instructions}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, instructions: e.target.value })}
|
||||
value={data.instructions}
|
||||
onChange={(e) => handleFieldChange('instructions', e.target.value)}
|
||||
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)]"
|
||||
rows={4}
|
||||
@@ -221,8 +181,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={recipeData.recipeCode}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, recipeCode: e.target.value })}
|
||||
value={data.recipeCode}
|
||||
onChange={(e) => handleFieldChange('recipeCode', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -234,8 +194,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={recipeData.version}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, version: e.target.value })}
|
||||
value={data.version}
|
||||
onChange={(e) => handleFieldChange('version', e.target.value)}
|
||||
placeholder="1.0"
|
||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
/>
|
||||
@@ -250,8 +210,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={recipeData.difficultyLevel}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, difficultyLevel: parseInt(e.target.value) || 1 })}
|
||||
value={data.difficultyLevel}
|
||||
onChange={(e) => handleFieldChange('difficultyLevel', parseInt(e.target.value) || 1)}
|
||||
min="1"
|
||||
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)]"
|
||||
@@ -264,8 +224,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={recipeData.cookTime}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, cookTime: e.target.value })}
|
||||
value={data.cookTime}
|
||||
onChange={(e) => handleFieldChange('cookTime', e.target.value)}
|
||||
placeholder="30"
|
||||
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)]"
|
||||
@@ -281,8 +241,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={recipeData.restTime}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, restTime: e.target.value })}
|
||||
value={data.restTime}
|
||||
onChange={(e) => handleFieldChange('restTime', e.target.value)}
|
||||
placeholder="60"
|
||||
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)]"
|
||||
@@ -295,8 +255,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={recipeData.totalTime}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, totalTime: e.target.value })}
|
||||
value={data.totalTime}
|
||||
onChange={(e) => handleFieldChange('totalTime', e.target.value)}
|
||||
placeholder="90"
|
||||
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)]"
|
||||
@@ -309,8 +269,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={recipeData.servesCount}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, servesCount: e.target.value })}
|
||||
value={data.servesCount}
|
||||
onChange={(e) => handleFieldChange('servesCount', e.target.value)}
|
||||
placeholder="8"
|
||||
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)]"
|
||||
@@ -326,8 +286,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={recipeData.batchSizeMultiplier}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, batchSizeMultiplier: parseFloat(e.target.value) || 1 })}
|
||||
value={data.batchSizeMultiplier}
|
||||
onChange={(e) => handleFieldChange('batchSizeMultiplier', parseFloat(e.target.value) || 1)}
|
||||
placeholder="1.0"
|
||||
min="0.1"
|
||||
step="0.1"
|
||||
@@ -341,8 +301,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={recipeData.minBatchSize}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, minBatchSize: e.target.value })}
|
||||
value={data.minBatchSize}
|
||||
onChange={(e) => handleFieldChange('minBatchSize', e.target.value)}
|
||||
placeholder="5"
|
||||
min="0"
|
||||
step="0.1"
|
||||
@@ -356,8 +316,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={recipeData.maxBatchSize}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, maxBatchSize: e.target.value })}
|
||||
value={data.maxBatchSize}
|
||||
onChange={(e) => handleFieldChange('maxBatchSize', e.target.value)}
|
||||
placeholder="100"
|
||||
min="0"
|
||||
step="0.1"
|
||||
@@ -371,8 +331,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={recipeData.optimalProductionTemp}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, optimalProductionTemp: e.target.value })}
|
||||
value={data.optimalProductionTemp}
|
||||
onChange={(e) => handleFieldChange('optimalProductionTemp', e.target.value)}
|
||||
placeholder="24"
|
||||
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)]"
|
||||
@@ -385,8 +345,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={recipeData.optimalHumidity}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, optimalHumidity: e.target.value })}
|
||||
value={data.optimalHumidity}
|
||||
onChange={(e) => handleFieldChange('optimalHumidity', e.target.value)}
|
||||
placeholder="65"
|
||||
min="0"
|
||||
max="100"
|
||||
@@ -401,8 +361,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={recipeData.targetMargin}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, targetMargin: e.target.value })}
|
||||
value={data.targetMargin}
|
||||
onChange={(e) => handleFieldChange('targetMargin', e.target.value)}
|
||||
placeholder="30"
|
||||
min="0"
|
||||
max="100"
|
||||
@@ -417,8 +377,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isSeasonal"
|
||||
checked={recipeData.isSeasonal}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, isSeasonal: e.target.checked })}
|
||||
checked={data.isSeasonal}
|
||||
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)]"
|
||||
/>
|
||||
<label htmlFor="isSeasonal" className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
@@ -430,8 +390,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isSignatureItem"
|
||||
checked={recipeData.isSignatureItem}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, isSignatureItem: e.target.checked })}
|
||||
checked={data.isSignatureItem}
|
||||
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)]"
|
||||
/>
|
||||
<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>
|
||||
|
||||
{recipeData.isSeasonal && (
|
||||
{data.isSeasonal && (
|
||||
<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">
|
||||
Season Start Month
|
||||
</label>
|
||||
<select
|
||||
value={recipeData.seasonStartMonth}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, seasonStartMonth: e.target.value })}
|
||||
value={data.seasonStartMonth}
|
||||
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)]"
|
||||
>
|
||||
<option value="">Select month...</option>
|
||||
@@ -465,8 +425,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
Season End Month
|
||||
</label>
|
||||
<select
|
||||
value={recipeData.seasonEndMonth}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, seasonEndMonth: e.target.value })}
|
||||
value={data.seasonEndMonth}
|
||||
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)]"
|
||||
>
|
||||
<option value="">Select month...</option>
|
||||
@@ -485,8 +445,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={recipeData.description}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, description: e.target.value })}
|
||||
value={data.description}
|
||||
onChange={(e) => handleFieldChange('description', e.target.value)}
|
||||
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)]"
|
||||
rows={3}
|
||||
@@ -498,8 +458,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
Preparation Notes
|
||||
</label>
|
||||
<textarea
|
||||
value={recipeData.preparationNotes}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, preparationNotes: e.target.value })}
|
||||
value={data.preparationNotes}
|
||||
onChange={(e) => handleFieldChange('preparationNotes', e.target.value)}
|
||||
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)]"
|
||||
rows={3}
|
||||
@@ -511,8 +471,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
Storage Instructions
|
||||
</label>
|
||||
<textarea
|
||||
value={recipeData.storageInstructions}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, storageInstructions: e.target.value })}
|
||||
value={data.storageInstructions}
|
||||
onChange={(e) => handleFieldChange('storageInstructions', e.target.value)}
|
||||
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)]"
|
||||
rows={3}
|
||||
@@ -525,8 +485,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={recipeData.allergens}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, allergens: e.target.value })}
|
||||
value={data.allergens}
|
||||
onChange={(e) => handleFieldChange('allergens', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -538,8 +498,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={recipeData.dietaryTags}
|
||||
onChange={(e) => setRecipeData({ ...recipeData, dietaryTags: e.target.value })}
|
||||
value={data.dietaryTags}
|
||||
onChange={(e) => handleFieldChange('dietaryTags', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -558,10 +518,10 @@ interface SelectedIngredient {
|
||||
order: number;
|
||||
}
|
||||
|
||||
const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||
const IngredientsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||
const data = dataRef?.current || {};
|
||||
const { currentTenant } = useTenant();
|
||||
const [ingredients, setIngredients] = useState<IngredientResponse[]>([]);
|
||||
const [selectedIngredients, setSelectedIngredients] = useState<SelectedIngredient[]>(data.ingredients || []);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
@@ -570,10 +530,6 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||
fetchIngredients();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onDataChange({ ...data, ingredients: selectedIngredients });
|
||||
}, [selectedIngredients]);
|
||||
|
||||
const fetchIngredients = async () => {
|
||||
if (!currentTenant?.id) return;
|
||||
setLoading(true);
|
||||
@@ -596,21 +552,25 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||
quantity: 0,
|
||||
unit: MeasurementUnit.GRAMS,
|
||||
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) => {
|
||||
setSelectedIngredients(
|
||||
selectedIngredients.map(ing =>
|
||||
onDataChange?.({
|
||||
...data,
|
||||
ingredients: (data.ingredients || []).map(ing =>
|
||||
ing.id === id ? { ...ing, [field]: value } : ing
|
||||
)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
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 =>
|
||||
@@ -639,14 +599,14 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
||||
) : (
|
||||
<>
|
||||
<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">
|
||||
<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-sm text-[var(--text-tertiary)]">Click "Add Ingredient" to begin</p>
|
||||
</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 className="grid grid-cols-1 md:grid-cols-12 gap-3 items-start">
|
||||
<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 [templates, setTemplates] = useState<QualityCheckTemplateResponse[]>([]);
|
||||
const [selectedTemplateIds, setSelectedTemplateIds] = useState<string[]>(data.selectedTemplates || []);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -749,10 +709,6 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
||||
fetchTemplates();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onDataChange({ ...data, selectedTemplates: selectedTemplateIds });
|
||||
}, [selectedTemplateIds]);
|
||||
|
||||
const fetchTemplates = async () => {
|
||||
if (!currentTenant?.id) return;
|
||||
setLoading(true);
|
||||
@@ -768,11 +724,11 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
||||
};
|
||||
|
||||
const toggleTemplate = (templateId: string) => {
|
||||
setSelectedTemplateIds(prev =>
|
||||
prev.includes(templateId)
|
||||
? prev.filter(id => id !== templateId)
|
||||
: [...prev, templateId]
|
||||
);
|
||||
const currentTemplates = data.selectedTemplates || [];
|
||||
const newTemplates = currentTemplates.includes(templateId)
|
||||
? currentTemplates.filter(id => id !== templateId)
|
||||
: [...currentTemplates, templateId];
|
||||
onDataChange?.({ ...data, selectedTemplates: newTemplates });
|
||||
};
|
||||
|
||||
const handleCreateRecipe = async () => {
|
||||
@@ -795,11 +751,11 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
||||
}));
|
||||
|
||||
let qualityConfig: RecipeQualityConfiguration | undefined;
|
||||
if (selectedTemplateIds.length > 0) {
|
||||
if ((data.selectedTemplates || []).length > 0) {
|
||||
qualityConfig = {
|
||||
stages: {
|
||||
production: {
|
||||
template_ids: selectedTemplateIds,
|
||||
template_ids: data.selectedTemplates || [],
|
||||
required_checks: [],
|
||||
optional_checks: [],
|
||||
blocking_on_failure: true,
|
||||
@@ -901,7 +857,7 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
||||
type="button"
|
||||
onClick={() => toggleTemplate(template.id)}
|
||||
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(--border-secondary)] hover:border-[var(--color-primary)]/50'
|
||||
}`}
|
||||
@@ -928,7 +884,7 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
||||
)}
|
||||
</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" />
|
||||
)}
|
||||
</div>
|
||||
@@ -937,10 +893,10 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
||||
</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">
|
||||
<p className="text-sm text-[var(--text-primary)]">
|
||||
<strong>{selectedTemplateIds.length}</strong> template(s) selected
|
||||
<strong>{(data.selectedTemplates || []).length}</strong> template(s) selected
|
||||
</p>
|
||||
</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
|
||||
// dataRef and onDataChange are now passed through WizardModal props
|
||||
return [
|
||||
{
|
||||
id: 'recipe-details',
|
||||
title: 'Recipe Details',
|
||||
description: 'Name, category, yield',
|
||||
component: (props) => <RecipeDetailsStep {...props} data={data} onDataChange={setData} />,
|
||||
validate: () => {
|
||||
return !!(data.name && data.finishedProductId && data.yieldQuantity);
|
||||
},
|
||||
component: RecipeDetailsStep,
|
||||
},
|
||||
{
|
||||
id: 'recipe-ingredients',
|
||||
title: 'Ingredients',
|
||||
description: 'Selection and quantities',
|
||||
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;
|
||||
},
|
||||
component: IngredientsStep,
|
||||
},
|
||||
{
|
||||
id: 'recipe-quality-templates',
|
||||
title: 'Quality Templates',
|
||||
description: 'Applicable quality controls',
|
||||
component: (props) => <QualityTemplatesStep {...props} data={data} onDataChange={setData} />,
|
||||
component: QualityTemplatesStep,
|
||||
isOptional: true,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Edit3,
|
||||
Upload,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Download,
|
||||
FileSpreadsheet,
|
||||
Calendar,
|
||||
@@ -23,24 +22,18 @@ import { showToast } from '../../../../utils/toast';
|
||||
// STEP 1: Entry Method Selection
|
||||
// ========================================
|
||||
|
||||
interface EntryMethodStepProps extends WizardStepProps {
|
||||
data: Record<string, any>;
|
||||
onDataChange: (data: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
const EntryMethodStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, onNext }) => {
|
||||
const EntryMethodStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNext }) => {
|
||||
const data = dataRef?.current || {};
|
||||
const [selectedMethod, setSelectedMethod] = useState<'manual' | 'upload'>(
|
||||
data.entryMethod || 'manual'
|
||||
);
|
||||
|
||||
const handleSelect = (method: 'manual' | 'upload') => {
|
||||
setSelectedMethod(method);
|
||||
onDataChange({ ...data, entryMethod: method });
|
||||
};
|
||||
|
||||
const handleContinue = () => {
|
||||
onDataChange({ ...data, entryMethod: selectedMethod });
|
||||
onNext();
|
||||
const newData = { ...data, entryMethod: method };
|
||||
onDataChange?.(newData);
|
||||
// Automatically advance to next step after selection
|
||||
setTimeout(() => onNext?.(), 100);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -166,17 +159,6 @@ const EntryMethodStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
</div>
|
||||
</button>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -185,14 +167,9 @@ const EntryMethodStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
// 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 [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 [loadingProducts, setLoadingProducts] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -219,14 +196,15 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
};
|
||||
|
||||
const handleAddItem = () => {
|
||||
setSalesItems([
|
||||
...salesItems,
|
||||
const newItems = [
|
||||
...(data.salesItems || []),
|
||||
{ id: Date.now(), productId: '', product: '', quantity: 1, unitPrice: 0, subtotal: 0 },
|
||||
]);
|
||||
];
|
||||
onDataChange?.({ ...data, salesItems: newItems });
|
||||
};
|
||||
|
||||
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) {
|
||||
const newItem = { ...item, [field]: value };
|
||||
|
||||
@@ -247,28 +225,25 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
}
|
||||
return item;
|
||||
});
|
||||
setSalesItems(updated);
|
||||
onDataChange?.({ ...data, salesItems: updated });
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
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 = () => {
|
||||
onDataChange({
|
||||
// Auto-save totalAmount when items change
|
||||
useEffect(() => {
|
||||
onDataChange?.({
|
||||
...data,
|
||||
salesItems,
|
||||
saleDate,
|
||||
paymentMethod,
|
||||
notes,
|
||||
totalAmount: calculateTotal(),
|
||||
});
|
||||
onNext();
|
||||
};
|
||||
}, [data.salesItems]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -290,8 +265,8 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={saleDate}
|
||||
onChange={(e) => setSaleDate(e.target.value)}
|
||||
value={data.saleDate || new Date().toISOString().split('T')[0]}
|
||||
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)]"
|
||||
/>
|
||||
</div>
|
||||
@@ -302,8 +277,8 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
Método de Pago *
|
||||
</label>
|
||||
<select
|
||||
value={paymentMethod}
|
||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
||||
value={data.paymentMethod || 'cash'}
|
||||
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)]"
|
||||
>
|
||||
<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 className="text-sm">Agrega productos al inventario primero</p>
|
||||
</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)]">
|
||||
<Package className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p>No hay productos agregados</p>
|
||||
@@ -357,7 +332,7 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{salesItems.map((item: any, index: number) => (
|
||||
{(data.salesItems || []).map((item: any, index: number) => (
|
||||
<div
|
||||
key={item.id}
|
||||
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 */}
|
||||
{salesItems.length > 0 && (
|
||||
{(data.salesItems || []).length > 0 && (
|
||||
<div className="pt-3 border-t border-[var(--border-primary)] text-right">
|
||||
<span className="text-lg font-bold text-[var(--text-primary)]">
|
||||
Total: €{calculateTotal().toFixed(2)}
|
||||
@@ -436,24 +411,13 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
Notas (Opcional)
|
||||
</label>
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
value={data.notes || ''}
|
||||
onChange={(e) => onDataChange?.({ ...data, notes: e.target.value })}
|
||||
placeholder="Información adicional sobre esta venta..."
|
||||
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"
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -462,9 +426,9 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
||||
// 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 [file, setFile] = useState<File | null>(data.uploadedFile || null);
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [importing, setImporting] = useState(false);
|
||||
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 selectedFile = e.target.files?.[0];
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
onDataChange?.({ ...data, uploadedFile: selectedFile });
|
||||
setValidationResult(null);
|
||||
setError(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveFile = () => {
|
||||
setFile(null);
|
||||
onDataChange?.({ ...data, uploadedFile: null });
|
||||
setValidationResult(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleValidate = async () => {
|
||||
if (!file || !currentTenant?.id) return;
|
||||
if (!data.uploadedFile || !currentTenant?.id) return;
|
||||
|
||||
setValidating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await salesService.validateImportFile(currentTenant.id, file);
|
||||
const result = await salesService.validateImportFile(currentTenant.id, data.uploadedFile);
|
||||
setValidationResult(result);
|
||||
} catch (err: any) {
|
||||
console.error('Error validating file:', err);
|
||||
@@ -504,15 +468,15 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!file || !currentTenant?.id) return;
|
||||
if (!data.uploadedFile || !currentTenant?.id) return;
|
||||
|
||||
setImporting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await salesService.importSalesData(currentTenant.id, file, false);
|
||||
onDataChange({ ...data, uploadedFile: file, importResult: result });
|
||||
onNext();
|
||||
const result = await salesService.importSalesData(currentTenant.id, data.uploadedFile, false);
|
||||
onDataChange?.({ ...data, importResult: result });
|
||||
onNext?.();
|
||||
} catch (err: any) {
|
||||
console.error('Error importing file:', err);
|
||||
setError(err.response?.data?.detail || 'Error al importar el archivo');
|
||||
@@ -583,7 +547,7 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<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">
|
||||
@@ -613,9 +577,9 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
|
||||
<div className="flex items-center gap-3">
|
||||
<FileSpreadsheet className="w-8 h-8 text-[var(--color-primary)]" />
|
||||
<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)]">
|
||||
{(file.size / 1024).toFixed(2)} KB
|
||||
{(data.uploadedFile.size / 1024).toFixed(2)} KB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -701,53 +665,8 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
|
||||
// STEP 3: Review & Confirm
|
||||
// ========================================
|
||||
|
||||
const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
|
||||
const { currentTenant } = useTenant();
|
||||
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 ReviewStep: React.FC<WizardStepProps> = ({ dataRef }) => {
|
||||
const data = dataRef?.current || {};
|
||||
|
||||
const isManual = data.entryMethod === 'manual';
|
||||
const isUpload = data.entryMethod === 'upload';
|
||||
@@ -768,13 +687,6 @@ const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
|
||||
</p>
|
||||
</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 && (
|
||||
<div className="space-y-4">
|
||||
{/* Summary */}
|
||||
@@ -796,10 +708,10 @@ const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
|
||||
{/* Items */}
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-2">
|
||||
Productos ({data.salesItems.length})
|
||||
Productos ({(data.salesItems || []).length})
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{data.salesItems.map((item: any) => (
|
||||
{(data.salesItems || []).map((item: any) => (
|
||||
<div
|
||||
key={item.id}
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
};
|
||||
@@ -883,20 +774,19 @@ const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
|
||||
// ========================================
|
||||
|
||||
export const SalesEntryWizardSteps = (
|
||||
data: Record<string, any>,
|
||||
dataRef: React.MutableRefObject<Record<string, any>>,
|
||||
setData: (data: Record<string, any>) => void
|
||||
): 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[] = [
|
||||
{
|
||||
id: 'entry-method',
|
||||
title: 'Método de Entrada',
|
||||
description: 'Elige cómo registrar las ventas',
|
||||
component: (props) => (
|
||||
<EntryMethodStep {...props} data={data} onDataChange={setData} />
|
||||
),
|
||||
component: EntryMethodStep,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -905,14 +795,18 @@ export const SalesEntryWizardSteps = (
|
||||
id: 'manual-entry',
|
||||
title: 'Ingresar Datos',
|
||||
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') {
|
||||
steps.push({
|
||||
id: 'file-upload',
|
||||
title: 'Cargar 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',
|
||||
title: 'Revisar',
|
||||
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;
|
||||
|
||||
@@ -7,62 +7,23 @@ import { showToast } from '../../../../utils/toast';
|
||||
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
|
||||
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
||||
|
||||
interface WizardDataProps extends WizardStepProps {
|
||||
data: Record<string, any>;
|
||||
onDataChange: (data: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
||||
const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onComplete }) => {
|
||||
// New architecture: access data from dataRef.current
|
||||
const data = dataRef?.current || {};
|
||||
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 [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!supplierData.supplierCode && supplierData.name) {
|
||||
const code = `SUP-${supplierData.name.substring(0, 3).toUpperCase()}-${Date.now().toString().slice(-4)}`;
|
||||
setSupplierData(prev => ({ ...prev, supplierCode: code }));
|
||||
}
|
||||
}, [supplierData.name]);
|
||||
const handleFieldChange = (field: string, value: any) => {
|
||||
onDataChange?.({ ...data, [field]: value });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onDataChange({ ...data, ...supplierData });
|
||||
}, [supplierData]);
|
||||
if (!data.supplierCode && data.name) {
|
||||
const code = `SUP-${data.name.substring(0, 3).toUpperCase()}-${Date.now().toString().slice(-4)}`;
|
||||
onDataChange?.({ ...data, supplierCode: code });
|
||||
}
|
||||
}, [data.name]);
|
||||
|
||||
const handleCreateSupplier = async () => {
|
||||
if (!currentTenant?.id) {
|
||||
@@ -75,41 +36,41 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
name: supplierData.name,
|
||||
supplier_type: supplierData.supplierType,
|
||||
status: supplierData.status,
|
||||
payment_terms: supplierData.paymentTerms,
|
||||
currency: supplierData.currency,
|
||||
standard_lead_time: supplierData.standardLeadTime,
|
||||
supplier_code: supplierData.supplierCode || undefined,
|
||||
tax_id: supplierData.taxId || undefined,
|
||||
registration_number: supplierData.registrationNumber || undefined,
|
||||
contact_person: supplierData.contactPerson || undefined,
|
||||
email: supplierData.email || undefined,
|
||||
phone: supplierData.phone || undefined,
|
||||
mobile: supplierData.mobile || undefined,
|
||||
website: supplierData.website || undefined,
|
||||
address_line1: supplierData.addressLine1 || undefined,
|
||||
address_line2: supplierData.addressLine2 || undefined,
|
||||
city: supplierData.city || undefined,
|
||||
state_province: supplierData.stateProvince || undefined,
|
||||
postal_code: supplierData.postalCode || undefined,
|
||||
country: supplierData.country || undefined,
|
||||
credit_limit: supplierData.creditLimit ? parseFloat(supplierData.creditLimit) : undefined,
|
||||
minimum_order_amount: supplierData.minimumOrderAmount ? parseFloat(supplierData.minimumOrderAmount) : undefined,
|
||||
delivery_area: supplierData.deliveryArea || undefined,
|
||||
is_preferred_supplier: supplierData.isPreferredSupplier,
|
||||
auto_approve_enabled: supplierData.autoApproveEnabled,
|
||||
notes: supplierData.notes || undefined,
|
||||
certifications: supplierData.certifications ? JSON.parse(`{"items": ${JSON.stringify(supplierData.certifications.split(',').map(c => c.trim()))}}`) : undefined,
|
||||
specializations: supplierData.specializations ? JSON.parse(`{"items": ${JSON.stringify(supplierData.specializations.split(',').map(s => s.trim()))}}`) : undefined,
|
||||
name: data.name,
|
||||
supplier_type: data.supplierType,
|
||||
status: data.status,
|
||||
payment_terms: data.paymentTerms,
|
||||
currency: data.currency,
|
||||
standard_lead_time: data.standardLeadTime,
|
||||
supplier_code: data.supplierCode || undefined,
|
||||
tax_id: data.taxId || undefined,
|
||||
registration_number: data.registrationNumber || undefined,
|
||||
contact_person: data.contactPerson || undefined,
|
||||
email: data.email || undefined,
|
||||
phone: data.phone || undefined,
|
||||
mobile: data.mobile || undefined,
|
||||
website: data.website || undefined,
|
||||
address_line1: data.addressLine1 || undefined,
|
||||
address_line2: data.addressLine2 || undefined,
|
||||
city: data.city || undefined,
|
||||
state_province: data.stateProvince || undefined,
|
||||
postal_code: data.postalCode || undefined,
|
||||
country: data.country || undefined,
|
||||
credit_limit: data.creditLimit ? parseFloat(data.creditLimit) : undefined,
|
||||
minimum_order_amount: data.minimumOrderAmount ? parseFloat(data.minimumOrderAmount) : undefined,
|
||||
delivery_area: data.deliveryArea || undefined,
|
||||
is_preferred_supplier: data.isPreferredSupplier,
|
||||
auto_approve_enabled: data.autoApproveEnabled,
|
||||
notes: data.notes || undefined,
|
||||
certifications: data.certifications ? JSON.parse(`{"items": ${JSON.stringify(data.certifications.split(',').map(c => c.trim()))}}`) : undefined,
|
||||
specializations: data.specializations ? JSON.parse(`{"items": ${JSON.stringify(data.specializations.split(',').map(s => s.trim()))}}`) : undefined,
|
||||
created_by: currentTenant.id,
|
||||
updated_by: currentTenant.id,
|
||||
};
|
||||
|
||||
await suppliersService.createSupplier(currentTenant.id, payload);
|
||||
showToast.success('Supplier created successfully');
|
||||
onComplete();
|
||||
// Let the wizard handle completion via the Next/Complete button
|
||||
} catch (err: any) {
|
||||
console.error('Error creating supplier:', err);
|
||||
const errorMessage = err.response?.data?.detail || 'Error creating supplier';
|
||||
@@ -143,8 +104,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={supplierData.name}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, name: e.target.value })}
|
||||
value={data.name}
|
||||
onChange={(e) => handleFieldChange('name', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -158,8 +119,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</Tooltip>
|
||||
</label>
|
||||
<select
|
||||
value={supplierData.supplierType}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, supplierType: e.target.value })}
|
||||
value={data.supplierType}
|
||||
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)]"
|
||||
>
|
||||
<option value="ingredients">Ingredients</option>
|
||||
@@ -176,8 +137,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
Status *
|
||||
</label>
|
||||
<select
|
||||
value={supplierData.status}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, status: e.target.value })}
|
||||
value={data.status}
|
||||
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)]"
|
||||
>
|
||||
<option value="active">Active</option>
|
||||
@@ -193,8 +154,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
Payment Terms *
|
||||
</label>
|
||||
<select
|
||||
value={supplierData.paymentTerms}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, paymentTerms: e.target.value })}
|
||||
value={data.paymentTerms}
|
||||
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)]"
|
||||
>
|
||||
<option value="cod">COD (Cash on Delivery)</option>
|
||||
@@ -213,8 +174,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={supplierData.currency}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, currency: e.target.value })}
|
||||
value={data.currency}
|
||||
onChange={(e) => handleFieldChange('currency', e.target.value)}
|
||||
placeholder="EUR"
|
||||
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)]"
|
||||
@@ -230,8 +191,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={supplierData.standardLeadTime}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, standardLeadTime: parseInt(e.target.value) || 0 })}
|
||||
value={data.standardLeadTime}
|
||||
onChange={(e) => handleFieldChange('standardLeadTime', parseInt(e.target.value) || 0)}
|
||||
placeholder="3"
|
||||
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)]"
|
||||
@@ -246,8 +207,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={supplierData.contactPerson}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, contactPerson: e.target.value })}
|
||||
value={data.contactPerson}
|
||||
onChange={(e) => handleFieldChange('contactPerson', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -259,8 +220,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={supplierData.email}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, email: e.target.value })}
|
||||
value={data.email}
|
||||
onChange={(e) => handleFieldChange('email', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -272,8 +233,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={supplierData.phone}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, phone: e.target.value })}
|
||||
value={data.phone}
|
||||
onChange={(e) => handleFieldChange('phone', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -294,8 +255,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={supplierData.supplierCode}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, supplierCode: e.target.value })}
|
||||
value={data.supplierCode}
|
||||
onChange={(e) => handleFieldChange('supplierCode', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -307,8 +268,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={supplierData.mobile}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, mobile: e.target.value })}
|
||||
value={data.mobile}
|
||||
onChange={(e) => handleFieldChange('mobile', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -320,8 +281,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={supplierData.taxId}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, taxId: e.target.value })}
|
||||
value={data.taxId}
|
||||
onChange={(e) => handleFieldChange('taxId', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -333,8 +294,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={supplierData.registrationNumber}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, registrationNumber: e.target.value })}
|
||||
value={data.registrationNumber}
|
||||
onChange={(e) => handleFieldChange('registrationNumber', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -346,8 +307,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
value={supplierData.website}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, website: e.target.value })}
|
||||
value={data.website}
|
||||
onChange={(e) => handleFieldChange('website', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -359,8 +320,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={supplierData.addressLine1}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, addressLine1: e.target.value })}
|
||||
value={data.addressLine1}
|
||||
onChange={(e) => handleFieldChange('addressLine1', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -372,8 +333,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={supplierData.addressLine2}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, addressLine2: e.target.value })}
|
||||
value={data.addressLine2}
|
||||
onChange={(e) => handleFieldChange('addressLine2', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -385,8 +346,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={supplierData.city}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, city: e.target.value })}
|
||||
value={data.city}
|
||||
onChange={(e) => handleFieldChange('city', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -398,8 +359,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={supplierData.stateProvince}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, stateProvince: e.target.value })}
|
||||
value={data.stateProvince}
|
||||
onChange={(e) => handleFieldChange('stateProvince', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -411,8 +372,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={supplierData.postalCode}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, postalCode: e.target.value })}
|
||||
value={data.postalCode}
|
||||
onChange={(e) => handleFieldChange('postalCode', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -424,8 +385,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={supplierData.country}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, country: e.target.value })}
|
||||
value={data.country}
|
||||
onChange={(e) => handleFieldChange('country', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -437,8 +398,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={supplierData.creditLimit}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, creditLimit: e.target.value })}
|
||||
value={data.creditLimit}
|
||||
onChange={(e) => handleFieldChange('creditLimit', e.target.value)}
|
||||
placeholder="10000.00"
|
||||
min="0"
|
||||
step="0.01"
|
||||
@@ -452,8 +413,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={supplierData.minimumOrderAmount}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, minimumOrderAmount: e.target.value })}
|
||||
value={data.minimumOrderAmount}
|
||||
onChange={(e) => handleFieldChange('minimumOrderAmount', e.target.value)}
|
||||
placeholder="100.00"
|
||||
min="0"
|
||||
step="0.01"
|
||||
@@ -467,8 +428,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={supplierData.deliveryArea}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, deliveryArea: e.target.value })}
|
||||
value={data.deliveryArea}
|
||||
onChange={(e) => handleFieldChange('deliveryArea', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -480,8 +441,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isPreferredSupplier"
|
||||
checked={supplierData.isPreferredSupplier}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, isPreferredSupplier: e.target.checked })}
|
||||
checked={data.isPreferredSupplier}
|
||||
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)]"
|
||||
/>
|
||||
<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
|
||||
type="checkbox"
|
||||
id="autoApproveEnabled"
|
||||
checked={supplierData.autoApproveEnabled}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, autoApproveEnabled: e.target.checked })}
|
||||
checked={data.autoApproveEnabled}
|
||||
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)]"
|
||||
/>
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
value={supplierData.certifications}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, certifications: e.target.value })}
|
||||
value={data.certifications}
|
||||
onChange={(e) => handleFieldChange('certifications', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -522,8 +483,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={supplierData.specializations}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, specializations: e.target.value })}
|
||||
value={data.specializations}
|
||||
onChange={(e) => handleFieldChange('specializations', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -534,8 +495,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
Notes
|
||||
</label>
|
||||
<textarea
|
||||
value={supplierData.notes}
|
||||
onChange={(e) => setSupplierData({ ...supplierData, notes: e.target.value })}
|
||||
value={data.notes}
|
||||
onChange={(e) => handleFieldChange('notes', e.target.value)}
|
||||
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)]"
|
||||
rows={3}
|
||||
@@ -544,48 +505,22 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
export const SupplierWizardSteps = (
|
||||
data: Record<string, any>,
|
||||
dataRef: React.MutableRefObject<Record<string, any>>,
|
||||
setData: (data: Record<string, any>) => void
|
||||
): WizardStep[] => [
|
||||
): WizardStep[] => {
|
||||
// New architecture: return direct component reference instead of arrow function
|
||||
// dataRef and onDataChange are now passed through WizardModal props
|
||||
return [
|
||||
{
|
||||
id: 'supplier-details',
|
||||
title: 'Supplier Details',
|
||||
description: 'Essential supplier information',
|
||||
component: (props) => <SupplierDetailsStep {...props} data={data} onDataChange={setData} />,
|
||||
validate: () => {
|
||||
return !!(
|
||||
data.name &&
|
||||
data.supplierType &&
|
||||
data.status &&
|
||||
data.paymentTerms &&
|
||||
data.currency &&
|
||||
data.standardLeadTime >= 0
|
||||
);
|
||||
},
|
||||
component: SupplierDetailsStep,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
||||
import { UserPlus, Shield, CheckCircle2, Mail, Phone, Loader2 } from 'lucide-react';
|
||||
import { useTenant } from '../../../../stores/tenant.store';
|
||||
import { authService } from '../../../../api/services/auth';
|
||||
import { UserPlus, Shield, Mail, Phone } from 'lucide-react';
|
||||
|
||||
interface WizardDataProps extends WizardStepProps {
|
||||
data: Record<string, any>;
|
||||
onDataChange: (data: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
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',
|
||||
});
|
||||
const MemberDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||
const data = dataRef?.current || {};
|
||||
const handleFieldChange = (field: string, value: any) => {
|
||||
onDataChange?.({ ...data, [field]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
value={memberData.fullName}
|
||||
onChange={(e) => setMemberData({ ...memberData, fullName: e.target.value })}
|
||||
value={data.fullName || ''}
|
||||
onChange={(e) => handleFieldChange('fullName', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -42,8 +32,8 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={memberData.email}
|
||||
onChange={(e) => setMemberData({ ...memberData, email: e.target.value })}
|
||||
value={data.email || ''}
|
||||
onChange={(e) => handleFieldChange('email', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -55,8 +45,8 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={memberData.phone}
|
||||
onChange={(e) => setMemberData({ ...memberData, phone: e.target.value })}
|
||||
value={data.phone || ''}
|
||||
onChange={(e) => handleFieldChange('phone', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
@@ -64,8 +54,8 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Posición *</label>
|
||||
<select
|
||||
value={memberData.position}
|
||||
onChange={(e) => setMemberData({ ...memberData, position: e.target.value })}
|
||||
value={data.position || 'baker'}
|
||||
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)]"
|
||||
>
|
||||
<option value="baker">Panadero</option>
|
||||
@@ -78,8 +68,8 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Tipo de Empleo</label>
|
||||
<select
|
||||
value={memberData.employmentType}
|
||||
onChange={(e) => setMemberData({ ...memberData, employmentType: e.target.value })}
|
||||
value={data.employmentType || 'full-time'}
|
||||
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)]"
|
||||
>
|
||||
<option value="full-time">Tiempo Completo</option>
|
||||
@@ -88,72 +78,15 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
|
||||
</select>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
const PermissionsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
||||
const { currentTenant } = useTenant();
|
||||
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 PermissionsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||
const data = dataRef?.current || {};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!currentTenant?.id) {
|
||||
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);
|
||||
}
|
||||
const handleFieldChange = (field: string, value: any) => {
|
||||
onDataChange?.({ ...data, [field]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -164,17 +97,11 @@ const PermissionsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComp
|
||||
<p className="text-sm text-[var(--text-secondary)]">{data.fullName}</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-3">Rol del Sistema</label>
|
||||
<select
|
||||
value={permissions.role}
|
||||
onChange={(e) => setPermissions({ ...permissions, role: e.target.value })}
|
||||
value={data.role}
|
||||
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)]"
|
||||
>
|
||||
<option value="admin">Administrador</option>
|
||||
@@ -198,8 +125,8 @@ const PermissionsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComp
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={permissions[key as keyof typeof permissions] as boolean}
|
||||
onChange={(e) => setPermissions({ ...permissions, [key]: e.target.checked })}
|
||||
checked={data[key] ?? false}
|
||||
onChange={(e) => handleFieldChange(key, e.target.checked)}
|
||||
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>
|
||||
@@ -207,30 +134,68 @@ const PermissionsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComp
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
export const TeamMemberWizardSteps = (data: 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} /> },
|
||||
{ id: 'member-permissions', title: 'Rol y Permisos', description: 'Accesos al sistema', component: (props) => <PermissionsStep {...props} data={data} onDataChange={setData} /> },
|
||||
export const TeamMemberWizardSteps = (dataRef: React.MutableRefObject<Record<string, any>>, setData: (data: Record<string, any>) => void): WizardStep[] => {
|
||||
// New architecture: return direct component references instead of arrow functions
|
||||
// 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;
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { DraftRecoveryPrompt } from './DraftRecoveryPrompt';
|
||||
@@ -19,6 +19,9 @@ export interface WizardStepProps {
|
||||
currentStepIndex: number;
|
||||
totalSteps: number;
|
||||
goToStep: (index: number) => void;
|
||||
// New architecture: dataRef and onDataChange passed from UnifiedAddWizard
|
||||
dataRef?: React.MutableRefObject<any>;
|
||||
onDataChange?: (data: any) => void;
|
||||
}
|
||||
|
||||
interface WizardModalProps {
|
||||
@@ -29,6 +32,9 @@ interface WizardModalProps {
|
||||
steps: WizardStep[];
|
||||
icon?: React.ReactNode;
|
||||
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> = ({
|
||||
@@ -38,7 +44,9 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
||||
title,
|
||||
steps,
|
||||
icon,
|
||||
size = 'xl'
|
||||
size = 'xl',
|
||||
dataRef,
|
||||
onDataChange
|
||||
}) => {
|
||||
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
@@ -55,12 +63,36 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
||||
'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 () => {
|
||||
// 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
|
||||
if (currentStep.validate) {
|
||||
if (step.validate) {
|
||||
setIsValidating(true);
|
||||
try {
|
||||
const isValid = await currentStep.validate();
|
||||
const isValid = await step.validate();
|
||||
if (!isValid) {
|
||||
setIsValidating(false);
|
||||
return;
|
||||
@@ -73,32 +105,12 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
||||
setIsValidating(false);
|
||||
}
|
||||
|
||||
if (isLastStep) {
|
||||
if (lastStep) {
|
||||
handleComplete();
|
||||
} else {
|
||||
setCurrentStepIndex(prev => Math.min(prev + 1, steps.length - 1));
|
||||
}
|
||||
}, [currentStep, isLastStep, steps.length]);
|
||||
|
||||
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]);
|
||||
}, [steps, currentStepIndex, handleComplete]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
@@ -184,6 +196,8 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
||||
currentStepIndex={currentStepIndex}
|
||||
totalSteps={steps.length}
|
||||
goToStep={goToStep}
|
||||
dataRef={dataRef}
|
||||
onDataChange={onDataChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
@@ -162,6 +162,20 @@ export function NewDashboardPage() {
|
||||
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
|
||||
useEffect(() => {
|
||||
console.log('[Dashboard] Demo mode:', isDemoMode);
|
||||
@@ -221,18 +235,29 @@ export function NewDashboardPage() {
|
||||
<span className="hidden sm:inline">{t('common:actions.refresh')}</span>
|
||||
</button>
|
||||
|
||||
{/* Unified Add Button */}
|
||||
{/* Unified Add Button with Keyboard Shortcut */}
|
||||
<button
|
||||
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={{
|
||||
background: 'linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%)',
|
||||
color: 'white'
|
||||
}}
|
||||
title={`Quick Add (${navigator.platform.includes('Mac') ? 'Cmd' : 'Ctrl'}+K)`}
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
<span className="hidden sm:inline">{t('common:actions.add')}</span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user