diff --git a/frontend/src/components/domain/onboarding/UnifiedOnboardingWizard.tsx b/frontend/src/components/domain/onboarding/UnifiedOnboardingWizard.tsx index 18410d35..dfb6cd90 100644 --- a/frontend/src/components/domain/onboarding/UnifiedOnboardingWizard.tsx +++ b/frontend/src/components/domain/onboarding/UnifiedOnboardingWizard.tsx @@ -108,22 +108,28 @@ const OnboardingWizardContent: React.FC = () => { isConditional: true, condition: (ctx) => ctx.state.dataSource === 'ai-assisted' && ctx.state.categorizationCompleted, }, - // Phase 2b: Core Data Entry - { - id: 'suppliers-setup', - title: t('onboarding:steps.suppliers.title', 'Proveedores'), - description: t('onboarding:steps.suppliers.description', 'Configura tus proveedores'), - component: SuppliersSetupStep, - isConditional: true, - condition: (ctx) => ctx.state.dataSource !== null, - }, + // Phase 2b: Core Data Entry (Manual Path) + // IMPORTANT: Inventory must come BEFORE suppliers so suppliers can associate products { id: 'inventory-setup', title: t('onboarding:steps.inventory.title', 'Inventario'), description: t('onboarding:steps.inventory.description', 'Productos e ingredientes'), component: InventorySetupStep, isConditional: true, - condition: (ctx) => ctx.state.dataSource !== null, + condition: (ctx) => + // Only show for manual path (AI path creates inventory earlier) + ctx.state.dataSource === 'manual', + }, + { + id: 'suppliers-setup', + title: t('onboarding:steps.suppliers.title', 'Proveedores'), + description: t('onboarding:steps.suppliers.description', 'Configura tus proveedores'), + component: SuppliersSetupStep, + isConditional: true, + condition: (ctx) => + // Show after inventory exists (either from AI or manual path) + (ctx.state.dataSource === 'ai-assisted' && ctx.state.stockEntryCompleted) || + (ctx.state.dataSource === 'manual' && ctx.state.inventoryCompleted), }, { id: 'recipes-setup', diff --git a/frontend/src/components/domain/onboarding/steps/UploadSalesDataStep.tsx b/frontend/src/components/domain/onboarding/steps/UploadSalesDataStep.tsx index e6a38c65..51451f76 100644 --- a/frontend/src/components/domain/onboarding/steps/UploadSalesDataStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/UploadSalesDataStep.tsx @@ -432,22 +432,22 @@ export const UploadSalesDataStep: React.FC = ({

{item.category} • Unidad: {item.unit_of_measure}

