Improve onboarding flow
This commit is contained in:
@@ -2,8 +2,9 @@
|
||||
* Onboarding React Query hooks
|
||||
*/
|
||||
import { useMutation, useQuery, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { onboardingService } from '../services/onboarding';
|
||||
import { UserProgress, UpdateStepRequest } from '../types/onboarding';
|
||||
import { UserProgress, UpdateStepRequest, StepDraftResponse } from '../types/onboarding';
|
||||
import { ApiError } from '../client';
|
||||
|
||||
// Query Keys
|
||||
@@ -12,6 +13,7 @@ export const onboardingKeys = {
|
||||
progress: (userId: string) => [...onboardingKeys.all, 'progress', userId] as const,
|
||||
steps: () => [...onboardingKeys.all, 'steps'] as const,
|
||||
stepDetail: (stepName: string) => [...onboardingKeys.steps(), stepName] as const,
|
||||
stepDraft: (stepName: string) => [...onboardingKeys.all, 'draft', stepName] as const,
|
||||
} as const;
|
||||
|
||||
// Queries
|
||||
@@ -122,8 +124,6 @@ export const useMarkStepCompleted = (
|
||||
// Invalidate queries on error to ensure we get fresh data
|
||||
queryClient.invalidateQueries({ queryKey: onboardingKeys.progress(userId) });
|
||||
},
|
||||
// Prevent duplicate requests by using the step name as a mutation key
|
||||
mutationKey: (variables) => ['markStepCompleted', variables?.userId, variables?.stepName],
|
||||
...options,
|
||||
});
|
||||
};
|
||||
@@ -132,7 +132,7 @@ export const useResetProgress = (
|
||||
options?: UseMutationOptions<UserProgress, ApiError, string>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
return useMutation<UserProgress, ApiError, string>({
|
||||
mutationFn: (userId: string) => onboardingService.resetProgress(userId),
|
||||
onSuccess: (data, userId) => {
|
||||
@@ -141,4 +141,110 @@ export const useResetProgress = (
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// Draft Queries and Mutations
|
||||
|
||||
/**
|
||||
* Query hook to get draft data for a specific step
|
||||
*/
|
||||
export const useStepDraft = (
|
||||
stepName: string,
|
||||
options?: Omit<UseQueryOptions<StepDraftResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) => {
|
||||
return useQuery<StepDraftResponse, ApiError>({
|
||||
queryKey: onboardingKeys.stepDraft(stepName),
|
||||
queryFn: () => onboardingService.getStepDraft(stepName),
|
||||
enabled: !!stepName,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Mutation hook to save draft data for a step
|
||||
*/
|
||||
export const useSaveStepDraft = (
|
||||
options?: UseMutationOptions<{ success: boolean }, ApiError, { stepName: string; draftData: Record<string, any> }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ success: boolean }, ApiError, { stepName: string; draftData: Record<string, any> }>({
|
||||
mutationFn: ({ stepName, draftData }) => onboardingService.saveStepDraft(stepName, draftData),
|
||||
onSuccess: (_, { stepName }) => {
|
||||
// Invalidate the draft query to get fresh data
|
||||
queryClient.invalidateQueries({ queryKey: onboardingKeys.stepDraft(stepName) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Mutation hook to delete draft data for a step
|
||||
*/
|
||||
export const useDeleteStepDraft = (
|
||||
options?: UseMutationOptions<{ success: boolean }, ApiError, string>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ success: boolean }, ApiError, string>({
|
||||
mutationFn: (stepName: string) => onboardingService.deleteStepDraft(stepName),
|
||||
onSuccess: (_, stepName) => {
|
||||
// Invalidate the draft query
|
||||
queryClient.invalidateQueries({ queryKey: onboardingKeys.stepDraft(stepName) });
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook with debounced draft auto-save functionality.
|
||||
* Automatically saves draft data after a delay when form data changes.
|
||||
*/
|
||||
export const useAutoSaveDraft = (stepName: string, debounceMs: number = 2000) => {
|
||||
const saveStepDraft = useSaveStepDraft();
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const saveDraft = useCallback(
|
||||
(draftData: Record<string, any>) => {
|
||||
// Clear any existing timeout
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
// Set a new timeout for debounced save
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
saveStepDraft.mutate({ stepName, draftData });
|
||||
}, debounceMs);
|
||||
},
|
||||
[stepName, debounceMs, saveStepDraft]
|
||||
);
|
||||
|
||||
const saveDraftImmediately = useCallback(
|
||||
(draftData: Record<string, any>) => {
|
||||
// Clear any pending debounced save
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
// Save immediately
|
||||
saveStepDraft.mutate({ stepName, draftData });
|
||||
},
|
||||
[stepName, saveStepDraft]
|
||||
);
|
||||
|
||||
const cancelPendingSave = useCallback(() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
saveDraft,
|
||||
saveDraftImmediately,
|
||||
cancelPendingSave,
|
||||
isSaving: saveStepDraft.isPending,
|
||||
isError: saveStepDraft.isError,
|
||||
error: saveStepDraft.error,
|
||||
};
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
* Frontend and backend step names now match directly!
|
||||
*/
|
||||
import { apiClient } from '../client';
|
||||
import { UserProgress, UpdateStepRequest } from '../types/onboarding';
|
||||
import { UserProgress, UpdateStepRequest, SaveStepDraftRequest, StepDraftResponse } from '../types/onboarding';
|
||||
|
||||
// Backend onboarding steps (full list from backend - UPDATED to match refactored flow)
|
||||
// NOTE: poi-detection removed - now happens automatically in background during tenant registration
|
||||
@@ -155,7 +155,7 @@ export class OnboardingService {
|
||||
async getResumeStep(): Promise<{ stepId: string; stepIndex: number }> {
|
||||
try {
|
||||
const progress = await this.getUserProgress('');
|
||||
|
||||
|
||||
// If fully completed, go to completion
|
||||
if (progress.fully_completed) {
|
||||
return { stepId: 'completion', stepIndex: FRONTEND_STEP_ORDER.indexOf('completion') };
|
||||
@@ -163,10 +163,10 @@ export class OnboardingService {
|
||||
|
||||
// Get the current step from backend
|
||||
const currentStep = progress.current_step;
|
||||
|
||||
|
||||
// If current step is user_registered, start from setup
|
||||
const resumeStep = currentStep === 'user_registered' ? 'setup' : currentStep;
|
||||
|
||||
|
||||
// Find the step index in our frontend order
|
||||
let stepIndex = FRONTEND_STEP_ORDER.indexOf(resumeStep);
|
||||
if (stepIndex === -1) {
|
||||
@@ -179,6 +179,66 @@ export class OnboardingService {
|
||||
return { stepId: 'setup', stepIndex: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save in-progress step data without marking the step as complete.
|
||||
* This allows users to save their work and resume later.
|
||||
*/
|
||||
async saveStepDraft(stepName: string, draftData: Record<string, any>): Promise<{ success: boolean }> {
|
||||
const requestBody: SaveStepDraftRequest = {
|
||||
step_name: stepName,
|
||||
draft_data: draftData,
|
||||
};
|
||||
|
||||
console.log(`💾 Saving draft for step "${stepName}":`, draftData);
|
||||
|
||||
try {
|
||||
const response = await apiClient.put<{ success: boolean }>(`${this.baseUrl}/step-draft`, requestBody);
|
||||
console.log(`✅ Draft saved for step "${stepName}"`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`❌ Error saving draft for step "${stepName}":`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get saved draft data for a specific step.
|
||||
* Returns null if no draft exists or the step is already completed.
|
||||
*/
|
||||
async getStepDraft(stepName: string): Promise<StepDraftResponse> {
|
||||
console.log(`📖 Getting draft for step "${stepName}"`);
|
||||
|
||||
try {
|
||||
const response = await apiClient.get<StepDraftResponse>(`${this.baseUrl}/step-draft/${stepName}`);
|
||||
if (response.draft_data) {
|
||||
console.log(`✅ Found draft for step "${stepName}":`, response.draft_data);
|
||||
} else {
|
||||
console.log(`ℹ️ No draft found for step "${stepName}"`);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`❌ Error getting draft for step "${stepName}":`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete saved draft data for a specific step.
|
||||
* Called after step is completed to clean up draft data.
|
||||
*/
|
||||
async deleteStepDraft(stepName: string): Promise<{ success: boolean }> {
|
||||
console.log(`🗑️ Deleting draft for step "${stepName}"`);
|
||||
|
||||
try {
|
||||
const response = await apiClient.delete<{ success: boolean }>(`${this.baseUrl}/step-draft/${stepName}`);
|
||||
console.log(`✅ Draft deleted for step "${stepName}"`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`❌ Error deleting draft for step "${stepName}":`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const onboardingService = new OnboardingService();
|
||||
@@ -9,6 +9,27 @@ export interface OnboardingStepStatus {
|
||||
data?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wizard context state extracted from backend step data.
|
||||
* Used to restore frontend WizardContext on session restore.
|
||||
*/
|
||||
export interface WizardContextState {
|
||||
bakery_type: string | null;
|
||||
tenant_id: string | null;
|
||||
subscription_tier: string | null;
|
||||
ai_analysis_complete: boolean;
|
||||
inventory_review_completed: boolean;
|
||||
stock_entry_completed: boolean;
|
||||
categorization_completed: boolean;
|
||||
suppliers_completed: boolean;
|
||||
inventory_setup_completed: boolean;
|
||||
recipes_completed: boolean;
|
||||
quality_completed: boolean;
|
||||
team_completed: boolean;
|
||||
child_tenants_completed: boolean;
|
||||
ml_training_complete: boolean;
|
||||
}
|
||||
|
||||
export interface UserProgress {
|
||||
user_id: string;
|
||||
steps: OnboardingStepStatus[];
|
||||
@@ -17,10 +38,23 @@ export interface UserProgress {
|
||||
completion_percentage: number;
|
||||
fully_completed: boolean;
|
||||
last_updated: string;
|
||||
context_state?: WizardContextState;
|
||||
}
|
||||
|
||||
export interface UpdateStepRequest {
|
||||
step_name: string;
|
||||
completed: boolean;
|
||||
data?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface SaveStepDraftRequest {
|
||||
step_name: string;
|
||||
draft_data: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface StepDraftResponse {
|
||||
step_name: string;
|
||||
draft_data: Record<string, any> | null;
|
||||
draft_saved_at?: string;
|
||||
demo_mode?: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user