From cbe19a3cd1e27beeed098f5285de2d4b7c6d80fc Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Sun, 9 Nov 2025 09:22:08 +0100 Subject: [PATCH] IMPORVE ONBOARDING STEPS --- ARCHITECTURE_ANALYSIS.md | 877 ++++++++++++++++++ REFACTORING_ROADMAP.md | 315 +++++++ frontend/src/api/services/onboarding.ts | 42 +- frontend/src/api/types/inventory.ts | 15 +- .../domain/onboarding/OnboardingWizard.tsx | 567 ----------- .../onboarding/UnifiedOnboardingWizard.tsx | 72 +- .../onboarding/context/WizardContext.tsx | 25 +- .../src/components/domain/onboarding/index.ts | 3 +- .../steps/BakeryTypeSelectionStep.tsx | 107 ++- .../onboarding/steps/FileUploadStep.tsx | 322 +++++++ .../steps/InitialStockEntryStep.tsx | 44 +- .../onboarding/steps/InventoryReviewStep.tsx | 860 +++++++++++++++++ .../steps/ProductionProcessesStep.tsx | 6 +- .../onboarding/steps/RegisterTenantStep.tsx | 4 +- .../domain/onboarding/steps/index.ts | 6 + .../domain/setup-wizard/SetupWizard.tsx | 372 -------- .../components/domain/setup-wizard/index.ts | 4 +- .../src/pages/onboarding/OnboardingPage.tsx | 4 +- frontend/src/pages/setup/SetupPage.tsx | 18 - frontend/src/router/AppRouter.tsx | 15 +- frontend/src/router/routes.config.ts | 20 +- services/auth/app/api/onboarding_progress.py | 54 +- services/inventory/app/models/inventory.py | 9 +- services/inventory/app/schemas/inventory.py | 39 +- .../app/services/inventory_service.py | 9 +- ...0251108_1200_make_stock_fields_nullable.py | 84 ++ todo.md | 57 ++ 27 files changed, 2801 insertions(+), 1149 deletions(-) create mode 100644 ARCHITECTURE_ANALYSIS.md create mode 100644 REFACTORING_ROADMAP.md delete mode 100644 frontend/src/components/domain/onboarding/OnboardingWizard.tsx create mode 100644 frontend/src/components/domain/onboarding/steps/FileUploadStep.tsx create mode 100644 frontend/src/components/domain/onboarding/steps/InventoryReviewStep.tsx delete mode 100644 frontend/src/components/domain/setup-wizard/SetupWizard.tsx delete mode 100644 frontend/src/pages/setup/SetupPage.tsx create mode 100644 services/inventory/migrations/versions/20251108_1200_make_stock_fields_nullable.py create mode 100644 todo.md diff --git a/ARCHITECTURE_ANALYSIS.md b/ARCHITECTURE_ANALYSIS.md new file mode 100644 index 00000000..82782d01 --- /dev/null +++ b/ARCHITECTURE_ANALYSIS.md @@ -0,0 +1,877 @@ +# Comprehensive Architecture Analysis: UnifiedOnboardingWizard + +## Executive Summary + +The UnifiedOnboardingWizard is a 14-step onboarding flow that has evolved to integrate multiple concerns (file upload, inventory management, product categorization, stock entry, ML training) into a single, monolithic UploadSalesDataStep component. While the overall wizard architecture is sound, there is significant technical debt in the step implementations, particularly in UploadSalesDataStep which mixes file upload, inventory management, and state management responsibilities. + +--- + +## 1. CURRENT ARCHITECTURE OVERVIEW + +### 1.1 Wizard Structure + +**Location**: `/frontend/src/components/domain/onboarding/` + +The wizard consists of: +- **UnifiedOnboardingWizard.tsx** - Main wizard orchestrator (533 lines) +- **WizardContext.tsx** - State management (277 lines) +- **OnboardingWizard.tsx** - Deprecated (567 lines - SHOULD BE DELETED) +- **14 individual step components** (ranging from 736 to 74,627 lines) + +### 1.2 Visible Steps Flow + +The wizard displays 14 steps in this order: + +``` +Phase 1: Discovery +├─ 1. bakery-type-selection (BakeryTypeSelectionStep) +└─ 2. setup (RegisterTenantStep) + +Phase 2a: AI-Assisted Inventory Path +├─ 3. smart-inventory-setup (UploadSalesDataStep) ⚠️ MEGA-COMPONENT +├─ 4. product-categorization (ProductCategorizationStep) +├─ 5. initial-stock-entry (InitialStockEntryStep) +└─ (Auto-completes: suppliers-setup) + +Phase 2b: Setup Operations +├─ 6. suppliers-setup (SuppliersSetupStep) +├─ 7. recipes-setup (RecipesSetupStep - conditional) +├─ 8. production-processes (ProductionProcessesStep - conditional) + +Phase 3: Advanced Features +├─ 9. quality-setup (QualitySetupStep) +└─ 10. team-setup (TeamSetupStep) + +Phase 4: ML & Finalization +├─ 11. ml-training (MLTrainingStep) +├─ 12. setup-review (ReviewSetupStep) +└─ 13. completion (CompletionStep) +``` + +**Note**: Step numbering is complex because: +- Steps 1-13 are in UnifiedOnboardingWizard +- SetupWizard (deprecated) has separate steps (setup-welcome, suppliers-setup, etc.) +- Conditional visibility based on bakeryType +- Auto-completion logic for suppliers-setup + +--- + +## 2. DETAILED ARCHITECTURAL ISSUES + +### 2.1 ISSUE #1: UploadSalesDataStep is a Mega-Component (74,627 lines) + +**Severity**: CRITICAL + +**Current Responsibilities**: +1. File upload & validation (lines 1-169) +2. Automatic file analysis with AI (lines 141-169) +3. AI-based product classification (lines 171-247) +4. Inventory item form management (lines 249-433) +5. Stock lot management (lines 335-435) +6. Batch operations (lines 437-589) +7. Sales data import (lines 548-569) +8. UI rendering for two distinct phases: + - File upload phase (lines 1408-1575) + - Inventory list/editing phase (lines 612-1405) +9. Modal integration (BatchAddIngredientsModal) + +**Problems**: +- Violates Single Responsibility Principle +- Hard to test (>1500 lines in single component) +- Multiple state variables (23 useState calls) +- Tightly couples file upload with inventory management +- Complex conditional rendering logic +- Difficult to reuse any sub-functionality + +**Code Indicators**: +```typescript +// Line 22-114: THREE separate form interfaces and states +interface ProgressState { ... } +interface InventoryItemForm { ... } +// Plus: [selectedFile, isValidating, validationResult, inventoryItems, ...] +// TOTAL: 23+ state variables +const [selectedFile, setSelectedFile] = useState(null); +const [isValidating, setIsValidating] = useState(false); +const [validationResult, setValidationResult] = useState(null); +// ... 20 more state declarations +``` + +### 2.2 ISSUE #2: Separated File Upload & Inventory Concerns + +**Severity**: HIGH + +The UploadSalesDataStep handles TWO distinct user journeys: + +**Journey 1: File Upload & Auto-Classification** +``` +User uploads CSV + ↓ +Validate file (validateFileMutation) + ↓ +Classify products (classifyBatchMutation) + ↓ +Generate AI suggestions + ↓ +Show inventory list for review +``` + +**Journey 2: Manual Inventory Entry** +``` +After AI suggestions generated + ↓ +Edit/add inventory items manually + ↓ +Add stock lots per item + ↓ +Create all ingredients via API (createIngredient) + ↓ +Create all stock lots (addStockMutation) + ↓ +Import sales data +``` + +**Problem**: Both journeys are in ONE component with complex conditional rendering: +```typescript +// Line 613: Conditional branch for entire second phase +if (showInventoryStep) { + // 800+ lines of inventory management UI + return (
...
); +} + +// Line 1408: Final 167 lines for file upload UI +return (
...
); +``` + +### 2.3 ISSUE #3: State Management Fragmentation + +**Severity**: MEDIUM + +**Local Component State (UploadSalesDataStep)**: +- File selection: `selectedFile`, `isValidating`, `validationResult` +- Inventory: `inventoryItems`, `showInventoryStep`, `error`, `progressState` +- Form data: `formData`, `editingId`, `isAdding`, `formErrors` +- Stock lots: `addingStockForId`, `stockFormData`, `stockErrors`, `ingredientStocks` + +**Wizard Context State** (WizardContext.tsx): +- `bakeryType`, `dataSource`, `uploadedFileName`, `uploadedFileSize` +- `aiSuggestions`, `aiAnalysisComplete`, `categorizedProducts`, `productsWithStock` +- Various completion flags: `categorizationCompleted`, `stockEntryCompleted`, etc. + +**Backend Progress State** (API): +- `useUserProgress()` fetches current step, completed steps, completion percentage +- `useMarkStepCompleted()` updates backend after step completion + +**Problem**: No clear separation of concerns +- Wizard context is "source of truth" but individual steps also have local state +- UI state (isAdding, editingId) mixed with data state (inventoryItems) +- No clear data flow: Step data → Wizard context → Backend +- Difficult to understand which data persists across sessions + +### 2.4 ISSUE #4: Circular State Dependencies + +**Severity**: MEDIUM + +**UploadSalesDataStep → Wizard Context**: +```typescript +// Line 320: Updates wizard context on completion +wizardContext.updateAISuggestions(data.aiSuggestions); +wizardContext.setAIAnalysisComplete(true); +// Line 325: +wizardContext.updateCategorizedProducts(data.categorizedProducts); +``` + +**ProductCategorizationStep → Wizard Context**: +```typescript +// Receives products from wizard context +// Updates categorizedProducts in wizard context +onUpdate?.({ categorizedProducts: updatedProducts }); +``` + +**InitialStockEntryStep → Wizard Context**: +```typescript +// Receives products from categorization +// Updates productsWithStock in wizard context +onUpdate?.({ productsWithStock: updatedProducts }); +``` + +**Problem**: Steps update wizard context via `onUpdate()` AND `onComplete()` +- Data flows both ways (child → parent AND parent → child) +- Difficult to trace data transformations +- No clear contract between parent and child + +### 2.5 ISSUE #5: Tight Coupling Between Wizard & Setup Components + +**Severity**: HIGH + +**UnifiedOnboardingWizard mixes:** +- 8 onboarding-specific steps from `/onboarding/steps/` +- 5 setup-specific steps from `/setup-wizard/steps/` + +**Import at line 22-28**: +```typescript +import { + SuppliersSetupStep, + RecipesSetupStep, + QualitySetupStep, + TeamSetupStep, + ReviewSetupStep, +} from '../setup-wizard/steps'; +``` + +**Problem**: +- UnifiedOnboardingWizard shouldn't need setup-wizard imports +- Creates dependency between onboarding and setup domains +- Changes in setup steps affect onboarding flow +- Makes it harder to modify setup flow independently +- Violates layering/module boundaries + +### 2.6 ISSUE #6: Auto-Completion Logic Scattered + +**Severity**: MEDIUM + +**Auto-completion of suppliers-setup** (line 349-365): +```typescript +// In UnifiedOnboardingWizard.handleStepComplete() +if (currentStep.id === 'smart-inventory-setup' && data?.shouldAutoCompleteSuppliers) { + try { + await markStepCompleted.mutateAsync({ + userId: user.id, + stepName: 'suppliers-setup', + data: { auto_completed: true, ... } + }); + } catch (supplierError) { + console.warn('Could not auto-complete suppliers-setup step'); + } +} +``` + +**Auto-completion of user_registered** (line 204-232): +```typescript +// In OnboardingWizardContent.useEffect() +useEffect(() => { + if (userProgress && user?.id && !autoCompletionAttempted && !markStepCompleted.isPending) { + const userRegisteredStep = userProgress.steps.find(s => s.step_name === 'user_registered'); + if (!userRegisteredStep?.completed) { + // Auto-complete... + } + } +}, [...]); +``` + +**Problem**: +- Auto-completion logic is buried in step completion handler +- No clear policy for which steps can be auto-completed +- Difficult to add new auto-completion rules +- Inconsistent with manual step completion flow + +### 2.7 ISSUE #7: Condition Functions Are Scattered & Duplicated + +**Severity**: LOW-MEDIUM + +**Step visibility conditions** (line 59-165): +```typescript +const ALL_STEPS: StepConfig[] = [ + { + id: 'smart-inventory-setup', + condition: (ctx) => ctx.tenantId !== null, + }, + { + id: 'product-categorization', + condition: (ctx) => ctx.state.aiAnalysisComplete, + }, + { + id: 'initial-stock-entry', + condition: (ctx) => ctx.state.categorizationCompleted, + }, + { + id: 'suppliers-setup', + // Always show - no conditional + }, + { + id: 'recipes-setup', + condition: (ctx) => + ctx.state.bakeryType === 'production' || ctx.state.bakeryType === 'mixed', + }, + // ... more conditions +]; +``` + +**Identical conditions in WizardContext** (line 183-189): +```typescript +getVisibleSteps = (): string[] => { + // ... same conditions repeated + if (state.bakeryType === 'production' || state.bakeryType === 'mixed') { + steps.push('recipes-setup'); + } + if (state.bakeryType === 'retail' || state.bakeryType === 'mixed') { + steps.push('production-processes'); + } +}; +``` + +**Problem**: +- Same logic defined in TWO places +- Changes require updates in multiple files +- No single source of truth for visibility rules +- Conditions are business logic, should be defined separately + +### 2.8 ISSUE #8: Complex Data Transformations Without Clear Contracts + +**Severity**: MEDIUM** + +**UploadSalesDataStep → ProductCategorizationStep**: +```typescript +// Line 203-235: UploadSalesDataStep generates InventoryItemForm[] +const items: InventoryItemForm[] = classificationResponse.suggestions.map((suggestion) => ({ + id: `ai-${index}-${Date.now()}`, + name: suggestion.suggested_name, + // ... 15 more properties +})); + +// Then completes with: +onComplete({ + createdIngredients, + totalItems: createdIngredients.length, + validationResult, + // ... more data +}); + +// But ProductCategorizationStep expects Product[] (different interface!) +interface Product { + id: string; + name: string; + category?: string; + confidence?: number; + type?: 'ingredient' | 'finished_product' | null; + suggestedType?: 'ingredient' | 'finished_product'; +} +``` + +**Problem**: +- No clear transformation between step outputs and next step inputs +- Multiple data formats for similar data +- Type mismatches not caught until runtime +- No schema/contract for inter-step communication + +--- + +## 3. STEP DEPENDENCIES & DATA FLOW + +### 3.1 Dependency Diagram + +``` +BakeryTypeSelectionStep + └─> wizardContext.updateBakeryType() + └─> affects visibility of: recipes-setup, production-processes + +RegisterTenantStep + └─> wizardContext.tenantId (implicit - via tenant store) + └─> enables: smart-inventory-setup + +UploadSalesDataStep [MEGA-COMPONENT] + ├─> Validates file (validateFileMutation) + ├─> Classifies products (classifyBatchMutation) + ├─> Creates inventory (createIngredient) ← INVENTORY CONCERN + ├─> Creates stock lots (addStockMutation) ← INVENTORY CONCERN + ├─> Imports sales data (importMutation) + ├─> wizardContext.updateAISuggestions() + ├─> wizardContext.setAIAnalysisComplete() + ├─> markStepCompleted() [auto-completes suppliers-setup] ← ORCHESTRATION + └─> onComplete() with shouldAutoCompleteSuppliers flag + +ProductCategorizationStep + ├─> Input: wizard context aiSuggestions or initialData.categorizedProducts + ├─> onUpdate() → wizardContext.updateCategorizedProducts() + └─> onComplete() + └─> wizardContext.markStepComplete('categorizationCompleted') + +InitialStockEntryStep + ├─> Input: categorizedProducts (which type: ingredient or finished_product?) + ├─> onUpdate() → wizardContext.updateProductsWithStock() + └─> onComplete() + └─> wizardContext.markStepComplete('stockEntryCompleted') + +SuppliersSetupStep (from setup-wizard) + ├─> May be auto-completed by UploadSalesDataStep + └─> Creates suppliers + +RecipesSetupStep (conditional: production or mixed) + └─> Depends on suppliers existing + +ProductionProcessesStep (conditional: retail or mixed) + └─> May depend on recipes? + +MLTrainingStep + └─> Input: inventory data from previous steps + └─> No explicit dependency check + +ReviewSetupStep + └─> Shows summary of configuration + +CompletionStep + └─> Final step, navigates away +``` + +### 3.2 Data Flow Diagram + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ UnifiedOnboardingWizard │ +│ (Step Orchestrator) │ +└─────────────────────────────────────────────────────────────────┘ + ↑ ↑ + │ state (VISIBLE_STEPS, currentStepIndex) │ markStepCompleted() + │ │ + ┌────┴───────────────────────────────────────────────────┴──┐ + │ WizardContext │ + │ (State: bakeryType, aiSuggestions, categorizedProducts) │ + │ (State: productsWithStock, various completion flags) │ + └────┬───────────────────────────────────────────────────┬───┘ + │ onUpdate() / onComplete() │ + │ (data flows FROM steps TO context) │ + │ │ + ┌────┴────────────────────────────────────────────────┬──┘ + │ Individual Steps │ + │ │ + │ BakeryTypeSelectionStep │ + │ RegisterTenantStep │ + │ UploadSalesDataStep ⚠️ [73KB, 23 state vars] │ + │ ├─ File validation │ + │ ├─ AI classification │ + │ ├─ Inventory creation │ + │ └─ Stock lot management │ + │ ProductCategorizationStep │ + │ InitialStockEntryStep │ + │ SuppliersSetupStep (from setup-wizard) │ + │ ... │ + └──────────────────────────────────────────────────────┘ + │ │ + │ │ + API mutations Backend progress + (validateFile, classifyBatch, (useUserProgress, + createIngredient, addStock, useMarkStepCompleted) + importSalesData) +``` + +### 3.3 Data Transformation Pipeline + +``` +CSV/JSON File + ↓ +[UploadSalesDataStep] validateFileMutation + ↓ (ImportValidationResponse) + ├─ total_records, valid_records, invalid_records + ├─ product_list: string[] + └─ ... + ↓ +[UploadSalesDataStep] classifyBatchMutation + ↓ (ProductSuggestionResponse[]) + ├─ id, suggested_name, product_type + ├─ category, unit_of_measure + ├─ confidence_score + ├─ estimated_shelf_life_days + ├─ requires_refrigeration, requires_freezing, is_seasonal + ├─ low_stock_threshold, reorder_point + └─ sales_data: { total_quantity, average_daily_sales } + ↓ +[UploadSalesDataStep] Transform to InventoryItemForm[] + ├─ id: string (temporary UI ID) + ├─ name, product_type, category + ├─ unit_of_measure + ├─ stock_quantity (calculated from sales data) + ├─ cost_per_unit (estimated) + ├─ estimated_shelf_life_days + ├─ requires_refrigeration, requires_freezing, is_seasonal + ├─ low_stock_threshold, reorder_point + ├─ isSuggested: boolean + ├─ confidence_score + └─ sales_data: { total_quantity, average_daily_sales } + ↓ +[UploadSalesDataStep] User review & manual edits + ├─ Add/edit/delete inventory items + ├─ Add stock lots (optional) + └─ ... + ↓ +[UploadSalesDataStep] createIngredient mutations (parallel) + ↓ (Ingredient[]) + ├─ id (from backend) + ├─ name, product_type, category + ├─ unit_of_measure, shelf_life_days + ├─ requires_refrigeration, requires_freezing, is_seasonal + ├─ low_stock_threshold, max_stock_level, reorder_point + └─ average_cost + ↓ +[UploadSalesDataStep] addStockMutation (parallel) + ↓ (StockResponse[]) + ├─ id (from backend) + ├─ ingredient_id + ├─ current_quantity + ├─ expiration_date + ├─ supplier_id + ├─ batch_number + ├─ production_stage + └─ quality_status + ↓ +[UploadSalesDataStep] importMutation (sales data) + ↓ +[ProductCategorizationStep] ← BUT RECEIVES: wizard context aiSuggestions + ↓ (Product[] with type: 'ingredient' | 'finished_product') + ↓ +[InitialStockEntryStep] ← Receives: categorizedProducts + ↓ (ProductWithStock[] with initialStock) + ↓ +[Backend] Final inventory saved +``` + +**Problem**: Data transformations are implicit and scattered across files. No clear schema/types for inter-step data passing. + +--- + +## 4. LEGACY & DEPRECATED COMPONENTS + +### 4.1 OnboardingWizard.tsx - DEPRECATED + +**Location**: `/frontend/src/components/domain/onboarding/OnboardingWizard.tsx` + +**Status**: DEPRECATED but still exported in index.ts + +**Usage**: +```typescript +// frontend/src/components/domain/onboarding/index.ts +export { OnboardingWizard } from './OnboardingWizard'; ← EXPORTED BUT NOT USED +export { UnifiedOnboardingWizard } from './UnifiedOnboardingWizard'; ← THIS IS USED +``` + +**Confirmation of non-usage**: +- OnboardingPage.tsx uses only UnifiedOnboardingWizard +- No other imports of OnboardingWizard found in codebase +- Old wizard has simplified step flow (4 steps vs 14) + +**Recommendation**: DELETE OnboardingWizard.tsx and remove from index.ts exports + +### 4.2 SetupWizard.tsx - STILL IN USE + +**Location**: `/frontend/src/components/domain/setup-wizard/SetupWizard.tsx` + +**Status**: ACTIVE but DEPRECATED by design + +**Usage**: +```typescript +// frontend/src/pages/setup/SetupPage.tsx +const SetupPage: React.FC = () => { + return ; +}; +``` + +**Problem**: +- UnifiedOnboardingWizard imports setup-wizard steps directly +- Creates tight coupling between onboarding and setup domains +- SetupWizard is now redundant - setup steps are integrated into UnifiedOnboardingWizard +- Separate SetupPage route still exists (/app/setup) + +**Recommendation**: +1. Remove setup-wizard imports from UnifiedOnboardingWizard +2. Keep SetupWizard.tsx for backwards compatibility OR +3. Transition to new onboarding flow only and deprecate SetupPage + +### 4.3 Route Duplication + +**Current Routes**: +- `/app/onboarding` → OnboardingPage → UnifiedOnboardingWizard ✓ CORRECT +- `/app/setup` → SetupPage → SetupWizard ✓ LEGACY (now integrated into onboarding) + +**Recommendation**: +- Remove /app/setup route OR +- Make it redirect to /app/onboarding for backwards compatibility + +--- + +## 5. FILES TO DELETE + +| File | Status | Reason | +|------|--------|--------| +| `/frontend/src/components/domain/onboarding/OnboardingWizard.tsx` | DELETE | Fully deprecated, non-functional | +| `/frontend/src/pages/setup/SetupPage.tsx` | DELETE or REDIRECT | Setup flow now integrated into onboarding | +| Setup wizard route entry in routes.config.ts | REMOVE | Duplicate with onboarding | + +--- + +## 6. ARCHITECTURAL PROBLEMS SUMMARY + +### Problem #1: MEGA-COMPONENT (UploadSalesDataStep) +- **Type**: Single Responsibility Principle Violation +- **Severity**: CRITICAL +- **Impact**: Hard to test, maintain, and extend +- **Solution**: Split into: + - FileUploadPhase (controlled by parent) + - InventoryManagementPhase (separate component or reusable) + - AI classification logic (service) + +### Problem #2: State Management Fragmentation +- **Type**: State Distribution Anti-Pattern +- **Severity**: HIGH +- **Impact**: Difficult to trace data flow, understand state transitions +- **Solution**: + - Clear separation: UI state (local) vs Data state (context) + - Define explicit data contracts between steps + - Consider step output → context mapping layer + +### Problem #3: Mixed Concerns in One Component +- **Type**: Separation of Concerns Violation +- **Severity**: HIGH +- **Impact**: Tight coupling, difficult to test individual features +- **Solution**: + - File upload logic → FileUploadService + - Inventory form logic → InventoryFormComponent + - Stock lot logic → StockLotComponent + - Composition pattern to combine them + +### Problem #4: Duplicate Visibility Logic +- **Type**: DRY Principle Violation +- **Severity**: MEDIUM +- **Impact**: Maintenance burden, potential inconsistencies +- **Solution**: + - Extract visibility rules into dedicated config/service + - Single source of truth for step conditions + +### Problem #5: Circular Data Dependencies +- **Type**: Bidirectional Data Flow Anti-Pattern +- **Severity**: MEDIUM +- **Impact**: Hard to understand data flow direction +- **Solution**: + - Strict unidirectional data flow (parent → child via props, child → parent via callbacks) + - Clear input/output contracts for each step + +### Problem #6: Tight Coupling: Onboarding ↔ Setup Wizard +- **Type**: Module Boundary Violation +- **Severity**: HIGH +- **Impact**: Difficult to modify either wizard independently +- **Solution**: + - Remove setup-wizard imports from UnifiedOnboardingWizard + - Define step interface contract + - Load steps dynamically by configuration + +### Problem #7: Complex Auto-Completion Logic +- **Type**: Business Logic Distribution +- **Severity**: MEDIUM +- **Impact**: Difficult to understand which steps auto-complete +- **Solution**: + - Centralized auto-completion policy + - Explicit configuration for which steps can auto-complete + - Clear rules for completion dependencies + +### Problem #8: No Clear Inter-Step Communication Contract +- **Type**: Interface Definition Anti-Pattern +- **Severity**: MEDIUM +- **Impact**: Type mismatches between steps (InventoryItemForm vs Product vs ProductWithStock) +- **Solution**: + - Define Step Interface Contract + - Create transformation layer between step outputs + - Use consistent naming/typing across all steps + +--- + +## 7. STEP RESPONSIBILITIES MATRIX + +| Step | Primary Responsibility | State Type | Dependencies | Outputs | +|------|------------------------|-----------|--------------|---------| +| BakeryTypeSelectionStep | Select bakery model | UI | None | bakeryType | +| RegisterTenantStep | Register bakery details | API call | bakeryType | tenantId, tenant object | +| **UploadSalesDataStep** | **File upload + Inventory mgmt + Stock lots** | **API calls + Local forms** | **tenantId** | **Ingredients[] + Stock[] + Sales import result** | +| ProductCategorizationStep | Classify products | Drag-drop state | aiSuggestions | categorizedProducts | +| InitialStockEntryStep | Set initial stock levels | Form state | categorizedProducts | productsWithStock | +| SuppliersSetupStep | Add suppliers | API + Form | tenantId | suppliers (or auto-completed) | +| RecipesSetupStep | Create recipes | API + Complex forms | ingredients + suppliers | recipes | +| ProductionProcessesStep | Define processes | API + Complex forms | bakeryType | processes | +| QualitySetupStep | Set quality standards | API + Forms | tenantId | quality templates | +| TeamSetupStep | Add team members | API + Forms | tenantId | team members | +| MLTrainingStep | Train ML models | API poll + long-running | inventory data | trained models | +| ReviewSetupStep | Show configuration summary | Read-only | All previous data | (none, informational) | +| CompletionStep | Celebrate + redirect | Navigation | None | Navigate to dashboard | + +--- + +## 8. RECOMMENDATIONS FOR REFACTORING + +### Immediate Actions (Quick Wins) + +1. **Delete OnboardingWizard.tsx** + - Remove from exports in index.ts + - Impact: LOW (not used) + - Time: 5 minutes + +2. **Remove setup-wizard imports from UnifiedOnboardingWizard** + - Load setup steps dynamically + - Create step registry/configuration + - Impact: MEDIUM (refactoring pattern needed) + - Time: 2-3 hours + +3. **Extract visibility rules to dedicated config** + - Move bakeryType conditions to one place + - Impact: LOW (pure refactoring) + - Time: 1 hour + +### Medium-term Refactoring (1-2 Sprint) + +4. **Split UploadSalesDataStep** + - Extract FileUploadPhase (lines 1408-1575) + - Extract InventoryManagementPhase (lines 612-1405) + - Create composition layer + - Move AI classification to service + - Impact: HIGH (major refactoring) + - Time: 6-8 hours + +5. **Define Inter-Step Data Contracts** + - Create interfaces for step input/output + - Add transformation layer between steps + - Impact: HIGH (enforces consistency) + - Time: 4-6 hours + +6. **Separate UI State from Data State in WizardContext** + - Move isAdding, editingId to component state + - Keep only data state in context + - Impact: MEDIUM (refactoring) + - Time: 3-4 hours + +### Long-term Architecture (2-3 Months) + +7. **Implement Step Interface Contract** + - Define standard step props/callback signatures + - Enforce typing at wizard level + - Impact: HIGH (system-wide improvement) + - Time: 1-2 sprints + +8. **Extract Auto-Completion Policy Engine** + - Centralized configuration for auto-completion + - Clear rules for step dependencies + - Impact: MEDIUM (optional feature) + - Time: 1 sprint + +9. **Consolidate Setup & Onboarding Routes** + - Make /app/setup redirect to /app/onboarding + - OR keep SetupPage for post-onboarding scenarios + - Impact: LOW-MEDIUM (routing refactoring) + - Time: 2-3 hours + +--- + +## 9. CURRENT STEP-BY-STEP FLOW DIAGRAM + +``` +┌───────────────────────────────────────────────────────────┐ +│ USER FLOW: UnifiedOnboardingWizard (14 Steps) │ +└───────────────────────────────────────────────────────────┘ + +START: User completes registration + ↓ Auto-completes user_registered step + +STEP 1: Bakery Type Selection + └─> SELECT: production | retail | mixed + └─> STORES: bakeryType in context + +STEP 2: Register Tenant + └─> ENTER: Bakery name, address, phone, city + └─> API: registerBakery() + └─> STORES: tenantId, tenant object + +STEP 3: Upload Sales Data (⚠️ MEGA-COMPONENT) + ├─> UPLOAD: CSV/JSON file with sales history + │ └─> API: validateFile() + │ └─> API: classifyBatch() [AI classification] + │ └─> DISPLAY: Suggested inventory items + │ + └─> REVIEW & EDIT: Inventory items + ├─ ALLOW: Add, edit, delete items + ├─ ALLOW: Add stock lots per item + │ └─> FIELDS: Quantity, expiration date, supplier, batch number + │ + └─> API: createIngredient() [parallel for all items] + └─> API: addStock() [parallel for all stock lots] + └─> API: importSalesData() [background import] + └─> Auto-complete: suppliers-setup step + +STEP 4: Product Categorization + ├─> DISPLAY: Suggested AI classifications + ├─> ALLOW: Drag-drop to categorize as ingredient or finished_product + └─> STORES: categorizedProducts in context + +STEP 5: Initial Stock Entry + ├─> DISPLAY: All products from categorization + ├─> ALLOW: Enter initial stock quantities + └─> STORES: productsWithStock in context + +STEP 6: Suppliers Setup ⭐ (May be auto-completed) + ├─> IF auto-completed: Display "✓ Auto-setup" + └─> ELSE: ALLOW: Add suppliers (company, contact, details) + └─> API: createSupplier() + +STEP 7: Recipes Setup (CONDITIONAL: production or mixed bakeries) + ├─> ALLOW: Create recipes with ingredients + ├─> INPUT: Select ingredients from inventory + ├─> INPUT: Set quantities, proportions + └─> API: createRecipe() + +STEP 8: Production Processes (CONDITIONAL: retail or mixed bakeries) + ├─> ALLOW: Define production workflows + └─> API: createProcess() + +STEP 9: Quality Setup + ├─> ALLOW: Define quality templates/standards + └─> API: createQualityTemplate() + +STEP 10: Team Setup + ├─> ALLOW: Add team members, assign roles + └─> API: inviteUser() + +STEP 11: ML Training + ├─> INFO: "Training personalized AI model..." + ├─> DISPLAY: Progress indicator + ├─> WAIT: For training job completion (API poll) + └─> DISPLAY: Model accuracy metrics + +STEP 12: Setup Review + ├─> DISPLAY: Summary of all configuration + └─> ALLOW: Review before finalizing + +STEP 13: Completion + └─> DISPLAY: "✓ Setup complete!" + └─> REDIRECT: /app/dashboard + +END: User is now in the main application +``` + +--- + +## 10. SUMMARY TABLE + +| Aspect | Status | Priority | Notes | +|--------|--------|----------|-------| +| Overall Architecture | FUNCTIONAL | - | Works but has technical debt | +| UnifiedOnboardingWizard Main | GOOD | LOW | Well-structured orchestrator | +| WizardContext | ACCEPTABLE | MEDIUM | State management needs clarification | +| UploadSalesDataStep | POOR | CRITICAL | 74KB mega-component needs refactoring | +| OnboardingWizard (old) | DEPRECATED | HIGH | Should be deleted | +| SetupWizard Integration | PROBLEMATIC | HIGH | Tight coupling needs fixing | +| Visibility Rules | DUPLICATE | MEDIUM | DRY principle violation | +| Inter-Step Contracts | UNDEFINED | MEDIUM | No formal interface definition | +| Auto-Completion | AD-HOC | MEDIUM | Needs centralized policy | +| State Management | FRAGMENTED | MEDIUM | Mixing UI and data state | + +--- + +## APPENDIX: FILE STATISTICS + +| File | Size | LOC | Complexity | Status | +|------|------|-----|-----------|--------| +| UnifiedOnboardingWizard.tsx | 20.5 KB | 533 | MEDIUM | GOOD | +| WizardContext.tsx | 9.5 KB | 277 | MEDIUM | ACCEPTABLE | +| OnboardingWizard.tsx | 19.5 KB | 567 | MEDIUM | DEPRECATED | +| UploadSalesDataStep.tsx | 74.6 KB | 1577 | HIGH | POOR | +| ProductCategorizationStep.tsx | 14.3 KB | 365 | MEDIUM | ACCEPTABLE | +| InitialStockEntryStep.tsx | 11.2 KB | 286 | LOW | GOOD | +| BakeryTypeSelectionStep.tsx | 11 KB | 277 | MEDIUM | GOOD | +| RegisterTenantStep.tsx | 7.8 KB | ~200 | LOW | GOOD | +| SetupWizard.tsx | ~30 KB | ~800 | MEDIUM | ACTIVE | +| **TOTAL ONBOARDING** | **~169 KB** | **~3665** | - | - | + +--- diff --git a/REFACTORING_ROADMAP.md b/REFACTORING_ROADMAP.md new file mode 100644 index 00000000..16da6d46 --- /dev/null +++ b/REFACTORING_ROADMAP.md @@ -0,0 +1,315 @@ +# UnifiedOnboardingWizard Refactoring Roadmap + +## Quick Executive Summary + +**Current State**: Functional but architecturally problematic +**Main Issue**: UploadSalesDataStep is a 74KB mega-component mixing 8+ different concerns +**Debt Level**: HIGH - Technical debt is impacting maintainability + +--- + +## Critical Issues (Must Fix) + +### 1. UploadSalesDataStep is Too Large (CRITICAL) +- **File Size**: 74,627 bytes (73KB) +- **Lines**: 1,577 lines +- **State Variables**: 23 useState hooks +- **Methods**: Handles file upload, validation, classification, inventory management, stock lots, and more +- **Impact**: Almost impossible to test, modify, or reuse components + +**What it should do**: +- Single responsibility: Upload and validate file + +**What it actually does**: +1. File upload UI and drag-drop +2. File validation API call +3. AI classification API call +4. Inventory item form management (CRUD) +5. Stock lot management (CRUD) +6. Batch ingredient creation +7. Sales data import +8. Two entirely different UIs (upload phase vs inventory review phase) + +**Refactoring Approach**: +``` +Current: +UploadSalesDataStep (1577 lines, 23 state vars) + +Should be: +├─ FileUploadPhase (200 lines) +│ ├─ FileDropZone +│ ├─ FileValidator +│ └─ ProgressIndicator +│ +├─ InventoryManagementPhase (500 lines) +│ ├─ InventoryItemList +│ ├─ InventoryItemForm +│ ├─ StockLotManager +│ └─ BatchAddModal +│ +├─ InventoryService (classification, validation) +│ +└─ UploadSalesDataStepContainer (orchestrator, 150 lines) + ├─ Phase state machine + ├─ Data flow coordination + └─ API integration +``` + +### 2. Tight Coupling Between Onboarding & Setup (CRITICAL) +- UnifiedOnboardingWizard imports from setup-wizard/steps/ +- Changes to setup steps affect onboarding flow +- Makes it impossible to modify setup independently + +**Current Problem**: +```typescript +// UnifiedOnboardingWizard.tsx line 22-28 +import { + SuppliersSetupStep, + RecipesSetupStep, + QualitySetupStep, + TeamSetupStep, + ReviewSetupStep, +} from '../setup-wizard/steps'; // ← CROSS-DOMAIN IMPORT +``` + +**Solution**: +- Define step interface contract +- Move setup steps to configuration +- Load steps dynamically +- Create step registry pattern + +### 3. Circular State Dependencies (HIGH) +- Steps update wizard context via both onUpdate() and onComplete() +- Data flows both directions +- No clear source of truth + +**Current Flow**: +``` +UploadSalesDataStep.onUpdate() → wizardContext +ProductCategorizationStep.onUpdate() → wizardContext +InitialStockEntryStep.onUpdate() → wizardContext +All steps also call onComplete() with different data +``` + +**Should Be**: +``` +Step.onUpdate() → Local UI state (transient) +Step.onComplete() → Single source of truth (wizardContext + Backend) +``` + +--- + +## High Priority Issues (Fix in Next Sprint) + +### 4. Duplicate Visibility Logic +**Locations**: +- UnifiedOnboardingWizard.tsx (line 59-165): Step conditions +- WizardContext.tsx (line 183-189): getVisibleSteps() logic +- Same rules repeated in two places + +**Fix**: Extract to single configuration file + +### 5. Auto-Completion Logic is Scattered +- user_registered auto-completion: OnboardingWizardContent.useEffect() (line 204) +- suppliers-setup auto-completion: UnifiedOnboardingWizard.handleStepComplete() (line 349) +- No clear policy or pattern + +**Fix**: Centralized auto-completion policy engine + +### 6. State Management Confusion +- UI state mixed with data state in context +- No clear boundary between component state and context state +- Difficult to understand data persistence + +**Fix**: +- UI state (isAdding, editingId, showInventoryStep) → Component local state +- Data state (aiSuggestions, categorizedProducts) → Context +- Session state → Backend API + +--- + +## Medium Priority Issues (Fix Over Time) + +### 7. No Inter-Step Communication Contract +**Problem**: Different components use different interfaces for same data +- InventoryItemForm (line 28-50) +- Product (ProductCategorizationStep line 7-14) +- ProductWithStock (InitialStockEntryStep line 8-15) +- Ingredient (API type) +- Stock (API type) + +All represent similar data but with different shapes. + +**Fix**: Create unified type system for inventory throughout flow + +### 8. Complex Data Transformations Without Clear Schema +Data travels through pipeline with implicit transformations: +``` +File → ImportValidationResponse → ProductSuggestionResponse +→ InventoryItemForm → Ingredient → Stock → categorizedProducts +→ Product (different type!) → ProductWithStock +``` + +**Fix**: Create explicit transformation functions between steps + +--- + +## Files to Delete (Immediate) + +1. **OnboardingWizard.tsx** (567 lines, 19.5KB) + - Status: Deprecated, not used + - Usage: Only in exports, never imported + - Risk: LOW - completely safe to delete + - Action: DELETE and remove from index.ts exports + +2. **SetupPage.tsx** (18 lines, 567 bytes) - OPTIONAL + - Status: Deprecated by design + - Usage: Route /app/setup still references it + - Risk: MEDIUM - needs migration plan + - Action: Either delete or redirect to /app/onboarding + +3. **Setup route in routes.config.ts** (line 578-593) - OPTIONAL + - Status: Redundant with /app/onboarding + - Action: Remove or convert to redirect + +--- + +## Refactoring Priority & Timeline + +### Phase 1: Quick Wins (Week 1) +**Time**: 2-3 hours +**Impact**: LOW immediate, HIGH long-term + +- [ ] Delete OnboardingWizard.tsx +- [ ] Remove from index.ts exports +- [ ] Extract visibility rules to config file +- [ ] Add comments documenting current state + +### Phase 2: State Management Cleanup (Week 2) +**Time**: 3-4 hours +**Impact**: MEDIUM + +- [ ] Document which state belongs where (UI vs Context vs Backend) +- [ ] Move UI state out of context (isAdding, editingId, etc.) +- [ ] Clear data flow direction (parent → child → parent only) + +### Phase 3: Split UploadSalesDataStep (Sprint) +**Time**: 6-8 hours +**Impact**: CRITICAL + +- [ ] Extract FileUploadPhase component +- [ ] Extract InventoryManagementPhase component +- [ ] Extract classification logic to service +- [ ] Create container component to orchestrate +- [ ] Add unit tests for each extracted component + +### Phase 4: Decouple Onboarding from Setup (Sprint) +**Time**: 4-6 hours +**Impact**: HIGH + +- [ ] Define step interface contract +- [ ] Create step registry/configuration +- [ ] Move setup steps to configuration +- [ ] Load steps dynamically + +### Phase 5: Inter-Step Communication (Sprint) +**Time**: 4-6 hours +**Impact**: MEDIUM + +- [ ] Define unified inventory type system +- [ ] Create transformation functions between steps +- [ ] Add type validation at step boundaries +- [ ] Document data contracts + +--- + +## Current Architecture Summary + +### Strengths +1. **Main Wizard Orchestrator is Well-Designed** + - Clean step navigation + - Good progress tracking + - Proper initialization logic + - Clear completion flow + +2. **WizardContext is Reasonable** + - Good use of React Context API + - Most step data stored centrally + - Safe from localStorage corruption + +3. **Most Individual Steps are Well-Designed** + - ProductCategorizationStep: Good UI/UX + - InitialStockEntryStep: Simple and effective + - BakeryTypeSelectionStep: Beautiful design + +### Weaknesses +1. **UploadSalesDataStep is a Monster** + - Mixes file upload + inventory management + - Too many state variables + - Difficult to test + - Difficult to reuse + +2. **Module Boundaries are Blurred** + - Onboarding imports from setup-wizard + - Difficult to modify either independently + - Violates separation of concerns + +3. **State Management is Confusing** + - UI state mixed with data state + - Bidirectional data flow + - Multiple sources of truth + +4. **No Clear Contracts Between Steps** + - Data types change between steps + - Implicit transformations + - Runtime type errors possible + +--- + +## How to Use This Document + +1. **For Planning**: Use Phase 1-5 to plan sprints +2. **For Understanding**: Read ARCHITECTURE_ANALYSIS.md for detailed breakdown +3. **For Implementation**: Follow the refactoring approach in each section +4. **For Maintenance**: Reference this after refactoring to keep architecture clean + +--- + +## Success Criteria + +After refactoring, the codebase should have: + +- [ ] UploadSalesDataStep is <300 lines +- [ ] No imports between onboarding and setup-wizard domains +- [ ] Clear separation of UI state and data state +- [ ] All steps follow same interface contract +- [ ] Data transformations are explicit and testable +- [ ] Unit test coverage >80% for all step components +- [ ] All 8 architectural problems resolved +- [ ] Deprecated files deleted (OnboardingWizard.tsx) + +--- + +## Estimated Effort + +| Phase | Effort | Impact | Timeline | +|-------|--------|--------|----------| +| Phase 1 (Quick Wins) | 2-3 hours | LOW | Week 1 | +| Phase 2 (State Management) | 3-4 hours | MEDIUM | Week 2 | +| Phase 3 (Split Component) | 6-8 hours | CRITICAL | Sprint 2 | +| Phase 4 (Decouple Domains) | 4-6 hours | HIGH | Sprint 3 | +| Phase 5 (Data Contracts) | 4-6 hours | MEDIUM | Sprint 4 | +| **TOTAL** | **19-27 hours** | **HIGH** | **4-5 Weeks** | + +--- + +## Contact & Questions + +For detailed architecture information, see: `ARCHITECTURE_ANALYSIS.md` + +Key sections: +- Section 2: Detailed architectural issues with code examples +- Section 3: Step dependencies and data flow diagrams +- Section 6: Problem statements for each issue +- Section 8: Detailed recommendations for each refactoring task + diff --git a/frontend/src/api/services/onboarding.ts b/frontend/src/api/services/onboarding.ts index fb57944a..6bf5c92c 100644 --- a/frontend/src/api/services/onboarding.ts +++ b/frontend/src/api/services/onboarding.ts @@ -5,23 +5,41 @@ import { apiClient } from '../client'; import { UserProgress, UpdateStepRequest } from '../types/onboarding'; -// Backend onboarding steps (full list from backend) +// Backend onboarding steps (full list from backend - UPDATED to match refactored flow) export const BACKEND_ONBOARDING_STEPS = [ - 'user_registered', // Auto-completed: User account created - 'setup', // Step 1: Basic bakery setup and tenant creation - 'smart-inventory-setup', // Step 2: Sales data upload and inventory configuration - 'suppliers', // Step 3: Suppliers configuration (optional) - 'ml-training', // Step 4: AI model training - 'completion' // Step 5: Onboarding completed + 'user_registered', // Phase 0: User account created (auto-completed) + 'bakery-type-selection', // Phase 1: Choose bakery type + 'setup', // Phase 2: Basic bakery setup and tenant creation + 'upload-sales-data', // Phase 2a: File upload, validation, AI classification + 'inventory-review', // Phase 2a: Review AI-detected products with type selection + 'initial-stock-entry', // Phase 2a: Capture initial stock levels + 'product-categorization', // Phase 2b: Advanced categorization (optional) + 'suppliers-setup', // Phase 2c: Suppliers configuration + 'recipes-setup', // Phase 3: Production recipes (optional) + 'production-processes', // Phase 3: Finishing processes (optional) + 'quality-setup', // Phase 3: Quality standards (optional) + 'team-setup', // Phase 3: Team members (optional) + 'ml-training', // Phase 4: AI model training + 'setup-review', // Phase 4: Review all configuration + 'completion' // Phase 4: Onboarding completed ]; // Frontend step order for navigation (excludes user_registered as it's auto-completed) export const FRONTEND_STEP_ORDER = [ - 'setup', // Step 1: Basic bakery setup and tenant creation - 'smart-inventory-setup', // Step 2: Sales data upload and inventory configuration - 'suppliers', // Step 3: Suppliers configuration (optional) - 'ml-training', // Step 4: AI model training - 'completion' // Step 5: Onboarding completed + 'bakery-type-selection', // Phase 1: Choose bakery type + 'setup', // Phase 2: Basic bakery setup and tenant creation + 'upload-sales-data', // Phase 2a: File upload and AI classification + 'inventory-review', // Phase 2a: Review AI-detected products + 'initial-stock-entry', // Phase 2a: Initial stock levels + 'product-categorization', // Phase 2b: Advanced categorization (optional) + 'suppliers-setup', // Phase 2c: Suppliers configuration + 'recipes-setup', // Phase 3: Production recipes (optional) + 'production-processes', // Phase 3: Finishing processes (optional) + 'quality-setup', // Phase 3: Quality standards (optional) + 'team-setup', // Phase 3: Team members (optional) + 'ml-training', // Phase 4: AI model training + 'setup-review', // Phase 4: Review configuration + 'completion' // Phase 4: Onboarding completed ]; export class OnboardingService { diff --git a/frontend/src/api/types/inventory.ts b/frontend/src/api/types/inventory.ts index 10daa490..19073cb7 100644 --- a/frontend/src/api/types/inventory.ts +++ b/frontend/src/api/types/inventory.ts @@ -95,10 +95,11 @@ export interface IngredientCreate { // Note: average_cost is calculated automatically from purchases (not accepted on create) standard_cost?: number | null; - // Stock management - low_stock_threshold?: number; // Default: 10.0 - reorder_point?: number; // Default: 20.0 - reorder_quantity?: number; // Default: 50.0 + // Stock management - all optional for onboarding + // These can be configured later based on actual usage patterns + low_stock_threshold?: number | null; + reorder_point?: number | null; + reorder_quantity?: number | null; max_stock_level?: number | null; // Shelf life (default value only - actual per batch) @@ -158,9 +159,9 @@ export interface IngredientResponse { average_cost: number | null; last_purchase_price: number | null; standard_cost: number | null; - low_stock_threshold: number; - reorder_point: number; - reorder_quantity: number; + low_stock_threshold: number | null; // Now optional + reorder_point: number | null; // Now optional + reorder_quantity: number | null; // Now optional max_stock_level: number | null; shelf_life_days: number | null; // Default value only is_active: boolean; diff --git a/frontend/src/components/domain/onboarding/OnboardingWizard.tsx b/frontend/src/components/domain/onboarding/OnboardingWizard.tsx deleted file mode 100644 index efc58a5d..00000000 --- a/frontend/src/components/domain/onboarding/OnboardingWizard.tsx +++ /dev/null @@ -1,567 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useNavigate, useSearchParams } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; -import { Button } from '../../ui/Button'; -import { Card, CardHeader, CardBody } from '../../ui/Card'; -import { useAuth } from '../../../contexts/AuthContext'; -import { useUserProgress, useMarkStepCompleted } from '../../../api/hooks/onboarding'; -import { useTenantActions } from '../../../stores/tenant.store'; -import { useTenantInitializer } from '../../../stores/useTenantInitializer'; -import { - RegisterTenantStep, - UploadSalesDataStep, - MLTrainingStep, - CompletionStep -} from './steps'; -import { Building2 } from 'lucide-react'; - -interface StepConfig { - id: string; - title: string; - description: string; - component: React.ComponentType; -} - -interface StepProps { - onNext: () => void; - onPrevious: () => void; - onComplete: (data?: any) => void; - isFirstStep: boolean; - isLastStep: boolean; -} - -export const OnboardingWizard: React.FC = () => { - const { t } = useTranslation(); - const [searchParams] = useSearchParams(); - const navigate = useNavigate(); - const { user } = useAuth(); - - // Steps must match backend ONBOARDING_STEPS exactly - // Note: "user_registered" is auto-completed and not shown in UI - const STEPS: StepConfig[] = [ - { - id: 'setup', - title: t('onboarding:wizard.steps.setup.title', 'Registrar Panadería'), - description: t('onboarding:wizard.steps.setup.description', 'Configura la información básica de tu panadería'), - component: RegisterTenantStep, - }, - { - id: 'smart-inventory-setup', - title: t('onboarding:wizard.steps.smart_inventory_setup.title', 'Configurar Inventario'), - description: t('onboarding:wizard.steps.smart_inventory_setup.description', 'Sube datos de ventas y configura tu inventario inicial'), - component: UploadSalesDataStep, - }, - { - id: 'ml-training', - title: t('onboarding:wizard.steps.ml_training.title', 'Entrenamiento IA'), - description: t('onboarding:wizard.steps.ml_training.description', 'Entrena tu modelo de inteligencia artificial personalizado'), - component: MLTrainingStep, - }, - { - id: 'completion', - title: t('onboarding:wizard.steps.completion.title', 'Configuración Completa'), - description: t('onboarding:wizard.steps.completion.description', '¡Bienvenido a tu sistema de gestión inteligente!'), - component: CompletionStep, - }, - ]; - - // Check if this is a fresh onboarding (new tenant creation) - const isNewTenant = searchParams.get('new') === 'true'; - - // Initialize state based on whether this is a new tenant or not - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isInitialized, setIsInitialized] = useState(isNewTenant); // If new tenant, consider initialized immediately - - // Debug log for new tenant creation - useEffect(() => { - if (isNewTenant) { - console.log('🆕 New tenant creation detected - UI will reset to step 0'); - console.log('📊 Current step index:', currentStepIndex); - console.log('🎯 Is initialized:', isInitialized); - } - }, [isNewTenant, currentStepIndex, isInitialized]); - - // Initialize tenant data for authenticated users - useTenantInitializer(); - - // Get user progress from backend - const { data: userProgress, isLoading: isLoadingProgress, error: progressError } = useUserProgress( - user?.id || '', - { enabled: !!user?.id } - ); - - const markStepCompleted = useMarkStepCompleted(); - const { setCurrentTenant } = useTenantActions(); - - // Auto-complete user_registered step if needed (runs first) - const [autoCompletionAttempted, setAutoCompletionAttempted] = React.useState(false); - - useEffect(() => { - if (userProgress && user?.id && !autoCompletionAttempted && !markStepCompleted.isPending) { - const userRegisteredStep = userProgress.steps.find(s => s.step_name === 'user_registered'); - - if (!userRegisteredStep?.completed) { - console.log('🔄 Auto-completing user_registered step for new user...'); - setAutoCompletionAttempted(true); - - // Merge with any existing data (e.g., subscription_plan from registration) - const existingData = userRegisteredStep?.data || {}; - - markStepCompleted.mutate({ - userId: user.id, - stepName: 'user_registered', - data: { - ...existingData, // Preserve existing data like subscription_plan - auto_completed: true, - completed_at: new Date().toISOString(), - source: 'onboarding_wizard_auto_completion' - } - }, { - onSuccess: () => { - console.log('✅ user_registered step auto-completed successfully'); - // The query will automatically refetch and update userProgress - }, - onError: (error) => { - console.error('❌ Failed to auto-complete user_registered step:', error); - // Reset flag on error to allow retry - setAutoCompletionAttempted(false); - } - }); - } - } - }, [userProgress, user?.id, autoCompletionAttempted, markStepCompleted.isPending]); // Removed markStepCompleted from deps - - // Initialize step index based on backend progress with validation - useEffect(() => { - // Skip backend progress loading for new tenant creation - if (isNewTenant) { - return; // Already initialized to step 0 - } - - if (userProgress && !isInitialized) { - console.log('🔄 Initializing onboarding progress:', userProgress); - - // Check if user_registered step is completed - const userRegisteredStep = userProgress.steps.find(s => s.step_name === 'user_registered'); - if (!userRegisteredStep?.completed) { - console.log('⏳ Waiting for user_registered step to be auto-completed...'); - return; // Wait for auto-completion to finish - } - - let stepIndex = 0; // Default to first step - - // If this is a new tenant creation, always start from the beginning - if (isNewTenant) { - console.log('🆕 New tenant creation - starting from first step'); - stepIndex = 0; - } else { - // Find the current step index based on backend progress - const currentStepFromBackend = userProgress.current_step; - stepIndex = STEPS.findIndex(step => step.id === currentStepFromBackend); - - console.log(`🎯 Backend current step: "${currentStepFromBackend}", found at index: ${stepIndex}`); - - // If current step is not found (e.g., suppliers step), find the next incomplete step - if (stepIndex === -1) { - console.log('🔍 Current step not found in UI steps, finding first incomplete step...'); - - // Find the first incomplete step that user can access - for (let i = 0; i < STEPS.length; i++) { - const step = STEPS[i]; - const stepProgress = userProgress.steps.find(s => s.step_name === step.id); - - if (!stepProgress?.completed) { - stepIndex = i; - console.log(`📍 Found first incomplete step: "${step.id}" at index ${i}`); - break; - } - } - - // If all visible steps are completed, go to last step - if (stepIndex === -1) { - stepIndex = STEPS.length - 1; - console.log('✅ All steps completed, going to last step'); - } - } - - // Ensure user can't skip ahead - find the first incomplete step - const firstIncompleteStepIndex = STEPS.findIndex(step => { - const stepProgress = userProgress.steps.find(s => s.step_name === step.id); - return !stepProgress?.completed; - }); - - if (firstIncompleteStepIndex !== -1 && stepIndex > firstIncompleteStepIndex) { - console.log(`🚫 User trying to skip ahead. Redirecting to first incomplete step at index ${firstIncompleteStepIndex}`); - stepIndex = firstIncompleteStepIndex; - } - } - - console.log(`🎯 Final step index: ${stepIndex} ("${STEPS[stepIndex]?.id}")`); - - if (stepIndex !== currentStepIndex) { - setCurrentStepIndex(stepIndex); - } - setIsInitialized(true); - } - }, [userProgress, isInitialized, currentStepIndex, isNewTenant]); - - const currentStep = STEPS[currentStepIndex]; - - - const handleStepComplete = async (data?: any) => { - if (!user?.id) { - console.error('User ID not available'); - return; - } - - // Prevent concurrent mutations - if (markStepCompleted.isPending) { - console.warn(`⚠️ Step completion already in progress for "${currentStep.id}", skipping duplicate call`); - return; - } - - console.log(`🎯 Completing step: "${currentStep.id}" with data:`, data); - - try { - // Special handling for setup step - set the created tenant in tenant store - if (currentStep.id === 'setup' && data?.tenant) { - setCurrentTenant(data.tenant); - } - - // Mark step as completed in backend - console.log(`📤 Sending API request to complete step: "${currentStep.id}"`); - await markStepCompleted.mutateAsync({ - userId: user.id, - stepName: currentStep.id, - data - }); - - console.log(`✅ Successfully completed step: "${currentStep.id}"`); - - // Special handling for smart-inventory-setup: auto-complete suppliers step - if (currentStep.id === 'smart-inventory-setup' && data?.shouldAutoCompleteSuppliers) { - try { - console.log('🔄 Auto-completing suppliers step to enable ML training...'); - await markStepCompleted.mutateAsync({ - userId: user.id, - stepName: 'suppliers', - data: { - auto_completed: true, - completed_at: new Date().toISOString(), - source: 'inventory_creation_auto_completion', - message: 'Suppliers step auto-completed to proceed with ML training' - } - }); - console.log('✅ Suppliers step auto-completed successfully'); - } catch (supplierError) { - console.warn('⚠️ Could not auto-complete suppliers step:', supplierError); - // Don't fail the entire flow if suppliers step completion fails - } - } - - if (currentStep.id === 'completion') { - // Navigate to dashboard after completion - if (isNewTenant) { - // For new tenant creation, navigate to dashboard and remove the new param - navigate('/app/dashboard'); - } else { - navigate('/app'); - } - } else { - // Auto-advance to next step after successful completion - if (currentStepIndex < STEPS.length - 1) { - setCurrentStepIndex(currentStepIndex + 1); - } - } - } catch (error: any) { - console.error(`❌ Error completing step "${currentStep.id}":`, error); - - // Extract detailed error information - const errorMessage = error?.response?.data?.detail || error?.message || 'Unknown error'; - const statusCode = error?.response?.status; - - console.error(`📊 Error details: Status ${statusCode}, Message: ${errorMessage}`); - - // Handle different types of errors - if (statusCode === 207) { - // Multi-Status: Step updated but summary failed - console.warn(`⚠️ Partial success for step "${currentStep.id}": ${errorMessage}`); - - // Continue with step advancement since the actual step was completed - if (currentStep.id === 'completion') { - // Navigate to dashboard after completion - if (isNewTenant) { - navigate('/app/dashboard'); - } else { - navigate('/app'); - } - } else { - // Auto-advance to next step after successful completion - if (currentStepIndex < STEPS.length - 1) { - setCurrentStepIndex(currentStepIndex + 1); - } - } - - // Show a warning but don't block progress - console.warn(`Step "${currentStep.title}" completed with warnings: ${errorMessage}`); - return; // Don't show error alert - } - - // Check if it's a dependency error - if (errorMessage.includes('dependencies not met')) { - console.error('🚫 Dependencies not met for step:', currentStep.id); - - // Check what dependencies are missing - if (userProgress) { - console.log('📋 Current progress:', userProgress); - console.log('📋 Completed steps:', userProgress.steps.filter(s => s.completed).map(s => s.step_name)); - } - } - - // Don't advance automatically on real errors - user should see the issue - alert(`${t('onboarding:errors.step_failed', 'Error al completar paso')} "${currentStep.title}": ${errorMessage}`); - } - }; - - // Show loading state while initializing progress (skip for new tenant) - if (!isNewTenant && (isLoadingProgress || !isInitialized)) { - return ( -
- - -
-
-

