Improve onboarding flow

This commit is contained in:
Urtzi Alfaro
2026-01-04 21:37:44 +01:00
parent 47ccea4900
commit 429e724a2c
13 changed files with 1052 additions and 213 deletions

View File

@@ -6,7 +6,7 @@ import Card from '../../../ui/Card/Card';
export interface BakeryTypeSelectionStepProps {
onUpdate?: (data: { bakeryType: string }) => void;
onComplete?: () => void;
onComplete?: (data?: { bakeryType: string }) => void;
initialData?: {
bakeryType?: string;
};
@@ -113,7 +113,7 @@ export const BakeryTypeSelectionStep: React.FC<BakeryTypeSelectionStepProps> = (
const handleContinue = () => {
if (selectedType) {
onComplete?.();
onComplete?.({ bakeryType: selectedType });
}
};

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Package, Salad, AlertCircle, ArrowRight, ArrowLeft, CheckCircle } from 'lucide-react';
import Button from '../../../ui/Button/Button';
@@ -6,8 +6,11 @@ import Card from '../../../ui/Card/Card';
import Input from '../../../ui/Input/Input';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAddStock, useStock } from '../../../../api/hooks/inventory';
import { useAutoSaveDraft, useStepDraft, useDeleteStepDraft } from '../../../../api/hooks/onboarding';
import InfoCard from '../../../ui/InfoCard';
const STEP_NAME = 'initial-stock-entry';
export interface ProductWithStock {
id: string;
name: string;
@@ -55,6 +58,49 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
}));
});
// Draft auto-save hooks
const { data: draftData, isLoading: isLoadingDraft } = useStepDraft(STEP_NAME);
const { saveDraft, cancelPendingSave } = useAutoSaveDraft(STEP_NAME, 2000);
const deleteStepDraft = useDeleteStepDraft();
const initializedRef = useRef(false);
const draftCheckedRef = useRef(false);
const stepCompletedRef = useRef(false); // Track if step has been completed to prevent draft saves after completion
// Restore draft data on mount (only once)
useEffect(() => {
if (isLoadingDraft || initializedRef.current) return;
initializedRef.current = true;
draftCheckedRef.current = true;
if (draftData?.draft_data?.products && Array.isArray(draftData.draft_data.products)) {
// Restore products with stock values from draft
setProducts(draftData.draft_data.products);
console.log('✅ Restored initial-stock-entry draft:', draftData.draft_data.products.length, 'products');
}
}, [isLoadingDraft, draftData]);
// Auto-save draft when products change
useEffect(() => {
// CRITICAL: Do not save drafts after the step has been completed
// This prevents overwriting completed status with draft data
if (stepCompletedRef.current) {
console.log('⏸️ Skipping draft save - step already completed');
return;
}
if (!draftCheckedRef.current) return;
// Only save if we have products with stock values
const productsWithValues = products.filter(p => p.initialStock !== undefined);
if (productsWithValues.length === 0) return;
const draftPayload = {
products,
};
saveDraft(draftPayload);
}, [products, saveDraft]);
// Merge existing stock from backend on mount
useEffect(() => {
if (!stockData?.items || products.length === 0) return;
@@ -138,7 +184,7 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
if (stockEntriesToSync.length > 0) {
// Create or update stock entries
// Note: useAddStock currently handles creation/initial set.
// Note: useAddStock currently handles creation/initial set.
// If the backend requires a different endpoint for updates, this might need adjustment.
// For onboarding, we assume addStock is a "set-and-forget" for initial levels.
const stockPromises = stockEntriesToSync.map(product =>
@@ -156,6 +202,19 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
console.log(`✅ Synced ${stockEntriesToSync.length} stock entries successfully`);
}
// CRITICAL: Mark step as completed BEFORE canceling auto-save
// This prevents any pending auto-save from overwriting the completion
stepCompletedRef.current = true;
// Cancel any pending auto-save and delete draft on successful completion
cancelPendingSave();
try {
await deleteStepDraft.mutateAsync(STEP_NAME);
console.log('✅ Deleted initial-stock-entry draft after successful completion');
} catch {
// Ignore errors when deleting draft
}
onComplete?.();
} catch (error) {
console.error('Error syncing stock entries:', error);
@@ -255,7 +314,7 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
{hasStock && <CheckCircle className="w-4 h-4 text-[var(--color-success)]" />}
</div>
{product.category && (
<div className="text-xs text-[var(--text-secondary)]">{product.category}</div>
<div className="text-xs text-[var(--text-secondary)] uppercase">{t(`inventory:enums.ingredient_category.${product.category}`, product.category)}</div>
)}
</div>
<div className="flex items-center gap-2">
@@ -269,7 +328,7 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
className="w-20 sm:w-24 text-right min-h-[44px]"
/>
<span className="text-sm text-[var(--text-secondary)] whitespace-nowrap">
{product.unit || 'kg'}
{t(`inventory:enums.unit_of_measure.${product.unit}`, product.unit || 'kg')}
</span>
</div>
</div>
@@ -306,7 +365,7 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
{hasStock && <CheckCircle className="w-4 h-4 text-[var(--color-info)]" />}
</div>
{product.category && (
<div className="text-xs text-[var(--text-secondary)]">{product.category}</div>
<div className="text-xs text-[var(--text-secondary)] uppercase">{t(`inventory:enums.ingredient_category.${product.category}`, product.category)}</div>
)}
</div>
<div className="flex items-center gap-2">
@@ -320,7 +379,7 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
className="w-24 text-right"
/>
<span className="text-sm text-[var(--text-secondary)] whitespace-nowrap">
{product.unit || t('common:units', 'unidades')}
{t(`inventory:enums.unit_of_measure.${product.unit}`, product.unit || t('common:units', 'unidades'))}
</span>
</div>
</div>

View File

@@ -1,13 +1,16 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '../../../ui/Button';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useCreateIngredient, useBulkCreateIngredients, useIngredients } from '../../../../api/hooks/inventory';
import { useImportSalesData } from '../../../../api/hooks/sales';
import { useAutoSaveDraft, useStepDraft, useDeleteStepDraft } from '../../../../api/hooks/onboarding';
import type { ProductSuggestionResponse, IngredientCreate } from '../../../../api/types/inventory';
import { ProductType, UnitOfMeasure, IngredientCategory, ProductCategory } from '../../../../api/types/inventory';
import { Package, ShoppingBag, AlertCircle, CheckCircle2, Edit2, Trash2, Plus, Sparkles } from 'lucide-react';
const STEP_NAME = 'inventory-review';
interface InventoryReviewStepProps {
onNext: () => void;
onPrevious: () => void;
@@ -144,11 +147,35 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
const importSalesMutation = useImportSalesData();
const { data: existingIngredients } = useIngredients(tenantId);
// Initialize with AI suggestions AND existing ingredients
// Draft auto-save hooks
const { data: draftData, isLoading: isLoadingDraft } = useStepDraft(STEP_NAME);
const { saveDraft, cancelPendingSave } = useAutoSaveDraft(STEP_NAME, 2000);
const deleteStepDraft = useDeleteStepDraft();
const initializedRef = useRef(false);
const draftCheckedRef = useRef(false);
const stepCompletedRef = useRef(false); // Track if step has been completed to prevent draft saves after completion
// Initialize with AI suggestions, existing ingredients, OR draft data (priority: draft > existing > AI)
useEffect(() => {
// 1. Start with AI suggestions if available
// Wait for draft loading to complete before initializing
if (isLoadingDraft || initializedRef.current) return;
// Mark as initialized to prevent re-running
initializedRef.current = true;
draftCheckedRef.current = true;
// Check if we have draft data to restore
if (draftData?.draft_data?.inventoryItems && Array.isArray(draftData.draft_data.inventoryItems)) {
// Restore from draft - this takes priority
setInventoryItems(draftData.draft_data.inventoryItems);
console.log('✅ Restored inventory-review draft:', draftData.draft_data.inventoryItems.length, 'items');
return;
}
// No draft data - initialize from AI suggestions and existing ingredients
let items: InventoryItemForm[] = [];
// 1. Start with AI suggestions if available
if (initialData?.aiSuggestions && initialData.aiSuggestions.length > 0) {
items = initialData.aiSuggestions.map((suggestion, index) => ({
id: `ai-${index}-${Date.now()}`,
@@ -199,7 +226,26 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
if (items.length > 0) {
setInventoryItems(items);
}
}, [initialData, existingIngredients]);
}, [isLoadingDraft, draftData, initialData, existingIngredients]);
// Auto-save draft when inventoryItems change
useEffect(() => {
// CRITICAL: Do not save drafts after the step has been completed
// This prevents overwriting completed status with draft data
if (stepCompletedRef.current) {
console.log('⏸️ Skipping draft save - step already completed');
return;
}
// Only save after initialization is complete and we have items
if (!draftCheckedRef.current || inventoryItems.length === 0) return;
const draftPayload = {
inventoryItems,
};
saveDraft(draftPayload);
}, [inventoryItems, saveDraft]);
// Filter items
const filteredItems = inventoryItems.filter(item => {
@@ -426,6 +472,19 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
console.log('📦 Passing items with real IDs to next step:', allItemsWithRealIds);
// CRITICAL: Mark step as completed BEFORE canceling auto-save
// This prevents any pending auto-save from overwriting the completion
stepCompletedRef.current = true;
// Cancel any pending auto-save and delete draft on successful completion
cancelPendingSave();
try {
await deleteStepDraft.mutateAsync(STEP_NAME);
console.log('✅ Deleted inventory-review draft after successful completion');
} catch {
// Ignore errors when deleting draft
}
onComplete({
inventoryItemsCreated: newlyCreatedIngredients.length,
salesDataImported: salesImported,

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '../../../ui/Button';
import { useCurrentTenant } from '../../../../stores/tenant.store';
@@ -6,12 +6,15 @@ import { useTenantCurrency } from '../../../../hooks/useTenantCurrency';
import { useCreateIngredient, useClassifyBatch, useAddStock } from '../../../../api/hooks/inventory';
import { useValidateImportFile, useImportSalesData } from '../../../../api/hooks/sales';
import { useSuppliers } from '../../../../api/hooks/suppliers';
import { useAutoSaveDraft, useStepDraft, useDeleteStepDraft } from '../../../../api/hooks/onboarding';
import type { ImportValidationResponse } from '../../../../api/types/dataImport';
import type { ProductSuggestionResponse, StockCreate, StockResponse } from '../../../../api/types/inventory';
import { ProductionStage } from '../../../../api/types/inventory';
import { useAuth } from '../../../../contexts/AuthContext';
import { BatchAddIngredientsModal } from '../../inventory/BatchAddIngredientsModal';
const STEP_NAME = 'upload-sales-data';
interface UploadSalesDataStepProps {
onNext: () => void;
onPrevious: () => void;
@@ -115,6 +118,65 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
const [stockErrors, setStockErrors] = useState<Record<string, string>>({});
const [ingredientStocks, setIngredientStocks] = useState<Record<string, StockResponse[]>>({});
// Draft auto-save hooks
const { data: draftData, isLoading: isLoadingDraft } = useStepDraft(STEP_NAME);
const { saveDraft, cancelPendingSave } = useAutoSaveDraft(STEP_NAME, 2000);
const deleteStepDraft = useDeleteStepDraft();
const initializedRef = useRef(false);
const draftCheckedRef = useRef(false);
const stepCompletedRef = useRef(false); // Track if step has been completed to prevent draft saves after completion
// Restore draft data on mount (only once)
useEffect(() => {
if (isLoadingDraft || initializedRef.current) return;
initializedRef.current = true;
draftCheckedRef.current = true;
if (draftData?.draft_data) {
const draft = draftData.draft_data;
// Restore inventory items
if (draft.inventoryItems && Array.isArray(draft.inventoryItems)) {
setInventoryItems(draft.inventoryItems);
setShowInventoryStep(true);
console.log('✅ Restored upload-sales-data draft:', draft.inventoryItems.length, 'items');
}
// Restore ingredient stocks
if (draft.ingredientStocks && typeof draft.ingredientStocks === 'object') {
setIngredientStocks(draft.ingredientStocks);
}
// Restore validation result metadata (not the file itself)
if (draft.validationResult) {
setValidationResult(draft.validationResult);
}
}
}, [isLoadingDraft, draftData]);
// Auto-save draft when inventoryItems or ingredientStocks change
useEffect(() => {
// CRITICAL: Do not save drafts after the step has been completed
// This prevents overwriting completed status with draft data
if (stepCompletedRef.current) {
console.log('⏸️ Skipping draft save - step already completed');
return;
}
if (!draftCheckedRef.current) return;
// Only save if we have items to preserve
if (inventoryItems.length === 0 && Object.keys(ingredientStocks).length === 0) return;
const draftPayload = {
inventoryItems,
ingredientStocks,
validationResult,
};
saveDraft(draftPayload);
}, [inventoryItems, ingredientStocks, validationResult, saveDraft]);
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
@@ -572,6 +634,19 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
setProgressState(null);
// CRITICAL: Mark step as completed BEFORE canceling auto-save
// This prevents any pending auto-save from overwriting the completion
stepCompletedRef.current = true;
// Cancel any pending auto-save and delete draft on successful completion
cancelPendingSave();
try {
await deleteStepDraft.mutateAsync(STEP_NAME);
console.log('✅ Deleted upload-sales-data draft after successful completion');
} catch {
// Ignore errors when deleting draft
}
// Complete step
onComplete({
createdIngredients,