Imporve onboarding UI
This commit is contained in:
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '../../../ui/Button';
|
||||
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
||||
import { useCreateIngredient } from '../../../../api/hooks/inventory';
|
||||
import { useCreateIngredient, useIngredients } from '../../../../api/hooks/inventory';
|
||||
import { useImportSalesData } from '../../../../api/hooks/sales';
|
||||
import type { ProductSuggestionResponse, IngredientCreate } from '../../../../api/types/inventory';
|
||||
import { ProductType, UnitOfMeasure, IngredientCategory, ProductCategory } from '../../../../api/types/inventory';
|
||||
@@ -14,6 +14,7 @@ interface InventoryReviewStepProps {
|
||||
onComplete: (data: {
|
||||
inventoryItemsCreated: number;
|
||||
salesDataImported: boolean;
|
||||
inventoryItems?: any[];
|
||||
}) => void;
|
||||
isFirstStep: boolean;
|
||||
isLastStep: boolean;
|
||||
@@ -140,11 +141,15 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
|
||||
// API hooks
|
||||
const createIngredientMutation = useCreateIngredient();
|
||||
const importSalesMutation = useImportSalesData();
|
||||
const { data: existingIngredients } = useIngredients(tenantId);
|
||||
|
||||
// Initialize with AI suggestions
|
||||
// Initialize with AI suggestions AND existing ingredients
|
||||
useEffect(() => {
|
||||
if (initialData?.aiSuggestions) {
|
||||
const items: InventoryItemForm[] = initialData.aiSuggestions.map((suggestion, index) => ({
|
||||
// 1. Start with AI suggestions if available
|
||||
let items: InventoryItemForm[] = [];
|
||||
|
||||
if (initialData?.aiSuggestions && initialData.aiSuggestions.length > 0) {
|
||||
items = initialData.aiSuggestions.map((suggestion, index) => ({
|
||||
id: `ai-${index}-${Date.now()}`,
|
||||
name: suggestion.suggested_name,
|
||||
product_type: suggestion.product_type as ProductType,
|
||||
@@ -157,9 +162,43 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
|
||||
average_daily_sales: suggestion.sales_data.average_daily_sales,
|
||||
} : undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
// 2. Merge/Override with existing backend ingredients
|
||||
if (existingIngredients && existingIngredients.length > 0) {
|
||||
existingIngredients.forEach(ing => {
|
||||
// Check if we already have this by name (from AI)
|
||||
const existingIdx = items.findIndex(item =>
|
||||
item.name.toLowerCase() === ing.name.toLowerCase() &&
|
||||
item.product_type === ing.product_type
|
||||
);
|
||||
|
||||
if (existingIdx !== -1) {
|
||||
// Update existing item with real ID
|
||||
items[existingIdx] = {
|
||||
...items[existingIdx],
|
||||
id: ing.id,
|
||||
category: ing.category || items[existingIdx].category,
|
||||
unit_of_measure: ing.unit_of_measure as UnitOfMeasure,
|
||||
};
|
||||
} else {
|
||||
// Add as new item (this handles items created in previous sessions/attempts)
|
||||
items.push({
|
||||
id: ing.id,
|
||||
name: ing.name,
|
||||
product_type: ing.product_type,
|
||||
category: ing.category || '',
|
||||
unit_of_measure: ing.unit_of_measure as UnitOfMeasure,
|
||||
isSuggested: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (items.length > 0) {
|
||||
setInventoryItems(items);
|
||||
}
|
||||
}, [initialData]);
|
||||
}, [initialData, existingIngredients]);
|
||||
|
||||
// Filter items
|
||||
const filteredItems = inventoryItems.filter(item => {
|
||||
@@ -277,43 +316,45 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
|
||||
setFormErrors({});
|
||||
|
||||
try {
|
||||
// STEP 1: Create all inventory items in parallel
|
||||
// This MUST happen BEFORE sales import because sales records reference inventory IDs
|
||||
console.log('📦 Creating inventory items...', inventoryItems.length);
|
||||
console.log('📋 Items to create:', inventoryItems.map(item => ({
|
||||
name: item.name,
|
||||
product_type: item.product_type,
|
||||
category: item.category,
|
||||
unit_of_measure: item.unit_of_measure
|
||||
})));
|
||||
// STEP 0: Check for existing ingredients to avoid duplication
|
||||
const existingNamesAndTypes = new Set(
|
||||
existingIngredients?.map(i => `${i.name.toLowerCase()}-${i.product_type}`) || []
|
||||
);
|
||||
|
||||
const createPromises = inventoryItems.map((item, index) => {
|
||||
const itemsToCreate = inventoryItems.filter(item => {
|
||||
const key = `${item.name.toLowerCase()}-${item.product_type}`;
|
||||
return !existingNamesAndTypes.has(key);
|
||||
});
|
||||
|
||||
const existingMatches = existingIngredients?.filter(i => {
|
||||
const key = `${i.name.toLowerCase()}-${i.product_type}`;
|
||||
return inventoryItems.some(item => `${item.name.toLowerCase()}-${item.product_type}` === key);
|
||||
}) || [];
|
||||
|
||||
console.log(`📦 Inventory processing: ${itemsToCreate.length} to create, ${existingMatches.length} already exist.`);
|
||||
|
||||
// STEP 1: Create new inventory items in parallel
|
||||
const createPromises = itemsToCreate.map((item, index) => {
|
||||
const ingredientData: IngredientCreate = {
|
||||
name: item.name,
|
||||
product_type: item.product_type,
|
||||
category: item.category,
|
||||
unit_of_measure: item.unit_of_measure as UnitOfMeasure,
|
||||
// All other fields are optional now!
|
||||
};
|
||||
|
||||
console.log(`🔄 Creating ingredient ${index + 1}/${inventoryItems.length}:`, ingredientData);
|
||||
|
||||
return createIngredientMutation.mutateAsync({
|
||||
tenantId,
|
||||
ingredientData,
|
||||
}).catch(error => {
|
||||
console.error(`❌ Failed to create ingredient "${item.name}":`, error);
|
||||
console.error('Failed ingredient data:', ingredientData);
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
|
||||
const createdIngredients = await Promise.all(createPromises);
|
||||
console.log('✅ Inventory items created successfully');
|
||||
console.log('📋 Created ingredient IDs:', createdIngredients.map(ing => ({ name: ing.name, id: ing.id })));
|
||||
const newlyCreatedIngredients = await Promise.all(createPromises);
|
||||
console.log('✅ New inventory items created successfully');
|
||||
|
||||
// STEP 2: Import sales data (only if file was uploaded)
|
||||
// Now that inventory exists, sales records can reference the inventory IDs
|
||||
let salesImported = false;
|
||||
if (initialData?.uploadedFile && tenantId) {
|
||||
try {
|
||||
@@ -325,28 +366,34 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
|
||||
salesImported = true;
|
||||
console.log('✅ Sales data imported successfully');
|
||||
} catch (salesError) {
|
||||
console.error('⚠️ Sales import failed (non-blocking):', salesError);
|
||||
// Don't block onboarding if sales import fails
|
||||
// Inventory is already created, which is the critical part
|
||||
console.error('⚠️ Sales import failed (non-blocking):', salesError);
|
||||
}
|
||||
}
|
||||
|
||||
// Complete the step with metadata and inventory items
|
||||
// Map created ingredients to include their real UUIDs
|
||||
const itemsWithRealIds = createdIngredients.map(ingredient => ({
|
||||
id: ingredient.id, // Real UUID from the API
|
||||
name: ingredient.name,
|
||||
product_type: ingredient.product_type,
|
||||
category: ingredient.category,
|
||||
unit_of_measure: ingredient.unit_of_measure,
|
||||
}));
|
||||
// STEP 3: Consolidate all items (existing + newly created)
|
||||
const allItemsWithRealIds = [
|
||||
...existingMatches.map(i => ({
|
||||
id: i.id,
|
||||
name: i.name,
|
||||
product_type: i.product_type,
|
||||
category: i.category,
|
||||
unit_of_measure: i.unit_of_measure,
|
||||
})),
|
||||
...newlyCreatedIngredients.map(i => ({
|
||||
id: i.id,
|
||||
name: i.name,
|
||||
product_type: i.product_type,
|
||||
category: i.category,
|
||||
unit_of_measure: i.unit_of_measure,
|
||||
}))
|
||||
];
|
||||
|
||||
console.log('📦 Passing items with real IDs to next step:', itemsWithRealIds);
|
||||
console.log('📦 Passing items with real IDs to next step:', allItemsWithRealIds);
|
||||
|
||||
onComplete({
|
||||
inventoryItemsCreated: createdIngredients.length,
|
||||
inventoryItemsCreated: newlyCreatedIngredients.length,
|
||||
salesDataImported: salesImported,
|
||||
inventoryItems: itemsWithRealIds, // Pass the created items with real UUIDs to the next step
|
||||
inventoryItems: allItemsWithRealIds,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating inventory items:', error);
|
||||
@@ -439,21 +486,19 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
|
||||
<div className="flex gap-2 border-b border-[var(--border-color)] overflow-x-auto scrollbar-hide -mx-2 px-2">
|
||||
<button
|
||||
onClick={() => setActiveFilter('all')}
|
||||
className={`px-3 md:px-4 py-3 font-medium transition-colors relative whitespace-nowrap text-sm md:text-base ${
|
||||
activeFilter === 'all'
|
||||
? 'text-[var(--color-primary)] border-b-2 border-[var(--color-primary)] -mb-px'
|
||||
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
|
||||
}`}
|
||||
className={`px-3 md:px-4 py-3 font-medium transition-colors relative whitespace-nowrap text-sm md:text-base ${activeFilter === 'all'
|
||||
? 'text-[var(--color-primary)] border-b-2 border-[var(--color-primary)] -mb-px'
|
||||
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
|
||||
}`}
|
||||
>
|
||||
{t('inventory:filter.all', 'Todos')} ({counts.all})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveFilter('finished_products')}
|
||||
className={`px-3 md:px-4 py-3 font-medium transition-colors relative flex items-center gap-2 whitespace-nowrap text-sm md:text-base ${
|
||||
activeFilter === 'finished_products'
|
||||
? 'text-[var(--color-primary)] border-b-2 border-[var(--color-primary)] -mb-px'
|
||||
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
|
||||
}`}
|
||||
className={`px-3 md:px-4 py-3 font-medium transition-colors relative flex items-center gap-2 whitespace-nowrap text-sm md:text-base ${activeFilter === 'finished_products'
|
||||
? 'text-[var(--color-primary)] border-b-2 border-[var(--color-primary)] -mb-px'
|
||||
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
|
||||
}`}
|
||||
>
|
||||
<ShoppingBag className="w-5 h-5" />
|
||||
<span className="hidden sm:inline">{t('inventory:filter.finished_products', 'Productos Terminados')}</span>
|
||||
@@ -461,11 +506,10 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveFilter('ingredients')}
|
||||
className={`px-3 md:px-4 py-3 font-medium transition-colors relative flex items-center gap-2 whitespace-nowrap text-sm md:text-base ${
|
||||
activeFilter === 'ingredients'
|
||||
? 'text-[var(--color-primary)] border-b-2 border-[var(--color-primary)] -mb-px'
|
||||
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
|
||||
}`}
|
||||
className={`px-3 md:px-4 py-3 font-medium transition-colors relative flex items-center gap-2 whitespace-nowrap text-sm md:text-base ${activeFilter === 'ingredients'
|
||||
? 'text-[var(--color-primary)] border-b-2 border-[var(--color-primary)] -mb-px'
|
||||
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
|
||||
}`}
|
||||
>
|
||||
<Package className="w-5 h-5" />
|
||||
{t('inventory:filter.ingredients', 'Ingredientes')} ({counts.ingredients})
|
||||
@@ -492,11 +536,10 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
|
||||
<h5 className="font-medium text-[var(--text-primary)] truncate">{item.name}</h5>
|
||||
|
||||
{/* Product Type Badge */}
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${
|
||||
item.product_type === ProductType.FINISHED_PRODUCT
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-blue-100 text-blue-800'
|
||||
}`}>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${item.product_type === ProductType.FINISHED_PRODUCT
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-blue-100 text-blue-800'
|
||||
}`}>
|
||||
{item.product_type === ProductType.FINISHED_PRODUCT ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<ShoppingBag className="w-3 h-3" />
|
||||
@@ -579,11 +622,10 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFormData(prev => ({ ...prev, product_type: ProductType.INGREDIENT, category: '' }))}
|
||||
className={`relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${
|
||||
formData.product_type === ProductType.INGREDIENT
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
|
||||
: 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
|
||||
}`}
|
||||
className={`relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${formData.product_type === ProductType.INGREDIENT
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
|
||||
: 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
|
||||
}`}
|
||||
>
|
||||
{formData.product_type === ProductType.INGREDIENT && (
|
||||
<CheckCircle2 className="absolute top-2 right-2 w-5 h-5 text-[var(--color-primary)]" />
|
||||
@@ -600,11 +642,10 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFormData(prev => ({ ...prev, product_type: ProductType.FINISHED_PRODUCT, category: '' }))}
|
||||
className={`relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${
|
||||
formData.product_type === ProductType.FINISHED_PRODUCT
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
|
||||
: 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
|
||||
}`}
|
||||
className={`relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${formData.product_type === ProductType.FINISHED_PRODUCT
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
|
||||
: 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
|
||||
}`}
|
||||
>
|
||||
{formData.product_type === ProductType.FINISHED_PRODUCT && (
|
||||
<CheckCircle2 className="absolute top-2 right-2 w-5 h-5 text-[var(--color-primary)]" />
|
||||
@@ -724,11 +765,10 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFormData(prev => ({ ...prev, product_type: ProductType.INGREDIENT, category: '' }))}
|
||||
className={`relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${
|
||||
formData.product_type === ProductType.INGREDIENT
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
|
||||
: 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
|
||||
}`}
|
||||
className={`relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${formData.product_type === ProductType.INGREDIENT
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
|
||||
: 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
|
||||
}`}
|
||||
>
|
||||
{formData.product_type === ProductType.INGREDIENT && (
|
||||
<CheckCircle2 className="absolute top-2 right-2 w-5 h-5 text-[var(--color-primary)]" />
|
||||
@@ -745,11 +785,10 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFormData(prev => ({ ...prev, product_type: ProductType.FINISHED_PRODUCT, category: '' }))}
|
||||
className={`relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${
|
||||
formData.product_type === ProductType.FINISHED_PRODUCT
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
|
||||
: 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
|
||||
}`}
|
||||
className={`relative p-3 sm:p-4 border-2 rounded-lg text-left transition-all ${formData.product_type === ProductType.FINISHED_PRODUCT
|
||||
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/30 shadow-lg ring-2 ring-[var(--color-primary)]/50'
|
||||
: 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]'
|
||||
}`}
|
||||
>
|
||||
{formData.product_type === ProductType.FINISHED_PRODUCT && (
|
||||
<CheckCircle2 className="absolute top-2 right-2 w-5 h-5 text-[var(--color-primary)]" />
|
||||
@@ -862,9 +901,9 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
|
||||
{isSubmitting
|
||||
? t('common:saving', 'Guardando...')
|
||||
: <>
|
||||
<span className="hidden md:inline">{t('common:continue', 'Continuar')} ({inventoryItems.length} {t('common:items', 'productos')}) →</span>
|
||||
<span className="md:hidden">{t('common:continue', 'Continuar')} ({inventoryItems.length}) →</span>
|
||||
</>
|
||||
<span className="hidden md:inline">{t('common:continue', 'Continuar')} ({inventoryItems.length} {t('common:items', 'productos')}) →</span>
|
||||
<span className="md:hidden">{t('common:continue', 'Continuar')} ({inventoryItems.length}) →</span>
|
||||
</>
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user