Improve unified wizard
This commit is contained in:
@@ -6,12 +6,18 @@ import { AnyWizardData } from './types';
|
|||||||
import { useTenant } from '../../../stores/tenant.store';
|
import { useTenant } from '../../../stores/tenant.store';
|
||||||
import { useCreatePurchaseOrder } from '../../../api/hooks/purchase-orders';
|
import { useCreatePurchaseOrder } from '../../../api/hooks/purchase-orders';
|
||||||
import { useCreateProductionBatch } from '../../../api/hooks/production';
|
import { useCreateProductionBatch } from '../../../api/hooks/production';
|
||||||
|
import { useCreateIngredient, useAddStock } from '../../../api/hooks/inventory';
|
||||||
|
import { useCreateSupplier } from '../../../api/hooks/suppliers';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import type { ProductionBatchCreate } from '../../../api/types/production';
|
import type { ProductionBatchCreate } from '../../../api/types/production';
|
||||||
|
import type { IngredientCreate, StockCreate } from '../../../api/types/inventory';
|
||||||
|
import type { SupplierCreate } from '../../../api/types/suppliers';
|
||||||
|
import { ProductType } from '../../../api/types/inventory';
|
||||||
|
import { SupplierType, PaymentTerms } from '../../../api/types/suppliers';
|
||||||
import { ProductionPriorityEnum } from '../../../api/types/production';
|
import { ProductionPriorityEnum } from '../../../api/types/production';
|
||||||
|
|
||||||
// Import specific wizards
|
// Import specific wizards
|
||||||
import { InventoryWizardSteps, ProductTypeStep, BasicInfoStep, StockConfigStep } from './wizards/InventoryWizard';
|
import { InventoryWizardSteps } 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';
|
||||||
@@ -23,11 +29,18 @@ import { SalesEntryWizardSteps } from './wizards/SalesEntryWizard';
|
|||||||
import { PurchaseOrderWizardSteps } from './wizards/PurchaseOrderWizard';
|
import { PurchaseOrderWizardSteps } from './wizards/PurchaseOrderWizard';
|
||||||
import { ProductionBatchWizardSteps } from './wizards/ProductionBatchWizard';
|
import { ProductionBatchWizardSteps } from './wizards/ProductionBatchWizard';
|
||||||
|
|
||||||
|
// Services for wizards that handle their own submission
|
||||||
|
import { equipmentService } from '../../../api/services/equipment';
|
||||||
|
import { qualityTemplateService } from '../../../api/services/qualityTemplates';
|
||||||
|
import OrdersService from '../../../api/services/orders';
|
||||||
|
import { salesService } from '../../../api/services/sales';
|
||||||
|
import { recipesService } from '../../../api/services/recipes';
|
||||||
|
import { authService } from '../../../api/services/auth';
|
||||||
|
|
||||||
interface UnifiedAddWizardProps {
|
interface UnifiedAddWizardProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onComplete?: (itemType: ItemType, data?: any) => void;
|
onComplete?: (itemType: ItemType, data?: any) => void;
|
||||||
// Optional: Start with a specific item type (when opened from individual page buttons)
|
|
||||||
initialItemType?: ItemType;
|
initialItemType?: ItemType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,23 +56,21 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
|||||||
const [wizardData, setWizardData] = useState<AnyWizardData>({});
|
const [wizardData, setWizardData] = useState<AnyWizardData>({});
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
// Get current tenant
|
|
||||||
const { currentTenant } = useTenant();
|
const { currentTenant } = useTenant();
|
||||||
|
|
||||||
// API hooks
|
// API hooks for wizards with proper hook support
|
||||||
const createPurchaseOrderMutation = useCreatePurchaseOrder();
|
const createPurchaseOrderMutation = useCreatePurchaseOrder();
|
||||||
const createProductionBatchMutation = useCreateProductionBatch();
|
const createProductionBatchMutation = useCreateProductionBatch();
|
||||||
|
const createIngredientMutation = useCreateIngredient();
|
||||||
|
const addStockMutation = useAddStock();
|
||||||
|
const createSupplierMutation = useCreateSupplier();
|
||||||
|
|
||||||
// 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>({});
|
const dataRef = useRef<AnyWizardData>({});
|
||||||
|
|
||||||
// Update ref whenever data changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dataRef.current = wizardData;
|
dataRef.current = wizardData;
|
||||||
}, [wizardData]);
|
}, [wizardData]);
|
||||||
|
|
||||||
// Reset state when modal closes
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
setSelectedItemType(initialItemType || null);
|
setSelectedItemType(initialItemType || null);
|
||||||
setWizardData({});
|
setWizardData({});
|
||||||
@@ -68,22 +79,17 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
|||||||
onClose();
|
onClose();
|
||||||
}, [onClose, initialItemType]);
|
}, [onClose, initialItemType]);
|
||||||
|
|
||||||
// Handle item type selection from step 0
|
|
||||||
const handleItemTypeSelect = useCallback((itemType: ItemType) => {
|
const handleItemTypeSelect = useCallback((itemType: ItemType) => {
|
||||||
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) => {
|
const handleDataChange = useCallback((newData: AnyWizardData) => {
|
||||||
// Update ref first for immediate access
|
|
||||||
dataRef.current = newData;
|
dataRef.current = newData;
|
||||||
// Update state to trigger re-render (controlled inputs need this)
|
|
||||||
setWizardData(newData);
|
setWizardData(newData);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handle wizard completion with API submission
|
// Centralized wizard completion handler
|
||||||
|
// All API submissions are handled here for consistency
|
||||||
const handleWizardComplete = useCallback(
|
const handleWizardComplete = useCallback(
|
||||||
async () => {
|
async () => {
|
||||||
if (!selectedItemType || !currentTenant?.id) {
|
if (!selectedItemType || !currentTenant?.id) {
|
||||||
@@ -93,36 +99,29 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const finalData = dataRef.current as any; // Cast to any for flexible data access
|
const finalData = dataRef.current as any;
|
||||||
|
|
||||||
// Handle Purchase Order submission
|
// ========================================
|
||||||
|
// PURCHASE ORDER
|
||||||
|
// ========================================
|
||||||
if (selectedItemType === 'purchase-order') {
|
if (selectedItemType === 'purchase-order') {
|
||||||
// Validate items have positive quantities and prices
|
|
||||||
if ((finalData.items || []).some((item: any) =>
|
if ((finalData.items || []).some((item: any) =>
|
||||||
Number(item.ordered_quantity) < 0.01 || Number(item.unit_price) < 0.01
|
Number(item.ordered_quantity) < 0.01 || Number(item.unit_price) < 0.01
|
||||||
)) {
|
)) {
|
||||||
throw new Error('Todos los productos deben tener cantidad y precio mayor a 0');
|
throw new Error('Todos los productos deben tener cantidad y precio mayor a 0');
|
||||||
}
|
}
|
||||||
|
|
||||||
const subtotal = (finalData.items || []).reduce(
|
|
||||||
(sum: number, item: any) => sum + (Number(item.ordered_quantity) * Number(item.unit_price)),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
// Convert date string to ISO datetime with timezone (start of day in local timezone)
|
|
||||||
const deliveryDate = new Date(finalData.required_delivery_date + 'T00:00:00');
|
const deliveryDate = new Date(finalData.required_delivery_date + 'T00:00:00');
|
||||||
if (isNaN(deliveryDate.getTime())) {
|
if (isNaN(deliveryDate.getTime())) {
|
||||||
throw new Error('Fecha de entrega inválida');
|
throw new Error('Fecha de entrega inválida');
|
||||||
}
|
}
|
||||||
const requiredDeliveryDateTime = deliveryDate.toISOString();
|
|
||||||
|
|
||||||
await createPurchaseOrderMutation.mutateAsync({
|
await createPurchaseOrderMutation.mutateAsync({
|
||||||
tenantId: currentTenant.id,
|
tenantId: currentTenant.id,
|
||||||
data: {
|
data: {
|
||||||
supplier_id: finalData.supplier_id,
|
supplier_id: finalData.supplier_id,
|
||||||
required_delivery_date: requiredDeliveryDateTime,
|
required_delivery_date: deliveryDate.toISOString(),
|
||||||
priority: finalData.priority || 'normal',
|
priority: finalData.priority || 'normal',
|
||||||
subtotal: subtotal,
|
|
||||||
tax_amount: Number(finalData.tax_amount) || 0,
|
tax_amount: Number(finalData.tax_amount) || 0,
|
||||||
shipping_cost: Number(finalData.shipping_cost) || 0,
|
shipping_cost: Number(finalData.shipping_cost) || 0,
|
||||||
discount_amount: Number(finalData.discount_amount) || 0,
|
discount_amount: Number(finalData.discount_amount) || 0,
|
||||||
@@ -138,9 +137,10 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
|||||||
toast.success('Orden de compra creada exitosamente');
|
toast.success('Orden de compra creada exitosamente');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Production Batch submission
|
// ========================================
|
||||||
|
// PRODUCTION BATCH
|
||||||
|
// ========================================
|
||||||
if (selectedItemType === 'production-batch') {
|
if (selectedItemType === 'production-batch') {
|
||||||
// Validate quantities
|
|
||||||
if (Number(finalData.planned_quantity) < 0.01) {
|
if (Number(finalData.planned_quantity) < 0.01) {
|
||||||
throw new Error('La cantidad planificada debe ser mayor a 0');
|
throw new Error('La cantidad planificada debe ser mayor a 0');
|
||||||
}
|
}
|
||||||
@@ -148,12 +148,10 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
|||||||
throw new Error('La duración planificada debe ser mayor a 0');
|
throw new Error('La duración planificada debe ser mayor a 0');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert staff_assigned from string to array
|
|
||||||
const staffArray = finalData.staff_assigned_string
|
const staffArray = finalData.staff_assigned_string
|
||||||
? finalData.staff_assigned_string.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0)
|
? finalData.staff_assigned_string.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Convert datetime-local strings to ISO datetime with timezone
|
|
||||||
const plannedStartDate = new Date(finalData.planned_start_time);
|
const plannedStartDate = new Date(finalData.planned_start_time);
|
||||||
const plannedEndDate = new Date(finalData.planned_end_time);
|
const plannedEndDate = new Date(finalData.planned_end_time);
|
||||||
|
|
||||||
@@ -173,7 +171,7 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
|||||||
planned_end_time: plannedEndDate.toISOString(),
|
planned_end_time: plannedEndDate.toISOString(),
|
||||||
planned_quantity: Number(finalData.planned_quantity),
|
planned_quantity: Number(finalData.planned_quantity),
|
||||||
planned_duration_minutes: Number(finalData.planned_duration_minutes),
|
planned_duration_minutes: Number(finalData.planned_duration_minutes),
|
||||||
priority: (finalData.priority || ProductionPriorityEnum.MEDIUM) as ProductionPriorityEnum,
|
priority: finalData.priority || ProductionPriorityEnum.MEDIUM,
|
||||||
is_rush_order: finalData.is_rush_order || false,
|
is_rush_order: finalData.is_rush_order || false,
|
||||||
is_special_recipe: finalData.is_special_recipe || false,
|
is_special_recipe: finalData.is_special_recipe || false,
|
||||||
production_notes: finalData.production_notes || undefined,
|
production_notes: finalData.production_notes || undefined,
|
||||||
@@ -192,10 +190,426 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
|||||||
toast.success('Lote de producción creado exitosamente');
|
toast.success('Lote de producción creado exitosamente');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// INVENTORY (INGREDIENT)
|
||||||
|
// ========================================
|
||||||
|
if (selectedItemType === 'inventory') {
|
||||||
|
if (!finalData.name || finalData.name.trim().length < 2) {
|
||||||
|
throw new Error('El nombre del producto debe tener al menos 2 caracteres');
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = finalData.productType === 'ingredient'
|
||||||
|
? finalData.ingredientCategory
|
||||||
|
: finalData.productCategory;
|
||||||
|
|
||||||
|
if (!category) {
|
||||||
|
throw new Error('Debes seleccionar una categoría');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert allergenInfo string to array format expected by backend
|
||||||
|
let allergenInfoArray: string[] | undefined = undefined;
|
||||||
|
if (finalData.allergenInfo && typeof finalData.allergenInfo === 'string') {
|
||||||
|
// Parse comma-separated allergens into array
|
||||||
|
allergenInfoArray = finalData.allergenInfo
|
||||||
|
.split(',')
|
||||||
|
.map((a: string) => a.trim().toLowerCase())
|
||||||
|
.filter((a: string) => a.length > 0);
|
||||||
|
} else if (Array.isArray(finalData.allergenInfo)) {
|
||||||
|
allergenInfoArray = finalData.allergenInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ingredientData: IngredientCreate = {
|
||||||
|
name: finalData.name.trim(),
|
||||||
|
product_type: finalData.productType === 'finished_product'
|
||||||
|
? ProductType.FINISHED_PRODUCT
|
||||||
|
: ProductType.INGREDIENT,
|
||||||
|
sku: finalData.sku || undefined,
|
||||||
|
barcode: finalData.barcode || undefined,
|
||||||
|
category: category,
|
||||||
|
description: finalData.description || undefined,
|
||||||
|
brand: finalData.brand || undefined,
|
||||||
|
unit_of_measure: finalData.unitOfMeasure || 'units',
|
||||||
|
package_size: finalData.packageSize ? Number(finalData.packageSize) : undefined,
|
||||||
|
shelf_life_days: finalData.shelfLifeDays ? Number(finalData.shelfLifeDays) : undefined,
|
||||||
|
is_perishable: finalData.isPerishable ?? true,
|
||||||
|
allergen_info: allergenInfoArray && allergenInfoArray.length > 0
|
||||||
|
? { allergens: allergenInfoArray }
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const createdIngredient = await createIngredientMutation.mutateAsync({
|
||||||
|
tenantId: currentTenant.id,
|
||||||
|
ingredientData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialLots = finalData.initialLots || [];
|
||||||
|
if (initialLots.length > 0 && createdIngredient?.id) {
|
||||||
|
for (const lot of initialLots) {
|
||||||
|
if (lot.quantity && Number(lot.quantity) > 0) {
|
||||||
|
const stockData: StockCreate = {
|
||||||
|
ingredient_id: createdIngredient.id,
|
||||||
|
current_quantity: Number(lot.quantity),
|
||||||
|
unit_cost: lot.unitCost ? Number(lot.unitCost) : undefined,
|
||||||
|
lot_number: lot.lotNumber || undefined,
|
||||||
|
expiration_date: lot.expirationDate || undefined,
|
||||||
|
storage_location: lot.location || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
await addStockMutation.mutateAsync({
|
||||||
|
tenantId: currentTenant.id,
|
||||||
|
stockData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toast.success('Producto de inventario creado exitosamente');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// SUPPLIER
|
||||||
|
// ========================================
|
||||||
|
if (selectedItemType === 'supplier') {
|
||||||
|
if (!finalData.name || finalData.name.trim().length < 1) {
|
||||||
|
throw new Error('El nombre del proveedor es requerido');
|
||||||
|
}
|
||||||
|
if (!finalData.supplierType) {
|
||||||
|
throw new Error('El tipo de proveedor es requerido');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert comma-separated certifications string to array
|
||||||
|
let certificationsArray: string[] | undefined = undefined;
|
||||||
|
if (finalData.certifications && typeof finalData.certifications === 'string') {
|
||||||
|
certificationsArray = finalData.certifications
|
||||||
|
.split(',')
|
||||||
|
.map((c: string) => c.trim())
|
||||||
|
.filter((c: string) => c.length > 0);
|
||||||
|
} else if (Array.isArray(finalData.certifications)) {
|
||||||
|
certificationsArray = finalData.certifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert comma-separated specializations string to array
|
||||||
|
let specializationsArray: string[] | undefined = undefined;
|
||||||
|
if (finalData.specializations && typeof finalData.specializations === 'string') {
|
||||||
|
specializationsArray = finalData.specializations
|
||||||
|
.split(',')
|
||||||
|
.map((s: string) => s.trim())
|
||||||
|
.filter((s: string) => s.length > 0);
|
||||||
|
} else if (Array.isArray(finalData.specializations)) {
|
||||||
|
specializationsArray = finalData.specializations;
|
||||||
|
}
|
||||||
|
|
||||||
|
const supplierData: SupplierCreate = {
|
||||||
|
name: finalData.name.trim(),
|
||||||
|
supplier_type: finalData.supplierType as SupplierType,
|
||||||
|
supplier_code: finalData.supplierCode || undefined,
|
||||||
|
tax_id: finalData.taxId || undefined,
|
||||||
|
registration_number: finalData.registrationNumber || undefined,
|
||||||
|
contact_person: finalData.contactPerson || undefined,
|
||||||
|
email: finalData.email || undefined,
|
||||||
|
phone: finalData.phone || undefined,
|
||||||
|
mobile: finalData.mobile || undefined,
|
||||||
|
website: finalData.website || undefined,
|
||||||
|
address_line1: finalData.addressLine1 || undefined,
|
||||||
|
address_line2: finalData.addressLine2 || undefined,
|
||||||
|
city: finalData.city || undefined,
|
||||||
|
state_province: finalData.stateProvince || undefined,
|
||||||
|
postal_code: finalData.postalCode || undefined,
|
||||||
|
country: finalData.country || undefined,
|
||||||
|
payment_terms: (finalData.paymentTerms as PaymentTerms) || PaymentTerms.NET_30,
|
||||||
|
credit_limit: finalData.creditLimit ? Number(finalData.creditLimit) : undefined,
|
||||||
|
currency: finalData.currency || 'EUR',
|
||||||
|
standard_lead_time: finalData.standardLeadTime ? Number(finalData.standardLeadTime) : 3,
|
||||||
|
minimum_order_amount: finalData.minimumOrderAmount ? Number(finalData.minimumOrderAmount) : undefined,
|
||||||
|
delivery_area: finalData.deliveryArea || undefined,
|
||||||
|
notes: finalData.notes || undefined,
|
||||||
|
certifications: certificationsArray && certificationsArray.length > 0
|
||||||
|
? certificationsArray
|
||||||
|
: undefined,
|
||||||
|
specializations: specializationsArray && specializationsArray.length > 0
|
||||||
|
? specializationsArray
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
await createSupplierMutation.mutateAsync({
|
||||||
|
tenantId: currentTenant.id,
|
||||||
|
supplierData,
|
||||||
|
});
|
||||||
|
toast.success('Proveedor creado exitosamente');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// EQUIPMENT (using service directly)
|
||||||
|
// ========================================
|
||||||
|
if (selectedItemType === 'equipment') {
|
||||||
|
if (!finalData.name || finalData.name.trim().length < 2) {
|
||||||
|
throw new Error('El nombre del equipo es requerido');
|
||||||
|
}
|
||||||
|
if (!finalData.type) {
|
||||||
|
throw new Error('El tipo de equipo es requerido');
|
||||||
|
}
|
||||||
|
|
||||||
|
const equipmentData = {
|
||||||
|
name: finalData.name.trim(),
|
||||||
|
type: finalData.type,
|
||||||
|
model: finalData.model || undefined,
|
||||||
|
location: finalData.location || undefined,
|
||||||
|
status: 'operational',
|
||||||
|
install_date: finalData.installDate || new Date().toISOString().split('T')[0],
|
||||||
|
last_maintenance_date: new Date().toISOString().split('T')[0],
|
||||||
|
next_maintenance_date: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
||||||
|
maintenance_interval_days: 30,
|
||||||
|
is_active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await equipmentService.createEquipment(currentTenant.id, equipmentData as any);
|
||||||
|
toast.success('Equipo creado exitosamente');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// QUALITY TEMPLATE (using service directly)
|
||||||
|
// ========================================
|
||||||
|
if (selectedItemType === 'quality-template') {
|
||||||
|
if (!finalData.name || finalData.name.trim().length < 1) {
|
||||||
|
throw new Error('El nombre de la plantilla es requerido');
|
||||||
|
}
|
||||||
|
if (!finalData.checkType) {
|
||||||
|
throw new Error('El tipo de control es requerido');
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateData = {
|
||||||
|
name: finalData.name.trim(),
|
||||||
|
check_type: finalData.checkType,
|
||||||
|
template_code: finalData.templateCode || undefined,
|
||||||
|
category: finalData.category || undefined,
|
||||||
|
description: finalData.description || undefined,
|
||||||
|
weight: finalData.weight ? Number(finalData.weight) : 1,
|
||||||
|
applicable_stages: finalData.applicableStages || [],
|
||||||
|
min_value: finalData.minValue ? Number(finalData.minValue) : undefined,
|
||||||
|
max_value: finalData.maxValue ? Number(finalData.maxValue) : undefined,
|
||||||
|
target_value: finalData.targetValue ? Number(finalData.targetValue) : undefined,
|
||||||
|
unit: finalData.unit || undefined,
|
||||||
|
tolerance_percentage: finalData.tolerancePercentage ? Number(finalData.tolerancePercentage) : undefined,
|
||||||
|
instructions: finalData.instructions || undefined,
|
||||||
|
is_required: finalData.isRequired ?? true,
|
||||||
|
is_active: finalData.isActive ?? true,
|
||||||
|
requires_photo: finalData.requiresPhoto ?? false,
|
||||||
|
is_critical: finalData.isCritical ?? false,
|
||||||
|
created_by: currentTenant.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
await qualityTemplateService.createTemplate(currentTenant.id, templateData as any);
|
||||||
|
toast.success('Plantilla de calidad creada exitosamente');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// CUSTOMER (using service directly)
|
||||||
|
// ========================================
|
||||||
|
if (selectedItemType === 'customer') {
|
||||||
|
if (!finalData.name || finalData.name.trim().length < 1) {
|
||||||
|
throw new Error('El nombre del cliente es requerido');
|
||||||
|
}
|
||||||
|
|
||||||
|
const customerPayload = {
|
||||||
|
name: finalData.name.trim(),
|
||||||
|
customer_code: finalData.customerCode || `CUST-${Date.now().toString().slice(-6)}`,
|
||||||
|
customer_type: finalData.customerType || 'individual',
|
||||||
|
country: finalData.country || 'ES',
|
||||||
|
business_name: finalData.businessName || undefined,
|
||||||
|
email: finalData.email || undefined,
|
||||||
|
phone: finalData.phone || undefined,
|
||||||
|
address_line1: finalData.addressLine1 || undefined,
|
||||||
|
address_line2: finalData.addressLine2 || undefined,
|
||||||
|
city: finalData.city || undefined,
|
||||||
|
state: finalData.state || undefined,
|
||||||
|
postal_code: finalData.postalCode || undefined,
|
||||||
|
tax_id: finalData.taxId || undefined,
|
||||||
|
payment_terms: finalData.paymentTerms || 'immediate',
|
||||||
|
credit_limit: finalData.creditLimit ? parseFloat(finalData.creditLimit) : undefined,
|
||||||
|
discount_percentage: finalData.discountPercentage ? Number(finalData.discountPercentage) : 0,
|
||||||
|
customer_segment: finalData.customerSegment || 'regular',
|
||||||
|
priority_level: finalData.priorityLevel || 'normal',
|
||||||
|
preferred_delivery_method: finalData.preferredDeliveryMethod || 'delivery',
|
||||||
|
special_instructions: finalData.specialInstructions || undefined,
|
||||||
|
is_active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await OrdersService.createCustomer({ tenant_id: currentTenant.id, ...customerPayload } as any);
|
||||||
|
toast.success('Cliente creado exitosamente');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// CUSTOMER ORDER (using service directly)
|
||||||
|
// ========================================
|
||||||
|
if (selectedItemType === 'customer-order') {
|
||||||
|
if (!finalData.customer && !finalData.newCustomerName) {
|
||||||
|
throw new Error('Debes seleccionar o crear un cliente');
|
||||||
|
}
|
||||||
|
if (!finalData.orderItems || finalData.orderItems.length === 0) {
|
||||||
|
throw new Error('Debes agregar al menos un producto al pedido');
|
||||||
|
}
|
||||||
|
|
||||||
|
let customerId = finalData.customer?.id;
|
||||||
|
|
||||||
|
if (finalData.showNewCustomerForm && finalData.newCustomerName) {
|
||||||
|
const newCustomerPayload = {
|
||||||
|
name: finalData.newCustomerName,
|
||||||
|
customer_code: `CUST-${Date.now().toString().slice(-6)}`,
|
||||||
|
customer_type: finalData.newCustomerType || 'individual',
|
||||||
|
country: 'ES',
|
||||||
|
phone: finalData.newCustomerPhone || undefined,
|
||||||
|
email: finalData.newCustomerEmail || undefined,
|
||||||
|
is_active: true,
|
||||||
|
discount_percentage: 0,
|
||||||
|
customer_segment: 'regular',
|
||||||
|
priority_level: 'normal',
|
||||||
|
preferred_delivery_method: 'delivery',
|
||||||
|
payment_terms: 'immediate',
|
||||||
|
};
|
||||||
|
const newCustomer = await OrdersService.createCustomer({ tenant_id: currentTenant.id, ...newCustomerPayload } as any);
|
||||||
|
customerId = newCustomer.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!customerId) {
|
||||||
|
throw new Error('No se pudo obtener el ID del cliente');
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderPayload = {
|
||||||
|
tenant_id: currentTenant.id,
|
||||||
|
customer_id: customerId,
|
||||||
|
order_type: finalData.orderType || 'standard',
|
||||||
|
priority: finalData.priority || 'normal',
|
||||||
|
requested_delivery_date: finalData.requestedDeliveryDate || new Date().toISOString(),
|
||||||
|
delivery_method: finalData.deliveryMethod || 'pickup',
|
||||||
|
delivery_address: finalData.deliveryAddress || undefined,
|
||||||
|
payment_method: finalData.paymentMethod || undefined,
|
||||||
|
payment_terms: finalData.paymentTerms || 'immediate',
|
||||||
|
discount_percentage: finalData.discountPercentage ? Number(finalData.discountPercentage) : 0,
|
||||||
|
delivery_fee: finalData.deliveryFee ? Number(finalData.deliveryFee) : 0,
|
||||||
|
special_instructions: finalData.specialInstructions || undefined,
|
||||||
|
order_source: 'manual',
|
||||||
|
sales_channel: 'direct',
|
||||||
|
items: (finalData.orderItems || []).map((item: any) => ({
|
||||||
|
product_id: item.product_id || item.id,
|
||||||
|
product_name: item.product_name || item.name,
|
||||||
|
quantity: Number(item.quantity),
|
||||||
|
unit_of_measure: item.unit_of_measure || 'units',
|
||||||
|
unit_price: Number(item.unit_price || item.price),
|
||||||
|
line_discount: 0,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
await OrdersService.createOrder(orderPayload as any);
|
||||||
|
toast.success('Pedido creado exitosamente');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// TEAM MEMBER (using auth service)
|
||||||
|
// ========================================
|
||||||
|
if (selectedItemType === 'team-member') {
|
||||||
|
if (!finalData.fullName || finalData.fullName.trim().length < 1) {
|
||||||
|
throw new Error('El nombre completo es requerido');
|
||||||
|
}
|
||||||
|
if (!finalData.email || !finalData.email.includes('@')) {
|
||||||
|
throw new Error('El email es requerido y debe ser válido');
|
||||||
|
}
|
||||||
|
if (!finalData.role) {
|
||||||
|
throw new Error('El rol es requerido');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempPassword = `Temp${Math.random().toString(36).substring(2, 10)}!`;
|
||||||
|
const registrationData = {
|
||||||
|
email: finalData.email.trim(),
|
||||||
|
password: tempPassword,
|
||||||
|
full_name: finalData.fullName.trim(),
|
||||||
|
phone_number: finalData.phone || undefined,
|
||||||
|
tenant_id: currentTenant.id,
|
||||||
|
role: finalData.role,
|
||||||
|
};
|
||||||
|
|
||||||
|
await authService.register(registrationData);
|
||||||
|
toast.success('Miembro del equipo agregado exitosamente');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// SALES ENTRY (using service directly)
|
||||||
|
// ========================================
|
||||||
|
if (selectedItemType === 'sales-entry') {
|
||||||
|
if (finalData.entryMethod === 'manual') {
|
||||||
|
if (!finalData.salesItems || finalData.salesItems.length === 0) {
|
||||||
|
throw new Error('Debes agregar al menos una venta');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of finalData.salesItems) {
|
||||||
|
const salesData = {
|
||||||
|
inventory_product_id: item.productId || item.product_id,
|
||||||
|
quantity_sold: Number(item.quantity),
|
||||||
|
unit_price: Number(item.unitPrice || item.unit_price),
|
||||||
|
revenue: Number(item.quantity) * Number(item.unitPrice || item.unit_price),
|
||||||
|
date: finalData.saleDate || new Date().toISOString(),
|
||||||
|
sales_channel: 'in_store',
|
||||||
|
source: 'manual',
|
||||||
|
notes: finalData.notes || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
await salesService.createSalesRecord(currentTenant.id, salesData as any);
|
||||||
|
}
|
||||||
|
toast.success('Ventas registradas exitosamente');
|
||||||
|
} else if (finalData.entryMethod === 'file' || finalData.entryMethod === 'upload') {
|
||||||
|
toast.success('Datos de ventas importados exitosamente');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// RECIPE (using service directly)
|
||||||
|
// ========================================
|
||||||
|
if (selectedItemType === 'recipe') {
|
||||||
|
if (!finalData.name || finalData.name.trim().length < 1) {
|
||||||
|
throw new Error('El nombre de la receta es requerido');
|
||||||
|
}
|
||||||
|
if (!finalData.finishedProductId) {
|
||||||
|
throw new Error('Debes seleccionar un producto terminado');
|
||||||
|
}
|
||||||
|
if (!finalData.ingredients || finalData.ingredients.length === 0) {
|
||||||
|
throw new Error('Debes agregar al menos un ingrediente');
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipeIngredients = finalData.ingredients.map((ing: any, index: number) => ({
|
||||||
|
ingredient_id: ing.ingredientId || ing.ingredient_id || ing.id,
|
||||||
|
quantity: Number(ing.quantity),
|
||||||
|
unit: ing.unit,
|
||||||
|
ingredient_notes: ing.notes || null,
|
||||||
|
preparation_method: ing.preparationMethod || ing.preparation_method || null,
|
||||||
|
is_optional: ing.isOptional || ing.is_optional || false,
|
||||||
|
ingredient_order: index + 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const recipeData = {
|
||||||
|
name: finalData.name.trim(),
|
||||||
|
category: finalData.category || 'general',
|
||||||
|
finished_product_id: finalData.finishedProductId,
|
||||||
|
yield_quantity: parseFloat(finalData.yieldQuantity) || 1,
|
||||||
|
yield_unit: finalData.yieldUnit || 'units',
|
||||||
|
version: finalData.version || '1.0',
|
||||||
|
difficulty_level: finalData.difficultyLevel ? Number(finalData.difficultyLevel) : 3,
|
||||||
|
prep_time_minutes: finalData.prepTime ? parseInt(finalData.prepTime) : null,
|
||||||
|
cook_time_minutes: finalData.cookTime ? parseInt(finalData.cookTime) : null,
|
||||||
|
rest_time_minutes: finalData.restTime ? parseInt(finalData.restTime) : null,
|
||||||
|
total_time_minutes: finalData.totalTime ? parseInt(finalData.totalTime) : null,
|
||||||
|
recipe_code: finalData.recipeCode || null,
|
||||||
|
description: finalData.description || null,
|
||||||
|
preparation_notes: finalData.preparationNotes || null,
|
||||||
|
storage_instructions: finalData.storageInstructions || null,
|
||||||
|
instructions: finalData.instructions || null,
|
||||||
|
ingredients: recipeIngredients,
|
||||||
|
quality_check_configuration: finalData.qualityConfiguration || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
await recipesService.createRecipe(currentTenant.id, recipeData as any);
|
||||||
|
toast.success('Receta creada exitosamente');
|
||||||
|
}
|
||||||
|
|
||||||
// Call the parent's onComplete callback
|
// Call the parent's onComplete callback
|
||||||
onComplete?.(selectedItemType, finalData);
|
onComplete?.(selectedItemType, finalData);
|
||||||
|
|
||||||
// Close the modal
|
|
||||||
handleClose();
|
handleClose();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error submitting wizard data:', error);
|
console.error('Error submitting wizard data:', error);
|
||||||
@@ -209,19 +623,16 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
|||||||
currentTenant,
|
currentTenant,
|
||||||
createPurchaseOrderMutation,
|
createPurchaseOrderMutation,
|
||||||
createProductionBatchMutation,
|
createProductionBatchMutation,
|
||||||
|
createIngredientMutation,
|
||||||
|
addStockMutation,
|
||||||
|
createSupplierMutation,
|
||||||
onComplete,
|
onComplete,
|
||||||
handleClose,
|
handleClose,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get wizard steps based on selected item type
|
|
||||||
// ARCHITECTURAL SOLUTION: We pass dataRef and setWizardData to wizard step functions.
|
|
||||||
// The wizard steps use these in their component wrappers, which creates a closure
|
|
||||||
// that always accesses the CURRENT data from dataRef.current, without needing
|
|
||||||
// to recreate the steps array on every data change.
|
|
||||||
const wizardSteps = useMemo((): WizardStep[] => {
|
const wizardSteps = useMemo((): WizardStep[] => {
|
||||||
if (!selectedItemType) {
|
if (!selectedItemType) {
|
||||||
// Step 0: Item Type Selection
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'item-type-selection',
|
id: 'item-type-selection',
|
||||||
@@ -234,8 +645,6 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(dataRef, setWizardData);
|
return InventoryWizardSteps(dataRef, setWizardData);
|
||||||
@@ -262,9 +671,8 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
|||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}, [selectedItemType, handleItemTypeSelect, wizardData.entryMethod]); // Add entryMethod for dynamic sales-entry steps
|
}, [selectedItemType, handleItemTypeSelect, (wizardData as any).entryMethod]);
|
||||||
|
|
||||||
// Get wizard title based on selected item type
|
|
||||||
const getWizardTitle = (): string => {
|
const getWizardTitle = (): string => {
|
||||||
if (!selectedItemType) {
|
if (!selectedItemType) {
|
||||||
return 'Agregar Contenido';
|
return 'Agregar Contenido';
|
||||||
|
|||||||
@@ -350,64 +350,21 @@ export const CustomerWizardSteps = (
|
|||||||
dataRef: React.MutableRefObject<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
|
// Wizard steps only handle UI and validation
|
||||||
// dataRef and onDataChange are now passed through WizardModal props
|
// API submission is handled centrally in UnifiedAddWizard.handleWizardComplete
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'customer-details',
|
id: 'customer-details',
|
||||||
title: 'wizards:customer.steps.customerDetails',
|
title: 'wizards:customer.steps.customerDetails',
|
||||||
description: 'wizards:customer.steps.customerDetailsDescription',
|
description: 'wizards:customer.steps.customerDetailsDescription',
|
||||||
component: CustomerDetailsStep,
|
component: CustomerDetailsStep,
|
||||||
validate: async () => {
|
validate: () => {
|
||||||
// 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 i18next = (await import('i18next')).default;
|
|
||||||
|
|
||||||
const data = dataRef.current;
|
const data = dataRef.current;
|
||||||
const { currentTenant } = useTenant.getState();
|
// Basic validation - name is required
|
||||||
|
if (!data.name || data.name.trim().length < 1) {
|
||||||
if (!currentTenant?.id) {
|
|
||||||
showToast.error(i18next.t('wizards:customer.messages.errorObtainingTenantInfo'));
|
|
||||||
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(i18next.t('wizards:customer.messages.customerCreatedSuccessfully'));
|
|
||||||
return true;
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Error creating customer:', err);
|
|
||||||
const errorMessage = err.response?.data?.detail || i18next.t('wizards:customer.messages.errorCreatingCustomer');
|
|
||||||
showToast.error(errorMessage);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ const EquipmentDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
|
|||||||
</div>
|
</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 className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('equipment.fields.name')} *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={data.name || ''}
|
||||||
|
onChange={(e) => handleFieldChange('name', e.target.value)}
|
||||||
|
placeholder={t('equipment.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)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('equipment.fields.type')} *</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('equipment.fields.type')} *</label>
|
||||||
<select
|
<select
|
||||||
@@ -29,17 +39,18 @@ const EquipmentDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
|
|||||||
<option value="oven">{t('equipment.types.oven')}</option>
|
<option value="oven">{t('equipment.types.oven')}</option>
|
||||||
<option value="mixer">{t('equipment.types.mixer')}</option>
|
<option value="mixer">{t('equipment.types.mixer')}</option>
|
||||||
<option value="proofer">{t('equipment.types.proofer')}</option>
|
<option value="proofer">{t('equipment.types.proofer')}</option>
|
||||||
<option value="refrigerator">{t('equipment.types.refrigerator')}</option>
|
<option value="freezer">{t('equipment.types.freezer')}</option>
|
||||||
|
<option value="packaging">{t('equipment.types.packaging')}</option>
|
||||||
<option value="other">{t('equipment.types.other')}</option>
|
<option value="other">{t('equipment.types.other')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('equipment.fields.brand')}</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('equipment.fields.model')}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={data.brand || ''}
|
value={data.model || ''}
|
||||||
onChange={(e) => handleFieldChange('brand', e.target.value)}
|
onChange={(e) => handleFieldChange('model', e.target.value)}
|
||||||
placeholder={t('equipment.fields.brandPlaceholder')}
|
placeholder={t('equipment.fields.modelPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[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>
|
||||||
@@ -54,11 +65,11 @@ const EquipmentDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('equipment.fields.purchaseDate')}</label>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">{t('equipment.fields.installDate')}</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={data.purchaseDate || ''}
|
value={data.installDate || ''}
|
||||||
onChange={(e) => handleFieldChange('purchaseDate', e.target.value)}
|
onChange={(e) => handleFieldChange('installDate', 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>
|
||||||
@@ -68,52 +79,18 @@ const EquipmentDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const EquipmentWizardSteps = (dataRef: React.MutableRefObject<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[] => {
|
||||||
// New architecture: return direct component references instead of arrow functions
|
// Wizard steps only handle UI and validation
|
||||||
// dataRef and onDataChange are now passed through WizardModal props
|
// API submission is handled centrally in UnifiedAddWizard.handleWizardComplete
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'equipment-details',
|
id: 'equipment-details',
|
||||||
title: 'wizards:equipment.steps.equipmentDetails',
|
title: 'wizards:equipment.steps.equipmentDetails',
|
||||||
description: 'wizards:equipment.steps.equipmentDetailsDescription',
|
description: 'wizards:equipment.steps.equipmentDetailsDescription',
|
||||||
component: EquipmentDetailsStep,
|
component: EquipmentDetailsStep,
|
||||||
validate: async () => {
|
validate: () => {
|
||||||
const { useTenant } = await import('../../../../stores/tenant.store');
|
|
||||||
const { equipmentService } = await import('../../../../api/services/equipment');
|
|
||||||
const { showToast } = await import('../../../../utils/toast');
|
|
||||||
const i18next = (await import('i18next')).default;
|
|
||||||
|
|
||||||
const data = dataRef.current;
|
const data = dataRef.current;
|
||||||
const { currentTenant } = useTenant.getState();
|
// Name and type are required
|
||||||
|
return !!(data.name && data.name.trim().length >= 2 && data.type);
|
||||||
if (!currentTenant?.id) {
|
|
||||||
showToast.error(i18next.t('wizards:equipment.messages.errorGettingTenant'));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const equipmentCreateData: any = {
|
|
||||||
name: `${data.type || 'oven'} - ${data.brand || i18next.t('wizards:equipment.messages.noBrand')}`,
|
|
||||||
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(i18next.t('wizards:equipment.messages.successCreate'));
|
|
||||||
return true;
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Error creating equipment:', err);
|
|
||||||
const errorMessage = err.response?.data?.detail || i18next.t('wizards:equipment.messages.errorCreate');
|
|
||||||
showToast.error(errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -168,40 +168,40 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
|
|
||||||
{/* Advanced Options */}
|
{/* Advanced Options */}
|
||||||
<AdvancedOptionsSection
|
<AdvancedOptionsSection
|
||||||
title="Advanced Options"
|
title={t('recipe.advancedOptionsTitle')}
|
||||||
description="Optional fields for detailed recipe management"
|
description={t('recipe.advancedOptionsDescription')}
|
||||||
>
|
>
|
||||||
<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">
|
||||||
Recipe Code/SKU
|
{t('recipe.fields.recipeCode')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={data.recipeCode}
|
value={data.recipeCode}
|
||||||
onChange={(e) => handleFieldChange('recipeCode', e.target.value)}
|
onChange={(e) => handleFieldChange('recipeCode', e.target.value)}
|
||||||
placeholder="RCP-001"
|
placeholder={t('recipe.fields.recipeCodePlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[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>
|
||||||
<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">
|
||||||
Version
|
{t('recipe.fields.version')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={data.version}
|
value={data.version}
|
||||||
onChange={(e) => handleFieldChange('version', e.target.value)}
|
onChange={(e) => handleFieldChange('version', e.target.value)}
|
||||||
placeholder="1.0"
|
placeholder={t('recipe.fields.versionPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[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>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
|
||||||
Difficulty Level (1-5)
|
{t('recipe.fields.difficulty')}
|
||||||
<Tooltip content="1 = Very Easy, 5 = Expert Level">
|
<Tooltip content={t('recipe.fields.difficultyTooltip')}>
|
||||||
<span />
|
<span />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</label>
|
</label>
|
||||||
@@ -217,13 +217,13 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
|
|
||||||
<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">
|
||||||
Cook Time (minutes)
|
{t('recipe.fields.cookTime')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={data.cookTime}
|
value={data.cookTime}
|
||||||
onChange={(e) => handleFieldChange('cookTime', e.target.value)}
|
onChange={(e) => handleFieldChange('cookTime', e.target.value)}
|
||||||
placeholder="30"
|
placeholder={t('recipe.fields.cookTimePlaceholder')}
|
||||||
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)]"
|
||||||
/>
|
/>
|
||||||
@@ -231,8 +231,8 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
|
||||||
Rest Time (minutes)
|
{t('recipe.fields.restTime')}
|
||||||
<Tooltip content="Time for rising, cooling, or resting">
|
<Tooltip content={t('recipe.fields.restTimeTooltip')}>
|
||||||
<span />
|
<span />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</label>
|
</label>
|
||||||
@@ -240,7 +240,7 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
type="number"
|
type="number"
|
||||||
value={data.restTime}
|
value={data.restTime}
|
||||||
onChange={(e) => handleFieldChange('restTime', e.target.value)}
|
onChange={(e) => handleFieldChange('restTime', e.target.value)}
|
||||||
placeholder="60"
|
placeholder={t('recipe.fields.restTimePlaceholder')}
|
||||||
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)]"
|
||||||
/>
|
/>
|
||||||
@@ -248,13 +248,13 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
|
|
||||||
<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">
|
||||||
Total Time (minutes)
|
{t('recipe.fields.totalTime')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={data.totalTime}
|
value={data.totalTime}
|
||||||
onChange={(e) => handleFieldChange('totalTime', e.target.value)}
|
onChange={(e) => handleFieldChange('totalTime', e.target.value)}
|
||||||
placeholder="90"
|
placeholder={t('recipe.fields.totalTimePlaceholder')}
|
||||||
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)]"
|
||||||
/>
|
/>
|
||||||
@@ -262,13 +262,13 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
|
|
||||||
<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">
|
||||||
Serves Count
|
{t('recipe.fields.servesCount')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={data.servesCount}
|
value={data.servesCount}
|
||||||
onChange={(e) => handleFieldChange('servesCount', e.target.value)}
|
onChange={(e) => handleFieldChange('servesCount', e.target.value)}
|
||||||
placeholder="8"
|
placeholder={t('recipe.fields.servesCountPlaceholder')}
|
||||||
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)]"
|
||||||
/>
|
/>
|
||||||
@@ -276,8 +276,8 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
|
||||||
Batch Size Multiplier
|
{t('recipe.fields.batchSizeMultiplier')}
|
||||||
<Tooltip content="Default scaling factor for batch production">
|
<Tooltip content={t('recipe.fields.batchSizeMultiplierTooltip')}>
|
||||||
<span />
|
<span />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</label>
|
</label>
|
||||||
@@ -285,7 +285,7 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
type="number"
|
type="number"
|
||||||
value={data.batchSizeMultiplier}
|
value={data.batchSizeMultiplier}
|
||||||
onChange={(e) => handleFieldChange('batchSizeMultiplier', parseFloat(e.target.value) || 1)}
|
onChange={(e) => handleFieldChange('batchSizeMultiplier', parseFloat(e.target.value) || 1)}
|
||||||
placeholder="1.0"
|
placeholder={t('recipe.fields.batchSizeMultiplierPlaceholder')}
|
||||||
min="0.1"
|
min="0.1"
|
||||||
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)]"
|
||||||
@@ -294,13 +294,13 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
|
|
||||||
<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">
|
||||||
Min Batch Size
|
{t('recipe.fields.minBatchSize')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={data.minBatchSize}
|
value={data.minBatchSize}
|
||||||
onChange={(e) => handleFieldChange('minBatchSize', e.target.value)}
|
onChange={(e) => handleFieldChange('minBatchSize', e.target.value)}
|
||||||
placeholder="5"
|
placeholder={t('recipe.fields.minBatchSizePlaceholder')}
|
||||||
min="0"
|
min="0"
|
||||||
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)]"
|
||||||
@@ -309,13 +309,13 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
|
|
||||||
<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">
|
||||||
Max Batch Size
|
{t('recipe.fields.maxBatchSize')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={data.maxBatchSize}
|
value={data.maxBatchSize}
|
||||||
onChange={(e) => handleFieldChange('maxBatchSize', e.target.value)}
|
onChange={(e) => handleFieldChange('maxBatchSize', e.target.value)}
|
||||||
placeholder="100"
|
placeholder={t('recipe.fields.maxBatchSizePlaceholder')}
|
||||||
min="0"
|
min="0"
|
||||||
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)]"
|
||||||
@@ -324,13 +324,13 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
|
|
||||||
<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">
|
||||||
Optimal Production Temp (°C)
|
{t('recipe.fields.optimalTemp')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={data.optimalProductionTemp}
|
value={data.optimalProductionTemp}
|
||||||
onChange={(e) => handleFieldChange('optimalProductionTemp', e.target.value)}
|
onChange={(e) => handleFieldChange('optimalProductionTemp', e.target.value)}
|
||||||
placeholder="24"
|
placeholder={t('recipe.fields.optimalTempPlaceholder')}
|
||||||
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)]"
|
||||||
/>
|
/>
|
||||||
@@ -338,13 +338,13 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
|
|
||||||
<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">
|
||||||
Optimal Humidity (%)
|
{t('recipe.fields.optimalHumidity')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={data.optimalHumidity}
|
value={data.optimalHumidity}
|
||||||
onChange={(e) => handleFieldChange('optimalHumidity', e.target.value)}
|
onChange={(e) => handleFieldChange('optimalHumidity', e.target.value)}
|
||||||
placeholder="65"
|
placeholder={t('recipe.fields.optimalHumidityPlaceholder')}
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
@@ -354,13 +354,13 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
|
|
||||||
<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">
|
||||||
Target Margin (%)
|
{t('recipe.fields.targetMargin')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={data.targetMargin}
|
value={data.targetMargin}
|
||||||
onChange={(e) => handleFieldChange('targetMargin', e.target.value)}
|
onChange={(e) => handleFieldChange('targetMargin', e.target.value)}
|
||||||
placeholder="30"
|
placeholder={t('recipe.fields.targetMarginPlaceholder')}
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
@@ -379,7 +379,7 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
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)]">
|
||||||
Seasonal Item
|
{t('recipe.fields.seasonalItem')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -392,7 +392,7 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
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)]">
|
||||||
Signature Item
|
{t('recipe.fields.signatureItem')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -401,14 +401,14 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
<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
|
{t('recipe.fields.seasonStartMonth')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={data.seasonStartMonth}
|
value={data.seasonStartMonth}
|
||||||
onChange={(e) => handleFieldChange('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="">{t('recipe.fields.seasonStartMonthPlaceholder')}</option>
|
||||||
{Array.from({ length: 12 }, (_, i) => i + 1).map(month => (
|
{Array.from({ length: 12 }, (_, i) => i + 1).map(month => (
|
||||||
<option key={month} value={month}>
|
<option key={month} value={month}>
|
||||||
{new Date(2000, month - 1).toLocaleString('default', { month: 'long' })}
|
{new Date(2000, month - 1).toLocaleString('default', { month: 'long' })}
|
||||||
@@ -419,14 +419,14 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
|
|
||||||
<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 End Month
|
{t('recipe.fields.seasonEndMonth')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={data.seasonEndMonth}
|
value={data.seasonEndMonth}
|
||||||
onChange={(e) => handleFieldChange('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="">{t('recipe.fields.seasonEndMonthPlaceholder')}</option>
|
||||||
{Array.from({ length: 12 }, (_, i) => i + 1).map(month => (
|
{Array.from({ length: 12 }, (_, i) => i + 1).map(month => (
|
||||||
<option key={month} value={month}>
|
<option key={month} value={month}>
|
||||||
{new Date(2000, month - 1).toLocaleString('default', { month: 'long' })}
|
{new Date(2000, month - 1).toLocaleString('default', { month: 'long' })}
|
||||||
@@ -439,12 +439,12 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
|
|
||||||
<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">
|
||||||
Description
|
{t('recipe.fields.description')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={data.description}
|
value={data.description}
|
||||||
onChange={(e) => handleFieldChange('description', e.target.value)}
|
onChange={(e) => handleFieldChange('description', e.target.value)}
|
||||||
placeholder="Detailed description of the recipe..."
|
placeholder={t('recipe.fields.descriptionPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[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}
|
||||||
/>
|
/>
|
||||||
@@ -452,15 +452,15 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
|
||||||
Recipe Notes & Tips
|
{t('recipe.fields.prepNotes')}
|
||||||
<Tooltip content="General notes, tips, or context about this recipe (not step-by-step instructions)">
|
<Tooltip content={t('recipe.fields.prepNotesTooltip')}>
|
||||||
<span />
|
<span />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={data.preparationNotes}
|
value={data.preparationNotes}
|
||||||
onChange={(e) => handleFieldChange('preparationNotes', e.target.value)}
|
onChange={(e) => handleFieldChange('preparationNotes', e.target.value)}
|
||||||
placeholder="e.g., 'Works best in humid conditions', 'Can be prepared a day ahead', 'Traditional family recipe'..."
|
placeholder={t('recipe.fields.prepNotesPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[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}
|
||||||
/>
|
/>
|
||||||
@@ -468,12 +468,12 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
|
|
||||||
<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">
|
||||||
Storage Instructions
|
{t('recipe.fields.storageInstructions')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={data.storageInstructions}
|
value={data.storageInstructions}
|
||||||
onChange={(e) => handleFieldChange('storageInstructions', e.target.value)}
|
onChange={(e) => handleFieldChange('storageInstructions', e.target.value)}
|
||||||
placeholder="How to store the finished product..."
|
placeholder={t('recipe.fields.storageInstructionsPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[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}
|
||||||
/>
|
/>
|
||||||
@@ -481,26 +481,26 @@ const RecipeDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange })
|
|||||||
|
|
||||||
<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">
|
||||||
Allergens
|
{t('recipe.fields.allergens')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={data.allergens}
|
value={data.allergens}
|
||||||
onChange={(e) => handleFieldChange('allergens', e.target.value)}
|
onChange={(e) => handleFieldChange('allergens', e.target.value)}
|
||||||
placeholder="e.g., gluten, dairy, eggs (comma-separated)"
|
placeholder={t('recipe.fields.allergensPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[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>
|
||||||
<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">
|
||||||
Dietary Tags
|
{t('recipe.fields.dietaryTags')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={data.dietaryTags}
|
value={data.dietaryTags}
|
||||||
onChange={(e) => handleFieldChange('dietaryTags', e.target.value)}
|
onChange={(e) => handleFieldChange('dietaryTags', e.target.value)}
|
||||||
placeholder="e.g., vegan, gluten-free, organic (comma-separated)"
|
placeholder={t('recipe.fields.dietaryTagsPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-[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>
|
||||||
@@ -769,13 +769,13 @@ const IngredientsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
|
|||||||
};
|
};
|
||||||
|
|
||||||
// New Step 4: Enhanced Quality Control Configuration
|
// New Step 4: Enhanced Quality Control Configuration
|
||||||
const QualityControlStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onComplete }) => {
|
// API submission is handled centrally in UnifiedAddWizard.handleWizardComplete
|
||||||
|
const QualityControlStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||||
const { t } = useTranslation('wizards');
|
const { t } = useTranslation('wizards');
|
||||||
const data = dataRef?.current || {};
|
const data = dataRef?.current || {};
|
||||||
const { currentTenant } = useTenant();
|
const { currentTenant } = useTenant();
|
||||||
const [templates, setTemplates] = useState<QualityCheckTemplate[]>([]);
|
const [templates, setTemplates] = useState<QualityCheckTemplate[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -800,76 +800,6 @@ const QualityControlStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange,
|
|||||||
onDataChange?.({ ...data, qualityConfiguration: config });
|
onDataChange?.({ ...data, qualityConfiguration: config });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateRecipe = async () => {
|
|
||||||
if (!currentTenant?.id) {
|
|
||||||
setError('Could not obtain tenant information');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSaving(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const recipeIngredients: RecipeIngredientCreate[] = data.ingredients.map((ing: any, index: number) => ({
|
|
||||||
ingredient_id: ing.ingredientId,
|
|
||||||
quantity: ing.quantity,
|
|
||||||
unit: ing.unit,
|
|
||||||
ingredient_notes: ing.notes || null,
|
|
||||||
preparation_method: ing.preparationMethod || null,
|
|
||||||
is_optional: ing.isOptional || false,
|
|
||||||
ingredient_order: index + 1,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Use the quality configuration from the editor if available
|
|
||||||
const qualityConfig: RecipeQualityConfiguration | undefined = data.qualityConfiguration;
|
|
||||||
|
|
||||||
const recipeData: RecipeCreate = {
|
|
||||||
name: data.name,
|
|
||||||
category: data.category,
|
|
||||||
finished_product_id: data.finishedProductId,
|
|
||||||
yield_quantity: parseFloat(data.yieldQuantity),
|
|
||||||
yield_unit: data.yieldUnit as MeasurementUnit,
|
|
||||||
version: data.version || '1.0',
|
|
||||||
difficulty_level: data.difficultyLevel || 3,
|
|
||||||
prep_time_minutes: data.prepTime ? parseInt(data.prepTime) : null,
|
|
||||||
cook_time_minutes: data.cookTime ? parseInt(data.cookTime) : null,
|
|
||||||
rest_time_minutes: data.restTime ? parseInt(data.restTime) : null,
|
|
||||||
total_time_minutes: data.totalTime ? parseInt(data.totalTime) : null,
|
|
||||||
recipe_code: data.recipeCode || null,
|
|
||||||
description: data.description || null,
|
|
||||||
preparation_notes: data.preparationNotes || null,
|
|
||||||
storage_instructions: data.storageInstructions || null,
|
|
||||||
serves_count: data.servesCount ? parseInt(data.servesCount) : null,
|
|
||||||
batch_size_multiplier: data.batchSizeMultiplier || 1.0,
|
|
||||||
minimum_batch_size: data.minBatchSize ? parseFloat(data.minBatchSize) : null,
|
|
||||||
maximum_batch_size: data.maxBatchSize ? parseFloat(data.maxBatchSize) : null,
|
|
||||||
optimal_production_temperature: data.optimalProductionTemp ? parseFloat(data.optimalProductionTemp) : null,
|
|
||||||
optimal_humidity: data.optimalHumidity ? parseFloat(data.optimalHumidity) : null,
|
|
||||||
target_margin_percentage: data.targetMargin ? parseFloat(data.targetMargin) : null,
|
|
||||||
is_seasonal: data.isSeasonal || false,
|
|
||||||
is_signature_item: data.isSignatureItem || false,
|
|
||||||
season_start_month: data.seasonStartMonth ? parseInt(data.seasonStartMonth) : null,
|
|
||||||
season_end_month: data.seasonEndMonth ? parseInt(data.seasonEndMonth) : null,
|
|
||||||
instructions: data.instructions || null,
|
|
||||||
allergen_info: data.allergens ? data.allergens.split(',').map((a: string) => a.trim()) : null,
|
|
||||||
dietary_tags: data.dietaryTags ? data.dietaryTags.split(',').map((t: string) => t.trim()) : null,
|
|
||||||
ingredients: recipeIngredients,
|
|
||||||
quality_check_configuration: qualityConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
await recipesService.createRecipe(currentTenant.id, recipeData);
|
|
||||||
showToast.success(t('recipe.messages.successCreate'));
|
|
||||||
onComplete();
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Error creating recipe:', err);
|
|
||||||
const errorMessage = err.response?.data?.detail || t('recipe.messages.errorCreate');
|
|
||||||
setError(errorMessage);
|
|
||||||
showToast.error(errorMessage);
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Format templates for the editor
|
// Format templates for the editor
|
||||||
const formattedTemplates = templates.map(t => ({
|
const formattedTemplates = templates.map(t => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
@@ -921,27 +851,6 @@ const QualityControlStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange,
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-center pt-4 border-t border-[var(--border-primary)]">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleCreateRecipe}
|
|
||||||
disabled={saving || 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"
|
|
||||||
>
|
|
||||||
{saving ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
|
||||||
Creating recipe...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CheckCircle2 className="w-5 h-5" />
|
|
||||||
Create Recipe
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -788,8 +788,8 @@ export const SalesEntryWizardSteps = (
|
|||||||
): WizardStep[] => {
|
): WizardStep[] => {
|
||||||
const entryMethod = dataRef.current.entryMethod;
|
const entryMethod = dataRef.current.entryMethod;
|
||||||
|
|
||||||
// New architecture: return direct component references instead of arrow functions
|
// Wizard steps only handle UI and validation
|
||||||
// dataRef and onDataChange are now passed through WizardModal props
|
// API submission is handled centrally in UnifiedAddWizard.handleWizardComplete
|
||||||
const steps: WizardStep[] = [
|
const steps: WizardStep[] = [
|
||||||
{
|
{
|
||||||
id: 'entry-method',
|
id: 'entry-method',
|
||||||
@@ -824,52 +824,14 @@ export const SalesEntryWizardSteps = (
|
|||||||
title: 'salesEntry.steps.review',
|
title: 'salesEntry.steps.review',
|
||||||
description: 'salesEntry.steps.reviewDescription',
|
description: 'salesEntry.steps.reviewDescription',
|
||||||
component: ReviewStep,
|
component: ReviewStep,
|
||||||
validate: async () => {
|
validate: () => {
|
||||||
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 data = dataRef.current;
|
||||||
const { currentTenant } = useTenant.getState();
|
// Validate based on entry method
|
||||||
|
if (data.entryMethod === 'manual') {
|
||||||
if (!currentTenant?.id) {
|
return (data.salesItems || []).length > 0;
|
||||||
const { showToast } = await import('../../../../utils/toast');
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { showToast } = await import('../../../../utils/toast');
|
|
||||||
showToast.success('Registro de ventas guardado exitosamente');
|
|
||||||
return true;
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Error saving sales data:', err);
|
|
||||||
const { showToast } = await import('../../../../utils/toast');
|
|
||||||
const errorMessage = err.response?.data?.detail || 'Error al guardar los datos de ventas';
|
|
||||||
showToast.error(errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
// For file upload, validation was done during upload
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
||||||
import { Building2, CheckCircle2, Loader2 } from 'lucide-react';
|
import { Building2 } from 'lucide-react';
|
||||||
import { useTenant } from '../../../../stores/tenant.store';
|
|
||||||
import { suppliersService } from '../../../../api/services/suppliers';
|
|
||||||
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';
|
||||||
|
|
||||||
const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onComplete }) => {
|
const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) => {
|
||||||
const { t } = useTranslation('wizards');
|
const { t } = useTranslation('wizards');
|
||||||
// New architecture: access data from dataRef.current
|
// New architecture: access data from dataRef.current
|
||||||
const data = dataRef?.current || {};
|
const data = dataRef?.current || {};
|
||||||
const { currentTenant } = useTenant();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleFieldChange = (field: string, value: any) => {
|
const handleFieldChange = (field: string, value: any) => {
|
||||||
onDataChange?.({ ...data, [field]: value });
|
onDataChange?.({ ...data, [field]: value });
|
||||||
@@ -27,64 +21,7 @@ const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange,
|
|||||||
}
|
}
|
||||||
}, [data.name]);
|
}, [data.name]);
|
||||||
|
|
||||||
const handleCreateSupplier = async () => {
|
// API submission is handled centrally in UnifiedAddWizard.handleWizardComplete
|
||||||
const i18next = (await import('i18next')).default;
|
|
||||||
|
|
||||||
if (!currentTenant?.id) {
|
|
||||||
const errorMsg = i18next.t('wizards:supplier.messages.errorObtainingTenantInfo');
|
|
||||||
setError(errorMsg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload = {
|
|
||||||
name: data.name,
|
|
||||||
supplier_type: data.supplierType,
|
|
||||||
status: data.status,
|
|
||||||
payment_terms: data.paymentTerms,
|
|
||||||
currency: data.currency,
|
|
||||||
standard_lead_time: data.standardLeadTime,
|
|
||||||
supplier_code: data.supplierCode || undefined,
|
|
||||||
tax_id: data.taxId || undefined,
|
|
||||||
registration_number: data.registrationNumber || undefined,
|
|
||||||
contact_person: data.contactPerson || undefined,
|
|
||||||
email: data.email || undefined,
|
|
||||||
phone: data.phone || undefined,
|
|
||||||
mobile: data.mobile || undefined,
|
|
||||||
website: data.website || undefined,
|
|
||||||
address_line1: data.addressLine1 || undefined,
|
|
||||||
address_line2: data.addressLine2 || undefined,
|
|
||||||
city: data.city || undefined,
|
|
||||||
state_province: data.stateProvince || undefined,
|
|
||||||
postal_code: data.postalCode || undefined,
|
|
||||||
country: data.country || undefined,
|
|
||||||
credit_limit: data.creditLimit ? parseFloat(data.creditLimit) : undefined,
|
|
||||||
minimum_order_amount: data.minimumOrderAmount ? parseFloat(data.minimumOrderAmount) : undefined,
|
|
||||||
delivery_area: data.deliveryArea || undefined,
|
|
||||||
is_preferred_supplier: data.isPreferredSupplier,
|
|
||||||
auto_approve_enabled: data.autoApproveEnabled,
|
|
||||||
notes: data.notes || undefined,
|
|
||||||
certifications: data.certifications ? JSON.parse(`{"items": ${JSON.stringify(data.certifications.split(',').map(c => c.trim()))}}`) : undefined,
|
|
||||||
specializations: data.specializations ? JSON.parse(`{"items": ${JSON.stringify(data.specializations.split(',').map(s => s.trim()))}}`) : undefined,
|
|
||||||
created_by: currentTenant.id,
|
|
||||||
updated_by: currentTenant.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
await suppliersService.createSupplier(currentTenant.id, payload);
|
|
||||||
showToast.success(i18next.t('wizards:supplier.messages.supplierCreatedSuccessfully'));
|
|
||||||
// Let the wizard handle completion via the Next/Complete button
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Error creating supplier:', err);
|
|
||||||
const errorMessage = err.response?.data?.detail || i18next.t('wizards:supplier.messages.errorCreatingSupplier');
|
|
||||||
setError(errorMessage);
|
|
||||||
showToast.error(errorMessage);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -94,12 +31,6 @@ const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange,
|
|||||||
<p className="text-sm text-[var(--text-secondary)]">{t('supplier.subtitle')}</p>
|
<p className="text-sm text-[var(--text-secondary)]">{t('supplier.subtitle')}</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">
|
||||||
|
|||||||
@@ -142,8 +142,8 @@ const PermissionsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange }) =
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const TeamMemberWizardSteps = (dataRef: React.MutableRefObject<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[] => {
|
||||||
// New architecture: return direct component references instead of arrow functions
|
// Wizard steps only handle UI and validation
|
||||||
// dataRef and onDataChange are now passed through WizardModal props
|
// API submission is handled centrally in UnifiedAddWizard.handleWizardComplete
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'member-details',
|
id: 'member-details',
|
||||||
@@ -156,49 +156,19 @@ export const TeamMemberWizardSteps = (dataRef: React.MutableRefObject<Record<str
|
|||||||
title: 'wizards:teamMember.steps.roleAndPermissions',
|
title: 'wizards:teamMember.steps.roleAndPermissions',
|
||||||
description: 'wizards:teamMember.steps.roleAndPermissionsDescription',
|
description: 'wizards:teamMember.steps.roleAndPermissionsDescription',
|
||||||
component: PermissionsStep,
|
component: PermissionsStep,
|
||||||
validate: async () => {
|
validate: () => {
|
||||||
const { useTenant } = await import('../../../../stores/tenant.store');
|
|
||||||
const { authService } = await import('../../../../api/services/auth');
|
|
||||||
const { showToast } = await import('../../../../utils/toast');
|
|
||||||
const i18next = (await import('i18next')).default;
|
|
||||||
|
|
||||||
const data = dataRef.current;
|
const data = dataRef.current;
|
||||||
const { currentTenant } = useTenant.getState();
|
// Validate required fields
|
||||||
|
if (!data.fullName || data.fullName.trim().length < 1) {
|
||||||
if (!currentTenant?.id) {
|
|
||||||
showToast.error(i18next.t('wizards:teamMember.messages.errorGettingTenant'));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!data.email || !data.email.includes('@')) {
|
||||||
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(i18next.t('wizards:teamMember.messages.successCreate'));
|
|
||||||
return true;
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Error creating team member:', err);
|
|
||||||
const errorMessage = err.response?.data?.detail || i18next.t('wizards:teamMember.messages.errorCreate');
|
|
||||||
showToast.error(errorMessage);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!data.role) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"subtitle_option_b": "AI that knows your area predicts sales with 92% accuracy. Wake up with your plan ready: what to make, what to order, when it arrives. Save €500-2,000/month on waste.",
|
"subtitle_option_b": "AI that knows your area predicts sales with 92% accuracy. Wake up with your plan ready: what to make, what to order, when it arrives. Save €500-2,000/month on waste.",
|
||||||
"cta_primary": "Join Pilot Program",
|
"cta_primary": "Join Pilot Program",
|
||||||
"cta_secondary": "See How It Works (2 min)",
|
"cta_secondary": "See How It Works (2 min)",
|
||||||
"cta_demo": "See Demo",
|
"cta_demo": "View Interactive Demo",
|
||||||
"social_proof": {
|
"social_proof": {
|
||||||
"bakeries": "Reduce waste up to 40% from the first month",
|
"bakeries": "Reduce waste up to 40% from the first month",
|
||||||
"accuracy": "92% accurate predictions (vs 60% generic systems)",
|
"accuracy": "92% accurate predictions (vs 60% generic systems)",
|
||||||
@@ -225,10 +225,10 @@
|
|||||||
},
|
},
|
||||||
"final_cta": {
|
"final_cta": {
|
||||||
"scarcity_badge": "🔥 Only 12 spots left out of 20",
|
"scarcity_badge": "🔥 Only 12 spots left out of 20",
|
||||||
"title": "Stop Losing €2,000 per Month on Waste",
|
"title": "Stop Losing Money per Month on Waste",
|
||||||
"title_accent": "Trying This Technology",
|
"title_accent": "Trying This Technology",
|
||||||
"subtitle": "Join the first 20 bakeries. Only 12 spots left.",
|
"subtitle": "Join the first 20 bakeries.",
|
||||||
"button": "Start Now - No Card Required",
|
"button": "Start Now",
|
||||||
"cta_primary": "Request Pilot Spot",
|
"cta_primary": "Request Pilot Spot",
|
||||||
"cta_secondary": "See Demo (2 min)",
|
"cta_secondary": "See Demo (2 min)",
|
||||||
"guarantee": "Card required. No charge for 3 months. Cancel anytime."
|
"guarantee": "Card required. No charge for 3 months. Cancel anytime."
|
||||||
|
|||||||
@@ -994,6 +994,40 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"equipment": {
|
||||||
|
"title": "Add Equipment",
|
||||||
|
"equipmentDetails": "Equipment Details",
|
||||||
|
"subtitle": "Bakery Equipment",
|
||||||
|
"fields": {
|
||||||
|
"name": "Equipment Name",
|
||||||
|
"namePlaceholder": "E.g., Main Production Oven",
|
||||||
|
"type": "Equipment Type",
|
||||||
|
"model": "Model",
|
||||||
|
"modelPlaceholder": "E.g., Rational SCC 101",
|
||||||
|
"location": "Location",
|
||||||
|
"locationPlaceholder": "E.g., Main kitchen",
|
||||||
|
"status": "Status",
|
||||||
|
"installDate": "Install Date"
|
||||||
|
},
|
||||||
|
"types": {
|
||||||
|
"oven": "Oven",
|
||||||
|
"mixer": "Mixer",
|
||||||
|
"proofer": "Proofer",
|
||||||
|
"freezer": "Freezer",
|
||||||
|
"packaging": "Packaging",
|
||||||
|
"other": "Other"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"equipmentDetails": "Equipment Details",
|
||||||
|
"equipmentDetailsDescription": "Type, model, location"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"errorGettingTenant": "Could not get tenant information",
|
||||||
|
"noBrand": "No brand",
|
||||||
|
"successCreate": "Equipment created successfully",
|
||||||
|
"errorCreate": "Error creating equipment"
|
||||||
|
}
|
||||||
|
},
|
||||||
"purchaseOrder": {
|
"purchaseOrder": {
|
||||||
"orderItems": {
|
"orderItems": {
|
||||||
"titleHeader": "Products to Purchase",
|
"titleHeader": "Products to Purchase",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"subtitle_option_b": "IA que conoce tu zona predice ventas con 92% de precisión. Despierta con tu plan listo: qué hacer, qué pedir, cuándo llegará. Ahorra €500-2,000/mes en desperdicios.",
|
"subtitle_option_b": "IA que conoce tu zona predice ventas con 92% de precisión. Despierta con tu plan listo: qué hacer, qué pedir, cuándo llegará. Ahorra €500-2,000/mes en desperdicios.",
|
||||||
"cta_primary": "Únete al Programa Piloto",
|
"cta_primary": "Únete al Programa Piloto",
|
||||||
"cta_secondary": "Ver Cómo Funciona (2 min)",
|
"cta_secondary": "Ver Cómo Funciona (2 min)",
|
||||||
"cta_demo": "Ver Demo",
|
"cta_demo": "Ver Demostración interactiva",
|
||||||
"social_proof": {
|
"social_proof": {
|
||||||
"bakeries": "Reduce desperdicios hasta 40% desde el primer mes",
|
"bakeries": "Reduce desperdicios hasta 40% desde el primer mes",
|
||||||
"accuracy": "Predicciones 92% precisas (vs 60% sistemas genéricos)",
|
"accuracy": "Predicciones 92% precisas (vs 60% sistemas genéricos)",
|
||||||
@@ -225,10 +225,10 @@
|
|||||||
},
|
},
|
||||||
"final_cta": {
|
"final_cta": {
|
||||||
"scarcity_badge": "🔥 Solo 12 plazas restantes de 20",
|
"scarcity_badge": "🔥 Solo 12 plazas restantes de 20",
|
||||||
"title": "Deja de Perder €2,000 al Mes en Desperdicios",
|
"title": "Deja de Perder Dinero al Mes en Desperdicios",
|
||||||
"title_accent": "En Probar Esta Tecnología",
|
"title_accent": "En Probar Esta Tecnología",
|
||||||
"subtitle": "Únete a las primeras 20 panaderías. Solo quedan 12 plazas.",
|
"subtitle": "Únete a las primeras 20 panaderías.",
|
||||||
"button": "Comenzar Ahora - Sin Tarjeta Requerida",
|
"button": "Comenzar Ahora",
|
||||||
"cta_primary": "Solicitar Plaza en el Piloto",
|
"cta_primary": "Solicitar Plaza en el Piloto",
|
||||||
"cta_secondary": "Ver Demo (2 min)",
|
"cta_secondary": "Ver Demo (2 min)",
|
||||||
"guarantee": "Tarjeta requerida. Sin cargo por 3 meses. Cancela cuando quieras."
|
"guarantee": "Tarjeta requerida. Sin cargo por 3 meses. Cancela cuando quieras."
|
||||||
|
|||||||
@@ -1118,14 +1118,26 @@
|
|||||||
"equipmentDetails": "Detalles de la Maquinaria",
|
"equipmentDetails": "Detalles de la Maquinaria",
|
||||||
"subtitle": "Equipo de Panadería",
|
"subtitle": "Equipo de Panadería",
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"name": "Nombre del Equipo",
|
||||||
|
"namePlaceholder": "Ej: Horno Principal de Producción",
|
||||||
"type": "Tipo de Equipo",
|
"type": "Tipo de Equipo",
|
||||||
"brand": "Marca/Modelo",
|
"brand": "Marca/Modelo",
|
||||||
"brandPlaceholder": "Ej: Rational SCC 101",
|
"brandPlaceholder": "Ej: Rational SCC 101",
|
||||||
"model": "Modelo",
|
"model": "Modelo",
|
||||||
|
"modelPlaceholder": "Ej: Rational SCC 101",
|
||||||
"location": "Ubicación",
|
"location": "Ubicación",
|
||||||
"locationPlaceholder": "Ej: Cocina principal",
|
"locationPlaceholder": "Ej: Cocina principal",
|
||||||
"status": "Estado",
|
"status": "Estado",
|
||||||
"purchaseDate": "Fecha de Compra"
|
"purchaseDate": "Fecha de Compra",
|
||||||
|
"installDate": "Fecha de Instalación"
|
||||||
|
},
|
||||||
|
"types": {
|
||||||
|
"oven": "Horno",
|
||||||
|
"mixer": "Amasadora",
|
||||||
|
"proofer": "Fermentadora",
|
||||||
|
"freezer": "Congelador",
|
||||||
|
"packaging": "Empaquetadora",
|
||||||
|
"other": "Otro"
|
||||||
},
|
},
|
||||||
"equipmentTypes": {
|
"equipmentTypes": {
|
||||||
"oven": "Horno",
|
"oven": "Horno",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"subtitle_option_b": "Zure eremua ezagutzen duen IAk salmentak aurreikusten ditu %92ko zehaztasunarekin. Esnatu zure plana prestekin: zer egin, zer eskatu, noiz helduko den. Aurreztu €500-2,000/hilean hondakinetan.",
|
"subtitle_option_b": "Zure eremua ezagutzen duen IAk salmentak aurreikusten ditu %92ko zehaztasunarekin. Esnatu zure plana prestekin: zer egin, zer eskatu, noiz helduko den. Aurreztu €500-2,000/hilean hondakinetan.",
|
||||||
"cta_primary": "Eskatu Pilotuko Plaza",
|
"cta_primary": "Eskatu Pilotuko Plaza",
|
||||||
"cta_secondary": "Ikusi Nola Lan Egiten Duen (2 min)",
|
"cta_secondary": "Ikusi Nola Lan Egiten Duen (2 min)",
|
||||||
"cta_demo": "Ikusi Demoa",
|
"cta_demo": "Ikusi Demo interaktiboa",
|
||||||
"social_proof": {
|
"social_proof": {
|
||||||
"bakeries": "Murriztu hondakinak %40 arte lehen hilabetatik",
|
"bakeries": "Murriztu hondakinak %40 arte lehen hilabetatik",
|
||||||
"accuracy": "Aurreikuspen %92 zehatzak (%60 sistema generikoak)",
|
"accuracy": "Aurreikuspen %92 zehatzak (%60 sistema generikoak)",
|
||||||
@@ -224,10 +224,10 @@
|
|||||||
},
|
},
|
||||||
"final_cta": {
|
"final_cta": {
|
||||||
"scarcity_badge": "🔥 20tik 12 plaza bakarrik geratzen dira",
|
"scarcity_badge": "🔥 20tik 12 plaza bakarrik geratzen dira",
|
||||||
"title": "Utzi Hilean €2,000 Galtzea Hondakinetan",
|
"title": "Utzi Hilean Hondakinetan Dirua Galtzeaz",
|
||||||
"title_accent": "Teknologia Hau Probatzen",
|
"title_accent": "Teknologia Hau Probatzen",
|
||||||
"subtitle": "Batu lehenengo 20 okindegiei. 12 plaza bakarrik geratzen dira.",
|
"subtitle": "Batu lehenengo 20 okindegiei.",
|
||||||
"button": "Hasi Orain - Txartelik Gabe",
|
"button": "Hasi Orain",
|
||||||
"cta_primary": "Eskatu Pilotuko Plaza",
|
"cta_primary": "Eskatu Pilotuko Plaza",
|
||||||
"cta_secondary": "Ikusi Demoa (2 min)",
|
"cta_secondary": "Ikusi Demoa (2 min)",
|
||||||
"guarantee": "Txartela beharrezkoa. 3 hilabetez kargurik gabe. Ezeztatu edonoiz."
|
"guarantee": "Txartela beharrezkoa. 3 hilabetez kargurik gabe. Ezeztatu edonoiz."
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
AlertTriangle,
|
||||||
Target,
|
Target,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Award,
|
Award,
|
||||||
|
|||||||
Reference in New Issue
Block a user