Improve onboarding flow
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user