-
- +
+ Confianza: {Math.round(item.confidence_score * 100)}% {item.requires_refrigeration && ( - + Requiere refrigeración )} {item.requires_freezing && ( - + Requiere congelación )} {item.is_seasonal && ( - + Producto estacional )} diff --git a/frontend/src/components/domain/setup-wizard/steps/SupplierProductManager.tsx b/frontend/src/components/domain/setup-wizard/steps/SupplierProductManager.tsx new file mode 100644 index 00000000..d3787813 --- /dev/null +++ b/frontend/src/components/domain/setup-wizard/steps/SupplierProductManager.tsx @@ -0,0 +1,405 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + useSupplierPriceLists, + useCreateSupplierPriceList, + useUpdateSupplierPriceList, + useDeleteSupplierPriceList +} from '../../../../api/hooks/suppliers'; +import { useIngredients } from '../../../../api/hooks/inventory'; +import type { SupplierPriceListCreate, SupplierPriceListResponse } from '../../../../api/types/suppliers'; + +interface SupplierProductManagerProps { + tenantId: string; + supplierId: string; + supplierName: string; +} + +interface ProductFormData { + inventory_product_id: string; + product_name?: string; + unit_price: string; + unit_of_measure: string; + minimum_order_quantity: string; +} + +export const SupplierProductManager: React.FC = ({ + tenantId, + supplierId, + supplierName +}) => { + const { t } = useTranslation(); + + // Fetch existing price lists for this supplier + const { data: priceLists = [], isLoading: priceListsLoading } = useSupplierPriceLists( + tenantId, + supplierId, + true // only active + ); + + // Fetch all inventory items + const { data: inventoryItems = [], isLoading: inventoryLoading } = useIngredients(tenantId); + + // Mutations + const createPriceListMutation = useCreateSupplierPriceList(); + const updatePriceListMutation = useUpdateSupplierPriceList(); + const deletePriceListMutation = useDeleteSupplierPriceList(); + + // UI State + const [isExpanded, setIsExpanded] = useState(false); + const [isAdding, setIsAdding] = useState(false); + const [editingId, setEditingId] = useState(null); + const [selectedProducts, setSelectedProducts] = useState([]); + const [productForms, setProductForms] = useState>({}); + const [errors, setErrors] = useState>({}); + + // Filter available products (not already in price list) + const availableProducts = inventoryItems.filter( + item => !priceLists.some(pl => pl.inventory_product_id === item.id) + ); + + const handleToggleProduct = (productId: string) => { + setSelectedProducts(prev => { + if (prev.includes(productId)) { + // Remove + const newSelected = prev.filter(id => id !== productId); + const newForms = { ...productForms }; + delete newForms[productId]; + setProductForms(newForms); + return newSelected; + } else { + // Add with default form + const product = inventoryItems.find(p => p.id === productId); + setProductForms(prev => ({ + ...prev, + [productId]: { + inventory_product_id: productId, + product_name: product?.name || '', + unit_price: '', + unit_of_measure: product?.unit_of_measure || 'kg', + minimum_order_quantity: '1' + } + })); + return [...prev, productId]; + } + }); + }; + + const handleUpdateForm = (productId: string, field: string, value: string) => { + setProductForms(prev => ({ + ...prev, + [productId]: { + ...prev[productId], + [field]: value + } + })); + }; + + const validateForms = (): boolean => { + const newErrors: Record = {}; + + selectedProducts.forEach(productId => { + const form = productForms[productId]; + if (!form.unit_price || parseFloat(form.unit_price) <= 0) { + newErrors[`${productId}_price`] = 'Price must be greater than 0'; + } + if (!form.unit_of_measure) { + newErrors[`${productId}_unit`] = 'Unit of measure is required'; + } + }); + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSaveProducts = async () => { + if (!validateForms()) return; + + try { + const promises = selectedProducts.map(productId => { + const form = productForms[productId]; + const priceListData: SupplierPriceListCreate = { + inventory_product_id: form.inventory_product_id, + unit_price: parseFloat(form.unit_price), + unit_of_measure: form.unit_of_measure, + minimum_order_quantity: form.minimum_order_quantity ? parseInt(form.minimum_order_quantity) : 1, + price_per_unit: parseFloat(form.unit_price), // Same as unit_price for now + is_active: true + }; + + return createPriceListMutation.mutateAsync({ + tenantId, + supplierId, + priceListData + }); + }); + + await Promise.all(promises); + + // Reset form + setSelectedProducts([]); + setProductForms({}); + setIsAdding(false); + } catch (error) { + console.error('Error saving products:', error); + } + }; + + const handleDeleteProduct = async (priceListId: string) => { + if (!window.confirm(t('common:confirm_delete', 'Are you sure?'))) return; + + try { + await deletePriceListMutation.mutateAsync({ + tenantId, + supplierId, + priceListId + }); + } catch (error) { + console.error('Error deleting product:', error); + } + }; + + const getProductName = (inventoryProductId: string) => { + const product = inventoryItems.find(p => p.id === inventoryProductId); + return product?.name || inventoryProductId; + }; + + if (!isExpanded) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Header */} +
+
+ + + + + {t('setup_wizard:suppliers.products_for', 'Products for {{name}}', { name: supplierName })} + +
+ +
+ + {/* Existing products */} + {priceLists.length > 0 && ( +
+ {priceLists.map((priceList) => ( +
+
+ + {getProductName(priceList.inventory_product_id)} + + + €{priceList.unit_price.toFixed(2)}/{priceList.unit_of_measure} + + {priceList.minimum_order_quantity && priceList.minimum_order_quantity > 1 && ( + + (Min: {priceList.minimum_order_quantity}) + + )} +
+ +
+ ))} +
+ )} + + {/* Add products section */} + {!isAdding && ( + + )} + + {/* Product selection form */} + {isAdding && ( +
+
+

+ {t('setup_wizard:suppliers.select_products', 'Select Products')} +

+ +
+ + {/* Product checkboxes */} +
+ {availableProducts.map(product => ( +
+ + + {/* Price form for selected products */} + {selectedProducts.includes(product.id) && productForms[product.id] && ( +
+
+ + handleUpdateForm(product.id, 'unit_price', e.target.value)} + className={`w-full px-2 py-1 text-sm bg-[var(--bg-primary)] border ${ + errors[`${product.id}_price`] ? 'border-[var(--color-error)]' : 'border-[var(--border-secondary)]' + } rounded focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)] text-[var(--text-primary)]`} + placeholder="0.00" + /> +
+
+ + +
+
+ + handleUpdateForm(product.id, 'minimum_order_quantity', e.target.value)} + className="w-full px-2 py-1 text-sm bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)] text-[var(--text-primary)]" + /> +
+
+ )} +
+ ))} +
+ + {/* Actions */} + {selectedProducts.length > 0 && ( +
+ + +
+ )} +
+ )} + + {/* Warning if no products */} + {priceLists.length === 0 && !isAdding && ( +
+ ⚠️ {t('setup_wizard:suppliers.no_products_warning', 'Add at least 1 product to enable automatic purchase orders')} +
+ )} +
+ ); +}; diff --git a/frontend/src/components/domain/setup-wizard/steps/SuppliersSetupStep.tsx b/frontend/src/components/domain/setup-wizard/steps/SuppliersSetupStep.tsx index cf8f1cce..8dfdc5d0 100644 --- a/frontend/src/components/domain/setup-wizard/steps/SuppliersSetupStep.tsx +++ b/frontend/src/components/domain/setup-wizard/steps/SuppliersSetupStep.tsx @@ -6,6 +6,7 @@ import { useCurrentTenant } from '../../../../stores/tenant.store'; import { useAuthUser } from '../../../../stores/auth.store'; import { SupplierType } from '../../../../api/types/suppliers'; import type { SupplierCreate, SupplierUpdate } from '../../../../api/types/suppliers'; +import { SupplierProductManager } from './SupplierProductManager'; export const SuppliersSetupStep: React.FC = ({ onNext, @@ -195,65 +196,75 @@ export const SuppliersSetupStep: React.FC = ({ {suppliers.map((supplier) => (
-
-
-
{supplier.name}
- - {supplierTypeOptions.find(opt => opt.value === supplier.supplier_type)?.label || supplier.supplier_type} - + {/* Supplier Header */} +
+
+
+
{supplier.name}
+ + {supplierTypeOptions.find(opt => opt.value === supplier.supplier_type)?.label || supplier.supplier_type} + +
+
+ {supplier.contact_person && ( + + + + + {supplier.contact_person} + + )} + {supplier.phone && ( + + + + + {supplier.phone} + + )} + {supplier.email && ( + + + + + {supplier.email} + + )} +
-
- {supplier.contact_person && ( - - - - - {supplier.contact_person} - - )} - {supplier.phone && ( - - - - - {supplier.phone} - - )} - {supplier.email && ( - - - - - {supplier.email} - - )} +
+ +
-
- - -
+ + {/* Product Management */} +
))}