Improve the UI add button
This commit is contained in:
@@ -2,14 +2,14 @@ import React from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Package,
|
Package,
|
||||||
Building2,
|
Building,
|
||||||
ChefHat,
|
ChefHat,
|
||||||
Wrench,
|
Wrench,
|
||||||
ClipboardCheck,
|
ClipboardCheck,
|
||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
Users,
|
Users,
|
||||||
UserPlus,
|
UserPlus,
|
||||||
Euro,
|
Euro as EuroIcon,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
|
|||||||
id: 'sales-entry',
|
id: 'sales-entry',
|
||||||
title: 'Registro de Ventas',
|
title: 'Registro de Ventas',
|
||||||
subtitle: 'Manual o carga masiva',
|
subtitle: 'Manual o carga masiva',
|
||||||
icon: Euro,
|
icon: EuroIcon,
|
||||||
badge: '⭐ Más Común',
|
badge: '⭐ Más Común',
|
||||||
badgeColor: 'bg-gradient-to-r from-amber-100 to-orange-100 text-orange-800 font-semibold',
|
badgeColor: 'bg-gradient-to-r from-amber-100 to-orange-100 text-orange-800 font-semibold',
|
||||||
isHighlighted: true,
|
isHighlighted: true,
|
||||||
@@ -56,7 +56,7 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
|
|||||||
id: 'supplier',
|
id: 'supplier',
|
||||||
title: 'Proveedor',
|
title: 'Proveedor',
|
||||||
subtitle: 'Relación comercial',
|
subtitle: 'Relación comercial',
|
||||||
icon: Building2,
|
icon: Building,
|
||||||
badge: 'Configuración',
|
badge: 'Configuración',
|
||||||
badgeColor: 'bg-blue-100 text-blue-700',
|
badgeColor: 'bg-blue-100 text-blue-700',
|
||||||
},
|
},
|
||||||
@@ -117,83 +117,6 @@ interface ItemTypeSelectorProps {
|
|||||||
export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect }) => {
|
export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect }) => {
|
||||||
const { t } = useTranslation('wizards');
|
const { t } = useTranslation('wizards');
|
||||||
|
|
||||||
// Generate item types from translations
|
|
||||||
const itemTypes: ItemTypeConfig[] = [
|
|
||||||
{
|
|
||||||
id: 'sales-entry',
|
|
||||||
title: t('itemTypeSelector.types.sales-entry.title'),
|
|
||||||
subtitle: t('itemTypeSelector.types.sales-entry.description'),
|
|
||||||
icon: Euro,
|
|
||||||
badge: '⭐ ' + t('itemTypeSelector.mostCommon', { defaultValue: 'Most Common' }),
|
|
||||||
badgeColor: 'bg-gradient-to-r from-amber-100 to-orange-100 text-orange-800 font-semibold',
|
|
||||||
isHighlighted: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'inventory',
|
|
||||||
title: t('itemTypeSelector.types.inventory.title'),
|
|
||||||
subtitle: t('itemTypeSelector.types.inventory.description'),
|
|
||||||
icon: Package,
|
|
||||||
badge: t('itemTypeSelector.configuration', { defaultValue: 'Configuration' }),
|
|
||||||
badgeColor: 'bg-blue-100 text-blue-700',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'supplier',
|
|
||||||
title: t('itemTypeSelector.types.supplier.title'),
|
|
||||||
subtitle: t('itemTypeSelector.types.supplier.description'),
|
|
||||||
icon: Building2,
|
|
||||||
badge: t('itemTypeSelector.configuration', { defaultValue: 'Configuration' }),
|
|
||||||
badgeColor: 'bg-blue-100 text-blue-700',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'recipe',
|
|
||||||
title: t('itemTypeSelector.types.recipe.title'),
|
|
||||||
subtitle: t('itemTypeSelector.types.recipe.description'),
|
|
||||||
icon: ChefHat,
|
|
||||||
badge: t('itemTypeSelector.common', { defaultValue: 'Common' }),
|
|
||||||
badgeColor: 'bg-green-100 text-green-700',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'equipment',
|
|
||||||
title: t('itemTypeSelector.types.equipment.title'),
|
|
||||||
subtitle: t('itemTypeSelector.types.equipment.description'),
|
|
||||||
icon: Wrench,
|
|
||||||
badge: t('itemTypeSelector.configuration', { defaultValue: 'Configuration' }),
|
|
||||||
badgeColor: 'bg-blue-100 text-blue-700',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'quality-template',
|
|
||||||
title: t('itemTypeSelector.types.quality-template.title'),
|
|
||||||
subtitle: t('itemTypeSelector.types.quality-template.description'),
|
|
||||||
icon: ClipboardCheck,
|
|
||||||
badge: t('itemTypeSelector.configuration', { defaultValue: 'Configuration' }),
|
|
||||||
badgeColor: 'bg-blue-100 text-blue-700',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'customer-order',
|
|
||||||
title: t('itemTypeSelector.types.customer-order.title'),
|
|
||||||
subtitle: t('itemTypeSelector.types.customer-order.description'),
|
|
||||||
icon: ShoppingCart,
|
|
||||||
badge: t('itemTypeSelector.daily', { defaultValue: 'Daily' }),
|
|
||||||
badgeColor: 'bg-amber-100 text-amber-700',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'customer',
|
|
||||||
title: t('itemTypeSelector.types.customer.title'),
|
|
||||||
subtitle: t('itemTypeSelector.types.customer.description'),
|
|
||||||
icon: Users,
|
|
||||||
badge: t('itemTypeSelector.common', { defaultValue: 'Common' }),
|
|
||||||
badgeColor: 'bg-green-100 text-green-700',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'team-member',
|
|
||||||
title: t('itemTypeSelector.types.team-member.title'),
|
|
||||||
subtitle: t('itemTypeSelector.types.team-member.description'),
|
|
||||||
icon: UserPlus,
|
|
||||||
badge: t('itemTypeSelector.configuration', { defaultValue: 'Configuration' }),
|
|
||||||
badgeColor: 'bg-blue-100 text-blue-700',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -213,7 +136,7 @@ export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect })
|
|||||||
|
|
||||||
{/* Item Type Grid */}
|
{/* Item Type Grid */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 md:gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 md:gap-4">
|
||||||
{itemTypes.map((itemType) => {
|
{ITEM_TYPES.map((itemType) => {
|
||||||
const Icon = itemType.icon;
|
const Icon = itemType.icon;
|
||||||
const isHighlighted = itemType.isHighlighted;
|
const isHighlighted = itemType.isHighlighted;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { useState, useCallback, useMemo } from 'react';
|
import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
||||||
import { Sparkles } from 'lucide-react';
|
import { Sparkles } from 'lucide-react';
|
||||||
import { WizardModal, WizardStep } from '../../ui/WizardModal/WizardModal';
|
import { WizardModal, WizardStep } from '../../ui/WizardModal/WizardModal';
|
||||||
import { ItemTypeSelector, ItemType } from './ItemTypeSelector';
|
import { ItemTypeSelector, ItemType } from './ItemTypeSelector';
|
||||||
|
import { AnyWizardData } from './types';
|
||||||
|
|
||||||
// Import specific wizards
|
// Import specific wizards
|
||||||
import { InventoryWizardSteps } from './wizards/InventoryWizard';
|
import { InventoryWizardSteps, ProductTypeStep, BasicInfoStep, StockConfigStep } from './wizards/InventoryWizard';
|
||||||
import { SupplierWizardSteps } from './wizards/SupplierWizard';
|
import { SupplierWizardSteps } from './wizards/SupplierWizard';
|
||||||
import { RecipeWizardSteps } from './wizards/RecipeWizard';
|
import { RecipeWizardSteps } from './wizards/RecipeWizard';
|
||||||
import { EquipmentWizardSteps } from './wizards/EquipmentWizard';
|
import { EquipmentWizardSteps } from './wizards/EquipmentWizard';
|
||||||
@@ -31,12 +32,22 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
|||||||
const [selectedItemType, setSelectedItemType] = useState<ItemType | null>(
|
const [selectedItemType, setSelectedItemType] = useState<ItemType | null>(
|
||||||
initialItemType || null
|
initialItemType || null
|
||||||
);
|
);
|
||||||
const [wizardData, setWizardData] = useState<Record<string, any>>({});
|
const [wizardData, setWizardData] = useState<AnyWizardData>({});
|
||||||
|
|
||||||
|
// Use a ref to store the current data - this allows step components
|
||||||
|
// to always access the latest data without causing the steps array to be recreated
|
||||||
|
const dataRef = useRef<AnyWizardData>({});
|
||||||
|
|
||||||
|
// Update ref whenever data changes
|
||||||
|
useEffect(() => {
|
||||||
|
dataRef.current = wizardData;
|
||||||
|
}, [wizardData]);
|
||||||
|
|
||||||
// Reset state when modal closes
|
// Reset state when modal closes
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
setSelectedItemType(initialItemType || null);
|
setSelectedItemType(initialItemType || null);
|
||||||
setWizardData({});
|
setWizardData({});
|
||||||
|
dataRef.current = {};
|
||||||
onClose();
|
onClose();
|
||||||
}, [onClose, initialItemType]);
|
}, [onClose, initialItemType]);
|
||||||
|
|
||||||
@@ -45,11 +56,23 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
|||||||
setSelectedItemType(itemType);
|
setSelectedItemType(itemType);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// CRITICAL FIX: Update both ref AND state, but wizardSteps won't recreate
|
||||||
|
// The step component needs to re-render to show typed text (controlled inputs)
|
||||||
|
// But wizardSteps useMemo ensures steps array doesn't recreate, so no component recreation
|
||||||
|
const handleDataChange = useCallback((newData: AnyWizardData) => {
|
||||||
|
// Update ref first for immediate access
|
||||||
|
dataRef.current = newData;
|
||||||
|
// Update state to trigger re-render (controlled inputs need this)
|
||||||
|
setWizardData(newData);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Handle wizard completion
|
// Handle wizard completion
|
||||||
const handleWizardComplete = useCallback(
|
const handleWizardComplete = useCallback(
|
||||||
(data?: any) => {
|
(data?: any) => {
|
||||||
if (selectedItemType) {
|
if (selectedItemType) {
|
||||||
onComplete?.(selectedItemType, data);
|
// On completion, sync the ref to state for submission
|
||||||
|
setWizardData(dataRef.current);
|
||||||
|
onComplete?.(selectedItemType, dataRef.current);
|
||||||
}
|
}
|
||||||
handleClose();
|
handleClose();
|
||||||
},
|
},
|
||||||
@@ -57,10 +80,10 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Get wizard steps based on selected item type
|
// Get wizard steps based on selected item type
|
||||||
// CRITICAL: Memoize the steps to prevent component recreation on every render
|
// ARCHITECTURAL SOLUTION: We pass dataRef and setWizardData to wizard step functions.
|
||||||
// Without this, every keystroke causes the component to unmount/remount, losing focus
|
// The wizard steps use these in their component wrappers, which creates a closure
|
||||||
// IMPORTANT: For dynamic wizards (like sales-entry), we need to include the entryMethod
|
// that always accesses the CURRENT data from dataRef.current, without needing
|
||||||
// in the dependency array so steps update when the user selects manual vs upload
|
// to recreate the steps array on every data change.
|
||||||
const wizardSteps = useMemo((): WizardStep[] => {
|
const wizardSteps = useMemo((): WizardStep[] => {
|
||||||
if (!selectedItemType) {
|
if (!selectedItemType) {
|
||||||
// Step 0: Item Type Selection
|
// Step 0: Item Type Selection
|
||||||
@@ -76,30 +99,31 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return specific wizard steps based on selected type
|
// Pass dataRef and setWizardData - the wizard step functions will use
|
||||||
|
// dataRef.current to always access fresh data without recreating steps
|
||||||
switch (selectedItemType) {
|
switch (selectedItemType) {
|
||||||
case 'inventory':
|
case 'inventory':
|
||||||
return InventoryWizardSteps(wizardData, setWizardData);
|
return InventoryWizardSteps(dataRef, setWizardData);
|
||||||
case 'supplier':
|
case 'supplier':
|
||||||
return SupplierWizardSteps(wizardData, setWizardData);
|
return SupplierWizardSteps(dataRef, setWizardData);
|
||||||
case 'recipe':
|
case 'recipe':
|
||||||
return RecipeWizardSteps(wizardData, setWizardData);
|
return RecipeWizardSteps(dataRef, setWizardData);
|
||||||
case 'equipment':
|
case 'equipment':
|
||||||
return EquipmentWizardSteps(wizardData, setWizardData);
|
return EquipmentWizardSteps(dataRef, setWizardData);
|
||||||
case 'quality-template':
|
case 'quality-template':
|
||||||
return QualityTemplateWizardSteps(wizardData, setWizardData);
|
return QualityTemplateWizardSteps(dataRef, setWizardData);
|
||||||
case 'customer-order':
|
case 'customer-order':
|
||||||
return CustomerOrderWizardSteps(wizardData, setWizardData);
|
return CustomerOrderWizardSteps(dataRef, setWizardData);
|
||||||
case 'customer':
|
case 'customer':
|
||||||
return CustomerWizardSteps(wizardData, setWizardData);
|
return CustomerWizardSteps(dataRef, setWizardData);
|
||||||
case 'team-member':
|
case 'team-member':
|
||||||
return TeamMemberWizardSteps(wizardData, setWizardData);
|
return TeamMemberWizardSteps(dataRef, setWizardData);
|
||||||
case 'sales-entry':
|
case 'sales-entry':
|
||||||
return SalesEntryWizardSteps(wizardData, setWizardData);
|
return SalesEntryWizardSteps(dataRef, setWizardData);
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}, [selectedItemType, handleItemTypeSelect, wizardData.entryMethod]); // Include only critical fields for dynamic step generation
|
}, [selectedItemType, handleItemTypeSelect, wizardData.entryMethod]); // Add entryMethod for dynamic sales-entry steps
|
||||||
|
|
||||||
// Get wizard title based on selected item type
|
// Get wizard title based on selected item type
|
||||||
const getWizardTitle = (): string => {
|
const getWizardTitle = (): string => {
|
||||||
@@ -131,6 +155,8 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
|||||||
steps={wizardSteps}
|
steps={wizardSteps}
|
||||||
icon={<Sparkles className="w-6 h-6" />}
|
icon={<Sparkles className="w-6 h-6" />}
|
||||||
size="xl"
|
size="xl"
|
||||||
|
dataRef={dataRef}
|
||||||
|
onDataChange={handleDataChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 { inventoryService } from '../../../../api/services/inventory';
|
||||||
import { ProductType } from '../../../../api/types/inventory';
|
import { ProductType } from '../../../../api/types/inventory';
|
||||||
|
|
||||||
interface WizardDataProps extends WizardStepProps {
|
|
||||||
data: Record<string, any>;
|
|
||||||
onDataChange: (data: Record<string, any>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 1: Customer Selection
|
// Step 1: Customer Selection
|
||||||
const CustomerSelectionStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
const CustomerSelectionStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||||
|
const data = dataRef?.current || {};
|
||||||
const { t } = useTranslation('wizards');
|
const { t } = useTranslation('wizards');
|
||||||
const { currentTenant } = useTenant();
|
const { currentTenant } = useTenant();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
@@ -52,7 +48,7 @@ const CustomerSelectionStep: React.FC<WizardDataProps> = ({ data, onDataChange }
|
|||||||
const handleCustomerChange = (newCustomer: any, newShowForm: boolean) => {
|
const handleCustomerChange = (newCustomer: any, newShowForm: boolean) => {
|
||||||
setSelectedCustomer(newCustomer);
|
setSelectedCustomer(newCustomer);
|
||||||
setShowNewCustomerForm(newShowForm);
|
setShowNewCustomerForm(newShowForm);
|
||||||
onDataChange({
|
onDataChange?.({
|
||||||
...data,
|
...data,
|
||||||
customer: newCustomer,
|
customer: newCustomer,
|
||||||
showNewCustomerForm: newShowForm,
|
showNewCustomerForm: newShowForm,
|
||||||
@@ -63,7 +59,7 @@ const CustomerSelectionStep: React.FC<WizardDataProps> = ({ data, onDataChange }
|
|||||||
const handleNewCustomerChange = (updates: any) => {
|
const handleNewCustomerChange = (updates: any) => {
|
||||||
const updated = { ...newCustomer, ...updates };
|
const updated = { ...newCustomer, ...updates };
|
||||||
setNewCustomer(updated);
|
setNewCustomer(updated);
|
||||||
onDataChange({
|
onDataChange?.({
|
||||||
...data,
|
...data,
|
||||||
showNewCustomerForm,
|
showNewCustomerForm,
|
||||||
newCustomerName: updated.name,
|
newCustomerName: updated.name,
|
||||||
@@ -288,10 +284,10 @@ const CustomerSelectionStep: React.FC<WizardDataProps> = ({ data, onDataChange }
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Step 2: Order Items
|
// Step 2: Order Items
|
||||||
const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
const OrderItemsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||||
|
const data = dataRef?.current || {};
|
||||||
const { t } = useTranslation('wizards');
|
const { t } = useTranslation('wizards');
|
||||||
const { currentTenant } = useTenant();
|
const { currentTenant } = useTenant();
|
||||||
const [orderItems, setOrderItems] = useState(data.orderItems || []);
|
|
||||||
const [products, setProducts] = useState<any[]>([]);
|
const [products, setProducts] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -302,9 +298,8 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|||||||
|
|
||||||
// Update parent whenever order items change
|
// Update parent whenever order items change
|
||||||
const updateOrderItems = (newItems: any[]) => {
|
const updateOrderItems = (newItems: any[]) => {
|
||||||
setOrderItems(newItems);
|
|
||||||
const totalAmount = newItems.reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0);
|
const totalAmount = newItems.reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0);
|
||||||
onDataChange({ ...data, orderItems: newItems, totalAmount });
|
onDataChange?.({ ...data, orderItems: newItems, totalAmount });
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchProducts = async () => {
|
const fetchProducts = async () => {
|
||||||
@@ -329,7 +324,7 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|||||||
|
|
||||||
const handleAddItem = () => {
|
const handleAddItem = () => {
|
||||||
updateOrderItems([
|
updateOrderItems([
|
||||||
...orderItems,
|
...(data.orderItems || []),
|
||||||
{
|
{
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
productId: '',
|
productId: '',
|
||||||
@@ -344,7 +339,7 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateItem = (index: number, field: string, value: any) => {
|
const handleUpdateItem = (index: number, field: string, value: any) => {
|
||||||
const updated = orderItems.map((item: any, i: number) => {
|
const updated = (data.orderItems || []).map((item: any, i: number) => {
|
||||||
if (i === index) {
|
if (i === index) {
|
||||||
const newItem = { ...item, [field]: value };
|
const newItem = { ...item, [field]: value };
|
||||||
|
|
||||||
@@ -368,11 +363,11 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveItem = (index: number) => {
|
const handleRemoveItem = (index: number) => {
|
||||||
updateOrderItems(orderItems.filter((_: any, i: number) => i !== index));
|
updateOrderItems((data.orderItems || []).filter((_: any, i: number) => i !== index));
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateTotal = () => {
|
const calculateTotal = () => {
|
||||||
return orderItems.reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0);
|
return (data.orderItems || []).reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -414,7 +409,7 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{orderItems.length === 0 ? (
|
{(data.orderItems || []).length === 0 ? (
|
||||||
<div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-lg text-[var(--text-tertiary)]">
|
<div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-lg text-[var(--text-tertiary)]">
|
||||||
<Package className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
<Package className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||||
<p className="mb-2">{t('customerOrder.messages.noProductsInOrder')}</p>
|
<p className="mb-2">{t('customerOrder.messages.noProductsInOrder')}</p>
|
||||||
@@ -422,7 +417,7 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{orderItems.map((item: any, index: number) => (
|
{(data.orderItems || []).map((item: any, index: number) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="p-4 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]/30 space-y-3"
|
className="p-4 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]/30 space-y-3"
|
||||||
@@ -510,7 +505,7 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{orderItems.length > 0 && (
|
{(data.orderItems || []).length > 0 && (
|
||||||
<div className="p-4 bg-gradient-to-r from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 rounded-lg border-2 border-[var(--color-primary)]/20">
|
<div className="p-4 bg-gradient-to-r from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 rounded-lg border-2 border-[var(--color-primary)]/20">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-lg font-semibold text-[var(--text-primary)]">{t('customerOrder.messages.orderTotal')}:</span>
|
<span className="text-lg font-semibold text-[var(--text-primary)]">{t('customerOrder.messages.orderTotal')}:</span>
|
||||||
@@ -528,88 +523,18 @@ const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Step 3: Delivery & Payment with ALL fields
|
// Step 3: Delivery & Payment with ALL fields
|
||||||
const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
const DeliveryPaymentStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||||
|
const data = dataRef?.current || {};
|
||||||
const { t } = useTranslation('wizards');
|
const { t } = useTranslation('wizards');
|
||||||
const [orderData, setOrderData] = useState({
|
|
||||||
// Required fields
|
|
||||||
requestedDeliveryDate: data.requestedDeliveryDate || '',
|
|
||||||
orderNumber: data.orderNumber || '',
|
|
||||||
|
|
||||||
// Basic order info
|
// Helper to get field value with defaults
|
||||||
orderType: data.orderType || 'standard',
|
const getValue = (field: string, defaultValue: any = '') => {
|
||||||
priority: data.priority || 'normal',
|
return data[field] ?? defaultValue;
|
||||||
status: data.status || 'pending',
|
};
|
||||||
|
|
||||||
// Delivery fields
|
|
||||||
deliveryMethod: data.deliveryMethod || 'pickup',
|
|
||||||
deliveryAddress: data.deliveryAddress || '',
|
|
||||||
deliveryInstructions: data.deliveryInstructions || '',
|
|
||||||
deliveryContactName: data.deliveryContactName || '',
|
|
||||||
deliveryContactPhone: data.deliveryContactPhone || '',
|
|
||||||
deliveryTimeWindow: data.deliveryTimeWindow || '',
|
|
||||||
deliveryFee: data.deliveryFee || '',
|
|
||||||
|
|
||||||
// Payment fields
|
|
||||||
paymentMethod: data.paymentMethod || 'invoice',
|
|
||||||
paymentTerms: data.paymentTerms || 'net_30',
|
|
||||||
paymentStatus: data.paymentStatus || 'pending',
|
|
||||||
paymentDueDate: data.paymentDueDate || '',
|
|
||||||
|
|
||||||
// Pricing fields
|
|
||||||
subtotalAmount: data.subtotalAmount || '',
|
|
||||||
taxAmount: data.taxAmount || '',
|
|
||||||
discountPercentage: data.discountPercentage || '',
|
|
||||||
discountAmount: data.discountAmount || '',
|
|
||||||
shippingCost: data.shippingCost || '',
|
|
||||||
|
|
||||||
// Production & scheduling
|
|
||||||
productionStartDate: data.productionStartDate || '',
|
|
||||||
productionDueDate: data.productionDueDate || '',
|
|
||||||
productionBatchNumber: data.productionBatchNumber || '',
|
|
||||||
productionNotes: data.productionNotes || '',
|
|
||||||
|
|
||||||
// Fulfillment
|
|
||||||
actualDeliveryDate: data.actualDeliveryDate || '',
|
|
||||||
pickupLocation: data.pickupLocation || '',
|
|
||||||
shippingTrackingNumber: data.shippingTrackingNumber || '',
|
|
||||||
shippingCarrier: data.shippingCarrier || '',
|
|
||||||
|
|
||||||
// Source & channel
|
|
||||||
orderSource: data.orderSource || 'manual',
|
|
||||||
salesChannel: data.salesChannel || 'direct',
|
|
||||||
salesRepId: data.salesRepId || '',
|
|
||||||
|
|
||||||
// Communication
|
|
||||||
customerPurchaseOrder: data.customerPurchaseOrder || '',
|
|
||||||
internalNotes: data.internalNotes || '',
|
|
||||||
customerNotes: data.customerNotes || '',
|
|
||||||
specialInstructions: data.specialInstructions || '',
|
|
||||||
|
|
||||||
// Notifications
|
|
||||||
notifyCustomerOnStatusChange: data.notifyCustomerOnStatusChange ?? true,
|
|
||||||
notifyCustomerOnDelivery: data.notifyCustomerOnDelivery ?? true,
|
|
||||||
customerNotificationEmail: data.customerNotificationEmail || '',
|
|
||||||
customerNotificationPhone: data.customerNotificationPhone || '',
|
|
||||||
|
|
||||||
// Quality & requirements
|
|
||||||
qualityCheckRequired: data.qualityCheckRequired ?? false,
|
|
||||||
qualityCheckStatus: data.qualityCheckStatus || '',
|
|
||||||
packagingInstructions: data.packagingInstructions || '',
|
|
||||||
labelingRequirements: data.labelingRequirements || '',
|
|
||||||
|
|
||||||
// Advanced options
|
|
||||||
isRecurring: data.isRecurring ?? false,
|
|
||||||
recurringSchedule: data.recurringSchedule || '',
|
|
||||||
parentOrderId: data.parentOrderId || '',
|
|
||||||
relatedOrderIds: data.relatedOrderIds || '',
|
|
||||||
tags: data.tags || '',
|
|
||||||
metadata: data.metadata || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update parent whenever order data changes
|
// Update parent whenever order data changes
|
||||||
const handleOrderDataChange = (newOrderData: any) => {
|
const handleOrderDataChange = (updates: Record<string, any>) => {
|
||||||
setOrderData(newOrderData);
|
onDataChange?.({ ...data, ...updates });
|
||||||
onDataChange({ ...data, ...newOrderData });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -633,8 +558,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={orderData.requestedDeliveryDate}
|
value={getValue('requestedDeliveryDate')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, requestedDeliveryDate: e.target.value })}
|
onChange={(e) => handleOrderDataChange({ requestedDeliveryDate: e.target.value })}
|
||||||
min={new Date().toISOString().split('T')[0]}
|
min={new Date().toISOString().split('T')[0]}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -649,7 +574,7 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={orderData.orderNumber || t('customerOrder.deliveryPayment.autoGeneratedLabel')}
|
value={getValue('orderNumber') || t('customerOrder.deliveryPayment.autoGeneratedLabel')}
|
||||||
readOnly
|
readOnly
|
||||||
disabled
|
disabled
|
||||||
placeholder={t('customerOrder.deliveryPayment.autoGeneratedPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.autoGeneratedPlaceholder')}
|
||||||
@@ -667,8 +592,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
{t('customerOrder.deliveryPayment.orderType')}
|
{t('customerOrder.deliveryPayment.orderType')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={orderData.orderType}
|
value={getValue('orderType', 'standard')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, orderType: e.target.value })}
|
onChange={(e) => handleOrderDataChange({orderType: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="standard">{t('customerOrder.orderTypes.standard')}</option>
|
<option value="standard">{t('customerOrder.orderTypes.standard')}</option>
|
||||||
@@ -683,8 +608,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
{t('customerOrder.deliveryPayment.priority')}
|
{t('customerOrder.deliveryPayment.priority')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={orderData.priority}
|
value={getValue('priority', 'normal')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, priority: e.target.value })}
|
onChange={(e) => handleOrderDataChange({priority: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="low">{t('customerOrder.priorities.low')}</option>
|
<option value="low">{t('customerOrder.priorities.low')}</option>
|
||||||
@@ -699,8 +624,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
{t('customerOrder.deliveryPayment.status')}
|
{t('customerOrder.deliveryPayment.status')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={orderData.status}
|
value={getValue('status')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, status: e.target.value })}
|
onChange={(e) => handleOrderDataChange({status: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="pending">{t('customerOrder.statuses.pending')}</option>
|
<option value="pending">{t('customerOrder.statuses.pending')}</option>
|
||||||
@@ -725,9 +650,9 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleOrderDataChange({ ...orderData, deliveryMethod: 'pickup' })}
|
onClick={() => handleOrderDataChange({deliveryMethod: 'pickup' })}
|
||||||
className={`p-3 rounded-lg border-2 transition-all ${
|
className={`p-3 rounded-lg border-2 transition-all ${
|
||||||
orderData.deliveryMethod === 'pickup'
|
getValue('deliveryMethod', 'pickup') === 'pickup'
|
||||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
||||||
: 'border-[var(--border-secondary)]'
|
: 'border-[var(--border-secondary)]'
|
||||||
}`}
|
}`}
|
||||||
@@ -737,9 +662,9 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleOrderDataChange({ ...orderData, deliveryMethod: 'delivery' })}
|
onClick={() => handleOrderDataChange({deliveryMethod: 'delivery' })}
|
||||||
className={`p-3 rounded-lg border-2 transition-all ${
|
className={`p-3 rounded-lg border-2 transition-all ${
|
||||||
orderData.deliveryMethod === 'delivery'
|
getValue('deliveryMethod', 'pickup') === 'delivery'
|
||||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
||||||
: 'border-[var(--border-secondary)]'
|
: 'border-[var(--border-secondary)]'
|
||||||
}`}
|
}`}
|
||||||
@@ -749,9 +674,9 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleOrderDataChange({ ...orderData, deliveryMethod: 'shipping' })}
|
onClick={() => handleOrderDataChange({deliveryMethod: 'shipping' })}
|
||||||
className={`p-3 rounded-lg border-2 transition-all ${
|
className={`p-3 rounded-lg border-2 transition-all ${
|
||||||
orderData.deliveryMethod === 'shipping'
|
getValue('deliveryMethod', 'pickup') === 'shipping'
|
||||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
||||||
: 'border-[var(--border-secondary)]'
|
: 'border-[var(--border-secondary)]'
|
||||||
}`}
|
}`}
|
||||||
@@ -762,15 +687,15 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(orderData.deliveryMethod === 'delivery' || orderData.deliveryMethod === 'shipping') && (
|
{(getValue('deliveryMethod', 'pickup') === 'delivery' || getValue('deliveryMethod', 'pickup') === 'shipping') && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
<MapPin className="w-4 h-4 inline mr-1.5" />
|
<MapPin className="w-4 h-4 inline mr-1.5" />
|
||||||
{t('customerOrder.messages.deliveryAddress')} *
|
{t('customerOrder.messages.deliveryAddress')} *
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={orderData.deliveryAddress}
|
value={getValue('deliveryAddress')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryAddress: e.target.value })}
|
onChange={(e) => handleOrderDataChange({deliveryAddress: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.deliveryAddressPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.deliveryAddressPlaceholder')}
|
||||||
rows={2}
|
rows={2}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
@@ -785,8 +710,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={orderData.deliveryContactName}
|
value={getValue('deliveryContactName')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryContactName: e.target.value })}
|
onChange={(e) => handleOrderDataChange({deliveryContactName: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.deliveryContactNamePlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.deliveryContactNamePlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -798,8 +723,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
value={orderData.deliveryContactPhone}
|
value={getValue('deliveryContactPhone')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryContactPhone: e.target.value })}
|
onChange={(e) => handleOrderDataChange({deliveryContactPhone: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.phoneNumberPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.phoneNumberPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -818,8 +743,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
{t('customerOrder.messages.paymentMethod')}
|
{t('customerOrder.messages.paymentMethod')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={orderData.paymentMethod}
|
value={getValue('paymentMethod')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, paymentMethod: e.target.value })}
|
onChange={(e) => handleOrderDataChange({paymentMethod: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="cash">{t('customerOrder.paymentMethods.cash')}</option>
|
<option value="cash">{t('customerOrder.paymentMethods.cash')}</option>
|
||||||
@@ -835,8 +760,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
{t('customerOrder.deliveryPayment.paymentTerms')}
|
{t('customerOrder.deliveryPayment.paymentTerms')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={orderData.paymentTerms}
|
value={getValue('paymentTerms')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, paymentTerms: e.target.value })}
|
onChange={(e) => handleOrderDataChange({paymentTerms: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="immediate">{t('customerOrder.paymentTerms.immediate')}</option>
|
<option value="immediate">{t('customerOrder.paymentTerms.immediate')}</option>
|
||||||
@@ -850,8 +775,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
{t('customerOrder.deliveryPayment.paymentStatus')}
|
{t('customerOrder.deliveryPayment.paymentStatus')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={orderData.paymentStatus}
|
value={getValue('paymentStatus')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, paymentStatus: e.target.value })}
|
onChange={(e) => handleOrderDataChange({paymentStatus: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="pending">{t('customerOrder.paymentStatuses.pending')}</option>
|
<option value="pending">{t('customerOrder.paymentStatuses.pending')}</option>
|
||||||
@@ -867,8 +792,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={orderData.paymentDueDate}
|
value={getValue('paymentDueDate')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, paymentDueDate: e.target.value })}
|
onChange={(e) => handleOrderDataChange({paymentDueDate: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -913,8 +838,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={orderData.discountPercentage}
|
value={getValue('discountPercentage')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, discountPercentage: e.target.value })}
|
onChange={(e) => handleOrderDataChange({discountPercentage: e.target.value })}
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
min="0"
|
min="0"
|
||||||
@@ -929,8 +854,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={orderData.deliveryFee}
|
value={getValue('deliveryFee')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryFee: e.target.value })}
|
onChange={(e) => handleOrderDataChange({deliveryFee: e.target.value })}
|
||||||
placeholder="0.00"
|
placeholder="0.00"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
min="0"
|
min="0"
|
||||||
@@ -952,8 +877,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={orderData.productionStartDate}
|
value={getValue('productionStartDate')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, productionStartDate: e.target.value })}
|
onChange={(e) => handleOrderDataChange({productionStartDate: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -964,8 +889,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={orderData.productionDueDate}
|
value={getValue('productionDueDate')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, productionDueDate: e.target.value })}
|
onChange={(e) => handleOrderDataChange({productionDueDate: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -976,8 +901,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={orderData.productionBatchNumber}
|
value={getValue('productionBatchNumber')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, productionBatchNumber: e.target.value })}
|
onChange={(e) => handleOrderDataChange({productionBatchNumber: e.target.value })}
|
||||||
placeholder={t('customerOrder.messages.productionBatchNumberPlaceholder')}
|
placeholder={t('customerOrder.messages.productionBatchNumberPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -989,8 +914,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={orderData.deliveryTimeWindow}
|
value={getValue('deliveryTimeWindow')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryTimeWindow: e.target.value })}
|
onChange={(e) => handleOrderDataChange({deliveryTimeWindow: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.deliveryTimeWindowPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.deliveryTimeWindowPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -1001,8 +926,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
{t('customerOrder.deliveryPayment.productionNotes')}
|
{t('customerOrder.deliveryPayment.productionNotes')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={orderData.productionNotes}
|
value={getValue('productionNotes')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, productionNotes: e.target.value })}
|
onChange={(e) => handleOrderDataChange({productionNotes: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.productionNotesPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.productionNotesPlaceholder')}
|
||||||
rows={2}
|
rows={2}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
@@ -1023,8 +948,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={orderData.shippingTrackingNumber}
|
value={getValue('shippingTrackingNumber')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, shippingTrackingNumber: e.target.value })}
|
onChange={(e) => handleOrderDataChange({shippingTrackingNumber: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.shippingTrackingNumberPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.shippingTrackingNumberPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -1036,8 +961,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={orderData.shippingCarrier}
|
value={getValue('shippingCarrier')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, shippingCarrier: e.target.value })}
|
onChange={(e) => handleOrderDataChange({shippingCarrier: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.shippingCarrierPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.shippingCarrierPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -1049,8 +974,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={orderData.pickupLocation}
|
value={getValue('pickupLocation')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, pickupLocation: e.target.value })}
|
onChange={(e) => handleOrderDataChange({pickupLocation: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.pickupLocationPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.pickupLocationPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -1062,8 +987,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={orderData.actualDeliveryDate}
|
value={getValue('actualDeliveryDate')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, actualDeliveryDate: e.target.value })}
|
onChange={(e) => handleOrderDataChange({actualDeliveryDate: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1081,8 +1006,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
{t('customerOrder.deliveryPayment.orderSource')}
|
{t('customerOrder.deliveryPayment.orderSource')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={orderData.orderSource}
|
value={getValue('orderSource')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, orderSource: e.target.value })}
|
onChange={(e) => handleOrderDataChange({orderSource: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="manual">{t('customerOrder.orderSources.manual')}</option>
|
<option value="manual">{t('customerOrder.orderSources.manual')}</option>
|
||||||
@@ -1098,8 +1023,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
{t('customerOrder.deliveryPayment.salesChannel')}
|
{t('customerOrder.deliveryPayment.salesChannel')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={orderData.salesChannel}
|
value={getValue('salesChannel')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, salesChannel: e.target.value })}
|
onChange={(e) => handleOrderDataChange({salesChannel: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="direct">{t('customerOrder.salesChannels.direct')}</option>
|
<option value="direct">{t('customerOrder.salesChannels.direct')}</option>
|
||||||
@@ -1115,8 +1040,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={orderData.salesRepId}
|
value={getValue('salesRepId')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, salesRepId: e.target.value })}
|
onChange={(e) => handleOrderDataChange({salesRepId: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.salesRepIdPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.salesRepIdPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -1136,8 +1061,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={orderData.customerPurchaseOrder}
|
value={getValue('customerPurchaseOrder')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, customerPurchaseOrder: e.target.value })}
|
onChange={(e) => handleOrderDataChange({customerPurchaseOrder: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.customerPurchaseOrderPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.customerPurchaseOrderPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -1148,8 +1073,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
{t('customerOrder.deliveryPayment.deliveryInstructions')}
|
{t('customerOrder.deliveryPayment.deliveryInstructions')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={orderData.deliveryInstructions}
|
value={getValue('deliveryInstructions')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryInstructions: e.target.value })}
|
onChange={(e) => handleOrderDataChange({deliveryInstructions: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.deliveryInstructionsPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.deliveryInstructionsPlaceholder')}
|
||||||
rows={2}
|
rows={2}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
@@ -1161,8 +1086,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
{t('customerOrder.deliveryPayment.specialInstructions')}
|
{t('customerOrder.deliveryPayment.specialInstructions')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={orderData.specialInstructions}
|
value={getValue('specialInstructions')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, specialInstructions: e.target.value })}
|
onChange={(e) => handleOrderDataChange({specialInstructions: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.specialInstructionsPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.specialInstructionsPlaceholder')}
|
||||||
rows={2}
|
rows={2}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
@@ -1174,8 +1099,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
{t('customerOrder.deliveryPayment.internalNotes')}
|
{t('customerOrder.deliveryPayment.internalNotes')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={orderData.internalNotes}
|
value={getValue('internalNotes')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, internalNotes: e.target.value })}
|
onChange={(e) => handleOrderDataChange({internalNotes: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.internalNotesPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.internalNotesPlaceholder')}
|
||||||
rows={2}
|
rows={2}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
@@ -1187,8 +1112,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
{t('customerOrder.deliveryPayment.customerNotes')}
|
{t('customerOrder.deliveryPayment.customerNotes')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={orderData.customerNotes}
|
value={getValue('customerNotes')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, customerNotes: e.target.value })}
|
onChange={(e) => handleOrderDataChange({customerNotes: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.customerNotesPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.customerNotesPlaceholder')}
|
||||||
rows={2}
|
rows={2}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
@@ -1206,8 +1131,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={orderData.notifyCustomerOnStatusChange}
|
checked={getValue('notifyCustomerOnStatusChange')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, notifyCustomerOnStatusChange: e.target.checked })}
|
onChange={(e) => handleOrderDataChange({notifyCustomerOnStatusChange: e.target.checked })}
|
||||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||||
/>
|
/>
|
||||||
<label className="text-sm text-[var(--text-secondary)]">
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
@@ -1218,8 +1143,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={orderData.notifyCustomerOnDelivery}
|
checked={getValue('notifyCustomerOnDelivery')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, notifyCustomerOnDelivery: e.target.checked })}
|
onChange={(e) => handleOrderDataChange({notifyCustomerOnDelivery: e.target.checked })}
|
||||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||||
/>
|
/>
|
||||||
<label className="text-sm text-[var(--text-secondary)]">
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
@@ -1233,8 +1158,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={orderData.customerNotificationEmail}
|
value={getValue('customerNotificationEmail')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, customerNotificationEmail: e.target.value })}
|
onChange={(e) => handleOrderDataChange({customerNotificationEmail: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.notificationEmailPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.notificationEmailPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -1246,8 +1171,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
value={orderData.customerNotificationPhone}
|
value={getValue('customerNotificationPhone')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, customerNotificationPhone: e.target.value })}
|
onChange={(e) => handleOrderDataChange({customerNotificationPhone: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.phoneNumberPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.phoneNumberPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -1264,8 +1189,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={orderData.qualityCheckRequired}
|
checked={getValue('qualityCheckRequired')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, qualityCheckRequired: e.target.checked })}
|
onChange={(e) => handleOrderDataChange({qualityCheckRequired: e.target.checked })}
|
||||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||||
/>
|
/>
|
||||||
<label className="text-sm text-[var(--text-secondary)]">
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
@@ -1278,8 +1203,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
{t('customerOrder.deliveryPayment.qualityCheckStatus')}
|
{t('customerOrder.deliveryPayment.qualityCheckStatus')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={orderData.qualityCheckStatus}
|
value={getValue('qualityCheckStatus')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, qualityCheckStatus: e.target.value })}
|
onChange={(e) => handleOrderDataChange({qualityCheckStatus: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="">{t('customerOrder.qualityCheckStatuses.not_started')}</option>
|
<option value="">{t('customerOrder.qualityCheckStatuses.not_started')}</option>
|
||||||
@@ -1295,8 +1220,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={orderData.packagingInstructions}
|
value={getValue('packagingInstructions')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, packagingInstructions: e.target.value })}
|
onChange={(e) => handleOrderDataChange({packagingInstructions: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.packagingInstructionsPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.packagingInstructionsPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -1308,8 +1233,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={orderData.labelingRequirements}
|
value={getValue('labelingRequirements')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, labelingRequirements: e.target.value })}
|
onChange={(e) => handleOrderDataChange({labelingRequirements: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.labelingRequirementsPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.labelingRequirementsPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -1326,8 +1251,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={orderData.isRecurring}
|
checked={getValue('isRecurring')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, isRecurring: e.target.checked })}
|
onChange={(e) => handleOrderDataChange({isRecurring: e.target.checked })}
|
||||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||||
/>
|
/>
|
||||||
<label className="text-sm text-[var(--text-secondary)]">
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
@@ -1335,15 +1260,15 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{orderData.isRecurring && (
|
{getValue('isRecurring', false) && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
{t('customerOrder.deliveryPayment.recurringSchedule')}
|
{t('customerOrder.deliveryPayment.recurringSchedule')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={orderData.recurringSchedule}
|
value={getValue('recurringSchedule')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, recurringSchedule: e.target.value })}
|
onChange={(e) => handleOrderDataChange({recurringSchedule: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.recurringSchedulePlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.recurringSchedulePlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -1359,8 +1284,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={orderData.tags}
|
value={getValue('tags')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, tags: e.target.value })}
|
onChange={(e) => handleOrderDataChange({tags: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.tagsPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.tagsPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -1374,8 +1299,8 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={orderData.metadata}
|
value={getValue('metadata')}
|
||||||
onChange={(e) => handleOrderDataChange({ ...orderData, metadata: e.target.value })}
|
onChange={(e) => handleOrderDataChange({metadata: e.target.value })}
|
||||||
placeholder={t('customerOrder.deliveryPayment.metadataPlaceholder')}
|
placeholder={t('customerOrder.deliveryPayment.metadataPlaceholder')}
|
||||||
rows={2}
|
rows={2}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
||||||
@@ -1389,34 +1314,26 @@ const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange })
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CustomerOrderWizardSteps = (
|
export const CustomerOrderWizardSteps = (
|
||||||
data: Record<string, any>,
|
dataRef: React.MutableRefObject<Record<string, any>>,
|
||||||
setData: (data: Record<string, any>) => void
|
setData: (data: Record<string, any>) => void
|
||||||
): WizardStep[] => [
|
): WizardStep[] => {
|
||||||
|
// New architecture: return direct component references instead of arrow functions
|
||||||
|
// dataRef and onDataChange are now passed through WizardModal props
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
id: 'customer-selection',
|
id: 'customer-selection',
|
||||||
title: 'customerOrder.customerSelection.title',
|
title: 'customerOrder.customerSelection.title',
|
||||||
component: (props) => <CustomerSelectionStep {...props} data={data} onDataChange={setData} />,
|
component: CustomerSelectionStep,
|
||||||
validate: () => {
|
|
||||||
return !!(data.customer || (data.showNewCustomerForm && data.newCustomerName && data.newCustomerPhone));
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'order-items',
|
id: 'order-items',
|
||||||
title: 'customerOrder.orderItems.title',
|
title: 'customerOrder.orderItems.title',
|
||||||
component: (props) => <OrderItemsStep {...props} data={data} onDataChange={setData} />,
|
component: OrderItemsStep,
|
||||||
validate: () => {
|
|
||||||
return !!(data.orderItems && data.orderItems.length > 0);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'delivery-payment',
|
id: 'delivery-payment',
|
||||||
title: 'customerOrder.deliveryPayment.title',
|
title: 'customerOrder.deliveryPayment.title',
|
||||||
component: (props) => <DeliveryPaymentStep {...props} data={data} onDataChange={setData} />,
|
component: DeliveryPaymentStep,
|
||||||
validate: () => {
|
|
||||||
return !!(
|
|
||||||
data.requestedDeliveryDate &&
|
|
||||||
(data.deliveryMethod === 'pickup' || data.deliveryAddress)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,109 +1,22 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
||||||
import { Users, CheckCircle2, Loader2 } from 'lucide-react';
|
import { Users } from 'lucide-react';
|
||||||
import { useTenant } from '../../../../stores/tenant.store';
|
|
||||||
import OrdersService from '../../../../api/services/orders';
|
|
||||||
import { showToast } from '../../../../utils/toast';
|
|
||||||
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
|
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
|
||||||
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
||||||
|
|
||||||
interface WizardDataProps extends WizardStepProps {
|
const CustomerDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||||
data: Record<string, any>;
|
const data = dataRef?.current || {};
|
||||||
onDataChange: (data: Record<string, any>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
const handleFieldChange = (field: string, value: any) => {
|
||||||
const { currentTenant } = useTenant();
|
onDataChange?.({ ...data, [field]: value });
|
||||||
const [customerData, setCustomerData] = useState({
|
|
||||||
// Required fields
|
|
||||||
name: data.name || '',
|
|
||||||
customerCode: data.customerCode || '',
|
|
||||||
customerType: data.customerType || 'individual',
|
|
||||||
country: data.country || 'US',
|
|
||||||
|
|
||||||
// Basic optional fields
|
|
||||||
businessName: data.businessName || '',
|
|
||||||
email: data.email || '',
|
|
||||||
phone: data.phone || '',
|
|
||||||
|
|
||||||
// Advanced optional fields
|
|
||||||
addressLine1: data.addressLine1 || '',
|
|
||||||
addressLine2: data.addressLine2 || '',
|
|
||||||
city: data.city || '',
|
|
||||||
state: data.state || '',
|
|
||||||
postalCode: data.postalCode || '',
|
|
||||||
taxId: data.taxId || '',
|
|
||||||
businessLicense: data.businessLicense || '',
|
|
||||||
paymentTerms: data.paymentTerms || 'immediate',
|
|
||||||
creditLimit: data.creditLimit || '',
|
|
||||||
discountPercentage: data.discountPercentage || 0,
|
|
||||||
customerSegment: data.customerSegment || 'regular',
|
|
||||||
priorityLevel: data.priorityLevel || 'normal',
|
|
||||||
preferredDeliveryMethod: data.preferredDeliveryMethod || 'delivery',
|
|
||||||
specialInstructions: data.specialInstructions || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!customerData.customerCode && customerData.name) {
|
|
||||||
const code = `CUST-${customerData.name.substring(0, 3).toUpperCase()}-${Date.now().toString().slice(-4)}`;
|
|
||||||
setCustomerData(prev => ({ ...prev, customerCode: code }));
|
|
||||||
}
|
|
||||||
}, [customerData.name]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onDataChange({ ...data, ...customerData });
|
|
||||||
}, [customerData]);
|
|
||||||
|
|
||||||
const handleCreateCustomer = async () => {
|
|
||||||
if (!currentTenant?.id) {
|
|
||||||
setError('Could not obtain tenant information');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload = {
|
|
||||||
name: customerData.name,
|
|
||||||
customer_code: customerData.customerCode,
|
|
||||||
customer_type: customerData.customerType,
|
|
||||||
country: customerData.country,
|
|
||||||
business_name: customerData.businessName || undefined,
|
|
||||||
email: customerData.email || undefined,
|
|
||||||
phone: customerData.phone || undefined,
|
|
||||||
address_line1: customerData.addressLine1 || undefined,
|
|
||||||
address_line2: customerData.addressLine2 || undefined,
|
|
||||||
city: customerData.city || undefined,
|
|
||||||
state: customerData.state || undefined,
|
|
||||||
postal_code: customerData.postalCode || undefined,
|
|
||||||
tax_id: customerData.taxId || undefined,
|
|
||||||
business_license: customerData.businessLicense || undefined,
|
|
||||||
payment_terms: customerData.paymentTerms,
|
|
||||||
credit_limit: customerData.creditLimit ? parseFloat(customerData.creditLimit) : undefined,
|
|
||||||
discount_percentage: customerData.discountPercentage,
|
|
||||||
customer_segment: customerData.customerSegment,
|
|
||||||
priority_level: customerData.priorityLevel,
|
|
||||||
preferred_delivery_method: customerData.preferredDeliveryMethod,
|
|
||||||
special_instructions: customerData.specialInstructions || undefined,
|
|
||||||
is_active: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await OrdersService.createCustomer(currentTenant.id, payload);
|
useEffect(() => {
|
||||||
showToast.success('Customer created successfully');
|
if (!data.customerCode && data.name) {
|
||||||
onComplete();
|
const code = `CUST-${data.name.substring(0, 3).toUpperCase()}-${Date.now().toString().slice(-4)}`;
|
||||||
} catch (err: any) {
|
onDataChange?.({ ...data, customerCode: code });
|
||||||
console.error('Error creating customer:', err);
|
|
||||||
const errorMessage = err.response?.data?.detail || 'Error creating customer';
|
|
||||||
setError(errorMessage);
|
|
||||||
showToast.error(errorMessage);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
}, [data.name]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<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>
|
<p className="text-sm text-[var(--text-secondary)]">Essential customer information</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Required Fields */}
|
{/* Required Fields */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
@@ -128,8 +35,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={customerData.name}
|
value={data.name || ''}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, name: e.target.value })}
|
onChange={(e) => handleFieldChange('name', e.target.value)}
|
||||||
placeholder="e.g., Restaurant El Molino"
|
placeholder="e.g., Restaurant El Molino"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -144,8 +51,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={customerData.customerCode}
|
value={data.customerCode}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, customerCode: e.target.value })}
|
onChange={(e) => handleFieldChange('customerCode', e.target.value)}
|
||||||
placeholder="CUST-001"
|
placeholder="CUST-001"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -158,8 +65,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
Customer Type *
|
Customer Type *
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={customerData.customerType}
|
value={data.customerType}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, customerType: e.target.value })}
|
onChange={(e) => handleFieldChange('customerType', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="individual">Individual</option>
|
<option value="individual">Individual</option>
|
||||||
@@ -174,8 +81,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={customerData.country}
|
value={data.country}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, country: e.target.value })}
|
onChange={(e) => handleFieldChange('country', e.target.value)}
|
||||||
placeholder="US"
|
placeholder="US"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -188,8 +95,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={customerData.businessName}
|
value={data.businessName}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, businessName: e.target.value })}
|
onChange={(e) => handleFieldChange('businessName', e.target.value)}
|
||||||
placeholder="Legal business name"
|
placeholder="Legal business name"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -202,8 +109,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={customerData.email}
|
value={data.email}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, email: e.target.value })}
|
onChange={(e) => handleFieldChange('email', e.target.value)}
|
||||||
placeholder="contact@company.com"
|
placeholder="contact@company.com"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -215,8 +122,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
value={customerData.phone}
|
value={data.phone}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, phone: e.target.value })}
|
onChange={(e) => handleFieldChange('phone', e.target.value)}
|
||||||
placeholder="+1 234 567 8900"
|
placeholder="+1 234 567 8900"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -237,8 +144,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={customerData.addressLine1}
|
value={data.addressLine1}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, addressLine1: e.target.value })}
|
onChange={(e) => handleFieldChange('addressLine1', e.target.value)}
|
||||||
placeholder="Street address"
|
placeholder="Street address"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -250,8 +157,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={customerData.addressLine2}
|
value={data.addressLine2}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, addressLine2: e.target.value })}
|
onChange={(e) => handleFieldChange('addressLine2', e.target.value)}
|
||||||
placeholder="Apartment, suite, etc."
|
placeholder="Apartment, suite, etc."
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -263,8 +170,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={customerData.city}
|
value={data.city}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, city: e.target.value })}
|
onChange={(e) => handleFieldChange('city', e.target.value)}
|
||||||
placeholder="City"
|
placeholder="City"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -276,8 +183,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={customerData.state}
|
value={data.state}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, state: e.target.value })}
|
onChange={(e) => handleFieldChange('state', e.target.value)}
|
||||||
placeholder="State"
|
placeholder="State"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -289,8 +196,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={customerData.postalCode}
|
value={data.postalCode}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, postalCode: e.target.value })}
|
onChange={(e) => handleFieldChange('postalCode', e.target.value)}
|
||||||
placeholder="12345"
|
placeholder="12345"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -302,8 +209,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={customerData.taxId}
|
value={data.taxId}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, taxId: e.target.value })}
|
onChange={(e) => handleFieldChange('taxId', e.target.value)}
|
||||||
placeholder="Tax identification number"
|
placeholder="Tax identification number"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -315,8 +222,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={customerData.businessLicense}
|
value={data.businessLicense}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, businessLicense: e.target.value })}
|
onChange={(e) => handleFieldChange('businessLicense', e.target.value)}
|
||||||
placeholder="Business license number"
|
placeholder="Business license number"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -327,8 +234,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
Payment Terms
|
Payment Terms
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={customerData.paymentTerms}
|
value={data.paymentTerms}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, paymentTerms: e.target.value })}
|
onChange={(e) => handleFieldChange('paymentTerms', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="immediate">Immediate</option>
|
<option value="immediate">Immediate</option>
|
||||||
@@ -343,8 +250,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={customerData.creditLimit}
|
value={data.creditLimit}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, creditLimit: e.target.value })}
|
onChange={(e) => handleFieldChange('creditLimit', e.target.value)}
|
||||||
placeholder="5000.00"
|
placeholder="5000.00"
|
||||||
min="0"
|
min="0"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
@@ -358,8 +265,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={customerData.discountPercentage}
|
value={data.discountPercentage}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, discountPercentage: parseFloat(e.target.value) || 0 })}
|
onChange={(e) => handleFieldChange('discountPercentage', parseFloat(e.target.value) || 0)}
|
||||||
placeholder="10"
|
placeholder="10"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
@@ -373,8 +280,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
Customer Segment
|
Customer Segment
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={customerData.customerSegment}
|
value={data.customerSegment}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, customerSegment: e.target.value })}
|
onChange={(e) => handleFieldChange('customerSegment', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="vip">VIP</option>
|
<option value="vip">VIP</option>
|
||||||
@@ -388,8 +295,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
Priority Level
|
Priority Level
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={customerData.priorityLevel}
|
value={data.priorityLevel}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, priorityLevel: e.target.value })}
|
onChange={(e) => handleFieldChange('priorityLevel', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="high">High</option>
|
<option value="high">High</option>
|
||||||
@@ -403,8 +310,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
Preferred Delivery Method
|
Preferred Delivery Method
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={customerData.preferredDeliveryMethod}
|
value={data.preferredDeliveryMethod}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, preferredDeliveryMethod: e.target.value })}
|
onChange={(e) => handleFieldChange('preferredDeliveryMethod', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="delivery">Delivery</option>
|
<option value="delivery">Delivery</option>
|
||||||
@@ -418,8 +325,8 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
Special Instructions
|
Special Instructions
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={customerData.specialInstructions}
|
value={data.specialInstructions}
|
||||||
onChange={(e) => setCustomerData({ ...customerData, specialInstructions: e.target.value })}
|
onChange={(e) => handleFieldChange('specialInstructions', e.target.value)}
|
||||||
placeholder="Any special notes or instructions for this customer..."
|
placeholder="Any special notes or instructions for this customer..."
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
rows={3}
|
rows={3}
|
||||||
@@ -427,42 +334,72 @@ const CustomerDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AdvancedOptionsSection>
|
</AdvancedOptionsSection>
|
||||||
|
|
||||||
<div className="flex justify-center pt-4 border-t border-[var(--border-primary)]">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleCreateCustomer}
|
|
||||||
disabled={loading}
|
|
||||||
className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-semibold inline-flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
|
||||||
Creating customer...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CheckCircle2 className="w-5 h-5" />
|
|
||||||
Create Customer
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CustomerWizardSteps = (
|
export const CustomerWizardSteps = (
|
||||||
data: Record<string, any>,
|
dataRef: React.MutableRefObject<Record<string, any>>,
|
||||||
setData: (data: Record<string, any>) => void
|
setData: (data: Record<string, any>) => void
|
||||||
): WizardStep[] => [
|
): WizardStep[] => {
|
||||||
|
// New architecture: return direct component references instead of arrow functions
|
||||||
|
// dataRef and onDataChange are now passed through WizardModal props
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
id: 'customer-details',
|
id: 'customer-details',
|
||||||
title: 'Customer Details',
|
title: 'Customer Details',
|
||||||
description: 'Contact and business information',
|
description: 'Contact and business information',
|
||||||
component: (props) => <CustomerDetailsStep {...props} data={data} onDataChange={setData} />,
|
component: CustomerDetailsStep,
|
||||||
validate: () => {
|
validate: async () => {
|
||||||
return !!(data.name && data.customerCode && data.customerType && data.country);
|
// 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 { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
||||||
import { Wrench, CheckCircle2, Loader2 } from 'lucide-react';
|
import { Wrench } from 'lucide-react';
|
||||||
import { useTenant } from '../../../../stores/tenant.store';
|
|
||||||
import { equipmentService } from '../../../../api/services/equipment';
|
|
||||||
import { showToast } from '../../../../utils/toast';
|
|
||||||
|
|
||||||
interface WizardDataProps extends WizardStepProps {
|
const EquipmentDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||||
data: Record<string, any>;
|
const data = dataRef?.current || {};
|
||||||
onDataChange: (data: Record<string, any>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EquipmentDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
const handleFieldChange = (field: string, value: any) => {
|
||||||
const { currentTenant } = useTenant();
|
onDataChange?.({ ...data, [field]: value });
|
||||||
const [equipmentData, setEquipmentData] = useState({
|
|
||||||
type: data.type || 'oven',
|
|
||||||
brand: data.brand || '',
|
|
||||||
model: data.model || '',
|
|
||||||
location: data.location || '',
|
|
||||||
purchaseDate: data.purchaseDate || '',
|
|
||||||
status: data.status || 'active',
|
|
||||||
});
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
if (!currentTenant?.id) {
|
|
||||||
setError('No se pudo obtener información del tenant');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const equipmentCreateData: any = {
|
|
||||||
name: `${equipmentData.type} - ${equipmentData.brand || 'Sin marca'}`,
|
|
||||||
type: equipmentData.type,
|
|
||||||
model: equipmentData.brand,
|
|
||||||
serialNumber: equipmentData.model,
|
|
||||||
location: equipmentData.location,
|
|
||||||
status: equipmentData.status,
|
|
||||||
installDate: equipmentData.purchaseDate || new Date().toISOString().split('T')[0],
|
|
||||||
lastMaintenance: new Date().toISOString().split('T')[0],
|
|
||||||
nextMaintenance: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
|
||||||
maintenanceInterval: 30,
|
|
||||||
is_active: true
|
|
||||||
};
|
|
||||||
|
|
||||||
await equipmentService.createEquipment(currentTenant.id, equipmentCreateData);
|
|
||||||
|
|
||||||
showToast.success('Equipo creado exitosamente');
|
|
||||||
onDataChange({ ...data, ...equipmentData });
|
|
||||||
onComplete();
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Error creating equipment:', err);
|
|
||||||
const errorMessage = err.response?.data?.detail || 'Error al crear el equipo';
|
|
||||||
setError(errorMessage);
|
|
||||||
showToast.error(errorMessage);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -69,18 +16,12 @@ const EquipmentDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
|||||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">Equipo de Panadería</h3>
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">Equipo de Panadería</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Tipo de Equipo *</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Tipo de Equipo *</label>
|
||||||
<select
|
<select
|
||||||
value={equipmentData.type}
|
value={data.type || 'oven'}
|
||||||
onChange={(e) => setEquipmentData({ ...equipmentData, type: e.target.value })}
|
onChange={(e) => handleFieldChange('type', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="oven">Horno</option>
|
<option value="oven">Horno</option>
|
||||||
@@ -94,8 +35,8 @@ const EquipmentDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
|||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Marca/Modelo</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Marca/Modelo</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={equipmentData.brand}
|
value={data.brand || ''}
|
||||||
onChange={(e) => setEquipmentData({ ...equipmentData, brand: e.target.value })}
|
onChange={(e) => handleFieldChange('brand', e.target.value)}
|
||||||
placeholder="Ej: Rational SCC 101"
|
placeholder="Ej: Rational SCC 101"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -104,8 +45,8 @@ const EquipmentDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
|||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Ubicación</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Ubicación</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={equipmentData.location}
|
value={data.location || ''}
|
||||||
onChange={(e) => setEquipmentData({ ...equipmentData, location: e.target.value })}
|
onChange={(e) => handleFieldChange('location', e.target.value)}
|
||||||
placeholder="Ej: Cocina principal"
|
placeholder="Ej: Cocina principal"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -114,36 +55,63 @@ const EquipmentDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
|||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Fecha de Compra</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Fecha de Compra</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={equipmentData.purchaseDate}
|
value={data.purchaseDate || ''}
|
||||||
onChange={(e) => setEquipmentData({ ...equipmentData, purchaseDate: e.target.value })}
|
onChange={(e) => handleFieldChange('purchaseDate', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
|
|
||||||
<button
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={loading}
|
|
||||||
className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-semibold inline-flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
|
||||||
Guardando...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CheckCircle2 className="w-5 h-5" />
|
|
||||||
Agregar Equipo
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EquipmentWizardSteps = (data: Record<string, any>, setData: (data: Record<string, any>) => void): WizardStep[] => [
|
export const EquipmentWizardSteps = (dataRef: React.MutableRefObject<Record<string, any>>, setData: (data: Record<string, any>) => void): WizardStep[] => {
|
||||||
{ id: 'equipment-details', title: 'Detalles del Equipo', description: 'Tipo, modelo, ubicación', component: (props) => <EquipmentDetailsStep {...props} data={data} onDataChange={setData} /> },
|
// New architecture: return direct component references instead of arrow functions
|
||||||
|
// dataRef and onDataChange are now passed through WizardModal props
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'equipment-details',
|
||||||
|
title: 'Detalles del Equipo',
|
||||||
|
description: 'Tipo, modelo, ubicación',
|
||||||
|
component: EquipmentDetailsStep,
|
||||||
|
validate: async () => {
|
||||||
|
const { useTenant } = await import('../../../../stores/tenant.store');
|
||||||
|
const { equipmentService } = await import('../../../../api/services/equipment');
|
||||||
|
const { showToast } = await import('../../../../utils/toast');
|
||||||
|
|
||||||
|
const data = dataRef.current;
|
||||||
|
const { currentTenant } = useTenant.getState();
|
||||||
|
|
||||||
|
if (!currentTenant?.id) {
|
||||||
|
showToast.error('No se pudo obtener información del tenant');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const equipmentCreateData: any = {
|
||||||
|
name: `${data.type || 'oven'} - ${data.brand || 'Sin marca'}`,
|
||||||
|
type: data.type || 'oven',
|
||||||
|
model: data.brand || '',
|
||||||
|
serialNumber: data.model || '',
|
||||||
|
location: data.location || '',
|
||||||
|
status: data.status || 'active',
|
||||||
|
installDate: data.purchaseDate || new Date().toISOString().split('T')[0],
|
||||||
|
lastMaintenance: new Date().toISOString().split('T')[0],
|
||||||
|
nextMaintenance: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
||||||
|
maintenanceInterval: 30,
|
||||||
|
is_active: true
|
||||||
|
};
|
||||||
|
|
||||||
|
await equipmentService.createEquipment(currentTenant.id, equipmentCreateData);
|
||||||
|
showToast.success('Equipo creado exitosamente');
|
||||||
|
return true;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Error creating equipment:', err);
|
||||||
|
const errorMessage = err.response?.data?.detail || 'Error al crear el equipo';
|
||||||
|
showToast.error(errorMessage);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,21 +5,18 @@ import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
|
|||||||
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
||||||
import { Info, Package, ShoppingBag } from 'lucide-react';
|
import { Info, Package, ShoppingBag } from 'lucide-react';
|
||||||
|
|
||||||
interface WizardDataProps extends WizardStepProps {
|
|
||||||
data: Record<string, any>;
|
|
||||||
onDataChange: (data: Record<string, any>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP 1: Product Type Selection with advanced fields
|
// STEP 1: Product Type Selection with advanced fields
|
||||||
const ProductTypeStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
const ProductTypeStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||||
|
// New architecture: access data from dataRef.current
|
||||||
|
const data = dataRef?.current || {};
|
||||||
const { t } = useTranslation('wizards');
|
const { t } = useTranslation('wizards');
|
||||||
|
|
||||||
const handleFieldChange = (field: string, value: any) => {
|
const handleFieldChange = (field: string, value: any) => {
|
||||||
onDataChange({ ...data, [field]: value });
|
onDataChange?.({ ...data, [field]: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTypeSelect = (type: string) => {
|
const handleTypeSelect = (type: string) => {
|
||||||
onDataChange({ ...data, productType: type });
|
onDataChange?.({ ...data, productType: type });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -151,11 +148,12 @@ const ProductTypeStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// STEP 2: Basic Information with advanced fields
|
// STEP 2: Basic Information with advanced fields
|
||||||
const BasicInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
const BasicInfoStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||||
|
const data = dataRef?.current || {};
|
||||||
const { t } = useTranslation('wizards');
|
const { t } = useTranslation('wizards');
|
||||||
|
|
||||||
const handleFieldChange = (field: string, value: any) => {
|
const handleFieldChange = (field: string, value: any) => {
|
||||||
onDataChange({ ...data, [field]: value });
|
onDataChange?.({ ...data, [field]: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -308,41 +306,88 @@ const BasicInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// STEP 3: Stock Configuration with advanced fields
|
// STEP 3: Initial Stock/Lots Configuration
|
||||||
const StockConfigStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
const StockConfigStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||||
|
const data = dataRef?.current || {};
|
||||||
const { t } = useTranslation('wizards');
|
const { t } = useTranslation('wizards');
|
||||||
|
const [lots, setLots] = useState<any[]>(data.initialLots || []);
|
||||||
|
|
||||||
const handleFieldChange = (field: string, value: any) => {
|
const handleFieldChange = (field: string, value: any) => {
|
||||||
onDataChange({ ...data, [field]: value });
|
onDataChange?.({ ...data, [field]: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddLot = () => {
|
||||||
|
const newLot = {
|
||||||
|
id: Date.now(),
|
||||||
|
quantity: '',
|
||||||
|
unitCost: '',
|
||||||
|
expirationDate: '',
|
||||||
|
lotNumber: `LOT-${Date.now()}`,
|
||||||
|
location: '',
|
||||||
|
};
|
||||||
|
const updatedLots = [...lots, newLot];
|
||||||
|
setLots(updatedLots);
|
||||||
|
onDataChange?.({ ...data, initialLots: updatedLots });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveLot = (lotId: number) => {
|
||||||
|
const updatedLots = lots.filter(lot => lot.id !== lotId);
|
||||||
|
setLots(updatedLots);
|
||||||
|
onDataChange?.({ ...data, initialLots: updatedLots });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLotChange = (lotId: number, field: string, value: any) => {
|
||||||
|
const updatedLots = lots.map(lot =>
|
||||||
|
lot.id === lotId ? { ...lot, [field]: value } : lot
|
||||||
|
);
|
||||||
|
setLots(updatedLots);
|
||||||
|
onDataChange?.({ ...data, initialLots: updatedLots });
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalQuantity = lots.reduce((sum, lot) => sum + (parseFloat(lot.quantity) || 0), 0);
|
||||||
|
const totalValue = lots.reduce((sum, lot) => {
|
||||||
|
const qty = parseFloat(lot.quantity) || 0;
|
||||||
|
const cost = parseFloat(lot.unitCost) || 0;
|
||||||
|
return sum + (qty * cost);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Summary Card */}
|
{/* Header */}
|
||||||
|
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
||||||
|
<Package className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||||
|
{t('inventory.steps.initialStock', 'Stock Inicial')}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">
|
||||||
|
{t('inventory.initialStockDescription', 'Agrega uno o más lotes para registrar el inventario inicial')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Product Summary */}
|
||||||
<div className="bg-gradient-to-br from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 border border-[var(--color-primary)]/20 rounded-lg p-4">
|
<div className="bg-gradient-to-br from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 border border-[var(--color-primary)]/20 rounded-lg p-4">
|
||||||
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-3 flex items-center gap-2">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||||
<Package className="w-4 h-4" />
|
<div>
|
||||||
{t('inventory.summary', 'Summary')}
|
<span className="text-[var(--text-tertiary)] block mb-1">Producto</span>
|
||||||
</h4>
|
<span className="font-medium text-[var(--text-primary)]">{data.name || 'N/A'}</span>
|
||||||
<div className="space-y-2 text-sm">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-[var(--text-tertiary)]">{t('inventory.fields.productType')}:</span>
|
|
||||||
<span className="font-medium text-[var(--text-primary)]">
|
|
||||||
{data.productType === 'ingredient'
|
|
||||||
? t('inventory.productTypes.ingredient')
|
|
||||||
: t('inventory.productTypes.finished_product')}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{data.name && (
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<span className="text-[var(--text-tertiary)] block mb-1">Unidad</span>
|
||||||
<span className="text-[var(--text-tertiary)]">{t('inventory.fields.name')}:</span>
|
<span className="font-medium text-[var(--text-primary)]">{data.unitOfMeasure || 'N/A'}</span>
|
||||||
<span className="font-medium text-[var(--text-primary)]">{data.name}</span>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[var(--text-tertiary)] block mb-1">Cantidad Total</span>
|
||||||
|
<span className="font-medium text-[var(--color-primary)]">{totalQuantity.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[var(--text-tertiary)] block mb-1">Valor Total</span>
|
||||||
|
<span className="font-medium text-green-600">${totalValue.toFixed(2)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Unit of Measure - Required */}
|
{/* Unit of Measure - Required (moved from previous step conceptually but kept here) */}
|
||||||
|
{!data.unitOfMeasure && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
{t('inventory.fields.unitOfMeasure')} *
|
{t('inventory.fields.unitOfMeasure')} *
|
||||||
@@ -363,153 +408,181 @@ const StockConfigStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|||||||
<option value="oz">{t('inventory.units.oz')}</option>
|
<option value="oz">{t('inventory.units.oz')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
||||||
{t('inventory.fields.standardCost')}
|
Cantidad *
|
||||||
<Tooltip content={t('tooltips.standardCost')}>
|
|
||||||
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
|
||||||
</Tooltip>
|
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={data.standardCost || ''}
|
value={lot.quantity}
|
||||||
onChange={(e) => handleFieldChange('standardCost', e.target.value)}
|
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"
|
placeholder="0.00"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
min="0"
|
min="0"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-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>
|
||||||
|
|
||||||
{/* Advanced Fields Section */}
|
{/* Lot Number */}
|
||||||
<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 */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
||||||
{t('inventory.fields.lowStockThreshold')}
|
Número de Lote
|
||||||
<Tooltip content={t('tooltips.lowStockThreshold')}>
|
|
||||||
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
|
||||||
</Tooltip>
|
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="text"
|
||||||
value={data.lowStockThreshold || ''}
|
value={lot.lotNumber}
|
||||||
onChange={(e) => handleFieldChange('lowStockThreshold', e.target.value)}
|
onChange={(e) => handleLotChange(lot.id, 'lotNumber', e.target.value)}
|
||||||
placeholder="10"
|
placeholder="LOT-001"
|
||||||
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)]"
|
||||||
className="w-full px-3 py-2 border border-[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>
|
||||||
|
|
||||||
{/* Reorder Point */}
|
{/* Expiration Date */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
||||||
{t('inventory.fields.reorderPoint')}
|
Fecha de Expiración
|
||||||
<Tooltip content={t('tooltips.reorderPoint')}>
|
|
||||||
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
|
||||||
</Tooltip>
|
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="date"
|
||||||
value={data.reorderPoint || ''}
|
value={lot.expirationDate}
|
||||||
onChange={(e) => handleFieldChange('reorderPoint', e.target.value)}
|
onChange={(e) => handleLotChange(lot.id, 'expirationDate', e.target.value)}
|
||||||
placeholder="20"
|
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)]"
|
||||||
min="0"
|
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reorder Quantity */}
|
{/* Location */}
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
||||||
{t('inventory.fields.reorderQuantity')}
|
|
||||||
<Tooltip content={t('tooltips.reorderQuantity')}>
|
|
||||||
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
|
||||||
</Tooltip>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={data.reorderQuantity || ''}
|
|
||||||
onChange={(e) => handleFieldChange('reorderQuantity', e.target.value)}
|
|
||||||
placeholder="100"
|
|
||||||
min="0"
|
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Max Stock Level */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
||||||
{t('inventory.fields.maxStockLevel')}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={data.maxStockLevel || ''}
|
|
||||||
onChange={(e) => handleFieldChange('maxStockLevel', e.target.value)}
|
|
||||||
placeholder="500"
|
|
||||||
min="0"
|
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Lead Time Days */}
|
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
||||||
{t('inventory.fields.leadTimeDays')}
|
Ubicación
|
||||||
<Tooltip content={t('tooltips.leadTime')}>
|
|
||||||
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
|
||||||
</Tooltip>
|
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="text"
|
||||||
value={data.leadTimeDays || ''}
|
value={lot.location}
|
||||||
onChange={(e) => handleFieldChange('leadTimeDays', e.target.value)}
|
onChange={(e) => handleLotChange(lot.id, 'location', e.target.value)}
|
||||||
placeholder="7"
|
placeholder="Ej: Almacén A, Estante 3"
|
||||||
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)]"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InventoryWizardSteps = (
|
export const InventoryWizardSteps = (
|
||||||
data: Record<string, any>,
|
dataRef: React.MutableRefObject<Record<string, any>>,
|
||||||
setData: (data: Record<string, any>) => void
|
setData: (data: Record<string, any>) => void
|
||||||
): WizardStep[] => {
|
): WizardStep[] => {
|
||||||
|
// New architecture: return direct component references instead of arrow functions
|
||||||
|
// dataRef and onDataChange are now passed through WizardModal props
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'product-type',
|
id: 'product-type',
|
||||||
title: 'inventory.steps.productType',
|
title: 'inventory.steps.productType',
|
||||||
component: (props) => <ProductTypeStep {...props} data={data} onDataChange={setData} />,
|
component: ProductTypeStep,
|
||||||
validate: () => {
|
validate: () => {
|
||||||
return !!data.productType;
|
return !!dataRef.current.productType;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'basic-info',
|
id: 'basic-info',
|
||||||
title: 'inventory.steps.basicInfo',
|
title: 'inventory.steps.basicInfo',
|
||||||
component: (props) => <BasicInfoStep {...props} data={data} onDataChange={setData} />,
|
component: BasicInfoStep,
|
||||||
validate: () => {
|
validate: () => {
|
||||||
const category =
|
const category =
|
||||||
data.productType === 'ingredient' ? data.ingredientCategory : data.productCategory;
|
dataRef.current.productType === 'ingredient' ? dataRef.current.ingredientCategory : dataRef.current.productCategory;
|
||||||
return !!(data.name && data.name.trim().length >= 2 && category);
|
return !!(dataRef.current.name && dataRef.current.name.trim().length >= 2 && category);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stock-config',
|
id: 'stock-config',
|
||||||
title: 'inventory.steps.stockConfig',
|
title: 'inventory.steps.stockConfig',
|
||||||
component: (props) => <StockConfigStep {...props} data={data} onDataChange={setData} />,
|
component: StockConfigStep,
|
||||||
validate: () => {
|
validate: () => {
|
||||||
return !!data.unitOfMeasure;
|
// Step is optional - user can skip if not adding initial lots
|
||||||
|
// Only validate that if lots are added, they have required fields
|
||||||
|
const lots = dataRef.current.initialLots || [];
|
||||||
|
if (lots.length === 0) {
|
||||||
|
return true; // No lots, can proceed
|
||||||
|
}
|
||||||
|
// If lots exist, validate they have quantity and unit cost
|
||||||
|
return lots.every((lot: any) => {
|
||||||
|
const hasQuantity = lot.quantity && parseFloat(lot.quantity) > 0;
|
||||||
|
const hasUnitCost = lot.unitCost && parseFloat(lot.unitCost) >= 0;
|
||||||
|
return hasQuantity && hasUnitCost;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,57 +5,13 @@ import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
|
|||||||
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
interface WizardDataProps extends WizardStepProps {
|
|
||||||
data: Record<string, any>;
|
|
||||||
onDataChange: (data: Record<string, any>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single comprehensive step with all fields
|
// Single comprehensive step with all fields
|
||||||
const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
const QualityTemplateDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||||
|
const data = dataRef?.current || {};
|
||||||
const { t } = useTranslation('wizards');
|
const { t } = useTranslation('wizards');
|
||||||
const [templateData, setTemplateData] = useState({
|
|
||||||
// Required fields
|
|
||||||
name: data.name || '',
|
|
||||||
checkType: data.checkType || 'product_quality',
|
|
||||||
weight: data.weight || '5.0',
|
|
||||||
|
|
||||||
// Basic fields
|
const handleFieldChange = (field: string, value: any) => {
|
||||||
templateCode: data.templateCode || '',
|
onDataChange?.({ ...data, [field]: value });
|
||||||
description: data.description || '',
|
|
||||||
applicableStages: data.applicableStages || '',
|
|
||||||
|
|
||||||
// Check points configuration
|
|
||||||
checkPoints: data.checkPoints || '',
|
|
||||||
|
|
||||||
// Scoring configuration
|
|
||||||
scoringMethod: data.scoringMethod || 'weighted_average',
|
|
||||||
passThreshold: data.passThreshold || '70.0',
|
|
||||||
isRequired: data.isRequired ?? false,
|
|
||||||
frequencyDays: data.frequencyDays || '',
|
|
||||||
|
|
||||||
// Advanced configuration (JSONB fields)
|
|
||||||
parameters: data.parameters || '',
|
|
||||||
thresholds: data.thresholds || '',
|
|
||||||
scoringCriteria: data.scoringCriteria || '',
|
|
||||||
|
|
||||||
// Status
|
|
||||||
isActive: data.isActive ?? true,
|
|
||||||
version: data.version || '1.0',
|
|
||||||
|
|
||||||
// Helper fields for UI
|
|
||||||
requiresPhoto: data.requiresPhoto ?? false,
|
|
||||||
criticalControlPoint: data.criticalControlPoint ?? false,
|
|
||||||
notifyOnFail: data.notifyOnFail ?? false,
|
|
||||||
responsibleRole: data.responsibleRole || '',
|
|
||||||
requiredEquipment: data.requiredEquipment || '',
|
|
||||||
acceptanceCriteria: data.acceptanceCriteria || '',
|
|
||||||
specificConditions: data.specificConditions || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update parent whenever local state changes
|
|
||||||
const handleDataChange = (newTemplateData: any) => {
|
|
||||||
setTemplateData(newTemplateData);
|
|
||||||
onDataChange({ ...data, ...newTemplateData });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -77,8 +33,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={templateData.name}
|
value={data.name}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, name: e.target.value })}
|
onChange={(e) => handleFieldChange('name', e.target.value)}
|
||||||
placeholder={t('qualityTemplate.fields.namePlaceholder')}
|
placeholder={t('qualityTemplate.fields.namePlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -89,8 +45,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
{t('qualityTemplate.fields.checkType')} *
|
{t('qualityTemplate.fields.checkType')} *
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={templateData.checkType}
|
value={data.checkType}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, checkType: e.target.value })}
|
onChange={(e) => handleFieldChange('checkType', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="product_quality">{t('qualityTemplate.checkTypes.product_quality')}</option>
|
<option value="product_quality">{t('qualityTemplate.checkTypes.product_quality')}</option>
|
||||||
@@ -112,8 +68,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={templateData.weight}
|
value={data.weight}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, weight: e.target.value })}
|
onChange={(e) => handleFieldChange('weight', e.target.value)}
|
||||||
placeholder="5.0"
|
placeholder="5.0"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
min="0"
|
min="0"
|
||||||
@@ -136,8 +92,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={templateData.templateCode}
|
value={data.templateCode}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, templateCode: e.target.value })}
|
onChange={(e) => handleFieldChange('templateCode', e.target.value)}
|
||||||
placeholder={t('qualityTemplate.fields.templateCodePlaceholder')}
|
placeholder={t('qualityTemplate.fields.templateCodePlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -149,8 +105,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={templateData.version}
|
value={data.version}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, version: e.target.value })}
|
onChange={(e) => handleFieldChange('version', e.target.value)}
|
||||||
placeholder="1.0"
|
placeholder="1.0"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -161,8 +117,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
{t('qualityTemplate.fields.description')}
|
{t('qualityTemplate.fields.description')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={templateData.description}
|
value={data.description}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, description: e.target.value })}
|
onChange={(e) => handleFieldChange('description', e.target.value)}
|
||||||
placeholder={t('qualityTemplate.fields.descriptionPlaceholder')}
|
placeholder={t('qualityTemplate.fields.descriptionPlaceholder')}
|
||||||
rows={2}
|
rows={2}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
@@ -178,8 +134,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={templateData.applicableStages}
|
value={data.applicableStages}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, applicableStages: e.target.value })}
|
onChange={(e) => handleFieldChange('applicableStages', e.target.value)}
|
||||||
placeholder={t('qualityTemplate.fields.applicablePlaceholder')}
|
placeholder={t('qualityTemplate.fields.applicablePlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -196,8 +152,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
{t('qualityTemplate.scoringMethods.scoringMethod')}
|
{t('qualityTemplate.scoringMethods.scoringMethod')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={templateData.scoringMethod}
|
value={data.scoringMethod}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, scoringMethod: e.target.value })}
|
onChange={(e) => handleFieldChange('scoringMethod', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="weighted_average">{t('qualityTemplate.scoringMethods.weightedAverage')}</option>
|
<option value="weighted_average">{t('qualityTemplate.scoringMethods.weightedAverage')}</option>
|
||||||
@@ -216,8 +172,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={templateData.passThreshold}
|
value={data.passThreshold}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, passThreshold: e.target.value })}
|
onChange={(e) => handleFieldChange('passThreshold', e.target.value)}
|
||||||
placeholder="70.0"
|
placeholder="70.0"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
min="0"
|
min="0"
|
||||||
@@ -235,8 +191,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={templateData.frequencyDays}
|
value={data.frequencyDays}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, frequencyDays: e.target.value })}
|
onChange={(e) => handleFieldChange('frequencyDays', e.target.value)}
|
||||||
placeholder={t('qualityTemplate.advancedFields.frequencyPlaceholder')}
|
placeholder={t('qualityTemplate.advancedFields.frequencyPlaceholder')}
|
||||||
min="1"
|
min="1"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
@@ -246,8 +202,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={templateData.isRequired}
|
checked={data.isRequired}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, isRequired: e.target.checked })}
|
onChange={(e) => handleFieldChange('isRequired', e.target.checked)}
|
||||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||||
/>
|
/>
|
||||||
<label className="text-sm text-[var(--text-secondary)]">
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
@@ -276,8 +232,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={templateData.checkPoints}
|
value={data.checkPoints}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, checkPoints: e.target.value })}
|
onChange={(e) => handleFieldChange('checkPoints', e.target.value)}
|
||||||
placeholder={t('qualityTemplate.advancedFields.checkPointsPlaceholder')}
|
placeholder={t('qualityTemplate.advancedFields.checkPointsPlaceholder')}
|
||||||
rows={4}
|
rows={4}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
||||||
@@ -289,8 +245,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
{t('qualityTemplate.advancedFields.acceptanceCriteria')}
|
{t('qualityTemplate.advancedFields.acceptanceCriteria')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={templateData.acceptanceCriteria}
|
value={data.acceptanceCriteria}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, acceptanceCriteria: e.target.value })}
|
onChange={(e) => handleFieldChange('acceptanceCriteria', e.target.value)}
|
||||||
placeholder={t('qualityTemplate.advancedFields.acceptanceCriteriaPlaceholder')}
|
placeholder={t('qualityTemplate.advancedFields.acceptanceCriteriaPlaceholder')}
|
||||||
rows={2}
|
rows={2}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
@@ -313,8 +269,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={templateData.parameters}
|
value={data.parameters}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, parameters: e.target.value })}
|
onChange={(e) => handleFieldChange('parameters', e.target.value)}
|
||||||
placeholder={t('qualityTemplate.advancedFields.parametersPlaceholder')}
|
placeholder={t('qualityTemplate.advancedFields.parametersPlaceholder')}
|
||||||
rows={2}
|
rows={2}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
||||||
@@ -329,8 +285,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={templateData.thresholds}
|
value={data.thresholds}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, thresholds: e.target.value })}
|
onChange={(e) => handleFieldChange('thresholds', e.target.value)}
|
||||||
placeholder={t('qualityTemplate.advancedFields.thresholdsPlaceholder')}
|
placeholder={t('qualityTemplate.advancedFields.thresholdsPlaceholder')}
|
||||||
rows={2}
|
rows={2}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
||||||
@@ -345,8 +301,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={templateData.scoringCriteria}
|
value={data.scoringCriteria}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, scoringCriteria: e.target.value })}
|
onChange={(e) => handleFieldChange('scoringCriteria', e.target.value)}
|
||||||
placeholder={t('qualityTemplate.advancedFields.scoringCriteriaPlaceholder')}
|
placeholder={t('qualityTemplate.advancedFields.scoringCriteriaPlaceholder')}
|
||||||
rows={2}
|
rows={2}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
||||||
@@ -367,8 +323,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={templateData.responsibleRole}
|
value={data.responsibleRole}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, responsibleRole: e.target.value })}
|
onChange={(e) => handleFieldChange('responsibleRole', e.target.value)}
|
||||||
placeholder={t('qualityTemplate.advancedFields.responsibleRolePlaceholder')}
|
placeholder={t('qualityTemplate.advancedFields.responsibleRolePlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -380,8 +336,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={templateData.requiredEquipment}
|
value={data.requiredEquipment}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, requiredEquipment: e.target.value })}
|
onChange={(e) => handleFieldChange('requiredEquipment', e.target.value)}
|
||||||
placeholder={t('qualityTemplate.advancedFields.requiredEquipmentPlaceholder')}
|
placeholder={t('qualityTemplate.advancedFields.requiredEquipmentPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -392,8 +348,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
{t('qualityTemplate.advancedFields.specificConditions')}
|
{t('qualityTemplate.advancedFields.specificConditions')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={templateData.specificConditions}
|
value={data.specificConditions}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, specificConditions: e.target.value })}
|
onChange={(e) => handleFieldChange('specificConditions', e.target.value)}
|
||||||
placeholder={t('qualityTemplate.advancedFields.specificConditionsPlaceholder')}
|
placeholder={t('qualityTemplate.advancedFields.specificConditionsPlaceholder')}
|
||||||
rows={2}
|
rows={2}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
@@ -411,8 +367,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={templateData.isActive}
|
checked={data.isActive}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, isActive: e.target.checked })}
|
onChange={(e) => handleFieldChange('isActive', e.target.checked)}
|
||||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||||
/>
|
/>
|
||||||
<label className="text-sm text-[var(--text-secondary)]">
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
@@ -423,8 +379,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={templateData.requiresPhoto}
|
checked={data.requiresPhoto}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, requiresPhoto: e.target.checked })}
|
onChange={(e) => handleFieldChange('requiresPhoto', e.target.checked)}
|
||||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||||
/>
|
/>
|
||||||
<label className="text-sm text-[var(--text-secondary)]">
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
@@ -435,8 +391,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={templateData.criticalControlPoint}
|
checked={data.criticalControlPoint}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, criticalControlPoint: e.target.checked })}
|
onChange={(e) => handleFieldChange('criticalControlPoint', e.target.checked)}
|
||||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||||
/>
|
/>
|
||||||
<label className="text-sm text-[var(--text-secondary)]">
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
@@ -447,8 +403,8 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={templateData.notifyOnFail}
|
checked={data.notifyOnFail}
|
||||||
onChange={(e) => handleDataChange({ ...templateData, notifyOnFail: e.target.checked })}
|
onChange={(e) => handleFieldChange('notifyOnFail', e.target.checked)}
|
||||||
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
||||||
/>
|
/>
|
||||||
<label className="text-sm text-[var(--text-secondary)]">
|
<label className="text-sm text-[var(--text-secondary)]">
|
||||||
@@ -463,15 +419,16 @@ const QualityTemplateDetailsStep: React.FC<WizardDataProps> = ({ data, onDataCha
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const QualityTemplateWizardSteps = (
|
export const QualityTemplateWizardSteps = (
|
||||||
data: Record<string, any>,
|
dataRef: React.MutableRefObject<Record<string, any>>,
|
||||||
setData: (data: Record<string, any>) => void
|
setData: (data: Record<string, any>) => void
|
||||||
): WizardStep[] => [
|
): WizardStep[] => {
|
||||||
|
// New architecture: return direct component references instead of arrow functions
|
||||||
|
// dataRef and onDataChange are now passed through WizardModal props
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
id: 'template-details',
|
id: 'template-details',
|
||||||
title: 'qualityTemplate.advancedFields.templateDetailsTitle',
|
title: 'qualityTemplate.advancedFields.templateDetailsTitle',
|
||||||
component: (props) => <QualityTemplateDetailsStep {...props} data={data} onDataChange={setData} />,
|
component: QualityTemplateDetailsStep,
|
||||||
validate: () => {
|
|
||||||
return !!(data.name && data.checkType && data.weight);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
|||||||
@@ -12,60 +12,20 @@ import { showToast } from '../../../../utils/toast';
|
|||||||
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
|
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
|
||||||
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
||||||
|
|
||||||
interface WizardDataProps extends WizardStepProps {
|
const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||||
data: Record<string, any>;
|
const data = dataRef?.current || {};
|
||||||
onDataChange: (data: Record<string, any>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|
||||||
const { currentTenant } = useTenant();
|
const { currentTenant } = useTenant();
|
||||||
const [recipeData, setRecipeData] = useState({
|
|
||||||
// Required fields
|
|
||||||
name: data.name || '',
|
|
||||||
finishedProductId: data.finishedProductId || '',
|
|
||||||
yieldQuantity: data.yieldQuantity || '',
|
|
||||||
yieldUnit: data.yieldUnit || 'units',
|
|
||||||
|
|
||||||
// Optional basic fields
|
|
||||||
category: data.category || 'bread',
|
|
||||||
prepTime: data.prepTime || '',
|
|
||||||
instructions: data.instructions || '',
|
|
||||||
|
|
||||||
// Advanced optional fields
|
|
||||||
recipeCode: data.recipeCode || '',
|
|
||||||
version: data.version || '1.0',
|
|
||||||
difficultyLevel: data.difficultyLevel || 3,
|
|
||||||
cookTime: data.cookTime || '',
|
|
||||||
restTime: data.restTime || '',
|
|
||||||
totalTime: data.totalTime || '',
|
|
||||||
description: data.description || '',
|
|
||||||
preparationNotes: data.preparationNotes || '',
|
|
||||||
storageInstructions: data.storageInstructions || '',
|
|
||||||
servesCount: data.servesCount || '',
|
|
||||||
batchSizeMultiplier: data.batchSizeMultiplier || 1.0,
|
|
||||||
minBatchSize: data.minBatchSize || '',
|
|
||||||
maxBatchSize: data.maxBatchSize || '',
|
|
||||||
optimalProductionTemp: data.optimalProductionTemp || '',
|
|
||||||
optimalHumidity: data.optimalHumidity || '',
|
|
||||||
isSeasonal: data.isSeasonal || false,
|
|
||||||
isSignatureItem: data.isSignatureItem || false,
|
|
||||||
seasonStartMonth: data.seasonStartMonth || '',
|
|
||||||
seasonEndMonth: data.seasonEndMonth || '',
|
|
||||||
allergens: data.allergens || '',
|
|
||||||
dietaryTags: data.dietaryTags || '',
|
|
||||||
targetMargin: data.targetMargin || '',
|
|
||||||
});
|
|
||||||
const [finishedProducts, setFinishedProducts] = useState<IngredientResponse[]>([]);
|
const [finishedProducts, setFinishedProducts] = useState<IngredientResponse[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleFieldChange = (field: string, value: any) => {
|
||||||
|
onDataChange?.({ ...data, [field]: value });
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchFinishedProducts();
|
fetchFinishedProducts();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onDataChange({ ...data, ...recipeData });
|
|
||||||
}, [recipeData]);
|
|
||||||
|
|
||||||
const fetchFinishedProducts = async () => {
|
const fetchFinishedProducts = async () => {
|
||||||
if (!currentTenant?.id) return;
|
if (!currentTenant?.id) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -97,8 +57,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={recipeData.name}
|
value={data.name}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, name: e.target.value })}
|
onChange={(e) => handleFieldChange('name', e.target.value)}
|
||||||
placeholder="e.g., Traditional Baguette"
|
placeholder="e.g., Traditional Baguette"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -110,8 +70,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
Category *
|
Category *
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={recipeData.category}
|
value={data.category}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, category: e.target.value })}
|
onChange={(e) => handleFieldChange('category', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="bread">Bread</option>
|
<option value="bread">Bread</option>
|
||||||
@@ -133,8 +93,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={recipeData.finishedProductId}
|
value={data.finishedProductId}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, finishedProductId: e.target.value })}
|
onChange={(e) => handleFieldChange('finishedProductId', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
@@ -153,8 +113,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={recipeData.yieldQuantity}
|
value={data.yieldQuantity}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, yieldQuantity: e.target.value })}
|
onChange={(e) => handleFieldChange('yieldQuantity', e.target.value)}
|
||||||
placeholder="12"
|
placeholder="12"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
min="0.01"
|
min="0.01"
|
||||||
@@ -167,8 +127,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
Yield Unit *
|
Yield Unit *
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={recipeData.yieldUnit}
|
value={data.yieldUnit}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, yieldUnit: e.target.value })}
|
onChange={(e) => handleFieldChange('yieldUnit', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="units">Units</option>
|
<option value="units">Units</option>
|
||||||
@@ -187,8 +147,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={recipeData.prepTime}
|
value={data.prepTime}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, prepTime: e.target.value })}
|
onChange={(e) => handleFieldChange('prepTime', e.target.value)}
|
||||||
placeholder="60"
|
placeholder="60"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
min="0"
|
min="0"
|
||||||
@@ -200,8 +160,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
Instructions
|
Instructions
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={recipeData.instructions}
|
value={data.instructions}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, instructions: e.target.value })}
|
onChange={(e) => handleFieldChange('instructions', e.target.value)}
|
||||||
placeholder="Step-by-step preparation instructions..."
|
placeholder="Step-by-step preparation instructions..."
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
rows={4}
|
rows={4}
|
||||||
@@ -221,8 +181,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={recipeData.recipeCode}
|
value={data.recipeCode}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, recipeCode: e.target.value })}
|
onChange={(e) => handleFieldChange('recipeCode', e.target.value)}
|
||||||
placeholder="RCP-001"
|
placeholder="RCP-001"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -234,8 +194,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={recipeData.version}
|
value={data.version}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, version: e.target.value })}
|
onChange={(e) => handleFieldChange('version', e.target.value)}
|
||||||
placeholder="1.0"
|
placeholder="1.0"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -250,8 +210,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={recipeData.difficultyLevel}
|
value={data.difficultyLevel}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, difficultyLevel: parseInt(e.target.value) || 1 })}
|
onChange={(e) => handleFieldChange('difficultyLevel', parseInt(e.target.value) || 1)}
|
||||||
min="1"
|
min="1"
|
||||||
max="5"
|
max="5"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
@@ -264,8 +224,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={recipeData.cookTime}
|
value={data.cookTime}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, cookTime: e.target.value })}
|
onChange={(e) => handleFieldChange('cookTime', e.target.value)}
|
||||||
placeholder="30"
|
placeholder="30"
|
||||||
min="0"
|
min="0"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
@@ -281,8 +241,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={recipeData.restTime}
|
value={data.restTime}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, restTime: e.target.value })}
|
onChange={(e) => handleFieldChange('restTime', e.target.value)}
|
||||||
placeholder="60"
|
placeholder="60"
|
||||||
min="0"
|
min="0"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
@@ -295,8 +255,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={recipeData.totalTime}
|
value={data.totalTime}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, totalTime: e.target.value })}
|
onChange={(e) => handleFieldChange('totalTime', e.target.value)}
|
||||||
placeholder="90"
|
placeholder="90"
|
||||||
min="0"
|
min="0"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
@@ -309,8 +269,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={recipeData.servesCount}
|
value={data.servesCount}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, servesCount: e.target.value })}
|
onChange={(e) => handleFieldChange('servesCount', e.target.value)}
|
||||||
placeholder="8"
|
placeholder="8"
|
||||||
min="0"
|
min="0"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
@@ -326,8 +286,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={recipeData.batchSizeMultiplier}
|
value={data.batchSizeMultiplier}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, batchSizeMultiplier: parseFloat(e.target.value) || 1 })}
|
onChange={(e) => handleFieldChange('batchSizeMultiplier', parseFloat(e.target.value) || 1)}
|
||||||
placeholder="1.0"
|
placeholder="1.0"
|
||||||
min="0.1"
|
min="0.1"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
@@ -341,8 +301,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={recipeData.minBatchSize}
|
value={data.minBatchSize}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, minBatchSize: e.target.value })}
|
onChange={(e) => handleFieldChange('minBatchSize', e.target.value)}
|
||||||
placeholder="5"
|
placeholder="5"
|
||||||
min="0"
|
min="0"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
@@ -356,8 +316,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={recipeData.maxBatchSize}
|
value={data.maxBatchSize}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, maxBatchSize: e.target.value })}
|
onChange={(e) => handleFieldChange('maxBatchSize', e.target.value)}
|
||||||
placeholder="100"
|
placeholder="100"
|
||||||
min="0"
|
min="0"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
@@ -371,8 +331,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={recipeData.optimalProductionTemp}
|
value={data.optimalProductionTemp}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, optimalProductionTemp: e.target.value })}
|
onChange={(e) => handleFieldChange('optimalProductionTemp', e.target.value)}
|
||||||
placeholder="24"
|
placeholder="24"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
@@ -385,8 +345,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={recipeData.optimalHumidity}
|
value={data.optimalHumidity}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, optimalHumidity: e.target.value })}
|
onChange={(e) => handleFieldChange('optimalHumidity', e.target.value)}
|
||||||
placeholder="65"
|
placeholder="65"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
@@ -401,8 +361,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={recipeData.targetMargin}
|
value={data.targetMargin}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, targetMargin: e.target.value })}
|
onChange={(e) => handleFieldChange('targetMargin', e.target.value)}
|
||||||
placeholder="30"
|
placeholder="30"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
@@ -417,8 +377,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="isSeasonal"
|
id="isSeasonal"
|
||||||
checked={recipeData.isSeasonal}
|
checked={data.isSeasonal}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, isSeasonal: e.target.checked })}
|
onChange={(e) => handleFieldChange('isSeasonal', e.target.checked)}
|
||||||
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
|
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="isSeasonal" className="text-sm font-medium text-[var(--text-secondary)]">
|
<label htmlFor="isSeasonal" className="text-sm font-medium text-[var(--text-secondary)]">
|
||||||
@@ -430,8 +390,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="isSignatureItem"
|
id="isSignatureItem"
|
||||||
checked={recipeData.isSignatureItem}
|
checked={data.isSignatureItem}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, isSignatureItem: e.target.checked })}
|
onChange={(e) => handleFieldChange('isSignatureItem', e.target.checked)}
|
||||||
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
|
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="isSignatureItem" className="text-sm font-medium text-[var(--text-secondary)]">
|
<label htmlFor="isSignatureItem" className="text-sm font-medium text-[var(--text-secondary)]">
|
||||||
@@ -440,15 +400,15 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{recipeData.isSeasonal && (
|
{data.isSeasonal && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
Season Start Month
|
Season Start Month
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={recipeData.seasonStartMonth}
|
value={data.seasonStartMonth}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, seasonStartMonth: e.target.value })}
|
onChange={(e) => handleFieldChange('seasonStartMonth', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="">Select month...</option>
|
<option value="">Select month...</option>
|
||||||
@@ -465,8 +425,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
Season End Month
|
Season End Month
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={recipeData.seasonEndMonth}
|
value={data.seasonEndMonth}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, seasonEndMonth: e.target.value })}
|
onChange={(e) => handleFieldChange('seasonEndMonth', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="">Select month...</option>
|
<option value="">Select month...</option>
|
||||||
@@ -485,8 +445,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
Description
|
Description
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={recipeData.description}
|
value={data.description}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, description: e.target.value })}
|
onChange={(e) => handleFieldChange('description', e.target.value)}
|
||||||
placeholder="Detailed description of the recipe..."
|
placeholder="Detailed description of the recipe..."
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
rows={3}
|
rows={3}
|
||||||
@@ -498,8 +458,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
Preparation Notes
|
Preparation Notes
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={recipeData.preparationNotes}
|
value={data.preparationNotes}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, preparationNotes: e.target.value })}
|
onChange={(e) => handleFieldChange('preparationNotes', e.target.value)}
|
||||||
placeholder="Tips and notes for preparation..."
|
placeholder="Tips and notes for preparation..."
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
rows={3}
|
rows={3}
|
||||||
@@ -511,8 +471,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
Storage Instructions
|
Storage Instructions
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={recipeData.storageInstructions}
|
value={data.storageInstructions}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, storageInstructions: e.target.value })}
|
onChange={(e) => handleFieldChange('storageInstructions', e.target.value)}
|
||||||
placeholder="How to store the finished product..."
|
placeholder="How to store the finished product..."
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
rows={3}
|
rows={3}
|
||||||
@@ -525,8 +485,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={recipeData.allergens}
|
value={data.allergens}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, allergens: e.target.value })}
|
onChange={(e) => handleFieldChange('allergens', e.target.value)}
|
||||||
placeholder="e.g., gluten, dairy, eggs (comma-separated)"
|
placeholder="e.g., gluten, dairy, eggs (comma-separated)"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -538,8 +498,8 @@ const RecipeDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) =>
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={recipeData.dietaryTags}
|
value={data.dietaryTags}
|
||||||
onChange={(e) => setRecipeData({ ...recipeData, dietaryTags: e.target.value })}
|
onChange={(e) => handleFieldChange('dietaryTags', e.target.value)}
|
||||||
placeholder="e.g., vegan, gluten-free, organic (comma-separated)"
|
placeholder="e.g., vegan, gluten-free, organic (comma-separated)"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -558,10 +518,10 @@ interface SelectedIngredient {
|
|||||||
order: number;
|
order: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
const IngredientsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||||
|
const data = dataRef?.current || {};
|
||||||
const { currentTenant } = useTenant();
|
const { currentTenant } = useTenant();
|
||||||
const [ingredients, setIngredients] = useState<IngredientResponse[]>([]);
|
const [ingredients, setIngredients] = useState<IngredientResponse[]>([]);
|
||||||
const [selectedIngredients, setSelectedIngredients] = useState<SelectedIngredient[]>(data.ingredients || []);
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
@@ -570,10 +530,6 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|||||||
fetchIngredients();
|
fetchIngredients();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onDataChange({ ...data, ingredients: selectedIngredients });
|
|
||||||
}, [selectedIngredients]);
|
|
||||||
|
|
||||||
const fetchIngredients = async () => {
|
const fetchIngredients = async () => {
|
||||||
if (!currentTenant?.id) return;
|
if (!currentTenant?.id) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -596,21 +552,25 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|||||||
quantity: 0,
|
quantity: 0,
|
||||||
unit: MeasurementUnit.GRAMS,
|
unit: MeasurementUnit.GRAMS,
|
||||||
notes: '',
|
notes: '',
|
||||||
order: selectedIngredients.length + 1,
|
order: (data.ingredients || []).length + 1,
|
||||||
};
|
};
|
||||||
setSelectedIngredients([...selectedIngredients, newIngredient]);
|
onDataChange?.({ ...data, ingredients: [...(data.ingredients || []), newIngredient] });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateIngredient = (id: string, field: keyof SelectedIngredient, value: any) => {
|
const handleUpdateIngredient = (id: string, field: keyof SelectedIngredient, value: any) => {
|
||||||
setSelectedIngredients(
|
onDataChange?.({
|
||||||
selectedIngredients.map(ing =>
|
...data,
|
||||||
|
ingredients: (data.ingredients || []).map(ing =>
|
||||||
ing.id === id ? { ...ing, [field]: value } : ing
|
ing.id === id ? { ...ing, [field]: value } : ing
|
||||||
)
|
)
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveIngredient = (id: string) => {
|
const handleRemoveIngredient = (id: string) => {
|
||||||
setSelectedIngredients(selectedIngredients.filter(ing => ing.id !== id));
|
onDataChange?.({
|
||||||
|
...data,
|
||||||
|
ingredients: (data.ingredients || []).filter(ing => ing.id !== id)
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredIngredients = ingredients.filter(ing =>
|
const filteredIngredients = ingredients.filter(ing =>
|
||||||
@@ -639,14 +599,14 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{selectedIngredients.length === 0 ? (
|
{(data.ingredients || []).length === 0 ? (
|
||||||
<div className="text-center py-8 border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
|
<div className="text-center py-8 border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
|
||||||
<Package className="w-12 h-12 mx-auto mb-3 text-[var(--text-tertiary)]" />
|
<Package className="w-12 h-12 mx-auto mb-3 text-[var(--text-tertiary)]" />
|
||||||
<p className="text-[var(--text-secondary)] mb-2">No ingredients added</p>
|
<p className="text-[var(--text-secondary)] mb-2">No ingredients added</p>
|
||||||
<p className="text-sm text-[var(--text-tertiary)]">Click "Add Ingredient" to begin</p>
|
<p className="text-sm text-[var(--text-tertiary)]">Click "Add Ingredient" to begin</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
selectedIngredients.map((selectedIng) => (
|
(data.ingredients || []).map((selectedIng) => (
|
||||||
<div key={selectedIng.id} className="p-4 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]">
|
<div key={selectedIng.id} className="p-4 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-3 items-start">
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-3 items-start">
|
||||||
<div className="md:col-span-5">
|
<div className="md:col-span-5">
|
||||||
@@ -737,10 +697,10 @@ const IngredientsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
const QualityTemplatesStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onComplete }) => {
|
||||||
|
const data = dataRef?.current || {};
|
||||||
const { currentTenant } = useTenant();
|
const { currentTenant } = useTenant();
|
||||||
const [templates, setTemplates] = useState<QualityCheckTemplateResponse[]>([]);
|
const [templates, setTemplates] = useState<QualityCheckTemplateResponse[]>([]);
|
||||||
const [selectedTemplateIds, setSelectedTemplateIds] = useState<string[]>(data.selectedTemplates || []);
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -749,10 +709,6 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
|||||||
fetchTemplates();
|
fetchTemplates();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onDataChange({ ...data, selectedTemplates: selectedTemplateIds });
|
|
||||||
}, [selectedTemplateIds]);
|
|
||||||
|
|
||||||
const fetchTemplates = async () => {
|
const fetchTemplates = async () => {
|
||||||
if (!currentTenant?.id) return;
|
if (!currentTenant?.id) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -768,11 +724,11 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleTemplate = (templateId: string) => {
|
const toggleTemplate = (templateId: string) => {
|
||||||
setSelectedTemplateIds(prev =>
|
const currentTemplates = data.selectedTemplates || [];
|
||||||
prev.includes(templateId)
|
const newTemplates = currentTemplates.includes(templateId)
|
||||||
? prev.filter(id => id !== templateId)
|
? currentTemplates.filter(id => id !== templateId)
|
||||||
: [...prev, templateId]
|
: [...currentTemplates, templateId];
|
||||||
);
|
onDataChange?.({ ...data, selectedTemplates: newTemplates });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateRecipe = async () => {
|
const handleCreateRecipe = async () => {
|
||||||
@@ -795,11 +751,11 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
let qualityConfig: RecipeQualityConfiguration | undefined;
|
let qualityConfig: RecipeQualityConfiguration | undefined;
|
||||||
if (selectedTemplateIds.length > 0) {
|
if ((data.selectedTemplates || []).length > 0) {
|
||||||
qualityConfig = {
|
qualityConfig = {
|
||||||
stages: {
|
stages: {
|
||||||
production: {
|
production: {
|
||||||
template_ids: selectedTemplateIds,
|
template_ids: data.selectedTemplates || [],
|
||||||
required_checks: [],
|
required_checks: [],
|
||||||
optional_checks: [],
|
optional_checks: [],
|
||||||
blocking_on_failure: true,
|
blocking_on_failure: true,
|
||||||
@@ -901,7 +857,7 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleTemplate(template.id)}
|
onClick={() => toggleTemplate(template.id)}
|
||||||
className={`w-full p-4 rounded-lg border-2 transition-all text-left ${
|
className={`w-full p-4 rounded-lg border-2 transition-all text-left ${
|
||||||
selectedTemplateIds.includes(template.id)
|
(data.selectedTemplates || []).includes(template.id)
|
||||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
||||||
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50'
|
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50'
|
||||||
}`}
|
}`}
|
||||||
@@ -928,7 +884,7 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{selectedTemplateIds.includes(template.id) && (
|
{(data.selectedTemplates || []).includes(template.id) && (
|
||||||
<CheckCircle2 className="w-5 h-5 text-[var(--color-primary)] flex-shrink-0 ml-3" />
|
<CheckCircle2 className="w-5 h-5 text-[var(--color-primary)] flex-shrink-0 ml-3" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -937,10 +893,10 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedTemplateIds.length > 0 && (
|
{(data.selectedTemplates || []).length > 0 && (
|
||||||
<div className="p-4 bg-[var(--color-primary)]/5 rounded-lg border border-[var(--color-primary)]/20">
|
<div className="p-4 bg-[var(--color-primary)]/5 rounded-lg border border-[var(--color-primary)]/20">
|
||||||
<p className="text-sm text-[var(--text-primary)]">
|
<p className="text-sm text-[var(--text-primary)]">
|
||||||
<strong>{selectedTemplateIds.length}</strong> template(s) selected
|
<strong>{(data.selectedTemplates || []).length}</strong> template(s) selected
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -971,36 +927,28 @@ const QualityTemplatesStep: React.FC<WizardDataProps> = ({ data, onDataChange, o
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecipeWizardSteps = (data: Record<string, any>, setData: (data: Record<string, any>) => void): WizardStep[] => [
|
export const RecipeWizardSteps = (dataRef: React.MutableRefObject<Record<string, any>>, setData: (data: Record<string, any>) => void): WizardStep[] => {
|
||||||
|
// New architecture: return direct component references instead of arrow functions
|
||||||
|
// dataRef and onDataChange are now passed through WizardModal props
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
id: 'recipe-details',
|
id: 'recipe-details',
|
||||||
title: 'Recipe Details',
|
title: 'Recipe Details',
|
||||||
description: 'Name, category, yield',
|
description: 'Name, category, yield',
|
||||||
component: (props) => <RecipeDetailsStep {...props} data={data} onDataChange={setData} />,
|
component: RecipeDetailsStep,
|
||||||
validate: () => {
|
|
||||||
return !!(data.name && data.finishedProductId && data.yieldQuantity);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'recipe-ingredients',
|
id: 'recipe-ingredients',
|
||||||
title: 'Ingredients',
|
title: 'Ingredients',
|
||||||
description: 'Selection and quantities',
|
description: 'Selection and quantities',
|
||||||
component: (props) => <IngredientsStep {...props} data={data} onDataChange={setData} />,
|
component: IngredientsStep,
|
||||||
validate: () => {
|
|
||||||
if (!data.ingredients || data.ingredients.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const invalidIngredients = data.ingredients.filter(
|
|
||||||
(ing: any) => !ing.ingredientId || ing.quantity <= 0
|
|
||||||
);
|
|
||||||
return invalidIngredients.length === 0;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'recipe-quality-templates',
|
id: 'recipe-quality-templates',
|
||||||
title: 'Quality Templates',
|
title: 'Quality Templates',
|
||||||
description: 'Applicable quality controls',
|
description: 'Applicable quality controls',
|
||||||
component: (props) => <QualityTemplatesStep {...props} data={data} onDataChange={setData} />,
|
component: QualityTemplatesStep,
|
||||||
isOptional: true,
|
isOptional: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
Edit3,
|
Edit3,
|
||||||
Upload,
|
Upload,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
AlertCircle,
|
|
||||||
Download,
|
Download,
|
||||||
FileSpreadsheet,
|
FileSpreadsheet,
|
||||||
Calendar,
|
Calendar,
|
||||||
@@ -23,24 +22,18 @@ import { showToast } from '../../../../utils/toast';
|
|||||||
// STEP 1: Entry Method Selection
|
// STEP 1: Entry Method Selection
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
interface EntryMethodStepProps extends WizardStepProps {
|
const EntryMethodStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNext }) => {
|
||||||
data: Record<string, any>;
|
const data = dataRef?.current || {};
|
||||||
onDataChange: (data: Record<string, any>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EntryMethodStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, onNext }) => {
|
|
||||||
const [selectedMethod, setSelectedMethod] = useState<'manual' | 'upload'>(
|
const [selectedMethod, setSelectedMethod] = useState<'manual' | 'upload'>(
|
||||||
data.entryMethod || 'manual'
|
data.entryMethod || 'manual'
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelect = (method: 'manual' | 'upload') => {
|
const handleSelect = (method: 'manual' | 'upload') => {
|
||||||
setSelectedMethod(method);
|
setSelectedMethod(method);
|
||||||
onDataChange({ ...data, entryMethod: method });
|
const newData = { ...data, entryMethod: method };
|
||||||
};
|
onDataChange?.(newData);
|
||||||
|
// Automatically advance to next step after selection
|
||||||
const handleContinue = () => {
|
setTimeout(() => onNext?.(), 100);
|
||||||
onDataChange({ ...data, entryMethod: selectedMethod });
|
|
||||||
onNext();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -166,17 +159,6 @@ const EntryMethodStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Continue Button */}
|
|
||||||
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
|
|
||||||
<button
|
|
||||||
onClick={handleContinue}
|
|
||||||
className="px-6 py-3 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 transition-colors font-medium inline-flex items-center gap-2"
|
|
||||||
>
|
|
||||||
Continuar
|
|
||||||
<CheckCircle2 className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -185,14 +167,9 @@ const EntryMethodStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
|||||||
// STEP 2a: Manual Entry Form
|
// STEP 2a: Manual Entry Form
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, onNext }) => {
|
const ManualEntryStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNext }) => {
|
||||||
|
const data = dataRef?.current || {};
|
||||||
const { currentTenant } = useTenant();
|
const { currentTenant } = useTenant();
|
||||||
const [salesItems, setSalesItems] = useState(data.salesItems || []);
|
|
||||||
const [saleDate, setSaleDate] = useState(
|
|
||||||
data.saleDate || new Date().toISOString().split('T')[0]
|
|
||||||
);
|
|
||||||
const [paymentMethod, setPaymentMethod] = useState(data.paymentMethod || 'cash');
|
|
||||||
const [notes, setNotes] = useState(data.notes || '');
|
|
||||||
const [products, setProducts] = useState<any[]>([]);
|
const [products, setProducts] = useState<any[]>([]);
|
||||||
const [loadingProducts, setLoadingProducts] = useState(true);
|
const [loadingProducts, setLoadingProducts] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -219,14 +196,15 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddItem = () => {
|
const handleAddItem = () => {
|
||||||
setSalesItems([
|
const newItems = [
|
||||||
...salesItems,
|
...(data.salesItems || []),
|
||||||
{ id: Date.now(), productId: '', product: '', quantity: 1, unitPrice: 0, subtotal: 0 },
|
{ id: Date.now(), productId: '', product: '', quantity: 1, unitPrice: 0, subtotal: 0 },
|
||||||
]);
|
];
|
||||||
|
onDataChange?.({ ...data, salesItems: newItems });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateItem = (index: number, field: string, value: any) => {
|
const handleUpdateItem = (index: number, field: string, value: any) => {
|
||||||
const updated = salesItems.map((item: any, i: number) => {
|
const updated = (data.salesItems || []).map((item: any, i: number) => {
|
||||||
if (i === index) {
|
if (i === index) {
|
||||||
const newItem = { ...item, [field]: value };
|
const newItem = { ...item, [field]: value };
|
||||||
|
|
||||||
@@ -247,28 +225,25 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
|||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
setSalesItems(updated);
|
onDataChange?.({ ...data, salesItems: updated });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveItem = (index: number) => {
|
const handleRemoveItem = (index: number) => {
|
||||||
setSalesItems(salesItems.filter((_: any, i: number) => i !== index));
|
const newItems = (data.salesItems || []).filter((_: any, i: number) => i !== index);
|
||||||
|
onDataChange?.({ ...data, salesItems: newItems });
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateTotal = () => {
|
const calculateTotal = () => {
|
||||||
return salesItems.reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0);
|
return (data.salesItems || []).reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
// Auto-save totalAmount when items change
|
||||||
onDataChange({
|
useEffect(() => {
|
||||||
|
onDataChange?.({
|
||||||
...data,
|
...data,
|
||||||
salesItems,
|
|
||||||
saleDate,
|
|
||||||
paymentMethod,
|
|
||||||
notes,
|
|
||||||
totalAmount: calculateTotal(),
|
totalAmount: calculateTotal(),
|
||||||
});
|
});
|
||||||
onNext();
|
}, [data.salesItems]);
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -290,8 +265,8 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={saleDate}
|
value={data.saleDate || new Date().toISOString().split('T')[0]}
|
||||||
onChange={(e) => setSaleDate(e.target.value)}
|
onChange={(e) => onDataChange?.({ ...data, saleDate: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -302,8 +277,8 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
|||||||
Método de Pago *
|
Método de Pago *
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={paymentMethod}
|
value={data.paymentMethod || 'cash'}
|
||||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
onChange={(e) => onDataChange?.({ ...data, paymentMethod: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="cash">Efectivo</option>
|
<option value="cash">Efectivo</option>
|
||||||
@@ -349,7 +324,7 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
|||||||
<p>No hay productos terminados disponibles</p>
|
<p>No hay productos terminados disponibles</p>
|
||||||
<p className="text-sm">Agrega productos al inventario primero</p>
|
<p className="text-sm">Agrega productos al inventario primero</p>
|
||||||
</div>
|
</div>
|
||||||
) : salesItems.length === 0 ? (
|
) : (data.salesItems || []).length === 0 ? (
|
||||||
<div className="text-center py-8 border-2 border-dashed border-[var(--border-secondary)] rounded-lg text-[var(--text-tertiary)]">
|
<div className="text-center py-8 border-2 border-dashed border-[var(--border-secondary)] rounded-lg text-[var(--text-tertiary)]">
|
||||||
<Package className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
<Package className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||||
<p>No hay productos agregados</p>
|
<p>No hay productos agregados</p>
|
||||||
@@ -357,7 +332,7 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{salesItems.map((item: any, index: number) => (
|
{(data.salesItems || []).map((item: any, index: number) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="p-3 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]/30"
|
className="p-3 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]/30"
|
||||||
@@ -421,7 +396,7 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Total */}
|
{/* Total */}
|
||||||
{salesItems.length > 0 && (
|
{(data.salesItems || []).length > 0 && (
|
||||||
<div className="pt-3 border-t border-[var(--border-primary)] text-right">
|
<div className="pt-3 border-t border-[var(--border-primary)] text-right">
|
||||||
<span className="text-lg font-bold text-[var(--text-primary)]">
|
<span className="text-lg font-bold text-[var(--text-primary)]">
|
||||||
Total: €{calculateTotal().toFixed(2)}
|
Total: €{calculateTotal().toFixed(2)}
|
||||||
@@ -436,24 +411,13 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
|||||||
Notas (Opcional)
|
Notas (Opcional)
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={notes}
|
value={data.notes || ''}
|
||||||
onChange={(e) => setNotes(e.target.value)}
|
onChange={(e) => onDataChange?.({ ...data, notes: e.target.value })}
|
||||||
placeholder="Información adicional sobre esta venta..."
|
placeholder="Información adicional sobre esta venta..."
|
||||||
rows={3}
|
rows={3}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] text-sm"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Save Button */}
|
|
||||||
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
|
|
||||||
<button
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={salesItems.length === 0}
|
|
||||||
className="px-6 py-3 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
|
|
||||||
>
|
|
||||||
Guardar y Continuar
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -462,9 +426,9 @@ const ManualEntryStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, o
|
|||||||
// STEP 2b: File Upload
|
// STEP 2b: File Upload
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, onNext }) => {
|
const FileUploadStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onNext }) => {
|
||||||
|
const data = dataRef?.current || {};
|
||||||
const { currentTenant } = useTenant();
|
const { currentTenant } = useTenant();
|
||||||
const [file, setFile] = useState<File | null>(data.uploadedFile || null);
|
|
||||||
const [validating, setValidating] = useState(false);
|
const [validating, setValidating] = useState(false);
|
||||||
const [importing, setImporting] = useState(false);
|
const [importing, setImporting] = useState(false);
|
||||||
const [validationResult, setValidationResult] = useState<any>(null);
|
const [validationResult, setValidationResult] = useState<any>(null);
|
||||||
@@ -474,26 +438,26 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
|
|||||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const selectedFile = e.target.files?.[0];
|
const selectedFile = e.target.files?.[0];
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
setFile(selectedFile);
|
onDataChange?.({ ...data, uploadedFile: selectedFile });
|
||||||
setValidationResult(null);
|
setValidationResult(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveFile = () => {
|
const handleRemoveFile = () => {
|
||||||
setFile(null);
|
onDataChange?.({ ...data, uploadedFile: null });
|
||||||
setValidationResult(null);
|
setValidationResult(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleValidate = async () => {
|
const handleValidate = async () => {
|
||||||
if (!file || !currentTenant?.id) return;
|
if (!data.uploadedFile || !currentTenant?.id) return;
|
||||||
|
|
||||||
setValidating(true);
|
setValidating(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await salesService.validateImportFile(currentTenant.id, file);
|
const result = await salesService.validateImportFile(currentTenant.id, data.uploadedFile);
|
||||||
setValidationResult(result);
|
setValidationResult(result);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Error validating file:', err);
|
console.error('Error validating file:', err);
|
||||||
@@ -504,15 +468,15 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleImport = async () => {
|
const handleImport = async () => {
|
||||||
if (!file || !currentTenant?.id) return;
|
if (!data.uploadedFile || !currentTenant?.id) return;
|
||||||
|
|
||||||
setImporting(true);
|
setImporting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await salesService.importSalesData(currentTenant.id, file, false);
|
const result = await salesService.importSalesData(currentTenant.id, data.uploadedFile, false);
|
||||||
onDataChange({ ...data, uploadedFile: file, importResult: result });
|
onDataChange?.({ ...data, importResult: result });
|
||||||
onNext();
|
onNext?.();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Error importing file:', err);
|
console.error('Error importing file:', err);
|
||||||
setError(err.response?.data?.detail || 'Error al importar el archivo');
|
setError(err.response?.data?.detail || 'Error al importar el archivo');
|
||||||
@@ -583,7 +547,7 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* File Upload Area */}
|
{/* File Upload Area */}
|
||||||
{!file ? (
|
{!data.uploadedFile ? (
|
||||||
<div className="border-2 border-dashed border-[var(--border-secondary)] rounded-xl p-8 text-center bg-[var(--bg-secondary)]/30">
|
<div className="border-2 border-dashed border-[var(--border-secondary)] rounded-xl p-8 text-center bg-[var(--bg-secondary)]/30">
|
||||||
<FileSpreadsheet className="w-16 h-16 mx-auto mb-4 text-[var(--color-primary)]/50" />
|
<FileSpreadsheet className="w-16 h-16 mx-auto mb-4 text-[var(--color-primary)]/50" />
|
||||||
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
<h4 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||||
@@ -613,9 +577,9 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<FileSpreadsheet className="w-8 h-8 text-[var(--color-primary)]" />
|
<FileSpreadsheet className="w-8 h-8 text-[var(--color-primary)]" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-[var(--text-primary)]">{file.name}</p>
|
<p className="font-medium text-[var(--text-primary)]">{data.uploadedFile.name}</p>
|
||||||
<p className="text-sm text-[var(--text-secondary)]">
|
<p className="text-sm text-[var(--text-secondary)]">
|
||||||
{(file.size / 1024).toFixed(2)} KB
|
{(data.uploadedFile.size / 1024).toFixed(2)} KB
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -701,53 +665,8 @@ const FileUploadStep: React.FC<EntryMethodStepProps> = ({ data, onDataChange, on
|
|||||||
// STEP 3: Review & Confirm
|
// STEP 3: Review & Confirm
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
|
const ReviewStep: React.FC<WizardStepProps> = ({ dataRef }) => {
|
||||||
const { currentTenant } = useTenant();
|
const data = dataRef?.current || {};
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
|
||||||
if (!currentTenant?.id) {
|
|
||||||
setError('No se pudo obtener información del tenant');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (data.entryMethod === 'manual' && data.salesItems) {
|
|
||||||
// Create individual sales records for each item
|
|
||||||
for (const item of data.salesItems) {
|
|
||||||
const salesData = {
|
|
||||||
inventory_product_id: item.productId || null, // Include inventory product ID for stock tracking
|
|
||||||
product_name: item.product,
|
|
||||||
product_category: 'general', // Could be enhanced with category selection
|
|
||||||
quantity_sold: item.quantity,
|
|
||||||
unit_price: item.unitPrice,
|
|
||||||
total_amount: item.subtotal,
|
|
||||||
sale_date: data.saleDate,
|
|
||||||
sales_channel: 'retail',
|
|
||||||
source: 'manual',
|
|
||||||
payment_method: data.paymentMethod,
|
|
||||||
notes: data.notes,
|
|
||||||
};
|
|
||||||
|
|
||||||
await salesService.createSalesRecord(currentTenant.id, salesData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showToast.success('Registro de ventas guardado exitosamente');
|
|
||||||
onComplete();
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Error saving sales data:', err);
|
|
||||||
const errorMessage = err.response?.data?.detail || 'Error al guardar los datos de ventas';
|
|
||||||
setError(errorMessage);
|
|
||||||
showToast.error(errorMessage);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isManual = data.entryMethod === 'manual';
|
const isManual = data.entryMethod === 'manual';
|
||||||
const isUpload = data.entryMethod === 'upload';
|
const isUpload = data.entryMethod === 'upload';
|
||||||
@@ -768,13 +687,6 @@ const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm flex items-start gap-2">
|
|
||||||
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
|
||||||
<span>{error}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isManual && data.salesItems && (
|
{isManual && data.salesItems && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Summary */}
|
{/* Summary */}
|
||||||
@@ -796,10 +708,10 @@ const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
|
|||||||
{/* Items */}
|
{/* Items */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-2">
|
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-2">
|
||||||
Productos ({data.salesItems.length})
|
Productos ({(data.salesItems || []).length})
|
||||||
</h4>
|
</h4>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{data.salesItems.map((item: any) => (
|
{(data.salesItems || []).map((item: any) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="p-3 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-primary)] flex justify-between items-center"
|
className="p-3 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-primary)] flex justify-between items-center"
|
||||||
@@ -853,27 +765,6 @@ const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Confirm Button */}
|
|
||||||
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-primary)]">
|
|
||||||
<button
|
|
||||||
onClick={handleConfirm}
|
|
||||||
disabled={loading || (isUpload && !data.importResult)}
|
|
||||||
className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-semibold inline-flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
|
||||||
Guardando...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CheckCircle2 className="w-5 h-5" />
|
|
||||||
Confirmar y Guardar
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -883,20 +774,19 @@ const ReviewStep: React.FC<EntryMethodStepProps> = ({ data, onComplete }) => {
|
|||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
export const SalesEntryWizardSteps = (
|
export const SalesEntryWizardSteps = (
|
||||||
data: Record<string, any>,
|
dataRef: React.MutableRefObject<Record<string, any>>,
|
||||||
setData: (data: Record<string, any>) => void
|
setData: (data: Record<string, any>) => void
|
||||||
): WizardStep[] => {
|
): WizardStep[] => {
|
||||||
const entryMethod = data.entryMethod;
|
const entryMethod = dataRef.current.entryMethod;
|
||||||
|
|
||||||
// Dynamic steps based on entry method
|
// New architecture: return direct component references instead of arrow functions
|
||||||
|
// dataRef and onDataChange are now passed through WizardModal props
|
||||||
const steps: WizardStep[] = [
|
const steps: WizardStep[] = [
|
||||||
{
|
{
|
||||||
id: 'entry-method',
|
id: 'entry-method',
|
||||||
title: 'Método de Entrada',
|
title: 'Método de Entrada',
|
||||||
description: 'Elige cómo registrar las ventas',
|
description: 'Elige cómo registrar las ventas',
|
||||||
component: (props) => (
|
component: EntryMethodStep,
|
||||||
<EntryMethodStep {...props} data={data} onDataChange={setData} />
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -905,14 +795,18 @@ export const SalesEntryWizardSteps = (
|
|||||||
id: 'manual-entry',
|
id: 'manual-entry',
|
||||||
title: 'Ingresar Datos',
|
title: 'Ingresar Datos',
|
||||||
description: 'Registra los detalles de la venta',
|
description: 'Registra los detalles de la venta',
|
||||||
component: (props) => <ManualEntryStep {...props} data={data} onDataChange={setData} />,
|
component: ManualEntryStep,
|
||||||
|
validate: () => {
|
||||||
|
const data = dataRef.current;
|
||||||
|
return (data.salesItems || []).length > 0;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} else if (entryMethod === 'upload') {
|
} else if (entryMethod === 'upload') {
|
||||||
steps.push({
|
steps.push({
|
||||||
id: 'file-upload',
|
id: 'file-upload',
|
||||||
title: 'Cargar Archivo',
|
title: 'Cargar Archivo',
|
||||||
description: 'Importa ventas desde archivo',
|
description: 'Importa ventas desde archivo',
|
||||||
component: (props) => <FileUploadStep {...props} data={data} onDataChange={setData} />,
|
component: FileUploadStep,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -920,7 +814,51 @@ export const SalesEntryWizardSteps = (
|
|||||||
id: 'review',
|
id: 'review',
|
||||||
title: 'Revisar',
|
title: 'Revisar',
|
||||||
description: 'Confirma los datos antes de guardar',
|
description: 'Confirma los datos antes de guardar',
|
||||||
component: (props) => <ReviewStep {...props} data={data} onDataChange={setData} />,
|
component: ReviewStep,
|
||||||
|
validate: async () => {
|
||||||
|
const { useTenant } = await import('../../../../stores/tenant.store');
|
||||||
|
const { salesService } = await import('../../../../api/services/sales');
|
||||||
|
const { showToast } = await import('../../../../utils/toast');
|
||||||
|
|
||||||
|
const data = dataRef.current;
|
||||||
|
const { currentTenant } = useTenant.getState();
|
||||||
|
|
||||||
|
if (!currentTenant?.id) {
|
||||||
|
showToast.error('No se pudo obtener información del tenant');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (data.entryMethod === 'manual' && data.salesItems) {
|
||||||
|
// Create individual sales records for each item
|
||||||
|
for (const item of data.salesItems) {
|
||||||
|
const salesData = {
|
||||||
|
inventory_product_id: item.productId || null,
|
||||||
|
product_name: item.product,
|
||||||
|
product_category: 'general',
|
||||||
|
quantity_sold: item.quantity,
|
||||||
|
unit_price: item.unitPrice,
|
||||||
|
total_amount: item.subtotal,
|
||||||
|
sale_date: data.saleDate,
|
||||||
|
sales_channel: 'retail',
|
||||||
|
source: 'manual',
|
||||||
|
payment_method: data.paymentMethod,
|
||||||
|
notes: data.notes,
|
||||||
|
};
|
||||||
|
|
||||||
|
await salesService.createSalesRecord(currentTenant.id, salesData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast.success('Registro de ventas guardado exitosamente');
|
||||||
|
return true;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Error saving sales data:', err);
|
||||||
|
const errorMessage = err.response?.data?.detail || 'Error al guardar los datos de ventas';
|
||||||
|
showToast.error(errorMessage);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return steps;
|
return steps;
|
||||||
|
|||||||
@@ -7,62 +7,23 @@ import { showToast } from '../../../../utils/toast';
|
|||||||
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
|
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
|
||||||
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
||||||
|
|
||||||
interface WizardDataProps extends WizardStepProps {
|
const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onComplete }) => {
|
||||||
data: Record<string, any>;
|
// New architecture: access data from dataRef.current
|
||||||
onDataChange: (data: Record<string, any>) => void;
|
const data = dataRef?.current || {};
|
||||||
}
|
|
||||||
|
|
||||||
const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
|
||||||
const { currentTenant } = useTenant();
|
const { currentTenant } = useTenant();
|
||||||
const [supplierData, setSupplierData] = useState({
|
|
||||||
// Required fields
|
|
||||||
name: data.name || '',
|
|
||||||
supplierType: data.supplierType || 'ingredients',
|
|
||||||
status: data.status || 'pending_approval',
|
|
||||||
paymentTerms: data.paymentTerms || 'net_30',
|
|
||||||
currency: data.currency || 'EUR',
|
|
||||||
standardLeadTime: data.standardLeadTime || 3,
|
|
||||||
|
|
||||||
// Basic optional fields
|
|
||||||
contactPerson: data.contactPerson || '',
|
|
||||||
email: data.email || '',
|
|
||||||
phone: data.phone || '',
|
|
||||||
|
|
||||||
// Advanced optional fields
|
|
||||||
supplierCode: data.supplierCode || '',
|
|
||||||
taxId: data.taxId || '',
|
|
||||||
registrationNumber: data.registrationNumber || '',
|
|
||||||
mobile: data.mobile || '',
|
|
||||||
website: data.website || '',
|
|
||||||
addressLine1: data.addressLine1 || '',
|
|
||||||
addressLine2: data.addressLine2 || '',
|
|
||||||
city: data.city || '',
|
|
||||||
stateProvince: data.stateProvince || '',
|
|
||||||
postalCode: data.postalCode || '',
|
|
||||||
country: data.country || '',
|
|
||||||
creditLimit: data.creditLimit || '',
|
|
||||||
minimumOrderAmount: data.minimumOrderAmount || '',
|
|
||||||
deliveryArea: data.deliveryArea || '',
|
|
||||||
isPreferredSupplier: data.isPreferredSupplier || false,
|
|
||||||
autoApproveEnabled: data.autoApproveEnabled || false,
|
|
||||||
notes: data.notes || '',
|
|
||||||
certifications: data.certifications || '',
|
|
||||||
specializations: data.specializations || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const handleFieldChange = (field: string, value: any) => {
|
||||||
if (!supplierData.supplierCode && supplierData.name) {
|
onDataChange?.({ ...data, [field]: value });
|
||||||
const code = `SUP-${supplierData.name.substring(0, 3).toUpperCase()}-${Date.now().toString().slice(-4)}`;
|
};
|
||||||
setSupplierData(prev => ({ ...prev, supplierCode: code }));
|
|
||||||
}
|
|
||||||
}, [supplierData.name]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onDataChange({ ...data, ...supplierData });
|
if (!data.supplierCode && data.name) {
|
||||||
}, [supplierData]);
|
const code = `SUP-${data.name.substring(0, 3).toUpperCase()}-${Date.now().toString().slice(-4)}`;
|
||||||
|
onDataChange?.({ ...data, supplierCode: code });
|
||||||
|
}
|
||||||
|
}, [data.name]);
|
||||||
|
|
||||||
const handleCreateSupplier = async () => {
|
const handleCreateSupplier = async () => {
|
||||||
if (!currentTenant?.id) {
|
if (!currentTenant?.id) {
|
||||||
@@ -75,41 +36,41 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
name: supplierData.name,
|
name: data.name,
|
||||||
supplier_type: supplierData.supplierType,
|
supplier_type: data.supplierType,
|
||||||
status: supplierData.status,
|
status: data.status,
|
||||||
payment_terms: supplierData.paymentTerms,
|
payment_terms: data.paymentTerms,
|
||||||
currency: supplierData.currency,
|
currency: data.currency,
|
||||||
standard_lead_time: supplierData.standardLeadTime,
|
standard_lead_time: data.standardLeadTime,
|
||||||
supplier_code: supplierData.supplierCode || undefined,
|
supplier_code: data.supplierCode || undefined,
|
||||||
tax_id: supplierData.taxId || undefined,
|
tax_id: data.taxId || undefined,
|
||||||
registration_number: supplierData.registrationNumber || undefined,
|
registration_number: data.registrationNumber || undefined,
|
||||||
contact_person: supplierData.contactPerson || undefined,
|
contact_person: data.contactPerson || undefined,
|
||||||
email: supplierData.email || undefined,
|
email: data.email || undefined,
|
||||||
phone: supplierData.phone || undefined,
|
phone: data.phone || undefined,
|
||||||
mobile: supplierData.mobile || undefined,
|
mobile: data.mobile || undefined,
|
||||||
website: supplierData.website || undefined,
|
website: data.website || undefined,
|
||||||
address_line1: supplierData.addressLine1 || undefined,
|
address_line1: data.addressLine1 || undefined,
|
||||||
address_line2: supplierData.addressLine2 || undefined,
|
address_line2: data.addressLine2 || undefined,
|
||||||
city: supplierData.city || undefined,
|
city: data.city || undefined,
|
||||||
state_province: supplierData.stateProvince || undefined,
|
state_province: data.stateProvince || undefined,
|
||||||
postal_code: supplierData.postalCode || undefined,
|
postal_code: data.postalCode || undefined,
|
||||||
country: supplierData.country || undefined,
|
country: data.country || undefined,
|
||||||
credit_limit: supplierData.creditLimit ? parseFloat(supplierData.creditLimit) : undefined,
|
credit_limit: data.creditLimit ? parseFloat(data.creditLimit) : undefined,
|
||||||
minimum_order_amount: supplierData.minimumOrderAmount ? parseFloat(supplierData.minimumOrderAmount) : undefined,
|
minimum_order_amount: data.minimumOrderAmount ? parseFloat(data.minimumOrderAmount) : undefined,
|
||||||
delivery_area: supplierData.deliveryArea || undefined,
|
delivery_area: data.deliveryArea || undefined,
|
||||||
is_preferred_supplier: supplierData.isPreferredSupplier,
|
is_preferred_supplier: data.isPreferredSupplier,
|
||||||
auto_approve_enabled: supplierData.autoApproveEnabled,
|
auto_approve_enabled: data.autoApproveEnabled,
|
||||||
notes: supplierData.notes || undefined,
|
notes: data.notes || undefined,
|
||||||
certifications: supplierData.certifications ? JSON.parse(`{"items": ${JSON.stringify(supplierData.certifications.split(',').map(c => c.trim()))}}`) : undefined,
|
certifications: data.certifications ? JSON.parse(`{"items": ${JSON.stringify(data.certifications.split(',').map(c => c.trim()))}}`) : undefined,
|
||||||
specializations: supplierData.specializations ? JSON.parse(`{"items": ${JSON.stringify(supplierData.specializations.split(',').map(s => s.trim()))}}`) : undefined,
|
specializations: data.specializations ? JSON.parse(`{"items": ${JSON.stringify(data.specializations.split(',').map(s => s.trim()))}}`) : undefined,
|
||||||
created_by: currentTenant.id,
|
created_by: currentTenant.id,
|
||||||
updated_by: currentTenant.id,
|
updated_by: currentTenant.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
await suppliersService.createSupplier(currentTenant.id, payload);
|
await suppliersService.createSupplier(currentTenant.id, payload);
|
||||||
showToast.success('Supplier created successfully');
|
showToast.success('Supplier created successfully');
|
||||||
onComplete();
|
// Let the wizard handle completion via the Next/Complete button
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Error creating supplier:', err);
|
console.error('Error creating supplier:', err);
|
||||||
const errorMessage = err.response?.data?.detail || 'Error creating supplier';
|
const errorMessage = err.response?.data?.detail || 'Error creating supplier';
|
||||||
@@ -143,8 +104,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={supplierData.name}
|
value={data.name}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, name: e.target.value })}
|
onChange={(e) => handleFieldChange('name', e.target.value)}
|
||||||
placeholder="e.g., Premium Flour Suppliers Ltd."
|
placeholder="e.g., Premium Flour Suppliers Ltd."
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -158,8 +119,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={supplierData.supplierType}
|
value={data.supplierType}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, supplierType: e.target.value })}
|
onChange={(e) => handleFieldChange('supplierType', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="ingredients">Ingredients</option>
|
<option value="ingredients">Ingredients</option>
|
||||||
@@ -176,8 +137,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
Status *
|
Status *
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={supplierData.status}
|
value={data.status}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, status: e.target.value })}
|
onChange={(e) => handleFieldChange('status', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="active">Active</option>
|
<option value="active">Active</option>
|
||||||
@@ -193,8 +154,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
Payment Terms *
|
Payment Terms *
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={supplierData.paymentTerms}
|
value={data.paymentTerms}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, paymentTerms: e.target.value })}
|
onChange={(e) => handleFieldChange('paymentTerms', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="cod">COD (Cash on Delivery)</option>
|
<option value="cod">COD (Cash on Delivery)</option>
|
||||||
@@ -213,8 +174,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={supplierData.currency}
|
value={data.currency}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, currency: e.target.value })}
|
onChange={(e) => handleFieldChange('currency', e.target.value)}
|
||||||
placeholder="EUR"
|
placeholder="EUR"
|
||||||
maxLength={3}
|
maxLength={3}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
@@ -230,8 +191,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={supplierData.standardLeadTime}
|
value={data.standardLeadTime}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, standardLeadTime: parseInt(e.target.value) || 0 })}
|
onChange={(e) => handleFieldChange('standardLeadTime', parseInt(e.target.value) || 0)}
|
||||||
placeholder="3"
|
placeholder="3"
|
||||||
min="0"
|
min="0"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
@@ -246,8 +207,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={supplierData.contactPerson}
|
value={data.contactPerson}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, contactPerson: e.target.value })}
|
onChange={(e) => handleFieldChange('contactPerson', e.target.value)}
|
||||||
placeholder="John Doe"
|
placeholder="John Doe"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -259,8 +220,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={supplierData.email}
|
value={data.email}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, email: e.target.value })}
|
onChange={(e) => handleFieldChange('email', e.target.value)}
|
||||||
placeholder="contact@supplier.com"
|
placeholder="contact@supplier.com"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -272,8 +233,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
value={supplierData.phone}
|
value={data.phone}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, phone: e.target.value })}
|
onChange={(e) => handleFieldChange('phone', e.target.value)}
|
||||||
placeholder="+1 234 567 8900"
|
placeholder="+1 234 567 8900"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -294,8 +255,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={supplierData.supplierCode}
|
value={data.supplierCode}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, supplierCode: e.target.value })}
|
onChange={(e) => handleFieldChange('supplierCode', e.target.value)}
|
||||||
placeholder="SUP-001"
|
placeholder="SUP-001"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -307,8 +268,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
value={supplierData.mobile}
|
value={data.mobile}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, mobile: e.target.value })}
|
onChange={(e) => handleFieldChange('mobile', e.target.value)}
|
||||||
placeholder="+1 234 567 8900"
|
placeholder="+1 234 567 8900"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -320,8 +281,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={supplierData.taxId}
|
value={data.taxId}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, taxId: e.target.value })}
|
onChange={(e) => handleFieldChange('taxId', e.target.value)}
|
||||||
placeholder="VAT/Tax ID"
|
placeholder="VAT/Tax ID"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -333,8 +294,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={supplierData.registrationNumber}
|
value={data.registrationNumber}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, registrationNumber: e.target.value })}
|
onChange={(e) => handleFieldChange('registrationNumber', e.target.value)}
|
||||||
placeholder="Business registration number"
|
placeholder="Business registration number"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -346,8 +307,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
value={supplierData.website}
|
value={data.website}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, website: e.target.value })}
|
onChange={(e) => handleFieldChange('website', e.target.value)}
|
||||||
placeholder="https://www.supplier.com"
|
placeholder="https://www.supplier.com"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -359,8 +320,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={supplierData.addressLine1}
|
value={data.addressLine1}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, addressLine1: e.target.value })}
|
onChange={(e) => handleFieldChange('addressLine1', e.target.value)}
|
||||||
placeholder="Street address"
|
placeholder="Street address"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -372,8 +333,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={supplierData.addressLine2}
|
value={data.addressLine2}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, addressLine2: e.target.value })}
|
onChange={(e) => handleFieldChange('addressLine2', e.target.value)}
|
||||||
placeholder="Suite, building, etc."
|
placeholder="Suite, building, etc."
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -385,8 +346,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={supplierData.city}
|
value={data.city}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, city: e.target.value })}
|
onChange={(e) => handleFieldChange('city', e.target.value)}
|
||||||
placeholder="City"
|
placeholder="City"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -398,8 +359,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={supplierData.stateProvince}
|
value={data.stateProvince}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, stateProvince: e.target.value })}
|
onChange={(e) => handleFieldChange('stateProvince', e.target.value)}
|
||||||
placeholder="State"
|
placeholder="State"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -411,8 +372,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={supplierData.postalCode}
|
value={data.postalCode}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, postalCode: e.target.value })}
|
onChange={(e) => handleFieldChange('postalCode', e.target.value)}
|
||||||
placeholder="12345"
|
placeholder="12345"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -424,8 +385,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={supplierData.country}
|
value={data.country}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, country: e.target.value })}
|
onChange={(e) => handleFieldChange('country', e.target.value)}
|
||||||
placeholder="Country"
|
placeholder="Country"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -437,8 +398,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={supplierData.creditLimit}
|
value={data.creditLimit}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, creditLimit: e.target.value })}
|
onChange={(e) => handleFieldChange('creditLimit', e.target.value)}
|
||||||
placeholder="10000.00"
|
placeholder="10000.00"
|
||||||
min="0"
|
min="0"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
@@ -452,8 +413,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={supplierData.minimumOrderAmount}
|
value={data.minimumOrderAmount}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, minimumOrderAmount: e.target.value })}
|
onChange={(e) => handleFieldChange('minimumOrderAmount', e.target.value)}
|
||||||
placeholder="100.00"
|
placeholder="100.00"
|
||||||
min="0"
|
min="0"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
@@ -467,8 +428,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={supplierData.deliveryArea}
|
value={data.deliveryArea}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, deliveryArea: e.target.value })}
|
onChange={(e) => handleFieldChange('deliveryArea', e.target.value)}
|
||||||
placeholder="e.g., New York Metro Area"
|
placeholder="e.g., New York Metro Area"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -480,8 +441,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="isPreferredSupplier"
|
id="isPreferredSupplier"
|
||||||
checked={supplierData.isPreferredSupplier}
|
checked={data.isPreferredSupplier}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, isPreferredSupplier: e.target.checked })}
|
onChange={(e) => handleFieldChange('isPreferredSupplier', e.target.checked)}
|
||||||
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
|
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="isPreferredSupplier" className="text-sm font-medium text-[var(--text-secondary)]">
|
<label htmlFor="isPreferredSupplier" className="text-sm font-medium text-[var(--text-secondary)]">
|
||||||
@@ -493,8 +454,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="autoApproveEnabled"
|
id="autoApproveEnabled"
|
||||||
checked={supplierData.autoApproveEnabled}
|
checked={data.autoApproveEnabled}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, autoApproveEnabled: e.target.checked })}
|
onChange={(e) => handleFieldChange('autoApproveEnabled', e.target.checked)}
|
||||||
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
|
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="autoApproveEnabled" className="text-sm font-medium text-[var(--text-secondary)]">
|
<label htmlFor="autoApproveEnabled" className="text-sm font-medium text-[var(--text-secondary)]">
|
||||||
@@ -509,8 +470,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={supplierData.certifications}
|
value={data.certifications}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, certifications: e.target.value })}
|
onChange={(e) => handleFieldChange('certifications', e.target.value)}
|
||||||
placeholder="e.g., ISO 9001, HACCP, Organic (comma-separated)"
|
placeholder="e.g., ISO 9001, HACCP, Organic (comma-separated)"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -522,8 +483,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={supplierData.specializations}
|
value={data.specializations}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, specializations: e.target.value })}
|
onChange={(e) => handleFieldChange('specializations', e.target.value)}
|
||||||
placeholder="e.g., Organic flours, Gluten-free products (comma-separated)"
|
placeholder="e.g., Organic flours, Gluten-free products (comma-separated)"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -534,8 +495,8 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
Notes
|
Notes
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={supplierData.notes}
|
value={data.notes}
|
||||||
onChange={(e) => setSupplierData({ ...supplierData, notes: e.target.value })}
|
onChange={(e) => handleFieldChange('notes', e.target.value)}
|
||||||
placeholder="Additional notes about this supplier..."
|
placeholder="Additional notes about this supplier..."
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
rows={3}
|
rows={3}
|
||||||
@@ -544,48 +505,22 @@ const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, on
|
|||||||
</div>
|
</div>
|
||||||
</AdvancedOptionsSection>
|
</AdvancedOptionsSection>
|
||||||
|
|
||||||
<div className="flex justify-center pt-4 border-t border-[var(--border-primary)]">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleCreateSupplier}
|
|
||||||
disabled={loading}
|
|
||||||
className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-semibold inline-flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
|
||||||
Creating supplier...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CheckCircle2 className="w-5 h-5" />
|
|
||||||
Create Supplier
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SupplierWizardSteps = (
|
export const SupplierWizardSteps = (
|
||||||
data: Record<string, any>,
|
dataRef: React.MutableRefObject<Record<string, any>>,
|
||||||
setData: (data: Record<string, any>) => void
|
setData: (data: Record<string, any>) => void
|
||||||
): WizardStep[] => [
|
): WizardStep[] => {
|
||||||
|
// New architecture: return direct component reference instead of arrow function
|
||||||
|
// dataRef and onDataChange are now passed through WizardModal props
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
id: 'supplier-details',
|
id: 'supplier-details',
|
||||||
title: 'Supplier Details',
|
title: 'Supplier Details',
|
||||||
description: 'Essential supplier information',
|
description: 'Essential supplier information',
|
||||||
component: (props) => <SupplierDetailsStep {...props} data={data} onDataChange={setData} />,
|
component: SupplierDetailsStep,
|
||||||
validate: () => {
|
|
||||||
return !!(
|
|
||||||
data.name &&
|
|
||||||
data.supplierType &&
|
|
||||||
data.status &&
|
|
||||||
data.paymentTerms &&
|
|
||||||
data.currency &&
|
|
||||||
data.standardLeadTime >= 0
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,22 +1,12 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
||||||
import { UserPlus, Shield, CheckCircle2, Mail, Phone, Loader2 } from 'lucide-react';
|
import { UserPlus, Shield, Mail, Phone } from 'lucide-react';
|
||||||
import { useTenant } from '../../../../stores/tenant.store';
|
|
||||||
import { authService } from '../../../../api/services/auth';
|
|
||||||
|
|
||||||
interface WizardDataProps extends WizardStepProps {
|
const MemberDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||||
data: Record<string, any>;
|
const data = dataRef?.current || {};
|
||||||
onDataChange: (data: Record<string, any>) => void;
|
const handleFieldChange = (field: string, value: any) => {
|
||||||
}
|
onDataChange?.({ ...data, [field]: value });
|
||||||
|
};
|
||||||
const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNext }) => {
|
|
||||||
const [memberData, setMemberData] = useState({
|
|
||||||
fullName: data.fullName || '',
|
|
||||||
email: data.email || '',
|
|
||||||
phone: data.phone || '',
|
|
||||||
position: data.position || 'baker',
|
|
||||||
employmentType: data.employmentType || 'full-time',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -29,8 +19,8 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
|
|||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Nombre Completo *</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Nombre Completo *</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={memberData.fullName}
|
value={data.fullName || ''}
|
||||||
onChange={(e) => setMemberData({ ...memberData, fullName: e.target.value })}
|
onChange={(e) => handleFieldChange('fullName', e.target.value)}
|
||||||
placeholder="Ej: Juan García"
|
placeholder="Ej: Juan García"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -42,8 +32,8 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={memberData.email}
|
value={data.email || ''}
|
||||||
onChange={(e) => setMemberData({ ...memberData, email: e.target.value })}
|
onChange={(e) => handleFieldChange('email', e.target.value)}
|
||||||
placeholder="juan@panaderia.com"
|
placeholder="juan@panaderia.com"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -55,8 +45,8 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
value={memberData.phone}
|
value={data.phone || ''}
|
||||||
onChange={(e) => setMemberData({ ...memberData, phone: e.target.value })}
|
onChange={(e) => handleFieldChange('phone', e.target.value)}
|
||||||
placeholder="+34 123 456 789"
|
placeholder="+34 123 456 789"
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
/>
|
/>
|
||||||
@@ -64,8 +54,8 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Posición *</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Posición *</label>
|
||||||
<select
|
<select
|
||||||
value={memberData.position}
|
value={data.position || 'baker'}
|
||||||
onChange={(e) => setMemberData({ ...memberData, position: e.target.value })}
|
onChange={(e) => handleFieldChange('position', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="baker">Panadero</option>
|
<option value="baker">Panadero</option>
|
||||||
@@ -78,8 +68,8 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Tipo de Empleo</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Tipo de Empleo</label>
|
||||||
<select
|
<select
|
||||||
value={memberData.employmentType}
|
value={data.employmentType || 'full-time'}
|
||||||
onChange={(e) => setMemberData({ ...memberData, employmentType: e.target.value })}
|
onChange={(e) => handleFieldChange('employmentType', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="full-time">Tiempo Completo</option>
|
<option value="full-time">Tiempo Completo</option>
|
||||||
@@ -88,72 +78,15 @@ const MemberDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNe
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
onDataChange({ ...data, ...memberData });
|
|
||||||
onNext();
|
|
||||||
}}
|
|
||||||
disabled={!memberData.fullName || !memberData.email}
|
|
||||||
className="px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
Continuar
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PermissionsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
const PermissionsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||||
const { currentTenant } = useTenant();
|
const data = dataRef?.current || {};
|
||||||
const [permissions, setPermissions] = useState({
|
|
||||||
role: data.role || 'staff',
|
|
||||||
canManageInventory: data.canManageInventory || false,
|
|
||||||
canViewRecipes: data.canViewRecipes || true,
|
|
||||||
canCreateOrders: data.canCreateOrders || false,
|
|
||||||
canViewFinancial: data.canViewFinancial || false,
|
|
||||||
});
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleFieldChange = (field: string, value: any) => {
|
||||||
if (!currentTenant?.id) {
|
onDataChange?.({ ...data, [field]: value });
|
||||||
setError('No se pudo obtener información del tenant');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Generate a temporary password (in production, this should be sent via email)
|
|
||||||
const tempPassword = `Temp${Math.random().toString(36).substring(2, 10)}!`;
|
|
||||||
|
|
||||||
// Register the new team member
|
|
||||||
const registrationData = {
|
|
||||||
email: data.email,
|
|
||||||
password: tempPassword,
|
|
||||||
full_name: data.fullName,
|
|
||||||
phone_number: data.phone || undefined,
|
|
||||||
tenant_id: currentTenant.id,
|
|
||||||
role: permissions.role,
|
|
||||||
};
|
|
||||||
|
|
||||||
await authService.register(registrationData);
|
|
||||||
|
|
||||||
// In a real implementation, you would:
|
|
||||||
// 1. Send email with temporary password
|
|
||||||
// 2. Store permissions in a separate permissions table
|
|
||||||
// 3. Link user to tenant with specific role
|
|
||||||
|
|
||||||
onDataChange({ ...data, ...permissions, tempPassword });
|
|
||||||
onComplete();
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Error creating team member:', err);
|
|
||||||
setError(err.response?.data?.detail || 'Error al crear el miembro del equipo');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -164,17 +97,11 @@ const PermissionsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComp
|
|||||||
<p className="text-sm text-[var(--text-secondary)]">{data.fullName}</p>
|
<p className="text-sm text-[var(--text-secondary)]">{data.fullName}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-3">Rol del Sistema</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-3">Rol del Sistema</label>
|
||||||
<select
|
<select
|
||||||
value={permissions.role}
|
value={data.role}
|
||||||
onChange={(e) => setPermissions({ ...permissions, role: e.target.value })}
|
onChange={(e) => handleFieldChange('role', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||||
>
|
>
|
||||||
<option value="admin">Administrador</option>
|
<option value="admin">Administrador</option>
|
||||||
@@ -198,8 +125,8 @@ const PermissionsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComp
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={permissions[key as keyof typeof permissions] as boolean}
|
checked={data[key] ?? false}
|
||||||
onChange={(e) => setPermissions({ ...permissions, [key]: e.target.checked })}
|
onChange={(e) => handleFieldChange(key, e.target.checked)}
|
||||||
className="w-4 h-4 text-[var(--color-primary)] rounded focus:ring-[var(--color-primary)]"
|
className="w-4 h-4 text-[var(--color-primary)] rounded focus:ring-[var(--color-primary)]"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-[var(--text-primary)]">{label}</span>
|
<span className="text-sm text-[var(--text-primary)]">{label}</span>
|
||||||
@@ -207,30 +134,68 @@ const PermissionsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComp
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-primary)]">
|
|
||||||
<button
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={loading}
|
|
||||||
className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-semibold inline-flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
|
||||||
Guardando...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CheckCircle2 className="w-5 h-5" />
|
|
||||||
Agregar Miembro
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TeamMemberWizardSteps = (data: Record<string, any>, setData: (data: Record<string, any>) => void): WizardStep[] => [
|
export const TeamMemberWizardSteps = (dataRef: React.MutableRefObject<Record<string, any>>, setData: (data: Record<string, any>) => void): WizardStep[] => {
|
||||||
{ id: 'member-details', title: 'Datos Personales', description: 'Nombre, contacto, posición', component: (props) => <MemberDetailsStep {...props} data={data} onDataChange={setData} /> },
|
// New architecture: return direct component references instead of arrow functions
|
||||||
{ id: 'member-permissions', title: 'Rol y Permisos', description: 'Accesos al sistema', component: (props) => <PermissionsStep {...props} data={data} onDataChange={setData} /> },
|
// dataRef and onDataChange are now passed through WizardModal props
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'member-details',
|
||||||
|
title: 'Datos Personales',
|
||||||
|
description: 'Nombre, contacto, posición',
|
||||||
|
component: MemberDetailsStep,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'member-permissions',
|
||||||
|
title: 'Rol y Permisos',
|
||||||
|
description: 'Accesos al sistema',
|
||||||
|
component: PermissionsStep,
|
||||||
|
validate: async () => {
|
||||||
|
const { useTenant } = await import('../../../../stores/tenant.store');
|
||||||
|
const { authService } = await import('../../../../api/services/auth');
|
||||||
|
const { showToast } = await import('../../../../utils/toast');
|
||||||
|
|
||||||
|
const data = dataRef.current;
|
||||||
|
const { currentTenant } = useTenant.getState();
|
||||||
|
|
||||||
|
if (!currentTenant?.id) {
|
||||||
|
showToast.error('No se pudo obtener información del tenant');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Generate a temporary password (in production, this should be sent via email)
|
||||||
|
const tempPassword = `Temp${Math.random().toString(36).substring(2, 10)}!`;
|
||||||
|
|
||||||
|
// Register the new team member
|
||||||
|
const registrationData = {
|
||||||
|
email: data.email,
|
||||||
|
password: tempPassword,
|
||||||
|
full_name: data.fullName,
|
||||||
|
phone_number: data.phone || undefined,
|
||||||
|
tenant_id: currentTenant.id,
|
||||||
|
role: data.role,
|
||||||
|
};
|
||||||
|
|
||||||
|
await authService.register(registrationData);
|
||||||
|
|
||||||
|
// In a real implementation, you would:
|
||||||
|
// 1. Send email with temporary password
|
||||||
|
// 2. Store permissions in a separate permissions table
|
||||||
|
// 3. Link user to tenant with specific role
|
||||||
|
|
||||||
|
showToast.success('Miembro del equipo agregado exitosamente');
|
||||||
|
return true;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Error creating team member:', err);
|
||||||
|
const errorMessage = err.response?.data?.detail || 'Error al crear el miembro del equipo';
|
||||||
|
showToast.error(errorMessage);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
currentStepIndex: number;
|
||||||
totalSteps: number;
|
totalSteps: number;
|
||||||
goToStep: (index: number) => void;
|
goToStep: (index: number) => void;
|
||||||
|
// New architecture: dataRef and onDataChange passed from UnifiedAddWizard
|
||||||
|
dataRef?: React.MutableRefObject<any>;
|
||||||
|
onDataChange?: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WizardModalProps {
|
interface WizardModalProps {
|
||||||
@@ -29,6 +32,9 @@ interface WizardModalProps {
|
|||||||
steps: WizardStep[];
|
steps: WizardStep[];
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
||||||
|
// New architecture: optional dataRef and onDataChange for wizards that use them
|
||||||
|
dataRef?: React.MutableRefObject<any>;
|
||||||
|
onDataChange?: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WizardModal: React.FC<WizardModalProps> = ({
|
export const WizardModal: React.FC<WizardModalProps> = ({
|
||||||
@@ -38,7 +44,9 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
|||||||
title,
|
title,
|
||||||
steps,
|
steps,
|
||||||
icon,
|
icon,
|
||||||
size = 'xl'
|
size = 'xl',
|
||||||
|
dataRef,
|
||||||
|
onDataChange
|
||||||
}) => {
|
}) => {
|
||||||
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||||||
const [isValidating, setIsValidating] = useState(false);
|
const [isValidating, setIsValidating] = useState(false);
|
||||||
@@ -55,12 +63,36 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
|||||||
'2xl': 'max-w-7xl'
|
'2xl': 'max-w-7xl'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
setCurrentStepIndex(0);
|
||||||
|
onClose();
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
const handleComplete = useCallback(() => {
|
||||||
|
onComplete();
|
||||||
|
handleClose();
|
||||||
|
}, [onComplete, handleClose]);
|
||||||
|
|
||||||
|
const goToStep = useCallback((index: number) => {
|
||||||
|
if (index >= 0 && index < steps.length) {
|
||||||
|
setCurrentStepIndex(index);
|
||||||
|
}
|
||||||
|
}, [steps.length]);
|
||||||
|
|
||||||
|
const handleBack = useCallback(() => {
|
||||||
|
setCurrentStepIndex(prev => Math.max(prev - 1, 0));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleNext = useCallback(async () => {
|
const handleNext = useCallback(async () => {
|
||||||
|
// Access current step inside callback to avoid re-creating function on every render
|
||||||
|
const step = steps[currentStepIndex];
|
||||||
|
const lastStep = currentStepIndex === steps.length - 1;
|
||||||
|
|
||||||
// Validate current step if validator exists
|
// Validate current step if validator exists
|
||||||
if (currentStep.validate) {
|
if (step.validate) {
|
||||||
setIsValidating(true);
|
setIsValidating(true);
|
||||||
try {
|
try {
|
||||||
const isValid = await currentStep.validate();
|
const isValid = await step.validate();
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
setIsValidating(false);
|
setIsValidating(false);
|
||||||
return;
|
return;
|
||||||
@@ -73,32 +105,12 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
|||||||
setIsValidating(false);
|
setIsValidating(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLastStep) {
|
if (lastStep) {
|
||||||
handleComplete();
|
handleComplete();
|
||||||
} else {
|
} else {
|
||||||
setCurrentStepIndex(prev => Math.min(prev + 1, steps.length - 1));
|
setCurrentStepIndex(prev => Math.min(prev + 1, steps.length - 1));
|
||||||
}
|
}
|
||||||
}, [currentStep, isLastStep, steps.length]);
|
}, [steps, currentStepIndex, handleComplete]);
|
||||||
|
|
||||||
const handleBack = useCallback(() => {
|
|
||||||
setCurrentStepIndex(prev => Math.max(prev - 1, 0));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleComplete = useCallback(() => {
|
|
||||||
onComplete();
|
|
||||||
handleClose();
|
|
||||||
}, [onComplete]);
|
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
|
||||||
setCurrentStepIndex(0);
|
|
||||||
onClose();
|
|
||||||
}, [onClose]);
|
|
||||||
|
|
||||||
const goToStep = useCallback((index: number) => {
|
|
||||||
if (index >= 0 && index < steps.length) {
|
|
||||||
setCurrentStepIndex(index);
|
|
||||||
}
|
|
||||||
}, [steps.length]);
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
@@ -184,6 +196,8 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
|||||||
currentStepIndex={currentStepIndex}
|
currentStepIndex={currentStepIndex}
|
||||||
totalSteps={steps.length}
|
totalSteps={steps.length}
|
||||||
goToStep={goToStep}
|
goToStep={goToStep}
|
||||||
|
dataRef={dataRef}
|
||||||
|
onDataChange={onDataChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
handleRefreshAll();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Keyboard shortcut for Quick Add (Cmd/Ctrl + K)
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
// Check for Cmd+K (Mac) or Ctrl+K (Windows/Linux)
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsAddWizardOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Demo tour auto-start logic
|
// Demo tour auto-start logic
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('[Dashboard] Demo mode:', isDemoMode);
|
console.log('[Dashboard] Demo mode:', isDemoMode);
|
||||||
@@ -221,18 +235,29 @@ export function NewDashboardPage() {
|
|||||||
<span className="hidden sm:inline">{t('common:actions.refresh')}</span>
|
<span className="hidden sm:inline">{t('common:actions.refresh')}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Unified Add Button */}
|
{/* Unified Add Button with Keyboard Shortcut */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsAddWizardOpen(true)}
|
onClick={() => setIsAddWizardOpen(true)}
|
||||||
className="flex items-center gap-2 px-6 py-2.5 rounded-lg font-semibold transition-all duration-200 shadow-lg hover:shadow-xl hover:-translate-y-0.5 active:translate-y-0"
|
className="group relative flex items-center gap-2 px-6 py-2.5 rounded-lg font-semibold transition-all duration-200 shadow-lg hover:shadow-xl hover:-translate-y-0.5 active:translate-y-0"
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%)',
|
background: 'linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%)',
|
||||||
color: 'white'
|
color: 'white'
|
||||||
}}
|
}}
|
||||||
|
title={`Quick Add (${navigator.platform.includes('Mac') ? 'Cmd' : 'Ctrl'}+K)`}
|
||||||
>
|
>
|
||||||
<Plus className="w-5 h-5" />
|
<Plus className="w-5 h-5" />
|
||||||
<span className="hidden sm:inline">{t('common:actions.add')}</span>
|
<span className="hidden sm:inline">{t('common:actions.add')}</span>
|
||||||
<Sparkles className="w-4 h-4 opacity-80" />
|
<Sparkles className="w-4 h-4 opacity-80" />
|
||||||
|
{/* Keyboard shortcut badge - shown on hover */}
|
||||||
|
<span className="hidden lg:flex absolute -bottom-8 left-1/2 -translate-x-1/2 items-center gap-1 px-2 py-1 rounded text-xs font-mono opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap pointer-events-none" style={{ backgroundColor: 'var(--bg-primary)', color: 'var(--text-secondary)', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
|
||||||
|
<kbd className="px-1.5 py-0.5 rounded text-xs font-semibold" style={{ backgroundColor: 'var(--bg-tertiary)', border: '1px solid var(--border-secondary)' }}>
|
||||||
|
{navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'}
|
||||||
|
</kbd>
|
||||||
|
<span>+</span>
|
||||||
|
<kbd className="px-1.5 py-0.5 rounded text-xs font-semibold" style={{ backgroundColor: 'var(--bg-tertiary)', border: '1px solid var(--border-secondary)' }}>
|
||||||
|
K
|
||||||
|
</kbd>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user