{t('common:loading', 'Cargando tu progreso...')}

-
-
-
-
- ); - } - - // Show error state if progress fails to load (skip for new tenant) - if (!isNewTenant && progressError) { - return ( -
- - -
-
- - - -
-
-

- {t('onboarding:errors.network_error', 'Error al cargar progreso')} -

-

- {t('onboarding:errors.try_again', 'No pudimos cargar tu progreso de configuración. Puedes continuar desde el inicio.')} -

- -
-
-
-
-
- ); - } - - const StepComponent = currentStep.component; - - // Calculate progress percentage - reset for new tenant creation - const progressPercentage = isNewTenant - ? ((currentStepIndex + 1) / STEPS.length) * 100 // For new tenant, base progress only on current step - : userProgress?.completion_percentage || ((currentStepIndex + 1) / STEPS.length) * 100; - - return ( -
- {/* New Tenant Info Banner */} - {isNewTenant && ( - - -
-
- -
-
-

- {t('onboarding:wizard.title', 'Creando Nueva Organización')} -

-

- {t('onboarding:wizard.subtitle', 'Configurarás una nueva panadería desde cero. Este proceso es independiente de tus organizaciones existentes.')} -

-
-
-
-
- )} - - {/* Enhanced Progress Header */} - -
-
-

- {isNewTenant ? t('onboarding:wizard.title', 'Crear Nueva Organización') : t('onboarding:wizard.title', 'Bienvenido a Bakery IA')} -

-

- {isNewTenant - ? t('onboarding:wizard.subtitle', 'Configura tu nueva panadería desde cero') - : t('onboarding:wizard.subtitle', 'Configura tu sistema de gestión inteligente paso a paso') - } -

-
-
-
- {t('onboarding:wizard.progress.step_of', 'Paso {{current}} de {{total}}', { current: currentStepIndex + 1, total: STEPS.length })} -
-
- {Math.round(progressPercentage)}% {t('onboarding:wizard.progress.completed', 'completado')} - {isNewTenant && (nuevo)} -
-
-
- - {/* Progress Bar */} -
-
-
- - {/* Mobile Step Indicators - Horizontal scroll on small screens */} -
-
- {STEPS.map((step, index) => { - // For new tenant creation, only show completed if index is less than current step - const isCompleted = isNewTenant - ? index < currentStepIndex - : userProgress?.steps.find(s => s.step_name === step.id)?.completed || index < currentStepIndex; - const isCurrent = index === currentStepIndex; - - return ( -
-
- {isCompleted ? ( -
- - - -
- ) : isCurrent ? ( -
- {index + 1} -
- ) : ( -
- {index + 1} -
- )} -
-
- {step.title} -
-
- ); - })} -
-
- - {/* Desktop Step Indicators */} -
- {STEPS.map((step, index) => { - // For new tenant creation, only show completed if index is less than current step - const isCompleted = isNewTenant - ? index < currentStepIndex - : userProgress?.steps.find(s => s.step_name === step.id)?.completed || index < currentStepIndex; - const isCurrent = index === currentStepIndex; - - return ( -
-
- {isCompleted ? ( -
- - - -
- ) : isCurrent ? ( -
- {index + 1} -
- ) : ( -
- {index + 1} -
- )} -
-
- {step.title} -
-
- {step.description} -
-
- ); - })} -
- - - {/* Step Content */} - - -
-
-
- {currentStepIndex + 1} -
-
-
-

- {currentStep.title} -

-

- {currentStep.description} -

-
-
-
- - - {}} // No-op - steps must use onComplete instead - onPrevious={() => {}} // No-op - users cannot go back once they've moved forward - onComplete={handleStepComplete} - isFirstStep={currentStepIndex === 0} - isLastStep={currentStepIndex === STEPS.length - 1} - /> - -
-
- ); -}; \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/UnifiedOnboardingWizard.tsx b/frontend/src/components/domain/onboarding/UnifiedOnboardingWizard.tsx index 6f66728c..915df239 100644 --- a/frontend/src/components/domain/onboarding/UnifiedOnboardingWizard.tsx +++ b/frontend/src/components/domain/onboarding/UnifiedOnboardingWizard.tsx @@ -11,7 +11,8 @@ import { WizardProvider, useWizardContext, BakeryType, DataSource } from './cont import { BakeryTypeSelectionStep, RegisterTenantStep, - UploadSalesDataStep, + FileUploadStep, + InventoryReviewStep, ProductCategorizationStep, InitialStockEntryStep, ProductionProcessesStep, @@ -73,20 +74,20 @@ const OnboardingWizardContent: React.FC = () => { isConditional: true, condition: (ctx) => ctx.state.bakeryType !== null, }, - // Phase 2a: AI-Assisted Path (ONLY PATH NOW) + // Phase 2a: AI-Assisted Inventory Setup (REFACTORED - split into 3 focused steps) { - id: 'smart-inventory-setup', - title: t('onboarding:steps.smart_inventory.title', 'Subir Datos de Ventas'), - description: t('onboarding:steps.smart_inventory.description', 'Configuración con IA'), - component: UploadSalesDataStep, + id: 'upload-sales-data', + title: t('onboarding:steps.upload_sales.title', 'Subir Datos de Ventas'), + description: t('onboarding:steps.upload_sales.description', 'Cargar archivo con historial de ventas'), + component: FileUploadStep, isConditional: true, - condition: (ctx) => ctx.tenantId !== null, + condition: (ctx) => ctx.state.bakeryType !== null, // Tenant created after bakeryType is set }, { - id: 'product-categorization', - title: t('onboarding:steps.categorization.title', 'Categorizar Productos'), - description: t('onboarding:steps.categorization.description', 'Clasifica ingredientes vs productos'), - component: ProductCategorizationStep, + id: 'inventory-review', + title: t('onboarding:steps.inventory_review.title', 'Revisar Inventario'), + description: t('onboarding:steps.inventory_review.description', 'Confirmar productos detectados'), + component: InventoryReviewStep, isConditional: true, condition: (ctx) => ctx.state.aiAnalysisComplete, }, @@ -96,7 +97,7 @@ const OnboardingWizardContent: React.FC = () => { description: t('onboarding:steps.stock.description', 'Cantidades iniciales'), component: InitialStockEntryStep, isConditional: true, - condition: (ctx) => ctx.state.categorizationCompleted, + condition: (ctx) => ctx.state.inventoryReviewCompleted, }, { id: 'suppliers-setup', @@ -130,7 +131,7 @@ const OnboardingWizardContent: React.FC = () => { description: t('onboarding:steps.quality.description', 'Estándares de calidad'), component: QualitySetupStep, isConditional: true, - condition: (ctx) => ctx.tenantId !== null, + condition: (ctx) => ctx.state.bakeryType !== null, // Tenant created after bakeryType is set }, { id: 'team-setup', @@ -138,7 +139,7 @@ const OnboardingWizardContent: React.FC = () => { description: t('onboarding:steps.team.description', 'Miembros del equipo'), component: TeamSetupStep, isConditional: true, - condition: (ctx) => ctx.tenantId !== null, + condition: (ctx) => ctx.state.bakeryType !== null, // Tenant created after bakeryType is set }, // Phase 4: ML & Finalization { @@ -154,7 +155,7 @@ const OnboardingWizardContent: React.FC = () => { description: t('onboarding:steps.review.description', 'Confirma tu configuración'), component: ReviewSetupStep, isConditional: true, - condition: (ctx) => ctx.tenantId !== null, + condition: (ctx) => ctx.state.bakeryType !== null, // Tenant created after bakeryType is set }, { id: 'completion', @@ -182,7 +183,7 @@ const OnboardingWizardContent: React.FC = () => { }); return visibleSteps; - }, [wizardContext.state, wizardContext.tenantId]); + }, [wizardContext.state]); const isNewTenant = searchParams.get('new') === 'true'; const [currentStepIndex, setCurrentStepIndex] = useState(0); @@ -316,10 +317,15 @@ const OnboardingWizardContent: React.FC = () => { if (currentStep.id === 'data-source-choice' && data?.dataSource) { wizardContext.updateDataSource(data.dataSource as DataSource); } - if (currentStep.id === 'smart-inventory-setup' && data?.aiSuggestions) { + // REFACTORED: Handle new split steps for AI-assisted inventory + if (currentStep.id === 'upload-sales-data' && data?.aiSuggestions) { wizardContext.updateAISuggestions(data.aiSuggestions); + wizardContext.updateUploadedFile(data.uploadedFile, data.validationResult); wizardContext.setAIAnalysisComplete(true); } + if (currentStep.id === 'inventory-review') { + wizardContext.markStepComplete('inventoryReviewCompleted'); + } if (currentStep.id === 'product-categorization' && data?.categorizedProducts) { wizardContext.updateCategorizedProducts(data.categorizedProducts); wizardContext.markStepComplete('categorizationCompleted'); @@ -345,8 +351,8 @@ const OnboardingWizardContent: React.FC = () => { console.log(`✅ Successfully completed step: "${currentStep.id}"`); - // Special handling for smart-inventory-setup - if (currentStep.id === 'smart-inventory-setup' && data?.shouldAutoCompleteSuppliers) { + // Special handling for inventory-review - auto-complete suppliers if requested + if (currentStep.id === 'inventory-review' && data?.shouldAutoCompleteSuppliers) { try { console.log('🔄 Auto-completing suppliers-setup step...'); await markStepCompleted.mutateAsync({ @@ -452,12 +458,12 @@ const OnboardingWizardContent: React.FC = () => { : userProgress?.completion_percentage || ((currentStepIndex + 1) / VISIBLE_STEPS.length) * 100; return ( -
+
{/* Progress Header */} - -
+ +
-

+

{isNewTenant ? t('onboarding:wizard.title_new', 'Nueva Panadería') : t('onboarding:wizard.title', 'Configuración Inicial')}

@@ -488,9 +494,9 @@ const OnboardingWizardContent: React.FC = () => { {/* Step Content */} - -

-
+ +
+
{currentStepIndex + 1}
@@ -506,7 +512,7 @@ const OnboardingWizardContent: React.FC = () => {
- + {}} onPrevious={() => {}} @@ -515,6 +521,18 @@ const OnboardingWizardContent: React.FC = () => { isFirstStep={currentStepIndex === 0} isLastStep={currentStepIndex === VISIBLE_STEPS.length - 1} canContinue={canContinue} + initialData={ + // Pass AI data and file to InventoryReviewStep + currentStep.id === 'inventory-review' + ? { + uploadedFile: wizardContext.state.uploadedFile, + validationResult: wizardContext.state.uploadedFileValidation, + aiSuggestions: wizardContext.state.aiSuggestions, + uploadedFileName: wizardContext.state.uploadedFileName || '', + uploadedFileSize: wizardContext.state.uploadedFileSize || 0, + } + : undefined + } /> diff --git a/frontend/src/components/domain/onboarding/context/WizardContext.tsx b/frontend/src/components/domain/onboarding/context/WizardContext.tsx index 4e1ddb04..d8ea80e1 100644 --- a/frontend/src/components/domain/onboarding/context/WizardContext.tsx +++ b/frontend/src/components/domain/onboarding/context/WizardContext.tsx @@ -1,8 +1,11 @@ import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import type { ProductSuggestionResponse } from '../../../api/types/inventory'; +import type { ImportValidationResponse } from '../../../api/types/dataImport'; export type BakeryType = 'production' | 'retail' | 'mixed' | null; export type DataSource = 'ai-assisted' | 'manual' | null; +// Legacy AISuggestion type - kept for backward compatibility export interface AISuggestion { id: string; name: string; @@ -19,15 +22,18 @@ export interface WizardState { dataSource: DataSource; // AI-Assisted Path Data + uploadedFile?: File; // NEW: The actual file object needed for sales import API uploadedFileName?: string; uploadedFileSize?: number; - aiSuggestions: AISuggestion[]; + uploadedFileValidation?: ImportValidationResponse; // NEW: Validation result + aiSuggestions: ProductSuggestionResponse[]; // UPDATED: Use full ProductSuggestionResponse type aiAnalysisComplete: boolean; categorizedProducts?: any[]; // Products with type classification productsWithStock?: any[]; // Products with initial stock levels // Setup Progress categorizationCompleted: boolean; + inventoryReviewCompleted: boolean; // NEW: Tracks completion of InventoryReviewStep stockEntryCompleted: boolean; suppliersCompleted: boolean; inventoryCompleted: boolean; @@ -49,7 +55,8 @@ export interface WizardContextValue { state: WizardState; updateBakeryType: (type: BakeryType) => void; updateDataSource: (source: DataSource) => void; - updateAISuggestions: (suggestions: AISuggestion[]) => void; + updateAISuggestions: (suggestions: ProductSuggestionResponse[]) => void; // UPDATED type + updateUploadedFile: (file: File, validation: ImportValidationResponse) => void; // UPDATED: store file object and validation setAIAnalysisComplete: (complete: boolean) => void; updateCategorizedProducts: (products: any[]) => void; updateProductsWithStock: (products: any[]) => void; @@ -67,6 +74,7 @@ const initialState: WizardState = { categorizedProducts: undefined, productsWithStock: undefined, categorizationCompleted: false, + inventoryReviewCompleted: false, // NEW: Initially false stockEntryCompleted: false, suppliersCompleted: false, inventoryCompleted: false, @@ -118,10 +126,20 @@ export const WizardProvider: React.FC = ({ setState(prev => ({ ...prev, dataSource: source })); }; - const updateAISuggestions = (suggestions: AISuggestion[]) => { + const updateAISuggestions = (suggestions: ProductSuggestionResponse[]) => { setState(prev => ({ ...prev, aiSuggestions: suggestions })); }; + const updateUploadedFile = (file: File, validation: ImportValidationResponse) => { + setState(prev => ({ + ...prev, + uploadedFile: file, + uploadedFileName: file.name, + uploadedFileSize: file.size, + uploadedFileValidation: validation, + })); + }; + const setAIAnalysisComplete = (complete: boolean) => { setState(prev => ({ ...prev, aiAnalysisComplete: complete })); }; @@ -227,6 +245,7 @@ export const WizardProvider: React.FC = ({ updateBakeryType, updateDataSource, updateAISuggestions, + updateUploadedFile, setAIAnalysisComplete, updateCategorizedProducts, updateProductsWithStock, diff --git a/frontend/src/components/domain/onboarding/index.ts b/frontend/src/components/domain/onboarding/index.ts index c4b5de01..ca8ccccf 100644 --- a/frontend/src/components/domain/onboarding/index.ts +++ b/frontend/src/components/domain/onboarding/index.ts @@ -1,2 +1,3 @@ -export { OnboardingWizard } from './OnboardingWizard'; +// OnboardingWizard.tsx has been deleted - it was deprecated and unused +// All onboarding now uses UnifiedOnboardingWizard export { UnifiedOnboardingWizard } from './UnifiedOnboardingWizard'; \ No newline at end of file diff --git a/frontend/src/components/domain/onboarding/steps/BakeryTypeSelectionStep.tsx b/frontend/src/components/domain/onboarding/steps/BakeryTypeSelectionStep.tsx index f3794a8d..e4e2e85e 100644 --- a/frontend/src/components/domain/onboarding/steps/BakeryTypeSelectionStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/BakeryTypeSelectionStep.tsx @@ -118,13 +118,13 @@ export const BakeryTypeSelectionStep: React.FC = ( }; return ( -
+
{/* Header */} -
-

+
+

{t('onboarding:bakery_type.title', '¿Qué tipo de panadería tienes?')}

-

+

{t( 'onboarding:bakery_type.subtitle', 'Esto nos ayudará a personalizar la experiencia y mostrarte solo las funciones que necesitas' @@ -139,54 +139,60 @@ export const BakeryTypeSelectionStep: React.FC = ( const isHovered = hoveredType === type.id; return ( - handleSelectType(type.id)} onMouseEnter={() => setHoveredType(type.id)} onMouseLeave={() => setHoveredType(null)} + className={` + relative cursor-pointer transition-all duration-300 overflow-hidden + border-2 rounded-lg text-left w-full + bg-[var(--bg-secondary)] + ${isSelected + ? 'border-[var(--color-primary)] shadow-lg ring-2 ring-[var(--color-primary)]/50 scale-[1.02]' + : 'border-[var(--border-color)] hover:border-[var(--color-primary)]/50 hover:shadow-md' + } + ${isHovered && !isSelected ? 'shadow-sm' : ''} + `} > {/* Selection Indicator */} {isSelected && (

-
+
)} - {/* Gradient Background */} -
+ {/* Accent Background */} +
{/* Content */} -
+
{/* Icon & Title */} -
-
{type.icon}
-

+
+
{type.icon}
+

{type.name}

-

+

{type.description}

{/* Features */}
-

+

{t('onboarding:bakery_type.features_label', 'Características')}

    {type.features.map((feature, index) => (
  • - + {feature}
  • ))} @@ -194,15 +200,15 @@ export const BakeryTypeSelectionStep: React.FC = (
{/* Examples */} -
-

+
+

{t('onboarding:bakery_type.examples_label', 'Ejemplos')}

{type.examples.map((example, index) => ( {example} @@ -210,45 +216,23 @@ export const BakeryTypeSelectionStep: React.FC = (

- + ); })}

- {/* Help Text */} -
-

- {t( - 'onboarding:bakery_type.help_text', - '💡 No te preocupes, siempre puedes cambiar esto más tarde en la configuración' - )} -

- - {/* Continue Button */} -
- -
-
- {/* Additional Info */} {selectedType && ( -
+
-
+
{bakeryTypes.find(t => t.id === selectedType)?.icon}
-

+

{t('onboarding:bakery_type.selected_info_title', 'Perfecto para tu panadería')}

-

+

{selectedType === 'production' && t( 'onboarding:bakery_type.production.selected_info', @@ -269,6 +253,27 @@ export const BakeryTypeSelectionStep: React.FC = (

)} + + {/* Help Text & Continue Button */} +
+

+ {t( + 'onboarding:bakery_type.help_text', + '💡 No te preocupes, siempre puedes cambiar esto más tarde en la configuración' + )} +

+ +
+ +
+
); }; diff --git a/frontend/src/components/domain/onboarding/steps/FileUploadStep.tsx b/frontend/src/components/domain/onboarding/steps/FileUploadStep.tsx new file mode 100644 index 00000000..09ab06f0 --- /dev/null +++ b/frontend/src/components/domain/onboarding/steps/FileUploadStep.tsx @@ -0,0 +1,322 @@ +import React, { useState, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button } from '../../../ui/Button'; +import { useCurrentTenant } from '../../../../stores/tenant.store'; +import { useClassifyBatch } from '../../../../api/hooks/inventory'; +import { useValidateImportFile } from '../../../../api/hooks/sales'; +import type { ImportValidationResponse } from '../../../../api/types/dataImport'; +import type { ProductSuggestionResponse } from '../../../../api/types/inventory'; +import { Upload, FileText, AlertCircle, CheckCircle2, Loader2 } from 'lucide-react'; + +interface FileUploadStepProps { + onNext: () => void; + onPrevious: () => void; + onComplete: (data: { + uploadedFile: File; // NEW: Pass the file object for sales import + validationResult: ImportValidationResponse; + aiSuggestions: ProductSuggestionResponse[]; + uploadedFileName: string; + uploadedFileSize: number; + }) => void; + isFirstStep: boolean; + isLastStep: boolean; +} + +interface ProgressState { + stage: 'preparing' | 'validating' | 'analyzing' | 'classifying'; + progress: number; + message: string; +} + +export const FileUploadStep: React.FC = ({ + onComplete, + onPrevious, + isFirstStep +}) => { + const { t } = useTranslation(); + const [selectedFile, setSelectedFile] = useState(null); + const [isProcessing, setIsProcessing] = useState(false); + const [error, setError] = useState(''); + const [progressState, setProgressState] = useState(null); + const [showGuide, setShowGuide] = useState(false); + const fileInputRef = useRef(null); + + const currentTenant = useCurrentTenant(); + + // API hooks + const validateFileMutation = useValidateImportFile(); + const classifyBatchMutation = useClassifyBatch(); + + const handleFileSelect = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + setSelectedFile(file); + setError(''); + } + }; + + const handleDrop = async (event: React.DragEvent) => { + event.preventDefault(); + const file = event.dataTransfer.files[0]; + if (file) { + setSelectedFile(file); + setError(''); + } + }; + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault(); + }; + + const handleUploadAndProcess = async () => { + if (!selectedFile || !currentTenant?.id) return; + + setIsProcessing(true); + setError(''); + setProgressState({ + stage: 'preparing', + progress: 10, + message: t('onboarding:file_upload.preparing', 'Preparando archivo...') + }); + + try { + // Step 1: Validate the file + setProgressState({ + stage: 'validating', + progress: 30, + message: t('onboarding:file_upload.validating', 'Validando formato del archivo...') + }); + + const validationResult = await validateFileMutation.mutateAsync({ + tenantId: currentTenant.id, + file: selectedFile + }); + + if (!validationResult || validationResult.is_valid === undefined) { + throw new Error('Invalid validation response from server'); + } + + if (!validationResult.is_valid) { + const errorMsg = validationResult.errors?.join(', ') || 'Archivo inválido'; + throw new Error(errorMsg); + } + + // Step 2: Extract product list + setProgressState({ + stage: 'analyzing', + progress: 50, + message: t('onboarding:file_upload.analyzing', 'Analizando productos en el archivo...') + }); + + const products = validationResult.product_list?.map((productName: string) => ({ + product_name: productName + })) || []; + + if (products.length === 0) { + throw new Error(t('onboarding:file_upload.no_products', 'No se encontraron productos en el archivo')); + } + + // Step 3: AI Classification + setProgressState({ + stage: 'classifying', + progress: 75, + message: t('onboarding:file_upload.classifying', `Clasificando ${products.length} productos con IA...`) + }); + + const classificationResponse = await classifyBatchMutation.mutateAsync({ + tenantId: currentTenant.id, + products + }); + + // Step 4: Complete with success + setProgressState({ + stage: 'classifying', + progress: 100, + message: t('onboarding:file_upload.success', '¡Análisis completado!') + }); + + // Pass data to parent and move to next step + setTimeout(() => { + onComplete({ + uploadedFile: selectedFile, // NEW: Pass the file for sales import + validationResult, + aiSuggestions: classificationResponse.suggestions, + uploadedFileName: selectedFile.name, + uploadedFileSize: selectedFile.size, + }); + }, 500); + + } catch (err) { + console.error('Error processing file:', err); + setError(err instanceof Error ? err.message : 'Error procesando archivo'); + setProgressState(null); + setIsProcessing(false); + } + }; + + const handleRemoveFile = () => { + setSelectedFile(null); + setError(''); + setProgressState(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + return ( +
+ {/* Header */} +
+

+ {t('onboarding:file_upload.title', 'Subir Datos de Ventas')} +

+

+ {t('onboarding:file_upload.description', 'Sube un archivo con tus datos de ventas y nuestro sistema detectará automáticamente tus productos')} +

+
+ + {/* Why This Matters */} +
+

+ + {t('setup_wizard:why_this_matters', '¿Por qué es importante?')} +

+

+ {t('onboarding:file_upload.why', 'Analizaremos tus datos de ventas históricas para configurar automáticamente tu inventario inicial con inteligencia artificial, ahorrándote horas de trabajo manual.')} +

+
+ + {/* File Upload Area */} + {!selectedFile && !isProcessing && ( +
fileInputRef.current?.click()} + > + +

+ {t('onboarding:file_upload.drop_zone_title', 'Arrastra tu archivo aquí')} +

+

+ {t('onboarding:file_upload.drop_zone_subtitle', 'o haz clic para seleccionar')} +

+

+ {t('onboarding:file_upload.formats', 'Formatos soportados: CSV, JSON (máx. 10MB)')} +

+ +
+ )} + + {/* Selected File Preview */} + {selectedFile && !isProcessing && ( +
+
+
+ +
+

{selectedFile.name}

+

+ {(selectedFile.size / 1024).toFixed(2)} KB +

+
+
+ +
+
+ )} + + {/* Progress Indicator */} + {isProcessing && progressState && ( +
+
+ +
+

{progressState.message}

+
+
+
+
+
+

+ {progressState.progress}% completado +

+
+ )} + + {/* Error Display */} + {error && ( +
+
+ +
+

Error

+

{error}

+
+
+
+ )} + + {/* Help Guide Toggle */} + + + {showGuide && ( +
+

+ {t('onboarding:file_upload.guide_title', 'Formato requerido del archivo:')} +

+
    +
  • {t('onboarding:file_upload.guide_1', 'Columnas: Fecha, Producto, Cantidad')}
  • +
  • {t('onboarding:file_upload.guide_2', 'Formato de fecha: YYYY-MM-DD')}
  • +
  • {t('onboarding:file_upload.guide_3', 'Los nombres de productos deben ser consistentes')}
  • +
  • {t('onboarding:file_upload.guide_4', 'Ejemplo: 2024-01-15,Pan de Molde,25')}
  • +
+
+ )} + + {/* Navigation Buttons */} +
+ + +
+
+ ); +}; diff --git a/frontend/src/components/domain/onboarding/steps/InitialStockEntryStep.tsx b/frontend/src/components/domain/onboarding/steps/InitialStockEntryStep.tsx index 8fabde29..03c75fa8 100644 --- a/frontend/src/components/domain/onboarding/steps/InitialStockEntryStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/InitialStockEntryStep.tsx @@ -15,7 +15,7 @@ export interface ProductWithStock { } export interface InitialStockEntryStepProps { - products: ProductWithStock[]; + products?: ProductWithStock[]; // Made optional - will use empty array if not provided onUpdate?: (data: { productsWithStock: ProductWithStock[] }) => void; onComplete?: () => void; onPrevious?: () => void; @@ -36,6 +36,10 @@ export const InitialStockEntryStep: React.FC = ({ if (initialData?.productsWithStock) { return initialData.productsWithStock; } + // Handle case where initialProducts is undefined (shouldn't happen, but defensive) + if (!initialProducts || initialProducts.length === 0) { + return []; + } return initialProducts.map(p => ({ ...p, initialStock: p.initialStock ?? undefined, @@ -78,17 +82,37 @@ export const InitialStockEntryStep: React.FC = ({ const productsWithStock = products.filter(p => p.initialStock !== undefined && p.initialStock >= 0); const productsWithoutStock = products.filter(p => p.initialStock === undefined); - const completionPercentage = (productsWithStock.length / products.length) * 100; + const completionPercentage = products.length > 0 ? (productsWithStock.length / products.length) * 100 : 100; const allCompleted = productsWithoutStock.length === 0; + // If no products, show a skip message + if (products.length === 0) { + return ( +
+
+
+

+ {t('onboarding:stock.no_products_title', 'Stock Inicial')} +

+

+ {t('onboarding:stock.no_products_message', 'Podrás configurar los niveles de stock más tarde en la sección de inventario.')} +

+ +
+
+ ); + } + return ( -
+
{/* Header */} -
-

+
+

{t('onboarding:stock.title', 'Niveles de Stock Inicial')}

-

+

{t( 'onboarding:stock.subtitle', 'Ingresa las cantidades actuales de cada producto. Esto permite que el sistema rastree el inventario desde hoy.' @@ -133,11 +157,11 @@ export const InitialStockEntryStep: React.FC = ({

{/* Quick Actions */} -
- -
@@ -178,7 +202,7 @@ export const InitialStockEntryStep: React.FC = ({ placeholder="0" min="0" step="0.01" - className="w-24 text-right" + className="w-20 sm:w-24 text-right min-h-[44px]" /> {product.unit || 'kg'} diff --git a/frontend/src/components/domain/onboarding/steps/InventoryReviewStep.tsx b/frontend/src/components/domain/onboarding/steps/InventoryReviewStep.tsx new file mode 100644 index 00000000..b45dc82b --- /dev/null +++ b/frontend/src/components/domain/onboarding/steps/InventoryReviewStep.tsx @@ -0,0 +1,860 @@ +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 { useImportSalesData } from '../../../../api/hooks/sales'; +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'; + +interface InventoryReviewStepProps { + onNext: () => void; + onPrevious: () => void; + onComplete: (data: { + inventoryItemsCreated: number; + salesDataImported: boolean; + }) => void; + isFirstStep: boolean; + isLastStep: boolean; + initialData?: { + uploadedFile?: File; // NEW: File object for sales import + validationResult?: any; // NEW: Validation result + aiSuggestions: ProductSuggestionResponse[]; + uploadedFileName: string; + uploadedFileSize: number; + }; +} + +interface InventoryItemForm { + id: string; // Unique ID for UI tracking + name: string; + product_type: ProductType; + category: string; + unit_of_measure: UnitOfMeasure | string; + // AI suggestion metadata (if from AI) + isSuggested: boolean; + confidence_score?: number; + sales_data?: { + total_quantity: number; + average_daily_sales: number; + }; +} + +type FilterType = 'all' | 'ingredients' | 'finished_products'; + +// Template Definitions - Common Bakery Ingredients +interface TemplateItem { + name: string; + product_type: ProductType; + category: string; + unit_of_measure: UnitOfMeasure; +} + +interface IngredientTemplate { + id: string; + name: string; + description: string; + icon: string; + items: TemplateItem[]; +} + +const INGREDIENT_TEMPLATES: IngredientTemplate[] = [ + { + id: 'basic-bakery', + name: 'Ingredientes Básicos de Panadería', + description: 'Esenciales para cualquier panadería', + icon: '🍞', + items: [ + { name: 'Harina de Trigo', product_type: ProductType.INGREDIENT, category: IngredientCategory.FLOUR, unit_of_measure: UnitOfMeasure.KILOGRAMS }, + { name: 'Azúcar', product_type: ProductType.INGREDIENT, category: IngredientCategory.SUGAR, unit_of_measure: UnitOfMeasure.KILOGRAMS }, + { name: 'Sal', product_type: ProductType.INGREDIENT, category: IngredientCategory.SALT, unit_of_measure: UnitOfMeasure.KILOGRAMS }, + { name: 'Levadura Fresca', product_type: ProductType.INGREDIENT, category: IngredientCategory.YEAST, unit_of_measure: UnitOfMeasure.KILOGRAMS }, + { name: 'Agua', product_type: ProductType.INGREDIENT, category: IngredientCategory.OTHER, unit_of_measure: UnitOfMeasure.LITERS }, + ], + }, + { + id: 'pastry-essentials', + name: 'Esenciales para Pastelería', + description: 'Ingredientes para pasteles y postres', + icon: '🎂', + items: [ + { name: 'Huevos', product_type: ProductType.INGREDIENT, category: IngredientCategory.EGGS, unit_of_measure: UnitOfMeasure.UNITS }, + { name: 'Mantequilla', product_type: ProductType.INGREDIENT, category: IngredientCategory.FATS, unit_of_measure: UnitOfMeasure.KILOGRAMS }, + { name: 'Leche', product_type: ProductType.INGREDIENT, category: IngredientCategory.DAIRY, unit_of_measure: UnitOfMeasure.LITERS }, + { name: 'Vainilla', product_type: ProductType.INGREDIENT, category: IngredientCategory.SPICES, unit_of_measure: UnitOfMeasure.MILLILITERS }, + { name: 'Azúcar Glass', product_type: ProductType.INGREDIENT, category: IngredientCategory.SUGAR, unit_of_measure: UnitOfMeasure.KILOGRAMS }, + ], + }, + { + id: 'bread-basics', + name: 'Básicos para Pan Artesanal', + description: 'Todo lo necesario para pan artesanal', + icon: '🥖', + items: [ + { name: 'Harina Integral', product_type: ProductType.INGREDIENT, category: IngredientCategory.FLOUR, unit_of_measure: UnitOfMeasure.KILOGRAMS }, + { name: 'Masa Madre', product_type: ProductType.INGREDIENT, category: IngredientCategory.YEAST, unit_of_measure: UnitOfMeasure.KILOGRAMS }, + { name: 'Aceite de Oliva', product_type: ProductType.INGREDIENT, category: IngredientCategory.FATS, unit_of_measure: UnitOfMeasure.LITERS }, + { name: 'Semillas de Sésamo', product_type: ProductType.INGREDIENT, category: IngredientCategory.OTHER, unit_of_measure: UnitOfMeasure.KILOGRAMS }, + ], + }, + { + id: 'chocolate-specialties', + name: 'Especialidades de Chocolate', + description: 'Para productos con chocolate', + icon: '🍫', + items: [ + { name: 'Chocolate Negro', product_type: ProductType.INGREDIENT, category: IngredientCategory.OTHER, unit_of_measure: UnitOfMeasure.KILOGRAMS }, + { name: 'Cacao en Polvo', product_type: ProductType.INGREDIENT, category: IngredientCategory.OTHER, unit_of_measure: UnitOfMeasure.KILOGRAMS }, + { name: 'Chocolate con Leche', product_type: ProductType.INGREDIENT, category: IngredientCategory.OTHER, unit_of_measure: UnitOfMeasure.KILOGRAMS }, + { name: 'Crema de Avellanas', product_type: ProductType.INGREDIENT, category: IngredientCategory.OTHER, unit_of_measure: UnitOfMeasure.KILOGRAMS }, + ], + }, +]; + +export const InventoryReviewStep: React.FC = ({ + onComplete, + onPrevious, + isFirstStep, + initialData +}) => { + const { t } = useTranslation(); + const [inventoryItems, setInventoryItems] = useState([]); + const [activeFilter, setActiveFilter] = useState('all'); + const [isAdding, setIsAdding] = useState(false); + const [editingId, setEditingId] = useState(null); + const [formData, setFormData] = useState({ + id: '', + name: '', + product_type: ProductType.INGREDIENT, + category: '', + unit_of_measure: UnitOfMeasure.KILOGRAMS, + isSuggested: false, + }); + const [formErrors, setFormErrors] = useState>({}); + const [isSubmitting, setIsSubmitting] = useState(false); + + const currentTenant = useCurrentTenant(); + const tenantId = currentTenant?.id || ''; + + // API hooks + const createIngredientMutation = useCreateIngredient(); + const importSalesMutation = useImportSalesData(); + + // Initialize with AI suggestions + useEffect(() => { + if (initialData?.aiSuggestions) { + const items: InventoryItemForm[] = initialData.aiSuggestions.map((suggestion, index) => ({ + id: `ai-${index}-${Date.now()}`, + name: suggestion.suggested_name, + product_type: suggestion.product_type as ProductType, + category: suggestion.category, + unit_of_measure: suggestion.unit_of_measure as UnitOfMeasure, + isSuggested: true, + confidence_score: suggestion.confidence_score, + sales_data: suggestion.sales_data ? { + total_quantity: suggestion.sales_data.total_quantity, + average_daily_sales: suggestion.sales_data.average_daily_sales, + } : undefined, + })); + setInventoryItems(items); + } + }, [initialData]); + + // Filter items + const filteredItems = inventoryItems.filter(item => { + if (activeFilter === 'ingredients') return item.product_type === ProductType.INGREDIENT; + if (activeFilter === 'finished_products') return item.product_type === ProductType.FINISHED_PRODUCT; + return true; + }); + + // Count by type + const counts = { + all: inventoryItems.length, + ingredients: inventoryItems.filter(i => i.product_type === ProductType.INGREDIENT).length, + finished_products: inventoryItems.filter(i => i.product_type === ProductType.FINISHED_PRODUCT).length, + }; + + // Form handlers + const handleAdd = () => { + setFormData({ + id: `manual-${Date.now()}`, + name: '', + product_type: ProductType.INGREDIENT, + category: '', + unit_of_measure: UnitOfMeasure.KILOGRAMS, + isSuggested: false, + }); + setEditingId(null); + setIsAdding(true); + setFormErrors({}); + }; + + const handleEdit = (item: InventoryItemForm) => { + setFormData({ ...item }); + setEditingId(item.id); + setIsAdding(true); + setFormErrors({}); + }; + + const handleDelete = (id: string) => { + setInventoryItems(items => items.filter(item => item.id !== id)); + }; + + const validateForm = (): boolean => { + const errors: Record = {}; + + if (!formData.name.trim()) { + errors.name = t('validation:name_required', 'El nombre es requerido'); + } + + if (!formData.category) { + errors.category = t('validation:category_required', 'La categoría es requerida'); + } + + setFormErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleSave = () => { + if (!validateForm()) return; + + if (editingId) { + // Update existing + setInventoryItems(items => + items.map(item => (item.id === editingId ? formData : item)) + ); + } else { + // Add new + setInventoryItems(items => [...items, formData]); + } + + setIsAdding(false); + setEditingId(null); + setFormData({ + id: '', + name: '', + product_type: ProductType.INGREDIENT, + category: '', + unit_of_measure: UnitOfMeasure.KILOGRAMS, + isSuggested: false, + }); + }; + + const handleCancel = () => { + setIsAdding(false); + setEditingId(null); + setFormErrors({}); + }; + + const handleAddTemplate = (template: IngredientTemplate) => { + // Check for duplicates by name + const existingNames = new Set(inventoryItems.map(item => item.name.toLowerCase())); + + const newItems = template.items + .filter(item => !existingNames.has(item.name.toLowerCase())) + .map((item, index) => ({ + id: `template-${template.id}-${index}-${Date.now()}`, + name: item.name, + product_type: item.product_type, + category: item.category, + unit_of_measure: item.unit_of_measure, + isSuggested: false, + })); + + if (newItems.length > 0) { + setInventoryItems(items => [...items, ...newItems]); + } + }; + + const handleCompleteStep = async () => { + if (inventoryItems.length === 0) { + setFormErrors({ submit: t('validation:min_items', 'Agrega al menos 1 producto para continuar') }); + return; + } + + setIsSubmitting(true); + 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 + }))); + + const createPromises = inventoryItems.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; + }); + }); + + await Promise.all(createPromises); + console.log('✅ 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 { + console.log('📊 Importing sales data from file:', initialData.uploadedFileName); + await importSalesMutation.mutateAsync({ + tenantId, + file: initialData.uploadedFile, + }); + 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 + } + } + + // Complete the step with metadata + onComplete({ + inventoryItemsCreated: inventoryItems.length, + salesDataImported: salesImported, + }); + } catch (error) { + console.error('Error creating inventory items:', error); + setFormErrors({ submit: t('error:creating_items', 'Error al crear los productos. Inténtalo de nuevo.') }); + setIsSubmitting(false); + } + }; + + // Category options based on product type + const getCategoryOptions = (productType: ProductType) => { + if (productType === ProductType.INGREDIENT) { + return Object.values(IngredientCategory).map(cat => ({ + value: cat, + label: t(`inventory:enums.ingredient_category.${cat}`, cat) + })); + } else { + return Object.values(ProductCategory).map(cat => ({ + value: cat, + label: t(`inventory:enums.product_category.${cat}`, cat) + })); + } + }; + + const unitOptions = Object.values(UnitOfMeasure).map(unit => ({ + value: unit, + label: t(`inventory:enums.unit_of_measure.${unit}`, unit) + })); + + return ( +
+ {/* Header */} +
+

+ {t('onboarding:inventory_review.title', 'Revisar Inventario')} +

+

+ {t('onboarding:inventory_review.description', 'Revisa y ajusta los productos detectados. Puedes editar, eliminar o agregar más productos.')} +

+
+ + {/* Why This Matters */} +
+

+ + {t('setup_wizard:why_this_matters', '¿Por qué es importante?')} +

+

+ {t('onboarding:inventory_review.why', 'Estos productos serán la base de tu sistema. Diferenciamos entre Ingredientes (lo que usas para producir) y Productos Terminados (lo que vendes).')} +

+
+ + {/* Quick Add Templates */} +
+
+ +

+ {t('inventory:templates.title', 'Plantillas de Ingredientes')} +

+
+

+ {t('inventory:templates.description', 'Agrega ingredientes comunes con un solo clic. Solo se agregarán los que no tengas ya.')} +

+
+ {INGREDIENT_TEMPLATES.map((template) => ( + + ))} +
+
+ + {/* Filter Tabs */} +
+ + + +
+ + {/* Inventory List */} +
+ {filteredItems.length === 0 && ( +
+ {activeFilter === 'all' + ? t('inventory:empty_state', 'No hay productos. Agrega uno para comenzar.') + : t('inventory:no_results', 'No hay productos de este tipo.')} +
+ )} + + {filteredItems.map((item) => ( + + {/* Item Card */} +
+
+
+
+
{item.name}
+ + {/* Product Type Badge */} + + {item.product_type === ProductType.FINISHED_PRODUCT ? ( + + + {t('inventory:type.finished_product', 'Producto')} + + ) : ( + + + {t('inventory:type.ingredient', 'Ingrediente')} + + )} + + + {/* AI Suggested Badge */} + {item.isSuggested && item.confidence_score && ( + + IA {Math.round(item.confidence_score * 100)}% + + )} +
+ +
+ {item.product_type === ProductType.INGREDIENT ? t(`inventory:enums.ingredient_category.${item.category}`, item.category) : t(`inventory:enums.product_category.${item.category}`, item.category)} + + {t(`inventory:enums.unit_of_measure.${item.unit_of_measure}`, item.unit_of_measure)} + {item.sales_data && ( + <> + + {t('inventory:sales_avg', 'Ventas')}: {item.sales_data.average_daily_sales.toFixed(1)}/día + + )} +
+
+ + {/* Actions */} +
+ + +
+
+
+ + {/* Inline Edit Form - appears right below the card being edited */} + {editingId === item.id && ( +
+
+

+ {t('inventory:edit_item', 'Editar Producto')} +

+ +
+ +
+ {/* Product Type Selector */} +
+ +
+ + + +
+
+ + {/* Name */} +
+ + setFormData(prev => ({ ...prev, name: e.target.value }))} + className="w-full px-3 py-2 border border-[var(--border-color)] rounded-lg bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]" + placeholder={t('inventory:name_placeholder', 'Ej: Harina de trigo')} + /> + {formErrors.name && ( +

{formErrors.name}

+ )} +
+ + {/* Category */} +
+ + + {formErrors.category && ( +

{formErrors.category}

+ )} +
+ + {/* Unit of Measure */} +
+ + +
+ + {/* Form Actions */} +
+ +
+
+
+ )} +
+ ))} +
+ + {/* Add Button - hidden when adding or editing */} + {!isAdding && !editingId && ( + + )} + + {/* Add New Item Form - only shown when adding (not editing) */} + {isAdding && !editingId && ( +
+
+

+ {t('inventory:add_item', 'Agregar Producto')} +

+ +
+ +
+ {/* Product Type Selector */} +
+ +
+ + + +
+
+ + {/* Name */} +
+ + setFormData(prev => ({ ...prev, name: e.target.value }))} + className="w-full px-3 py-2 border border-[var(--border-color)] rounded-lg bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]" + placeholder={t('inventory:name_placeholder', 'Ej: Harina de trigo')} + /> + {formErrors.name && ( +

{formErrors.name}

+ )} +
+ + {/* Category */} +
+ + + {formErrors.category && ( +

{formErrors.category}

+ )} +
+ + {/* Unit of Measure */} +
+ + +
+ + {/* Form Actions */} +
+ +
+
+
+ )} + + {/* Submit Error */} + {formErrors.submit && ( +
+

{formErrors.submit}

+
+ )} + + {/* Summary */} +
+

+ {t('inventory:summary', 'Resumen')}: {counts.finished_products} {t('inventory:finished_products', 'productos terminados')}, {counts.ingredients} {t('inventory:ingredients_count', 'ingredientes')} +

+
+ + {/* Navigation */} +
+ + +
+
+ ); +}; diff --git a/frontend/src/components/domain/onboarding/steps/ProductionProcessesStep.tsx b/frontend/src/components/domain/onboarding/steps/ProductionProcessesStep.tsx index dc5042cc..93664dbe 100644 --- a/frontend/src/components/domain/onboarding/steps/ProductionProcessesStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/ProductionProcessesStep.tsx @@ -157,13 +157,13 @@ export const ProductionProcessesStep: React.FC = ( }; return ( -
+
{/* Header */}
-

+

{t('onboarding:processes.title', 'Procesos de Producción')}

-

+

{t( 'onboarding:processes.subtitle', 'Define los procesos que usas para transformar productos pre-elaborados en productos terminados' diff --git a/frontend/src/components/domain/onboarding/steps/RegisterTenantStep.tsx b/frontend/src/components/domain/onboarding/steps/RegisterTenantStep.tsx index eac665f3..f5fc0b6d 100644 --- a/frontend/src/components/domain/onboarding/steps/RegisterTenantStep.tsx +++ b/frontend/src/components/domain/onboarding/steps/RegisterTenantStep.tsx @@ -153,8 +153,8 @@ export const RegisterTenantStep: React.FC = ({ }; return ( -

-
+
+
; - minRequired?: number; // Minimum items to proceed - isOptional?: boolean; // Can be skipped - estimatedMinutes?: number; // For UI display - weight: number; // For progress calculation -} - -export interface SetupStepProps { - onNext: () => void; - onPrevious: () => void; - onComplete: (data?: any) => void; - onSkip?: () => void; - onUpdate?: (state: { itemsCount?: number; canContinue?: boolean }) => void; - isFirstStep: boolean; - isLastStep: boolean; - canContinue?: boolean; -} - -export const SetupWizard: React.FC = () => { - const { t } = useTranslation(); - const navigate = useNavigate(); - const { user } = useAuth(); - - // Define setup wizard steps (Steps 5-11 in overall onboarding) - const SETUP_STEPS: SetupStepConfig[] = [ - { - id: 'setup-welcome', - title: t('setup_wizard:steps.welcome.title', 'Welcome & Setup Overview'), - description: t('setup_wizard:steps.welcome.description', 'Let\'s set up your bakery operations'), - component: WelcomeStep, - isOptional: true, - estimatedMinutes: 2, - weight: STEP_WEIGHTS['setup-welcome'] - }, - { - id: 'suppliers-setup', - title: t('setup_wizard:steps.suppliers.title', 'Add Suppliers'), - description: t('setup_wizard:steps.suppliers.description', 'Your ingredient and material providers'), - component: SuppliersSetupStep, - minRequired: 1, - isOptional: false, - estimatedMinutes: 5, - weight: STEP_WEIGHTS['suppliers-setup'] - }, - { - id: 'inventory-items-setup', - title: t('setup_wizard:steps.inventory.title', 'Set Up Inventory Items'), - description: t('setup_wizard:steps.inventory.description', 'Ingredients and materials you use'), - component: InventorySetupStep, - minRequired: 3, - isOptional: false, - estimatedMinutes: 10, - weight: STEP_WEIGHTS['inventory-items-setup'] - }, - { - id: 'recipes-setup', - title: t('setup_wizard:steps.recipes.title', 'Create Recipes'), - description: t('setup_wizard:steps.recipes.description', 'Your bakery\'s production formulas'), - component: RecipesSetupStep, - minRequired: 1, - isOptional: false, - estimatedMinutes: 10, - weight: STEP_WEIGHTS['recipes-setup'] - }, - { - id: 'quality-setup', - title: t('setup_wizard:steps.quality.title', 'Define Quality Standards'), - description: t('setup_wizard:steps.quality.description', 'Standards for consistent production'), - component: QualitySetupStep, - minRequired: 2, - isOptional: true, - estimatedMinutes: 7, - weight: STEP_WEIGHTS['quality-setup'] - }, - { - id: 'team-setup', - title: t('setup_wizard:steps.team.title', 'Add Team Members'), - description: t('setup_wizard:steps.team.description', 'Your bakery staff'), - component: TeamSetupStep, - minRequired: 0, - isOptional: true, - estimatedMinutes: 5, - weight: STEP_WEIGHTS['team-setup'] - }, - { - id: 'setup-review', - title: t('setup_wizard:steps.review.title', 'Review Your Setup'), - description: t('setup_wizard:steps.review.description', 'Confirm your configuration'), - component: ReviewSetupStep, - isOptional: false, - estimatedMinutes: 2, - weight: STEP_WEIGHTS['setup-review'] - }, - { - id: 'setup-completion', - title: t('setup_wizard:steps.completion.title', 'You\'re All Set!'), - description: t('setup_wizard:steps.completion.description', 'Your bakery system is ready'), - component: CompletionStep, - isOptional: false, - estimatedMinutes: 2, - weight: STEP_WEIGHTS['setup-completion'] - } - ]; - - // State management - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isInitialized, setIsInitialized] = useState(false); - const [canContinue, setCanContinue] = useState(false); - - // Handle updates from step components - const handleStepUpdate = (state: { itemsCount?: number; canContinue?: boolean }) => { - if (state.canContinue !== undefined) { - setCanContinue(state.canContinue); - } - }; - - // Get user progress from backend - const { data: userProgress, isLoading: isLoadingProgress } = useUserProgress( - user?.id || '', - { enabled: !!user?.id } - ); - - const markStepCompleted = useMarkStepCompleted(); - - // Calculate weighted progress percentage - const calculateProgress = (): number => { - if (!userProgress) return 0; - - const totalWeight = Object.values(STEP_WEIGHTS).reduce((a, b) => a + b); - let completedWeight = 0; - - // Add weight of fully completed steps - SETUP_STEPS.forEach((step, index) => { - if (index < currentStepIndex) { - const stepProgress = userProgress.steps.find(s => s.step_name === step.id); - if (stepProgress?.completed) { - completedWeight += step.weight; - } - } - }); - - // Add 50% of current step weight (user is midway through) - const currentStep = SETUP_STEPS[currentStepIndex]; - completedWeight += currentStep.weight * 0.5; - - return Math.round((completedWeight / totalWeight) * 100); - }; - - const progressPercentage = calculateProgress(); - - // Initialize step index based on backend progress - useEffect(() => { - if (userProgress && !isInitialized) { - console.log('🔄 Initializing setup wizard progress:', userProgress); - - // Find first incomplete step - let stepIndex = 0; - for (let i = 0; i < SETUP_STEPS.length; i++) { - const step = SETUP_STEPS[i]; - const stepProgress = userProgress.steps.find(s => s.step_name === step.id); - - if (!stepProgress?.completed && stepProgress?.status !== 'skipped') { - stepIndex = i; - console.log(`📍 Resuming at step: "${step.id}" (index ${i})`); - break; - } - } - - // If all steps complete, go to last step - if (stepIndex === 0 && SETUP_STEPS.every(step => { - const stepProgress = userProgress.steps.find(s => s.step_name === step.id); - return stepProgress?.completed || stepProgress?.status === 'skipped'; - })) { - stepIndex = SETUP_STEPS.length - 1; - console.log('✅ All steps completed, going to completion step'); - } - - setCurrentStepIndex(stepIndex); - setIsInitialized(true); - } - }, [userProgress, isInitialized]); - - const currentStep = SETUP_STEPS[currentStepIndex]; - - // Navigation handlers - const handleNext = () => { - if (currentStepIndex < SETUP_STEPS.length - 1) { - setCurrentStepIndex(currentStepIndex + 1); - setCanContinue(false); // Reset for next step - } - }; - - const handlePrevious = () => { - if (currentStepIndex > 0) { - setCurrentStepIndex(currentStepIndex - 1); - } - }; - - const handleSkip = async () => { - if (!user?.id || !currentStep.isOptional) return; - - console.log(`⏭️ Skipping step: "${currentStep.id}"`); - - try { - // Mark step as skipped (not completed) - await markStepCompleted.mutateAsync({ - userId: user.id, - stepName: currentStep.id, - data: { - skipped: true, - skipped_at: new Date().toISOString() - } - }); - - console.log(`✅ Step "${currentStep.id}" marked as skipped`); - - // Move to next step - handleNext(); - } catch (error) { - console.error(`❌ Error skipping step "${currentStep.id}":`, error); - } - }; - - const handleStepComplete = async (data?: any) => { - if (!user?.id) { - console.error('User ID not available'); - return; - } - - // Prevent concurrent mutations - if (markStepCompleted.isPending) { - console.warn(`⚠️ Step completion already in progress for "${currentStep.id}"`); - return; - } - - console.log(`🎯 Completing step: "${currentStep.id}" with data:`, data); - - try { - // Mark step as completed in backend - await markStepCompleted.mutateAsync({ - userId: user.id, - stepName: currentStep.id, - data: { - ...data, - completed_at: new Date().toISOString() - } - }); - - console.log(`✅ Successfully completed step: "${currentStep.id}"`); - - // Handle completion step navigation - if (currentStep.id === 'setup-completion') { - console.log('🎉 Setup wizard completed! Navigating to dashboard...'); - navigate('/app/dashboard'); - } else { - // Auto-advance to next step - handleNext(); - } - } catch (error: any) { - console.error(`❌ Error completing step "${currentStep.id}":`, error); - - const errorMessage = error?.response?.data?.detail || error?.message || 'Unknown error'; - alert(`${t('setup_wizard:errors.step_failed', 'Error completing step')} "${currentStep.title}": ${errorMessage}`); - } - }; - - // Show loading state while initializing - if (isLoadingProgress || !isInitialized) { - return ( -
- - -
-
-

- {t('common:loading', 'Loading your setup progress...')} -

-
-
-
-
- ); - } - - const StepComponent = currentStep.component; - - return ( -
- {/* Progress Header */} - - - {/* Step Content */} - - -
-
-
- {currentStepIndex + 1} -
-
-
-

- {currentStep.title} -

-

- {currentStep.description} -

-
- {currentStep.estimatedMinutes && ( -
- ⏱️ ~{currentStep.estimatedMinutes} min -
- )} -
-
- - - - -
-
- ); -}; diff --git a/frontend/src/components/domain/setup-wizard/index.ts b/frontend/src/components/domain/setup-wizard/index.ts index 37cdb60d..b7978dd2 100644 --- a/frontend/src/components/domain/setup-wizard/index.ts +++ b/frontend/src/components/domain/setup-wizard/index.ts @@ -1,4 +1,4 @@ -export { SetupWizard } from './SetupWizard'; -export type { SetupStepConfig, SetupStepProps } from './SetupWizard'; +// SetupWizard.tsx has been deleted - setup is now integrated into UnifiedOnboardingWizard +// Individual setup steps are still used by UnifiedOnboardingWizard export * from './steps'; export * from './components'; diff --git a/frontend/src/pages/onboarding/OnboardingPage.tsx b/frontend/src/pages/onboarding/OnboardingPage.tsx index 7f0f2c5a..2ad06928 100644 --- a/frontend/src/pages/onboarding/OnboardingPage.tsx +++ b/frontend/src/pages/onboarding/OnboardingPage.tsx @@ -14,8 +14,8 @@ const OnboardingPage: React.FC = () => { variant: "minimal" }} > -
-
+
+
diff --git a/frontend/src/pages/setup/SetupPage.tsx b/frontend/src/pages/setup/SetupPage.tsx deleted file mode 100644 index a88b7605..00000000 --- a/frontend/src/pages/setup/SetupPage.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { SetupWizard } from '../../components/domain/setup-wizard'; - -/** - * Setup Page - Wrapper for the Setup Wizard - * This page is accessed after completing the initial onboarding - * and guides users through setting up their bakery operations - * (suppliers, inventory, recipes, quality standards, team) - */ -const SetupPage: React.FC = () => { - return ( -
- -
- ); -}; - -export default SetupPage; diff --git a/frontend/src/router/AppRouter.tsx b/frontend/src/router/AppRouter.tsx index d08ee9e5..e6f295ff 100644 --- a/frontend/src/router/AppRouter.tsx +++ b/frontend/src/router/AppRouter.tsx @@ -57,9 +57,8 @@ const ModelsConfigPage = React.lazy(() => import('../pages/app/database/models/M const QualityTemplatesPage = React.lazy(() => import('../pages/app/database/quality-templates/QualityTemplatesPage')); const SustainabilityPage = React.lazy(() => import('../pages/app/database/sustainability/SustainabilityPage')); -// Onboarding & Setup pages +// Onboarding page (Setup is now integrated into UnifiedOnboardingWizard) const OnboardingPage = React.lazy(() => import('../pages/onboarding/OnboardingPage')); -const SetupPage = React.lazy(() => import('../pages/setup/SetupPage')); export const AppRouter: React.FC = () => { return ( @@ -389,17 +388,7 @@ export const AppRouter: React.FC = () => { } /> - {/* Setup Wizard Route - Protected with AppShell */} - - - - - - } - /> + {/* Setup is now integrated into UnifiedOnboardingWizard */} {/* Default redirects */} } /> diff --git a/frontend/src/router/routes.config.ts b/frontend/src/router/routes.config.ts index c4e456c8..2a4d0b65 100644 --- a/frontend/src/router/routes.config.ts +++ b/frontend/src/router/routes.config.ts @@ -165,9 +165,8 @@ export const ROUTES = { HELP_SUPPORT: '/help/support', HELP_FEEDBACK: '/help/feedback', - // Onboarding & Setup + // Onboarding (Setup is now integrated into UnifiedOnboardingWizard) ONBOARDING: '/app/onboarding', - SETUP: '/app/setup', // Error pages NOT_FOUND: '/404', @@ -575,22 +574,7 @@ export const routesConfig: RouteConfig[] = [ }, }, - // Setup Wizard - Bakery operations setup (post-onboarding) - { - path: '/app/setup', - name: 'Setup', - component: 'SetupPage', - title: 'Configurar Operaciones', - description: 'Configure suppliers, inventory, recipes, and quality standards', - icon: 'settings', - requiresAuth: true, - showInNavigation: false, - meta: { - hideHeader: false, // Show header for easy navigation - hideSidebar: false, // Show sidebar for context - fullScreen: false, - }, - }, + // Setup is now integrated into UnifiedOnboardingWizard - route removed // Error pages diff --git a/services/auth/app/api/onboarding_progress.py b/services/auth/app/api/onboarding_progress.py index dfcd905f..5c037758 100644 --- a/services/auth/app/api/onboarding_progress.py +++ b/services/auth/app/api/onboarding_progress.py @@ -49,12 +49,15 @@ ONBOARDING_STEPS = [ # Phase 2: Core Setup "setup", # Basic bakery setup and tenant creation - # Phase 2a: AI-Assisted Path (ONLY PATH - manual path removed) - "smart-inventory-setup", # Sales data upload and AI analysis - "product-categorization", # Categorize products as ingredients vs finished products + # Phase 2a: AI-Assisted Inventory Setup (REFACTORED - split into 3 focused steps) + "upload-sales-data", # File upload, validation, and AI classification + "inventory-review", # Review and confirm AI-detected products with type selection "initial-stock-entry", # Capture initial stock levels - # Phase 2b: Suppliers (shared by all paths) + # Phase 2b: Product Categorization (optional advanced categorization) + "product-categorization", # Advanced categorization (may be deprecated) + + # Phase 2c: Suppliers (shared by all paths) "suppliers-setup", # Suppliers configuration # Phase 3: Advanced Configuration (all optional) @@ -78,13 +81,16 @@ STEP_DEPENDENCIES = { # Core setup - no longer depends on data-source-choice (removed) "setup": ["user_registered", "bakery-type-selection"], - # AI-Assisted path dependencies (ONLY path now) - "smart-inventory-setup": ["user_registered", "setup"], - "product-categorization": ["user_registered", "setup", "smart-inventory-setup"], - "initial-stock-entry": ["user_registered", "setup", "smart-inventory-setup", "product-categorization"], + # AI-Assisted Inventory Setup - REFACTORED into 3 sequential steps + "upload-sales-data": ["user_registered", "setup"], + "inventory-review": ["user_registered", "setup", "upload-sales-data"], + "initial-stock-entry": ["user_registered", "setup", "upload-sales-data", "inventory-review"], - # Suppliers (after AI inventory setup) - "suppliers-setup": ["user_registered", "setup", "smart-inventory-setup"], + # Advanced product categorization (optional, may be deprecated) + "product-categorization": ["user_registered", "setup", "upload-sales-data"], + + # Suppliers (after inventory review) + "suppliers-setup": ["user_registered", "setup", "inventory-review"], # Advanced configuration (optional, minimal dependencies) "recipes-setup": ["user_registered", "setup"], @@ -92,8 +98,8 @@ STEP_DEPENDENCIES = { "quality-setup": ["user_registered", "setup"], "team-setup": ["user_registered", "setup"], - # ML Training - requires AI path completion - "ml-training": ["user_registered", "setup", "smart-inventory-setup"], + # ML Training - requires AI path completion (upload-sales-data with inventory review) + "ml-training": ["user_registered", "setup", "upload-sales-data", "inventory-review"], # Review and completion "setup-review": ["user_registered", "setup"], @@ -277,20 +283,24 @@ class OnboardingService: # SPECIAL VALIDATION FOR ML TRAINING STEP if step_name == "ml-training": - # ML training requires AI-assisted path completion (only path available now) - ai_path_complete = user_progress_data.get("smart-inventory-setup", {}).get("completed", False) + # ML training requires AI-assisted path completion + # Check if upload-sales-data and inventory-review are completed + upload_complete = user_progress_data.get("upload-sales-data", {}).get("completed", False) + inventory_complete = user_progress_data.get("inventory-review", {}).get("completed", False) - if ai_path_complete: + if upload_complete and inventory_complete: # Validate sales data was imported - smart_inventory_data = user_progress_data.get("smart-inventory-setup", {}).get("data", {}) - sales_import_result = smart_inventory_data.get("salesImportResult", {}) - has_sales_data_imported = ( - sales_import_result.get("records_created", 0) > 0 or - sales_import_result.get("success", False) or - sales_import_result.get("imported", False) + upload_data = user_progress_data.get("upload-sales-data", {}).get("data", {}) + inventory_data = user_progress_data.get("inventory-review", {}).get("data", {}) + + # Check if sales data was processed + has_sales_data = ( + upload_data.get("validationResult", {}).get("is_valid", False) or + upload_data.get("aiSuggestions", []) or + inventory_data.get("inventoryItemsCreated", 0) > 0 ) - if has_sales_data_imported: + if has_sales_data: logger.info(f"ML training allowed for user {user_id}: AI path with sales data") return True diff --git a/services/inventory/app/models/inventory.py b/services/inventory/app/models/inventory.py index 0e81cae2..9edb0c22 100644 --- a/services/inventory/app/models/inventory.py +++ b/services/inventory/app/models/inventory.py @@ -114,10 +114,11 @@ class Ingredient(Base): last_purchase_price = Column(Numeric(10, 2), nullable=True) standard_cost = Column(Numeric(10, 2), nullable=True) - # Stock management - low_stock_threshold = Column(Float, nullable=False, default=10.0) - reorder_point = Column(Float, nullable=False, default=20.0) - reorder_quantity = Column(Float, nullable=False, default=50.0) + # Stock management - now optional to simplify onboarding + # These can be configured later based on actual usage patterns + low_stock_threshold = Column(Float, nullable=True, default=None) + reorder_point = Column(Float, nullable=True, default=None) + reorder_quantity = Column(Float, nullable=True, default=None) max_stock_level = Column(Float, nullable=True) # Shelf life (critical for finished products) - default values only diff --git a/services/inventory/app/schemas/inventory.py b/services/inventory/app/schemas/inventory.py index db7d8bf0..b3f8c17a 100644 --- a/services/inventory/app/schemas/inventory.py +++ b/services/inventory/app/schemas/inventory.py @@ -46,12 +46,14 @@ class IngredientCreate(InventoryBaseSchema): # Pricing # Note: average_cost is calculated automatically from purchases (not set on create) + # All cost fields are optional - can be added later after onboarding standard_cost: Optional[Decimal] = Field(None, ge=0, description="Standard/target cost per unit for budgeting") - - # Stock management - low_stock_threshold: float = Field(10.0, ge=0, description="Low stock alert threshold") - reorder_point: float = Field(20.0, ge=0, description="Reorder point") - reorder_quantity: float = Field(50.0, gt=0, description="Default reorder quantity") + + # Stock management - all optional with sensible defaults for onboarding + # These can be configured later based on actual usage patterns + low_stock_threshold: Optional[float] = Field(None, ge=0, description="Low stock alert threshold") + reorder_point: Optional[float] = Field(None, ge=0, description="Reorder point") + reorder_quantity: Optional[float] = Field(None, gt=0, description="Default reorder quantity") max_stock_level: Optional[float] = Field(None, gt=0, description="Maximum stock level") # Shelf life (default value only - actual per batch) @@ -67,8 +69,15 @@ class IngredientCreate(InventoryBaseSchema): @validator('reorder_point') def validate_reorder_point(cls, v, values): - if 'low_stock_threshold' in values and v <= values['low_stock_threshold']: - raise ValueError('Reorder point must be greater than low stock threshold') + # Only validate if both values are provided and not None + low_stock = values.get('low_stock_threshold') + if v is not None and low_stock is not None: + try: + if v <= low_stock: + raise ValueError('Reorder point must be greater than low stock threshold') + except TypeError: + # Skip validation if comparison fails due to type mismatch + pass return v @@ -125,9 +134,9 @@ class IngredientResponse(InventoryBaseSchema): average_cost: Optional[float] last_purchase_price: Optional[float] standard_cost: Optional[float] - low_stock_threshold: float - reorder_point: float - reorder_quantity: float + low_stock_threshold: Optional[float] # Now optional + reorder_point: Optional[float] # Now optional + reorder_quantity: Optional[float] # Now optional max_stock_level: Optional[float] shelf_life_days: Optional[int] # Default value only is_active: bool @@ -209,9 +218,15 @@ class StockCreate(InventoryBaseSchema): @validator('storage_temperature_max') def validate_temperature_range(cls, v, values): + # Only validate if both values are provided and not None min_temp = values.get('storage_temperature_min') - if v is not None and min_temp is not None and v <= min_temp: - raise ValueError('Max temperature must be greater than min temperature') + if v is not None and min_temp is not None: + try: + if v <= min_temp: + raise ValueError('Max temperature must be greater than min temperature') + except TypeError: + # Skip validation if comparison fails due to type mismatch + pass return v class StockUpdate(InventoryBaseSchema): diff --git a/services/inventory/app/services/inventory_service.py b/services/inventory/app/services/inventory_service.py index 4aa5cc85..6b18a0cc 100644 --- a/services/inventory/app/services/inventory_service.py +++ b/services/inventory/app/services/inventory_service.py @@ -1062,9 +1062,12 @@ class InventoryService: async def _validate_ingredient_data(self, ingredient_data: IngredientCreate, tenant_id: UUID): """Validate ingredient data for business rules""" - # Add business validation logic here - if ingredient_data.reorder_point <= ingredient_data.low_stock_threshold: - raise ValueError("Reorder point must be greater than low stock threshold") + # Only validate reorder_point if both values are provided + # During onboarding, these fields may be None, which is valid + if (ingredient_data.reorder_point is not None and + ingredient_data.low_stock_threshold is not None): + if ingredient_data.reorder_point <= ingredient_data.low_stock_threshold: + raise ValueError("Reorder point must be greater than low stock threshold") # Storage requirements validation moved to stock level (not ingredient level) # This is now handled in stock creation/update validation diff --git a/services/inventory/migrations/versions/20251108_1200_make_stock_fields_nullable.py b/services/inventory/migrations/versions/20251108_1200_make_stock_fields_nullable.py new file mode 100644 index 00000000..26e916a0 --- /dev/null +++ b/services/inventory/migrations/versions/20251108_1200_make_stock_fields_nullable.py @@ -0,0 +1,84 @@ +"""make_stock_management_fields_nullable + +Revision ID: make_stock_fields_nullable +Revises: add_local_production_support +Create Date: 2025-11-08 12:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'make_stock_fields_nullable' +down_revision = 'add_local_production_support' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Make stock management fields nullable to simplify onboarding + + These fields (low_stock_threshold, reorder_point, reorder_quantity) are now optional + during onboarding and can be configured later based on actual usage patterns. + """ + + # Make low_stock_threshold nullable + op.alter_column('ingredients', 'low_stock_threshold', + existing_type=sa.Float(), + nullable=True, + existing_nullable=False) + + # Make reorder_point nullable + op.alter_column('ingredients', 'reorder_point', + existing_type=sa.Float(), + nullable=True, + existing_nullable=False) + + # Make reorder_quantity nullable + op.alter_column('ingredients', 'reorder_quantity', + existing_type=sa.Float(), + nullable=True, + existing_nullable=False) + + +def downgrade() -> None: + """Revert stock management fields to NOT NULL + + WARNING: This will fail if any records have NULL values in these fields. + You must set default values before running this downgrade. + """ + + # Set default values for any NULL records before making fields NOT NULL + op.execute(""" + UPDATE ingredients + SET low_stock_threshold = 10.0 + WHERE low_stock_threshold IS NULL + """) + + op.execute(""" + UPDATE ingredients + SET reorder_point = 20.0 + WHERE reorder_point IS NULL + """) + + op.execute(""" + UPDATE ingredients + SET reorder_quantity = 50.0 + WHERE reorder_quantity IS NULL + """) + + # Make fields NOT NULL again + op.alter_column('ingredients', 'low_stock_threshold', + existing_type=sa.Float(), + nullable=False, + existing_nullable=True) + + op.alter_column('ingredients', 'reorder_point', + existing_type=sa.Float(), + nullable=False, + existing_nullable=True) + + op.alter_column('ingredients', 'reorder_quantity', + existing_type=sa.Float(), + nullable=False, + existing_nullable=True) diff --git a/todo.md b/todo.md new file mode 100644 index 00000000..57f15cc3 --- /dev/null +++ b/todo.md @@ -0,0 +1,57 @@ +Role + +Act as a senior product strategist and UX designer with deep expertise in Jobs To Be Done (JTBD) methodology and user-centered design. You think in terms of user goals, progress-making moments, and unmet needs — similar to approaches used at companies like Intercom, Basecamp, or IDEO. + +Context + +You are helping a product team break down a broad user or business problem into a structured map of Jobs To Be Done. This decomposition will guide discovery, prioritization, and solution design. + +Task & Instructions + +The user needs to add new any type of new contet to the bakery in the easiest posible way. + +Thebakery has those items: + +1- Add new ingredients to the bakery inventory list +2- Add new suppliers to the bakery supplier list +3- Add new recipe to the bakery recipe list +4- Add new equipment to the bakery equipment list +5- Add new quality templates to the bakery quality templates list +6- Add new customer orders to the order list +7- Add new customers to the customer list +8- Add new team member to the bakery team +9- Add new sales records to teh bakery + +The idea is to have a unique button that opens and wizard of step by step modal. The first step must to answer the queston of what teh user needs to add from the given list of 9 items. + +Each item , once selected, must have a component that ows the setp-by step guide for that particular item. + +1- The inventory must have teh following steps: select which type of inventory ingredient or finished product-> add must-have conetnt to that ingredients-> add initial lot or lots to the stock with the critical content needed. +2- The supplier step must contain the step to include supplier information-> ingredient that lhis supplie povides form teh inventory and the price in which you buy it and the minimun order quantities. +3- The recipe must have an step by step guide with the recipe information -> the ingredient need from teh inventory list-> aand the quality templates that apply to this recipes +4- the equipoment steps. +5- Thecustomer orders steps +6- The customers add steps. +7- Add team member steps. +8- Add qualite tempalte configuration steps. +9- Add manual entry of sales or upload of file for chunch upload step-> and then the step to add the sales. This is very crtitical as most the the small bakaery may do not have a POS system and collect the sales manually or in and excel file. So , in order to keep our system consuming the new historical and current sales, this add sales step is very important to design it porperly form teh UX and UI prespective. + + +The individual button that nowdays exist in the project in each page as inventory, sypplier, equipment, must open the new step -by step component related to taht particular block. + + +Use JTBD thinking to uncover: + +The main functional job the user is trying to get done; +Related emotional or social jobs; +Sub-jobs or tasks users must complete along the way; +Forces of progress and barriers that influence behavior. + +Checkpoints Before finalizing, check yourself: + +Are the jobs clearly goal-oriented and not solution-oriented? +Are sub-jobs specific steps toward the main job? +Are emotional/social jobs captured? +Are user struggles or unmet needs listed? + +If anything’s missing or unclear, revise and explain what was added or changed. \ No newline at end of file