From 61376b7a9f2b39713b91a2b0b018fc46492ff3cf Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Fri, 24 Oct 2025 13:05:04 +0200 Subject: [PATCH] Improve the frontend and fix TODOs --- Tiltfile | 211 +-- Tiltfile.secure | 541 -------- docs/DASHBOARD_JTBD_ANALYSIS.md | 1165 +++++++++++++++++ .../SUSTAINABILITY_COMPLETE_IMPLEMENTATION.md | 40 +- frontend/src/api/hooks/inventory.ts | 20 +- frontend/src/api/hooks/production.ts | 38 +- frontend/src/api/hooks/tenant.ts | 35 +- frontend/src/api/index.ts | 1 + frontend/src/api/services/production.ts | 19 + frontend/src/api/services/tenant.ts | 15 +- frontend/src/api/types/tenant.ts | 31 +- .../domain/auth/PasswordResetForm.tsx | 70 +- .../production/CreateQualityTemplateModal.tsx | 47 +- .../production/EditQualityTemplateModal.tsx | 763 +++++------ .../production/QualityTemplateManager.tsx | 50 +- .../production/ViewQualityTemplateModal.tsx | 331 +++++ .../domain/team/AddTeamMemberModal.tsx | 200 ++- .../src/components/layout/Sidebar/Sidebar.tsx | 92 +- frontend/src/locales/en/common.json | 56 +- frontend/src/locales/en/landing.json | 20 +- frontend/src/locales/en/production.json | 18 + frontend/src/locales/en/settings.json | 157 ++- frontend/src/locales/en/suppliers.json | 24 +- frontend/src/locales/en/sustainability.json | 52 +- frontend/src/locales/es/common.json | 80 +- frontend/src/locales/es/landing.json | 20 +- frontend/src/locales/es/production.json | 18 + frontend/src/locales/es/settings.json | 157 ++- frontend/src/locales/es/suppliers.json | 24 +- frontend/src/locales/es/sustainability.json | 52 +- frontend/src/locales/eu/common.json | 56 +- frontend/src/locales/eu/landing.json | 20 +- frontend/src/locales/eu/production.json | 21 + frontend/src/locales/eu/settings.json | 157 ++- frontend/src/locales/eu/suppliers.json | 105 +- frontend/src/locales/eu/sustainability.json | 52 +- frontend/src/locales/index.ts | 10 +- .../analytics/ProcurementAnalyticsPage.tsx | 32 +- .../database/information/InformationPage.tsx | 577 -------- .../operations/inventory/InventoryPage.tsx | 11 +- .../operations/production/ProductionPage.tsx | 54 +- .../operations/suppliers/SuppliersPage.tsx | 132 +- .../settings/bakery/BakerySettingsPage.tsx | 734 +++++++++++ .../CommunicationPreferencesPage.tsx | 50 - .../personal-info/PersonalInfoPage.tsx | 393 ------ .../settings/privacy/PrivacySettingsPage.tsx | 547 -------- .../src/pages/app/settings/privacy/index.ts | 2 - .../profile/NewProfileSettingsPage.tsx | 799 +++++++++++ .../app/settings/profile/ProfilePage.tsx | 2 +- .../src/pages/app/settings/team/TeamPage.tsx | 160 ++- frontend/src/pages/public/LandingPage.tsx | 80 +- frontend/src/router/AppRouter.tsx | 58 +- frontend/src/router/routes.config.ts | 58 +- gateway/app/routes/tenant.py | 16 +- .../base/cronjobs/demo-cleanup-cronjob.yaml | 2 +- services/auth/app/api/users.py | 251 +++- services/auth/app/schemas/users.py | 25 +- services/demo_session/requirements.txt | 1 + .../app/api/scenario_operations.py | 15 +- services/inventory/app/api/internal_demo.py | 211 ++- services/inventory/app/api/stock_entries.py | 165 ++- services/inventory/app/models/inventory.py | 6 +- .../app/repositories/stock_repository.py | 25 +- .../inventory/app/schemas/sustainability.py | 11 + .../app/services/dashboard_service.py | 200 ++- .../app/services/inventory_alert_service.py | 22 +- .../app/services/inventory_service.py | 37 +- .../app/services/sustainability_service.py | 69 +- ...fcea67bf4e_initial_schema_20251015_1229.py | 2 +- .../inventory/scripts/demo/seed_demo_stock.py | 532 +++++++- .../app/api/notification_operations.py | 45 +- services/orders/app/api/internal_demo.py | 51 +- services/pos/app/api/internal_demo.py | 47 +- services/production/app/api/internal_demo.py | 52 +- .../app/api/production_operations.py | 50 +- services/production/app/models/production.py | 4 + .../production_batch_repository.py | 22 +- .../app/services/production_service.py | 40 +- .../app/services/quality_template_service.py | 30 +- ...20251023_0900_add_waste_tracking_fields.py | 51 + .../scripts/demo/lotes_produccion_es.json | 61 +- .../scripts/demo/seed_demo_batches.py | 2 + services/recipes/app/api/internal_demo.py | 82 +- services/recipes/app/api/recipes.py | 56 +- services/sales/app/api/internal_demo.py | 41 +- services/suppliers/app/api/analytics.py | 70 +- services/suppliers/app/api/internal_demo.py | 50 +- services/suppliers/app/api/suppliers.py | 6 +- .../app/services/dashboard_service.py | 132 +- .../app/services/purchase_order_service.py | 56 +- services/tenant/app/api/tenant_members.py | 114 +- services/tenant/app/api/tenant_operations.py | 89 +- .../repositories/tenant_member_repository.py | 120 +- services/tenant/app/schemas/tenants.py | 74 +- .../services/subscription_limit_service.py | 19 +- .../tenant/app/services/tenant_service.py | 16 +- shared/__init__.py | 1 + shared/auth/decorators.py | 92 +- shared/clients/auth_client.py | 53 +- shared/clients/notification_client.py | 130 ++ 100 files changed, 8284 insertions(+), 3419 deletions(-) delete mode 100644 Tiltfile.secure create mode 100644 docs/DASHBOARD_JTBD_ANALYSIS.md create mode 100644 frontend/src/components/domain/production/ViewQualityTemplateModal.tsx delete mode 100644 frontend/src/pages/app/database/information/InformationPage.tsx create mode 100644 frontend/src/pages/app/settings/bakery/BakerySettingsPage.tsx delete mode 100644 frontend/src/pages/app/settings/communication-preferences/CommunicationPreferencesPage.tsx delete mode 100644 frontend/src/pages/app/settings/personal-info/PersonalInfoPage.tsx delete mode 100644 frontend/src/pages/app/settings/privacy/PrivacySettingsPage.tsx delete mode 100644 frontend/src/pages/app/settings/privacy/index.ts create mode 100644 frontend/src/pages/app/settings/profile/NewProfileSettingsPage.tsx create mode 100644 services/production/migrations/versions/20251023_0900_add_waste_tracking_fields.py create mode 100644 shared/__init__.py create mode 100644 shared/clients/notification_client.py diff --git a/Tiltfile b/Tiltfile index 96e724b3..ac717aad 100644 --- a/Tiltfile +++ b/Tiltfile @@ -1,6 +1,41 @@ -# Tiltfile for Bakery IA - Local Development -# This replaces Skaffold for faster, smarter local Kubernetes development +# Tiltfile for Bakery IA - Secure Local Development +# Includes TLS encryption, strong passwords, PVCs, and audit logging +# ============================================================================= +# SECURITY SETUP +# ============================================================================= +print(""" +====================================== +πŸ” Bakery IA Secure Development Mode +====================================== + +Security Features: + βœ… TLS encryption for PostgreSQL and Redis + βœ… Strong 32-character passwords + βœ… PersistentVolumeClaims (no data loss) + βœ… pgcrypto extension for encryption + βœ… PostgreSQL audit logging + +Applying security configurations... +""") + +# Apply security configurations before loading main manifests +local_resource('security-setup', + cmd=''' + echo "πŸ“¦ Applying security secrets and configurations..." + kubectl apply -f infrastructure/kubernetes/base/secrets.yaml + kubectl apply -f infrastructure/kubernetes/base/secrets/postgres-tls-secret.yaml + kubectl apply -f infrastructure/kubernetes/base/secrets/redis-tls-secret.yaml + kubectl apply -f infrastructure/kubernetes/base/configs/postgres-init-config.yaml + kubectl apply -f infrastructure/kubernetes/base/configmaps/postgres-logging-config.yaml + echo "βœ… Security configurations applied" + ''', + labels=['security'], + auto_init=True) + +# ============================================================================= +# LOAD KUBERNETES MANIFESTS +# ============================================================================= # Load Kubernetes manifests using Kustomize k8s_yaml(kustomize('infrastructure/kubernetes/overlays/dev')) @@ -74,7 +109,7 @@ def build_python_service(service_name, service_path): # Sync service code sync('./services/' + service_path, '/app'), - # Sync shared libraries + # Sync shared libraries (includes updated TLS connection code) sync('./shared', '/app/shared'), # Sync scripts @@ -123,25 +158,73 @@ build_python_service('demo-session-service', 'demo_session') # RESOURCE DEPENDENCIES & ORDERING # ============================================================================= -# Databases and infrastructure should start first -k8s_resource('auth-db', labels=['databases']) -k8s_resource('tenant-db', labels=['databases']) -k8s_resource('training-db', labels=['databases']) -k8s_resource('forecasting-db', labels=['databases']) -k8s_resource('sales-db', labels=['databases']) -k8s_resource('external-db', labels=['databases']) -k8s_resource('notification-db', labels=['databases']) -k8s_resource('inventory-db', labels=['databases']) -k8s_resource('recipes-db', labels=['databases']) -k8s_resource('suppliers-db', labels=['databases']) -k8s_resource('pos-db', labels=['databases']) -k8s_resource('orders-db', labels=['databases']) -k8s_resource('production-db', labels=['databases']) -k8s_resource('demo-session-db', labels=['databases']) +# Security setup must complete before databases start +k8s_resource('auth-db', resource_deps=['security-setup'], labels=['databases']) +k8s_resource('tenant-db', resource_deps=['security-setup'], labels=['databases']) +k8s_resource('training-db', resource_deps=['security-setup'], labels=['databases']) +k8s_resource('forecasting-db', resource_deps=['security-setup'], labels=['databases']) +k8s_resource('sales-db', resource_deps=['security-setup'], labels=['databases']) +k8s_resource('external-db', resource_deps=['security-setup'], labels=['databases']) +k8s_resource('notification-db', resource_deps=['security-setup'], labels=['databases']) +k8s_resource('inventory-db', resource_deps=['security-setup'], labels=['databases']) +k8s_resource('recipes-db', resource_deps=['security-setup'], labels=['databases']) +k8s_resource('suppliers-db', resource_deps=['security-setup'], labels=['databases']) +k8s_resource('pos-db', resource_deps=['security-setup'], labels=['databases']) +k8s_resource('orders-db', resource_deps=['security-setup'], labels=['databases']) +k8s_resource('production-db', resource_deps=['security-setup'], labels=['databases']) +k8s_resource('alert-processor-db', resource_deps=['security-setup'], labels=['databases']) +k8s_resource('demo-session-db', resource_deps=['security-setup'], labels=['databases']) -k8s_resource('redis', labels=['infrastructure']) +k8s_resource('redis', resource_deps=['security-setup'], labels=['infrastructure']) k8s_resource('rabbitmq', labels=['infrastructure']) +# Verify TLS certificates are mounted correctly +local_resource('verify-tls', + cmd=''' + echo "πŸ” Verifying TLS configuration..." + sleep 5 # Wait for pods to be ready + + # Check if auth-db pod exists and has TLS certs + AUTH_POD=$(kubectl get pods -n bakery-ia -l app.kubernetes.io/name=auth-db -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") + + if [ -n "$AUTH_POD" ]; then + echo " Checking PostgreSQL TLS certificates..." + kubectl exec -n bakery-ia "$AUTH_POD" -- ls -la /tls/ 2>/dev/null && \ + echo " βœ… PostgreSQL TLS certificates mounted" || \ + echo " ⚠️ PostgreSQL TLS certificates not found (pods may still be starting)" + fi + + # Check if redis pod exists and has TLS certs + REDIS_POD=$(kubectl get pods -n bakery-ia -l app.kubernetes.io/name=redis -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") + + if [ -n "$REDIS_POD" ]; then + echo " Checking Redis TLS certificates..." + kubectl exec -n bakery-ia "$REDIS_POD" -- ls -la /tls/ 2>/dev/null && \ + echo " βœ… Redis TLS certificates mounted" || \ + echo " ⚠️ Redis TLS certificates not found (pods may still be starting)" + fi + + echo "βœ… TLS verification complete" + ''', + resource_deps=['auth-db', 'redis'], + auto_init=True, + trigger_mode=TRIGGER_MODE_MANUAL, + labels=['security']) + +# Verify PVCs are bound +local_resource('verify-pvcs', + cmd=''' + echo "πŸ” Verifying PersistentVolumeClaims..." + kubectl get pvc -n bakery-ia | grep -E "NAME|db-pvc" || echo " ⚠️ PVCs not yet bound" + PVC_COUNT=$(kubectl get pvc -n bakery-ia -o json | jq '.items | length') + echo " Found $PVC_COUNT PVCs" + echo "βœ… PVC verification complete" + ''', + resource_deps=['auth-db'], + auto_init=True, + trigger_mode=TRIGGER_MODE_MANUAL, + labels=['security']) + # Nominatim geocoding service (excluded in dev via kustomize patches) # Uncomment these if you want to test nominatim locally # k8s_resource('nominatim', @@ -178,33 +261,10 @@ k8s_resource('production-migration', resource_deps=['production-db'], labels=['m k8s_resource('alert-processor-migration', resource_deps=['alert-processor-db'], labels=['migrations']) k8s_resource('demo-session-migration', resource_deps=['demo-session-db'], labels=['migrations']) -# Alert processor DB -k8s_resource('alert-processor-db', labels=['databases']) - # ============================================================================= # DEMO INITIALIZATION JOBS # ============================================================================= -# Demo seed jobs run in strict order to ensure data consistency across services: -# -# Helm Hook Weight Order (5-40): -# Weight 5: demo-seed-users β†’ Creates demo user accounts (with staff) in auth service -# Weight 10: demo-seed-tenants β†’ Creates demo tenant records (depends on users) -# Weight 15: demo-seed-tenant-members β†’ Links staff users to tenants (depends on users & tenants) -# Weight 10: demo-seed-subscriptions β†’ Creates enterprise subscriptions for demo tenants -# Weight 15: demo-seed-inventory β†’ Creates ingredients & finished products (depends on tenants) -# Weight 15: demo-seed-recipes β†’ Creates recipes using ingredient IDs (depends on inventory) -# Weight 15: demo-seed-suppliers β†’ Creates suppliers with price lists for ingredients (depends on inventory) -# Weight 21: demo-seed-purchase-orders β†’ Creates demo POs in various states (depends on suppliers) -# Weight 15: demo-seed-sales β†’ Creates historical sales data using finished product IDs (depends on inventory) -# Weight 15: demo-seed-ai-models β†’ Creates fake AI model entries (depends on inventory) -# Weight 20: demo-seed-stock β†’ Creates stock batches with expiration dates (depends on inventory) -# Weight 22: demo-seed-quality-templates β†’ Creates quality check templates (depends on production migration) -# Weight 25: demo-seed-customers β†’ Creates customer records (depends on orders migration) -# Weight 25: demo-seed-equipment β†’ Creates production equipment (depends on production migration) -# Weight 30: demo-seed-production-batches β†’ Creates production batches (depends on recipes, equipment) -# Weight 30: demo-seed-orders β†’ Creates orders with line items (depends on customers) -# Weight 35: demo-seed-procurement β†’ Creates procurement plans (depends on orders migration) -# Weight 40: demo-seed-forecasts β†’ Creates demand forecasts (depends on forecasting migration) +# Demo seed jobs run in strict order to ensure data consistency across services # Weight 5: Seed users (auth service) - includes staff users k8s_resource('demo-seed-users', @@ -246,11 +306,6 @@ k8s_resource('demo-seed-suppliers', resource_deps=['suppliers-migration', 'demo-seed-inventory'], labels=['demo-init']) -# Weight 21: Seed purchase orders (uses suppliers and demonstrates auto-approval workflow) -k8s_resource('demo-seed-purchase-orders', - resource_deps=['suppliers-migration', 'demo-seed-suppliers'], - labels=['demo-init']) - # Weight 15: Seed sales (uses finished product IDs from inventory) k8s_resource('demo-seed-sales', resource_deps=['sales-migration', 'demo-seed-inventory'], @@ -296,11 +351,6 @@ k8s_resource('demo-seed-procurement', resource_deps=['orders-migration', 'demo-seed-tenants'], labels=['demo-init']) -# Weight 35: Seed POS configurations (pos service) -k8s_resource('demo-seed-pos-configs', - resource_deps=['pos-migration', 'demo-seed-tenants'], - labels=['demo-init']) - # Weight 40: Seed demand forecasts (forecasting service) k8s_resource('demo-seed-forecasts', resource_deps=['forecasting-migration', 'demo-seed-tenants'], @@ -367,17 +417,11 @@ k8s_resource('alert-processor-service', resource_deps=['alert-processor-migration', 'redis', 'rabbitmq'], labels=['services']) -k8s_resource('alert-processor-api', - resource_deps=['alert-processor-migration', 'redis'], - labels=['services']) - k8s_resource('demo-session-service', resource_deps=['demo-session-migration', 'redis'], labels=['services']) # Apply environment variable patch to demo-session-service with the inventory image -# Note: This fetches the CURRENT image tag dynamically when the resource runs -# Runs after both services are deployed to ensure correct image tag is used local_resource('patch-demo-session-env', cmd=''' # Wait a moment for deployments to stabilize @@ -391,16 +435,13 @@ local_resource('patch-demo-session-env', echo "βœ… Set CLONE_JOB_IMAGE to: $INVENTORY_IMAGE" ''', - resource_deps=['demo-session-service', 'inventory-service'], # Wait for BOTH services - auto_init=True, # Run automatically on Tilt startup + resource_deps=['demo-session-service', 'inventory-service'], + auto_init=True, labels=['config']) # ============================================================================= # DATA INITIALIZATION JOBS (External Service v2.0) # ============================================================================= -# External data initialization job loads 24 months of historical data -# This should run AFTER external migration but BEFORE external-service starts - k8s_resource('external-data-init', resource_deps=['external-migration', 'redis'], labels=['data-init']) @@ -408,12 +449,10 @@ k8s_resource('external-data-init', # ============================================================================= # CRONJOBS # ============================================================================= - k8s_resource('demo-session-cleanup', resource_deps=['demo-session-service'], labels=['cronjobs']) -# External data rotation cronjob (runs monthly on 1st at 2am UTC) k8s_resource('external-data-rotation', resource_deps=['external-service'], labels=['cronjobs']) @@ -421,9 +460,6 @@ k8s_resource('external-data-rotation', # ============================================================================= # GATEWAY & FRONTEND # ============================================================================= -# Gateway and Frontend depend on services being ready -# Access via ingress: http://localhost (frontend) and http://localhost/api (gateway) - k8s_resource('gateway', resource_deps=['auth-service'], labels=['frontend']) @@ -463,14 +499,43 @@ watch_settings( '**/.coverage', '**/dist/**', '**/build/**', - '**/*.egg-info/**' + '**/*.egg-info/**', + # Ignore TLS certificate files (don't trigger rebuilds) + '**/infrastructure/tls/**/*.pem', + '**/infrastructure/tls/**/*.cnf', + '**/infrastructure/tls/**/*.csr', + '**/infrastructure/tls/**/*.srl', ] ) +# Print security status on startup +print(""" +βœ… Security setup complete! + +Database Security Features Active: + πŸ” TLS encryption: PostgreSQL and Redis + πŸ”‘ Strong passwords: 32-character cryptographic + πŸ’Ύ Persistent storage: PVCs for all databases + πŸ”’ Column encryption: pgcrypto extension + πŸ“‹ Audit logging: PostgreSQL query logging + +Access your application: + Frontend: http://localhost:3000 (or via ingress) + Gateway: http://localhost:8000 (or via ingress) + +Verify security: + kubectl get pvc -n bakery-ia + kubectl get secrets -n bakery-ia | grep tls + kubectl logs -n bakery-ia | grep SSL + +Security documentation: + docs/SECURITY_IMPLEMENTATION_COMPLETE.md + docs/DATABASE_SECURITY_ANALYSIS_REPORT.md + +====================================== +""") + # Optimize for local development -# - Automatically stream logs from services with errors -# - Group resources by labels for better organization -# # Note: You may see "too many open files" warnings on macOS with many services. # This is a Kind/Kubernetes limitation and doesn't affect service functionality. # To work on specific services only, use: tilt up diff --git a/Tiltfile.secure b/Tiltfile.secure deleted file mode 100644 index ac717aad..00000000 --- a/Tiltfile.secure +++ /dev/null @@ -1,541 +0,0 @@ -# Tiltfile for Bakery IA - Secure Local Development -# Includes TLS encryption, strong passwords, PVCs, and audit logging - -# ============================================================================= -# SECURITY SETUP -# ============================================================================= -print(""" -====================================== -πŸ” Bakery IA Secure Development Mode -====================================== - -Security Features: - βœ… TLS encryption for PostgreSQL and Redis - βœ… Strong 32-character passwords - βœ… PersistentVolumeClaims (no data loss) - βœ… pgcrypto extension for encryption - βœ… PostgreSQL audit logging - -Applying security configurations... -""") - -# Apply security configurations before loading main manifests -local_resource('security-setup', - cmd=''' - echo "πŸ“¦ Applying security secrets and configurations..." - kubectl apply -f infrastructure/kubernetes/base/secrets.yaml - kubectl apply -f infrastructure/kubernetes/base/secrets/postgres-tls-secret.yaml - kubectl apply -f infrastructure/kubernetes/base/secrets/redis-tls-secret.yaml - kubectl apply -f infrastructure/kubernetes/base/configs/postgres-init-config.yaml - kubectl apply -f infrastructure/kubernetes/base/configmaps/postgres-logging-config.yaml - echo "βœ… Security configurations applied" - ''', - labels=['security'], - auto_init=True) - -# ============================================================================= -# LOAD KUBERNETES MANIFESTS -# ============================================================================= -# Load Kubernetes manifests using Kustomize -k8s_yaml(kustomize('infrastructure/kubernetes/overlays/dev')) - -# No registry needed for local development - images are built locally - -# Common live update configuration for Python FastAPI services -def python_live_update(service_name, service_path): - return sync(service_path, '/app') - -# ============================================================================= -# FRONTEND (React + Vite + Nginx) -# ============================================================================= -docker_build( - 'bakery/dashboard', - context='./frontend', - dockerfile='./frontend/Dockerfile.kubernetes', - # Note: Frontend is a multi-stage build with nginx, live updates are limited - # For true hot-reload during frontend development, consider running Vite locally - # and using Telepresence to connect to the cluster - live_update=[ - # Sync source changes (limited usefulness due to nginx serving static files) - sync('./frontend/src', '/app/src'), - sync('./frontend/public', '/app/public'), - ] -) - -# ============================================================================= -# GATEWAY -# ============================================================================= -docker_build( - 'bakery/gateway', - context='.', - dockerfile='./gateway/Dockerfile', - live_update=[ - # Fall back to full rebuild if Dockerfile or requirements change - fall_back_on(['./gateway/Dockerfile', './gateway/requirements.txt']), - - # Sync Python code changes - sync('./gateway', '/app'), - sync('./shared', '/app/shared'), - - # Restart on Python file changes - run('kill -HUP 1', trigger=['./gateway/**/*.py', './shared/**/*.py']), - ], - # Ignore common patterns that don't require rebuilds - ignore=[ - '.git', - '**/__pycache__', - '**/*.pyc', - '**/.pytest_cache', - '**/node_modules', - '**/.DS_Store' - ] -) - -# ============================================================================= -# MICROSERVICES - Python FastAPI Services -# ============================================================================= - -# Helper function to create docker build with live updates for Python services -def build_python_service(service_name, service_path): - docker_build( - 'bakery/' + service_name, - context='.', - dockerfile='./services/' + service_path + '/Dockerfile', - live_update=[ - # Fall back to full image build if Dockerfile or requirements change - fall_back_on(['./services/' + service_path + '/Dockerfile', - './services/' + service_path + '/requirements.txt']), - - # Sync service code - sync('./services/' + service_path, '/app'), - - # Sync shared libraries (includes updated TLS connection code) - sync('./shared', '/app/shared'), - - # Sync scripts - sync('./scripts', '/app/scripts'), - - # Install new dependencies if requirements.txt changes - run('pip install --no-cache-dir -r requirements.txt', - trigger=['./services/' + service_path + '/requirements.txt']), - - # Restart uvicorn on Python file changes (HUP signal triggers graceful reload) - run('kill -HUP 1', - trigger=[ - './services/' + service_path + '/**/*.py', - './shared/**/*.py' - ]), - ], - # Ignore common patterns that don't require rebuilds - ignore=[ - '.git', - '**/__pycache__', - '**/*.pyc', - '**/.pytest_cache', - '**/node_modules', - '**/.DS_Store' - ] - ) - -# Build all microservices -build_python_service('auth-service', 'auth') -build_python_service('tenant-service', 'tenant') -build_python_service('training-service', 'training') -build_python_service('forecasting-service', 'forecasting') -build_python_service('sales-service', 'sales') -build_python_service('external-service', 'external') -build_python_service('notification-service', 'notification') -build_python_service('inventory-service', 'inventory') -build_python_service('recipes-service', 'recipes') -build_python_service('suppliers-service', 'suppliers') -build_python_service('pos-service', 'pos') -build_python_service('orders-service', 'orders') -build_python_service('production-service', 'production') -build_python_service('alert-processor', 'alert_processor') -build_python_service('demo-session-service', 'demo_session') - -# ============================================================================= -# RESOURCE DEPENDENCIES & ORDERING -# ============================================================================= - -# Security setup must complete before databases start -k8s_resource('auth-db', resource_deps=['security-setup'], labels=['databases']) -k8s_resource('tenant-db', resource_deps=['security-setup'], labels=['databases']) -k8s_resource('training-db', resource_deps=['security-setup'], labels=['databases']) -k8s_resource('forecasting-db', resource_deps=['security-setup'], labels=['databases']) -k8s_resource('sales-db', resource_deps=['security-setup'], labels=['databases']) -k8s_resource('external-db', resource_deps=['security-setup'], labels=['databases']) -k8s_resource('notification-db', resource_deps=['security-setup'], labels=['databases']) -k8s_resource('inventory-db', resource_deps=['security-setup'], labels=['databases']) -k8s_resource('recipes-db', resource_deps=['security-setup'], labels=['databases']) -k8s_resource('suppliers-db', resource_deps=['security-setup'], labels=['databases']) -k8s_resource('pos-db', resource_deps=['security-setup'], labels=['databases']) -k8s_resource('orders-db', resource_deps=['security-setup'], labels=['databases']) -k8s_resource('production-db', resource_deps=['security-setup'], labels=['databases']) -k8s_resource('alert-processor-db', resource_deps=['security-setup'], labels=['databases']) -k8s_resource('demo-session-db', resource_deps=['security-setup'], labels=['databases']) - -k8s_resource('redis', resource_deps=['security-setup'], labels=['infrastructure']) -k8s_resource('rabbitmq', labels=['infrastructure']) - -# Verify TLS certificates are mounted correctly -local_resource('verify-tls', - cmd=''' - echo "πŸ” Verifying TLS configuration..." - sleep 5 # Wait for pods to be ready - - # Check if auth-db pod exists and has TLS certs - AUTH_POD=$(kubectl get pods -n bakery-ia -l app.kubernetes.io/name=auth-db -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") - - if [ -n "$AUTH_POD" ]; then - echo " Checking PostgreSQL TLS certificates..." - kubectl exec -n bakery-ia "$AUTH_POD" -- ls -la /tls/ 2>/dev/null && \ - echo " βœ… PostgreSQL TLS certificates mounted" || \ - echo " ⚠️ PostgreSQL TLS certificates not found (pods may still be starting)" - fi - - # Check if redis pod exists and has TLS certs - REDIS_POD=$(kubectl get pods -n bakery-ia -l app.kubernetes.io/name=redis -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") - - if [ -n "$REDIS_POD" ]; then - echo " Checking Redis TLS certificates..." - kubectl exec -n bakery-ia "$REDIS_POD" -- ls -la /tls/ 2>/dev/null && \ - echo " βœ… Redis TLS certificates mounted" || \ - echo " ⚠️ Redis TLS certificates not found (pods may still be starting)" - fi - - echo "βœ… TLS verification complete" - ''', - resource_deps=['auth-db', 'redis'], - auto_init=True, - trigger_mode=TRIGGER_MODE_MANUAL, - labels=['security']) - -# Verify PVCs are bound -local_resource('verify-pvcs', - cmd=''' - echo "πŸ” Verifying PersistentVolumeClaims..." - kubectl get pvc -n bakery-ia | grep -E "NAME|db-pvc" || echo " ⚠️ PVCs not yet bound" - PVC_COUNT=$(kubectl get pvc -n bakery-ia -o json | jq '.items | length') - echo " Found $PVC_COUNT PVCs" - echo "βœ… PVC verification complete" - ''', - resource_deps=['auth-db'], - auto_init=True, - trigger_mode=TRIGGER_MODE_MANUAL, - labels=['security']) - -# Nominatim geocoding service (excluded in dev via kustomize patches) -# Uncomment these if you want to test nominatim locally -# k8s_resource('nominatim', -# resource_deps=['nominatim-init'], -# labels=['infrastructure']) -# k8s_resource('nominatim-init', -# labels=['data-init']) - -# Monitoring stack -#k8s_resource('prometheus', -# labels=['monitoring']) - -#k8s_resource('grafana', -# resource_deps=['prometheus'], -# labels=['monitoring']) - -#k8s_resource('jaeger', -# labels=['monitoring']) - -# Migration jobs depend on databases -k8s_resource('auth-migration', resource_deps=['auth-db'], labels=['migrations']) -k8s_resource('tenant-migration', resource_deps=['tenant-db'], labels=['migrations']) -k8s_resource('training-migration', resource_deps=['training-db'], labels=['migrations']) -k8s_resource('forecasting-migration', resource_deps=['forecasting-db'], labels=['migrations']) -k8s_resource('sales-migration', resource_deps=['sales-db'], labels=['migrations']) -k8s_resource('external-migration', resource_deps=['external-db'], labels=['migrations']) -k8s_resource('notification-migration', resource_deps=['notification-db'], labels=['migrations']) -k8s_resource('inventory-migration', resource_deps=['inventory-db'], labels=['migrations']) -k8s_resource('recipes-migration', resource_deps=['recipes-db'], labels=['migrations']) -k8s_resource('suppliers-migration', resource_deps=['suppliers-db'], labels=['migrations']) -k8s_resource('pos-migration', resource_deps=['pos-db'], labels=['migrations']) -k8s_resource('orders-migration', resource_deps=['orders-db'], labels=['migrations']) -k8s_resource('production-migration', resource_deps=['production-db'], labels=['migrations']) -k8s_resource('alert-processor-migration', resource_deps=['alert-processor-db'], labels=['migrations']) -k8s_resource('demo-session-migration', resource_deps=['demo-session-db'], labels=['migrations']) - -# ============================================================================= -# DEMO INITIALIZATION JOBS -# ============================================================================= -# Demo seed jobs run in strict order to ensure data consistency across services - -# Weight 5: Seed users (auth service) - includes staff users -k8s_resource('demo-seed-users', - resource_deps=['auth-migration'], - labels=['demo-init']) - -# Weight 10: Seed tenants (tenant service) -k8s_resource('demo-seed-tenants', - resource_deps=['tenant-migration', 'demo-seed-users'], - labels=['demo-init']) - -# Weight 15: Seed tenant members (links staff users to tenants) -k8s_resource('demo-seed-tenant-members', - resource_deps=['tenant-migration', 'demo-seed-tenants', 'demo-seed-users'], - labels=['demo-init']) - -# Weight 10: Seed subscriptions (creates enterprise subscriptions for demo tenants) -k8s_resource('demo-seed-subscriptions', - resource_deps=['tenant-migration', 'demo-seed-tenants'], - labels=['demo-init']) - -# Seed pilot coupon (runs after tenant migration) -k8s_resource('tenant-seed-pilot-coupon', - resource_deps=['tenant-migration'], - labels=['demo-init']) - -# Weight 15: Seed inventory - CRITICAL: All other seeds depend on this -k8s_resource('demo-seed-inventory', - resource_deps=['inventory-migration', 'demo-seed-tenants'], - labels=['demo-init']) - -# Weight 15: Seed recipes (uses ingredient IDs from inventory) -k8s_resource('demo-seed-recipes', - resource_deps=['recipes-migration', 'demo-seed-inventory'], - labels=['demo-init']) - -# Weight 15: Seed suppliers (uses ingredient IDs for price lists) -k8s_resource('demo-seed-suppliers', - resource_deps=['suppliers-migration', 'demo-seed-inventory'], - labels=['demo-init']) - -# Weight 15: Seed sales (uses finished product IDs from inventory) -k8s_resource('demo-seed-sales', - resource_deps=['sales-migration', 'demo-seed-inventory'], - labels=['demo-init']) - -# Weight 15: Seed AI models (creates training/forecasting model records) -k8s_resource('demo-seed-ai-models', - resource_deps=['training-migration', 'demo-seed-inventory'], - labels=['demo-init']) - -# Weight 20: Seed stock batches (inventory service) -k8s_resource('demo-seed-stock', - resource_deps=['inventory-migration', 'demo-seed-inventory'], - labels=['demo-init']) - -# Weight 22: Seed quality check templates (production service) -k8s_resource('demo-seed-quality-templates', - resource_deps=['production-migration', 'demo-seed-tenants'], - labels=['demo-init']) - -# Weight 25: Seed customers (orders service) -k8s_resource('demo-seed-customers', - resource_deps=['orders-migration', 'demo-seed-tenants'], - labels=['demo-init']) - -# Weight 25: Seed equipment (production service) -k8s_resource('demo-seed-equipment', - resource_deps=['production-migration', 'demo-seed-tenants', 'demo-seed-quality-templates'], - labels=['demo-init']) - -# Weight 30: Seed production batches (production service) -k8s_resource('demo-seed-production-batches', - resource_deps=['production-migration', 'demo-seed-recipes', 'demo-seed-equipment'], - labels=['demo-init']) - -# Weight 30: Seed orders with line items (orders service) -k8s_resource('demo-seed-orders', - resource_deps=['orders-migration', 'demo-seed-customers'], - labels=['demo-init']) - -# Weight 35: Seed procurement plans (orders service) -k8s_resource('demo-seed-procurement', - resource_deps=['orders-migration', 'demo-seed-tenants'], - labels=['demo-init']) - -# Weight 40: Seed demand forecasts (forecasting service) -k8s_resource('demo-seed-forecasts', - resource_deps=['forecasting-migration', 'demo-seed-tenants'], - labels=['demo-init']) - -# ============================================================================= -# SERVICES -# ============================================================================= -# Services depend on their databases AND migrations - -k8s_resource('auth-service', - resource_deps=['auth-migration', 'redis'], - labels=['services']) - -k8s_resource('tenant-service', - resource_deps=['tenant-migration', 'redis'], - labels=['services']) - -k8s_resource('training-service', - resource_deps=['training-migration', 'redis'], - labels=['services']) - -k8s_resource('forecasting-service', - resource_deps=['forecasting-migration', 'redis'], - labels=['services']) - -k8s_resource('sales-service', - resource_deps=['sales-migration', 'redis'], - labels=['services']) - -k8s_resource('external-service', - resource_deps=['external-migration', 'external-data-init', 'redis'], - labels=['services']) - -k8s_resource('notification-service', - resource_deps=['notification-migration', 'redis', 'rabbitmq'], - labels=['services']) - -k8s_resource('inventory-service', - resource_deps=['inventory-migration', 'redis'], - labels=['services']) - -k8s_resource('recipes-service', - resource_deps=['recipes-migration', 'redis'], - labels=['services']) - -k8s_resource('suppliers-service', - resource_deps=['suppliers-migration', 'redis'], - labels=['services']) - -k8s_resource('pos-service', - resource_deps=['pos-migration', 'redis'], - labels=['services']) - -k8s_resource('orders-service', - resource_deps=['orders-migration', 'redis'], - labels=['services']) - -k8s_resource('production-service', - resource_deps=['production-migration', 'redis'], - labels=['services']) - -k8s_resource('alert-processor-service', - resource_deps=['alert-processor-migration', 'redis', 'rabbitmq'], - labels=['services']) - -k8s_resource('demo-session-service', - resource_deps=['demo-session-migration', 'redis'], - labels=['services']) - -# Apply environment variable patch to demo-session-service with the inventory image -local_resource('patch-demo-session-env', - cmd=''' - # Wait a moment for deployments to stabilize - sleep 2 - - # Get current inventory-service image tag - INVENTORY_IMAGE=$(kubectl get deployment inventory-service -n bakery-ia -o jsonpath="{.spec.template.spec.containers[0].image}" 2>/dev/null || echo "bakery/inventory-service:latest") - - # Update demo-session-service environment variable - kubectl set env deployment/demo-session-service -n bakery-ia CLONE_JOB_IMAGE=$INVENTORY_IMAGE - - echo "βœ… Set CLONE_JOB_IMAGE to: $INVENTORY_IMAGE" - ''', - resource_deps=['demo-session-service', 'inventory-service'], - auto_init=True, - labels=['config']) - -# ============================================================================= -# DATA INITIALIZATION JOBS (External Service v2.0) -# ============================================================================= -k8s_resource('external-data-init', - resource_deps=['external-migration', 'redis'], - labels=['data-init']) - -# ============================================================================= -# CRONJOBS -# ============================================================================= -k8s_resource('demo-session-cleanup', - resource_deps=['demo-session-service'], - labels=['cronjobs']) - -k8s_resource('external-data-rotation', - resource_deps=['external-service'], - labels=['cronjobs']) - -# ============================================================================= -# GATEWAY & FRONTEND -# ============================================================================= -k8s_resource('gateway', - resource_deps=['auth-service'], - labels=['frontend']) - -k8s_resource('frontend', - resource_deps=['gateway'], - labels=['frontend']) - -# ============================================================================= -# CONFIGURATION -# ============================================================================= - -# Update check interval - how often Tilt checks for file changes -update_settings( - max_parallel_updates=3, - k8s_upsert_timeout_secs=60 -) - -# Watch settings - configure file watching behavior -watch_settings( - # Ignore patterns that should never trigger rebuilds - ignore=[ - '.git/**', - '**/__pycache__/**', - '**/*.pyc', - '**/.pytest_cache/**', - '**/node_modules/**', - '**/.DS_Store', - '**/*.swp', - '**/*.swo', - '**/.venv/**', - '**/venv/**', - '**/.mypy_cache/**', - '**/.ruff_cache/**', - '**/.tox/**', - '**/htmlcov/**', - '**/.coverage', - '**/dist/**', - '**/build/**', - '**/*.egg-info/**', - # Ignore TLS certificate files (don't trigger rebuilds) - '**/infrastructure/tls/**/*.pem', - '**/infrastructure/tls/**/*.cnf', - '**/infrastructure/tls/**/*.csr', - '**/infrastructure/tls/**/*.srl', - ] -) - -# Print security status on startup -print(""" -βœ… Security setup complete! - -Database Security Features Active: - πŸ” TLS encryption: PostgreSQL and Redis - πŸ”‘ Strong passwords: 32-character cryptographic - πŸ’Ύ Persistent storage: PVCs for all databases - πŸ”’ Column encryption: pgcrypto extension - πŸ“‹ Audit logging: PostgreSQL query logging - -Access your application: - Frontend: http://localhost:3000 (or via ingress) - Gateway: http://localhost:8000 (or via ingress) - -Verify security: - kubectl get pvc -n bakery-ia - kubectl get secrets -n bakery-ia | grep tls - kubectl logs -n bakery-ia | grep SSL - -Security documentation: - docs/SECURITY_IMPLEMENTATION_COMPLETE.md - docs/DATABASE_SECURITY_ANALYSIS_REPORT.md - -====================================== -""") - -# Optimize for local development -# Note: You may see "too many open files" warnings on macOS with many services. -# This is a Kind/Kubernetes limitation and doesn't affect service functionality. -# To work on specific services only, use: tilt up diff --git a/docs/DASHBOARD_JTBD_ANALYSIS.md b/docs/DASHBOARD_JTBD_ANALYSIS.md new file mode 100644 index 00000000..e55c7db9 --- /dev/null +++ b/docs/DASHBOARD_JTBD_ANALYSIS.md @@ -0,0 +1,1165 @@ +# Jobs To Be Done (JTBD) Analysis: Bakery Owner Dashboard Reimagination + +**Date:** 2025-10-24 +**Status:** Planning Phase +**Objective:** Transform the current "Panel de Control" into a decision support companion aligned with bakery owner workflows + +--- + +## 🎯 MAIN FUNCTIONAL JOB + +**When** a bakery owner starts their workday and throughout operations, +**They want to** quickly assess the health of their bakery business and make informed decisions, +**So they can** prevent problems, optimize operations, maximize profitability, and go home confident that tomorrow is under control. + +--- + +## πŸ“Š CURRENT STATE ANALYSIS + +### Current Dashboard Components + +**Location:** `frontend/src/pages/app/DashboardPage.tsx` + +**Existing Widgets:** +1. **StatsGrid** - 4 critical metrics (Sales, Pending Orders, Products Sold, Critical Stock) +2. **RealTimeAlerts** - Today's alerts with filtering/grouping +3. **SustainabilityWidget** - SDG 12.3 compliance and environmental impact +4. **PendingPOApprovals** - Purchase orders needing approval +5. **TodayProduction** - Active production batches + +### What's Working Well βœ… + +- **Real-time data aggregation** from multiple services (sales, inventory, orders, production) +- **Sustainability tracking** aligned with SDG 12.3 (unique value proposition) +- **Action-oriented widgets** (approve POs, start batches directly from dashboard) +- **Multi-language support** (Spanish, Basque, English) +- **Mobile-responsive design** with proper breakpoints +- **Demo tour integration** for onboarding + +### Critical Gaps ❌ + +1. **No narrative/story** - Just data widgets without context +2. **Cognitive overload** - Too many metrics without prioritization +3. **Reactive, not proactive** - Shows problems but doesn't guide actions +4. **No time-based workflow** - Doesn't match bakery daily rhythms +5. **Limited business intelligence** - Raw metrics vs. actionable insights +6. **Missing emotional satisfaction** - No celebration of wins or progress +7. **No financial context** - Metrics without business impact in euros +8. **Team visibility absent** - No view of staff capacity or performance + +--- + +## 🎭 MAIN JOB DECOMPOSITION + +### 1. **START THE DAY PREPARED** + +**Functional Sub-jobs:** +- Understand what happened yesterday (wins/losses) +- Know what's critical TODAY (time-sensitive priorities) +- See what's coming tomorrow/this week (preparation needs) +- Feel confident about current state (peace of mind) + +**Emotional Job:** Feel in control and ready to lead + +**Social Job:** Be prepared for team questions and customer commitments + +**Current Gaps:** +- No "morning briefing" view +- Yesterday's performance buried in trend percentages +- Tomorrow's needs require navigation to production/procurement pages +- No prioritized action list + +**Success Metric:** Owner can answer "What do I need to focus on today?" in <30 seconds + +--- + +### 2. **PREVENT PROBLEMS BEFORE THEY HAPPEN** + +**Functional Sub-jobs:** +- Identify risks in real-time (stockouts, delays, quality issues) +- Understand WHY problems are occurring (root cause insights) +- Take immediate action or delegate (integrated workflow) +- Track that actions are being handled (accountability) + +**Emotional Job:** Feel proactive and competent, avoid embarrassment of stockouts/delays + +**Social Job:** Maintain reputation with customers and team + +**Current Gaps:** +- Alerts lack context and prioritization (all treated equally) +- No predictive warnings (only reactive alerts) +- Action tracking disconnected from alerts +- No financial impact shown (which alerts cost money?) + +**Success Metric:** 80% of problems prevented before impacting customers + +--- + +### 3. **MAKE PROFITABLE DECISIONS** + +**Functional Sub-jobs:** +- See financial impact of daily operations (P&L snapshot) +- Identify waste and inefficiency (cost savings opportunities) +- Understand sales performance vs. targets (goals tracking) +- Forecast cash flow needs (working capital) + +**Emotional Job:** Feel financially secure and savvy + +**Social Job:** Demonstrate business acumen to partners/investors + +**Current Gaps:** +- No financial dashboard view or daily P&L +- Sustainability savings shown but not tied to overall profitability +- No goal/target tracking visible +- Missing cost vs. revenue comparison +- Production costs not visible alongside sales + +**Success Metric:** Owner knows daily profit/loss and can explain key drivers + +--- + +### 4. **LEAD THE TEAM EFFECTIVELY** + +**Functional Sub-jobs:** +- See team workload and capacity (resource planning) +- Monitor production efficiency (team performance) +- Identify training needs (skill gaps) +- Recognize great work (team morale) + +**Social Job:** Be seen as a competent, caring leader by team + +**Emotional Job:** Feel confident in team management abilities + +**Current Gaps:** +- No team view on dashboard +- No performance recognition system +- Staff assignments buried in batch details (requires drill-down) +- No capacity planning visibility + +**Success Metric:** Balanced workload, recognized top performers weekly + +--- + +### 5. **ACHIEVE LONG-TERM GOALS** + +**Functional Sub-jobs:** +- Track progress toward sustainability certifications (SDG compliance) +- Monitor business growth trends (month-over-month) +- Prepare for audits/reporting (compliance readiness) +- Build brand reputation (quality, sustainability) + +**Emotional Job:** Feel proud and purposeful about their business + +**Social Job:** Build reputation as sustainable, quality-focused bakery + +**Current Gaps:** +- Sustainability widget is excellent but isolated from other goals +- No long-term trend visualization (only day-over-day) +- Missing quality score trends over time +- No certification readiness indicators +- Growth metrics not prominent + +**Success Metric:** Progress visible toward 3-6 month goals, certification-ready data + +--- + +## 🚧 FORCES OF PROGRESS + +### Push Forces (Problems with Current Situation) +- "I spend 30 minutes every morning checking different screens to know what's happening" +- "I discover problems too late to fix them without customer impact" +- "I don't know if we're making money until month-end accounting" +- "My team doesn't know what's most important each day" +- "I can't explain our sustainability efforts to certification auditors" +- "I miss opportunities to celebrate team wins" + +### Pull Forces (Attraction of New Solution) +- "One glance tells me everything I need to know to start my day" +- "I get early warnings with suggested actions before problems escalate" +- "I see profit impact in real-time and understand what drives it" +- "Clear priorities that everyone on the team can follow" +- "Sustainability progress tracked automatically for certifications" +- "System highlights achievements to share with the team" + +### Anxiety Forces (Fears About Change) +- "Will it be overwhelming with too much information?" +- "Will I lose visibility into details I occasionally need?" +- "Will my team resist learning a new interface?" +- "Will setup and configuration take too long?" +- "What if the AI insights are wrong?" + +### Habit Forces (What Keeps Them in Current State) +- "I'm used to my morning routine of checking multiple tabs" +- "I know where to find what I need in the current layout" +- "The team knows the current process and workflows" +- "It's working 'well enough' - why risk breaking it?" +- "I don't have time to learn something new right now" + +--- + +## 🎯 DESIGN PRINCIPLES FOR REIMAGINED DASHBOARD + +### 1. **Time-Contextualized Design** (vs. Static Metrics) + +**Morning View (6-10 AM):** +- Yesterday's summary (what you missed overnight) +- TODAY's priorities (time-ordered, actionable) +- Quick wins available (morale boost, easy completions) +- Team arrival and readiness status + +**Midday View (10 AM - 5 PM):** +- Real-time operations status +- Production vs. sales gap analysis +- Critical alerts only (P0/P1) +- Current profitability snapshot + +**Evening View (5-8 PM):** +- Tomorrow's preparation checklist +- Today's achievements (celebrate!) +- Delegation status (what's covered for tomorrow) +- Unresolved items requiring attention + +**Implementation:** Use `useTimeContext` hook to dynamically adjust widget visibility and content + +--- + +### 2. **Narrative-Driven Metrics** (vs. Raw Numbers) + +**Before:** +``` +Sales Today: €1,247 +↑ 15.2% vs yesterday +``` + +**After:** +``` +€1,247 today β€” €200 above Tuesday average +On track for best week this month 🎯 +Next milestone: €1,500 day (€253 to go) +``` + +**Implementation Approach:** +- Add context layer to all metrics +- Compare to meaningful benchmarks (not just yesterday) +- Show progress toward goals +- Use natural language, not just percentages + +--- + +### 3. **Action Priority System** (vs. All Alerts Equal) + +**Priority Levels:** + +- **P0 (NOW - Red Zone):** Revenue-impacting, customer-facing, <2 hours to resolve + - Example: "Stockout on bestseller, 5 customer orders waiting" + - Auto-escalate to owner's phone if not acknowledged in 30 min + +- **P1 (TODAY - Amber Zone):** Must be resolved before close of business + - Example: "PO #1847 approval needed for tomorrow's delivery" + - Show on morning briefing + +- **P2 (THIS WEEK - Blue Zone):** Optimization opportunities + - Example: "Waste trending up 10% this week" + - Show in evening checklist + +- **P3 (BACKLOG - Green Zone):** Nice-to-have improvements + - Example: "Consider alternative supplier for flour (5% cheaper)" + - Show in weekly review only + +**Priority Calculation Algorithm:** +```typescript +priority = (financial_impact * 0.4) + + (time_urgency * 0.3) + + (customer_impact * 0.2) + + (compliance_risk * 0.1) +``` + +--- + +### 4. **Financial Context Always** (vs. Operational Metrics Only) + +**Every widget shows business impact:** + +- Inventory: "23 kg waste prevented β†’ €187 saved this month" +- Production: "Batch 15% faster β†’ 2.5 hours labor saved (€45)" +- Procurement: "3 urgent PO approvals β†’ €1,340 in orders unlocked" +- Alerts: "Critical stockout β†’ €450 in sales at risk" +- Sustainability: "COβ‚‚ reduction β†’ €230 potential grant value" + +**Implementation:** +- Create `FinancialImpactBadge` component (reusable) +- Add `impact_euros` field to all data models +- Calculate impact server-side for consistency + +--- + +### 5. **Celebration & Progress** (vs. Problem-Focused) + +**Daily Wins Section:** +- βœ… All orders fulfilled on time +- βœ… Production target met (105%) +- βœ… Zero waste day! +- πŸ”₯ 3-day quality streak (score >9.0) + +**Milestone Tracking:** +- Sustainability: 47% toward SDG 12.3 compliance +- Growth: €2,340 from monthly goal (78% complete) +- Quality: 28 consecutive days >9.0 score + +**Visual Design:** +- Confetti animation on milestones +- Streak counters with fire emoji πŸ”₯ +- Progress bars with gradients +- Shareable achievement cards for social media + +--- + +## πŸ“‹ PROPOSED WIDGET STRUCTURE + +### **Hero Section** (Top - First 5 Seconds) + +1. **Business Health Score** (0-100) + - Aggregates: Sales, Quality, On-Time, Profitability, Team Morale + - Color-coded: Red <60, Yellow 60-80, Green >80 + - Trend: 7-day moving average + +2. **Today's Story** (AI-Generated Summary) + - 2-sentence natural language summary + - Example: "Strong start to the week! Sales up 18% and all production on schedule. Watch ingredient costs - flour prices rising." + +3. **Critical Actions** (Max 3) + - Sorted by: urgency Γ— impact + - One-click actions inline + - Delegate button with team assignment + +--- + +### **Context Sections** (Scrollable, Priority-Ordered) + +#### 1. **Financial Snapshot** (Always Visible) +- **Today's P&L:** + - Revenue: €1,247 (sales completed) + - COGS: €530 (production materials) + - Labor: €280 (staff hours Γ— rates) + - Waste: €18 (spoilage + mistakes) + - **Net Profit: €419** (34% margin) βœ… + +- **Weekly Trend:** Sparkline chart +- **Cash Flow:** Upcoming payments vs. receivables + +#### 2. **Operations Flow** (Production β†’ Inventory β†’ Sales Cycle) +- Production status: 3 batches in progress, 2 completed, 1 pending +- Inventory health: 94% stocked, 2 items low, 0 stockouts +- Sales velocity: 87 units sold today, 13 pending orders +- **Bottleneck indicator:** Highlights slowest stage + +#### 3. **Team & Capacity** +- Staff scheduled: 4/5 confirmed for today +- Current workload: Balanced (no one >110% capacity) +- Performance highlights: "MarΓ­a: 115% efficiency this week 🌟" +- Training needs: 2 staff need safety recertification + +#### 4. **Quality & Sustainability** (Enhanced Existing Widget) +- Quality score: 9.2/10 (28-day streak) +- Sustainability: SDG 12.3 progress, COβ‚‚ saved, waste reduction +- Certifications: Readiness indicators for audits +- **Grant opportunities:** 3 programs eligible (€12K potential) + +#### 5. **Tomorrow's Briefing** +- Scheduled deliveries: Flour (50kg, 8 AM), Eggs (200, 10 AM) +- Production plan: 5 batches, 320 units total +- Staff: 5/5 confirmed, no gaps +- **Preparation checklist:** + - [ ] Review PO #1892 (arrives 8 AM) + - [ ] Assign quality check for batch #489 + - [ ] Confirm catering order (pick-up 2 PM) + +--- + +### **Smart Widgets** (Contextual, Time-Based) + +These widgets appear/disappear based on context: + +- **Morning Briefing** (6-10 AM only) +- **Midday Operations** (10 AM - 5 PM only) +- **Evening Checklist** (5-8 PM only) +- **Weekend Prep** (Friday PM only) +- **Weekly Review** (Monday AM only) + +--- + +## πŸš€ INCREMENTAL IMPLEMENTATION PHASES + +### **PHASE 1: Quick Wins** (Week 1-2, ~10 hours) + +**Goal:** Add context and impact to existing components without breaking changes + +#### Deliverables: + +1. **Enhanced StatsGrid with Financial Impact** + - File: `frontend/src/pages/app/DashboardPage.tsx` (modify lines 179-264) + - Add `impact` and `actionable` fields to stat objects + - Show financial context in subtitle + + **Example Change:** + ```typescript + { + title: t('dashboard:stats.critical_stock', 'Critical Stock'), + value: dashboardStats.criticalStock.toString(), + icon: AlertTriangle, + variant: dashboardStats.criticalStock > 0 ? 'error' : 'success', + impact: dashboardStats.criticalStock > 0 + ? `€${(dashboardStats.criticalStock * 80).toFixed(0)} in delayed orders` + : null, // NEW + actionable: dashboardStats.criticalStock > 0 + ? 'Review procurement queue' + : null, // NEW + subtitle: dashboardStats.criticalStock > 0 + ? t('dashboard:messages.action_required', 'Action required') + : t('dashboard:messages.stock_healthy', 'Stock levels healthy') + } + ``` + +2. **Priority System for RealTimeAlerts** + - New hook: `frontend/src/hooks/business/useAlertPriority.ts` + - Modify: `frontend/src/components/domain/dashboard/RealTimeAlerts.tsx` + - Add priority calculation based on urgency + impact + - Group alerts by priority (P0, P1, P2, P3) + + **Algorithm:** + ```typescript + const calculatePriority = (alert: Notification): Priority => { + const urgency = calculateUrgency(alert.timestamp, alert.type); + const impact = estimateFinancialImpact(alert); + const score = (urgency * 0.6) + (impact * 0.4); + + if (score > 80) return 'P0'; + if (score > 60) return 'P1'; + if (score > 40) return 'P2'; + return 'P3'; + }; + ``` + +3. **Daily Briefing Widget** + - New component: `frontend/src/components/domain/dashboard/DailyBriefing.tsx` + - Uses existing `useDashboardStats` hook + - Simple summary generation (no AI yet) + + **Content Structure:** + ```typescript + interface DailyBriefing { + headline: string; // "Strong Tuesday!" + summary: string[]; // ["Sales up 15%", "3 tasks need attention"] + sentiment: 'positive' | 'neutral' | 'concern'; + topPriority: string; // Most urgent action + } + ``` + +**Success Criteria:** +- [ ] All stats show financial impact where relevant +- [ ] Alerts sorted by priority, not just time +- [ ] Daily briefing loads in <500ms +- [ ] No breaking changes to existing functionality + +**User Value:** Owners immediately see "why this matters" in money terms + +--- + +### **PHASE 2: Financial Context Layer** (Week 3-4, ~12 hours) + +**Goal:** Add daily profitability visibility and financial badges throughout + +#### Deliverables: + +1. **FinancialImpactBadge Component** + - New: `frontend/src/components/ui/Badge/FinancialImpactBadge.tsx` + - Reusable component showing € impact with color coding + - Variants: cost (red), savings (green), revenue (blue), neutral (gray) + + **Props:** + ```typescript + interface FinancialImpactBadgeProps { + amount: number; + type: 'cost' | 'savings' | 'revenue' | 'neutral'; + label?: string; + showIcon?: boolean; + } + ``` + +2. **Profit Snapshot Widget** + - New: `frontend/src/components/domain/dashboard/ProfitSnapshot.tsx` + - Shows TODAY's simple P&L + - Backend API needed: `GET /api/v1/tenants/{id}/dashboard/profit-snapshot` + + **Data Model:** + ```python + class ProfitSnapshot(BaseModel): + date: date + revenue: Decimal # from sales + cogs: Decimal # from production batches + labor_cost: Decimal # from staff hours + waste_cost: Decimal # from inventory + net_profit: Decimal + margin_percentage: float + trend_vs_yesterday: float + ``` + +3. **Backend API Endpoint** + - New: `services/orders/app/api/dashboard.py` - `get_daily_profit_snapshot()` + - Aggregates data from: sales, production, inventory services + - Caches result for 15 minutes + + **Implementation:** + ```python + @router.get( + "/api/v1/tenants/{tenant_id}/dashboard/profit-snapshot", + response_model=ProfitSnapshot + ) + async def get_daily_profit_snapshot( + tenant_id: UUID, + date: Optional[str] = None, # defaults to today + db: AsyncSession = Depends(get_db) + ): + # Aggregate: sales.revenue - production.costs - inventory.waste + # Calculate labor from staff_assigned Γ— hourly_rates + return ProfitSnapshot(...) + ``` + +4. **Integrate Financial Badges** + - Add to: AlertCard, PendingPOApprovals, TodayProduction + - Show € impact prominently + - Link to profit snapshot when clicked + +**Success Criteria:** +- [ ] Daily profit visible on dashboard +- [ ] All critical alerts show € impact +- [ ] Owner can explain profitability drivers +- [ ] Financial badges consistent across all widgets + +**User Value:** Daily profitability visibility (currently hidden until month-end) + +--- + +### **PHASE 3: Time-Based Smart Views** (Week 5-6, ~15 hours) + +**Goal:** Dashboard adapts to time of day and workflow rhythms + +#### Deliverables: + +1. **Time Context Hook** + - New: `frontend/src/hooks/business/useTimeContext.ts` + - Detects time of day and suggests dashboard mode + + **Implementation:** + ```typescript + type DashboardMode = 'morning' | 'midday' | 'evening' | 'weekend'; + + const useTimeContext = () => { + const now = new Date(); + const hour = now.getHours(); + const day = now.getDay(); + + const mode: DashboardMode = + day === 0 || day === 6 ? 'weekend' : + hour < 10 ? 'morning' : + hour < 17 ? 'midday' : 'evening'; + + return { + mode, + isWorkingHours: hour >= 6 && hour <= 20, + shouldShowBriefing: hour >= 6 && hour <= 10 + }; + }; + ``` + +2. **Morning Briefing Component** + - New: `frontend/src/components/domain/dashboard/MorningBriefing.tsx` + - Shows 6-10 AM only + + **Sections:** + ```typescript + interface MorningBriefingData { + yesterday: { + sales: number; + target: number; + orders_completed: number; + issues: string[]; + }; + today: { + priorities: Array<{ + time: string; + action: string; + urgency: Priority; + }>; + team_status: string; + }; + quick_wins: Array<{ + action: string; + impact: number; // euros + time_required: number; // minutes + }>; + } + ``` + +3. **Evening Checklist Component** + - New: `frontend/src/components/domain/dashboard/EveningChecklist.tsx` + - Shows 5-8 PM only + + **Sections:** + - Today's Achievements (wins to celebrate) + - Tomorrow's Prep (deliveries, production schedule) + - Open Items (unresolved alerts, pending approvals) + +4. **Weekend View** + - Simplified dashboard for off-hours + - Shows only critical alerts (P0/P1) + - "All quiet" message when no urgent items + +**Success Criteria:** +- [ ] Morning briefing shows yesterday recap + today priorities +- [ ] Evening checklist shows tomorrow prep +- [ ] Dashboard mode switches automatically +- [ ] Owner saves 15+ minutes in morning routine + +**User Value:** Dashboard anticipates needs based on time of day + +--- + +### **PHASE 4: Team & Capacity View** (Week 7-8, ~12 hours) + +**Goal:** Visibility into team workload, performance, and capacity + +#### Deliverables: + +1. **TeamCapacity Widget** + - New: `frontend/src/components/domain/dashboard/TeamCapacity.tsx` + - Shows staff scheduled, workload, performance + + **Data Model:** + ```typescript + interface TeamCapacityData { + date: string; + staff_scheduled: Array<{ + id: string; + name: string; + role: string; + shift_start: string; + shift_end: string; + workload_percentage: number; // 0-150% + batches_assigned: number; + efficiency_score: number; // vs. baseline + }>; + available_capacity: number; // hours + performance_highlights: Array<{ + staff_name: string; + achievement: string; + }>; + } + ``` + +2. **Backend API Endpoint** + - New: `services/production/app/api/dashboard.py` - `get_team_capacity()` + - Aggregates batches by `staff_assigned` + - Calculates workload based on planned_duration_minutes + + **Implementation:** + ```python + @router.get( + "/api/v1/tenants/{tenant_id}/dashboard/team-capacity", + response_model=TeamCapacityView + ) + async def get_team_capacity( + tenant_id: UUID, + date: str, + db: AsyncSession = Depends(get_db) + ): + # Query batches for date, group by staff_assigned + # Calculate: workload = sum(planned_duration) / (shift_hours * 60) + # Identify: efficiency = actual_duration / planned_duration + return TeamCapacityView(...) + ``` + +3. **Performance Recognition** + - Automatic detection of: + - High efficiency (actual < planned time consistently) + - Quality scores >9.5 + - Zero defects streaks + - Visual: Star icon, highlight in widget + - Action: One-click "Share with team" button + +**Success Criteria:** +- [ ] Team workload visible at a glance +- [ ] Performance highlights shown for top performers +- [ ] Owner can identify overloaded staff +- [ ] Capacity planning data available + +**User Value:** Resource planning and team recognition + +--- + +### **PHASE 5: Narrative & Intelligence** (Week 9-10, ~16 hours) + +**Goal:** AI-enhanced insights and celebration moments + +#### Deliverables: + +1. **Smart Insights Widget** + - New: `frontend/src/components/domain/dashboard/SmartInsights.tsx` + - Pattern-based suggestions + + **Insight Types:** + - **Demand patterns:** "Sales spike Fridays (+30%) β†’ Consider increasing Friday production" + - **Waste trends:** "Flour waste trending up 10% β†’ Check batch sizes" + - **Supplier issues:** "3 late deliveries from SupplierX this month β†’ Consider backup" + - **Opportunities:** "Sustainability metrics qualify for €5K grant β†’ Review eligibility" + + **Backend:** + ```python + # services/alert_processor/analytics_rules.py (NEW) + + class InsightRule: + def detect(self, data: TenantData) -> Optional[Insight]: + # Rule-based pattern detection + pass + + rules = [ + DemandPatternRule(), + WasteTrendRule(), + SupplierReliabilityRule(), + GrantEligibilityRule(), + ] + ``` + +2. **Win Streak Tracker** + - New: `frontend/src/components/domain/dashboard/WinStreak.tsx` + - Tracks consecutive days meeting goals + + **Streaks Tracked:** + ```typescript + interface WinStreak { + type: 'no_stockouts' | 'sales_target' | 'waste_reduction' | 'quality_score'; + current_streak: number; // days + longest_streak: number; + last_broken: string; // date + next_milestone: number; // days + } + ``` + + **Visual:** + - Fire emoji πŸ”₯ for active streaks + - Confetti animation on new records + - Milestone badges (7, 14, 30, 90 days) + +3. **AI Summary Enhancement** (Optional) + - Enhance DailyBriefing with OpenAI API + - Generate natural language summary + - Requires: OpenAI API key, backend service + + **Example Prompt:** + ``` + Based on this bakery data: + - Sales: €1,247 (up 15% vs yesterday) + - Critical stock: 2 items + - Pending POs: 3 urgent + - Production: on schedule + + Generate a 2-sentence briefing for the owner: + ``` + + **Output:** "Strong sales day with €1,247 revenue, tracking 15% above yesterday. Focus on approving 3 urgent purchase orders to prevent stockouts." + +4. **Celebration Moments** + - Auto-detect achievements: + - New sales record + - Longest quality streak + - Sustainability milestone (50% SDG target) + - Zero waste day + - Visual: Full-screen confetti animation + - Shareable: Generate achievement card with bakery branding + +**Success Criteria:** +- [ ] At least 3 actionable insights per week +- [ ] Win streaks visible and tracked +- [ ] Owner reports "dashboard anticipates my questions" +- [ ] Achievements celebrated automatically + +**User Value:** Proactive guidance and positive reinforcement + +--- + +## πŸ—‚οΈ FILE STRUCTURE + +### New Files + +``` +frontend/src/ +β”œβ”€β”€ components/domain/dashboard/ +β”‚ β”œβ”€β”€ DailyBriefing.tsx # Phase 1 - Today's story summary +β”‚ β”œβ”€β”€ MorningBriefing.tsx # Phase 3 - Yesterday + today priorities +β”‚ β”œβ”€β”€ EveningChecklist.tsx # Phase 3 - Tomorrow prep + today wins +β”‚ β”œβ”€β”€ ProfitSnapshot.tsx # Phase 2 - Daily P&L widget +β”‚ β”œβ”€β”€ TeamCapacity.tsx # Phase 4 - Staff workload & performance +β”‚ β”œβ”€β”€ SmartInsights.tsx # Phase 5 - AI-driven suggestions +β”‚ β”œβ”€β”€ WinStreak.tsx # Phase 5 - Achievement tracking +β”‚ └── FinancialImpactBadge.tsx # Phase 2 - Reusable € badge +β”‚ +β”œβ”€β”€ hooks/business/ +β”‚ β”œβ”€β”€ useTimeContext.ts # Phase 3 - Time-based modes +β”‚ β”œβ”€β”€ useAlertPriority.ts # Phase 1 - P0-P3 calculation +β”‚ β”œβ”€β”€ useWinStreak.ts # Phase 5 - Streak tracking +β”‚ └── useProfitSnapshot.ts # Phase 2 - Daily P&L data +β”‚ +└── utils/ + β”œβ”€β”€ alertPriority.ts # Phase 1 - Priority algorithms + β”œβ”€β”€ financialCalculations.ts # Phase 2 - Impact calculations + └── insightRules.ts # Phase 5 - Pattern detection + +backend/ +β”œβ”€β”€ services/orders/app/api/ +β”‚ └── dashboard.py # EXTEND - add profit-snapshot endpoint +β”‚ +β”œβ”€β”€ services/production/app/api/ +β”‚ └── dashboard.py # EXTEND - add team-capacity endpoint +β”‚ +└── services/alert_processor/ + └── analytics_rules.py # NEW - Phase 5 insights engine +``` + +### Modified Files + +``` +frontend/src/ +β”œβ”€β”€ pages/app/ +β”‚ └── DashboardPage.tsx # Orchestrate all new widgets +β”‚ +└── components/domain/dashboard/ + β”œβ”€β”€ RealTimeAlerts.tsx # Add priority grouping + └── (existing widgets) # Add financial badges + +backend/ +└── services/inventory/app/schemas/ + └── dashboard.py # Add profit, team schemas +``` + +--- + +## πŸ”„ MIGRATION STRATEGY + +### Gradual, Non-Breaking Rollout + +#### Week 1-2: Shadow Mode +- Add new components alongside existing ones +- Feature flag: `ENABLE_ENHANCED_DASHBOARD` (environment variable) +- Default: `false` (opt-in testing only) +- No changes to production dashboard + +**Implementation:** +```typescript +// DashboardPage.tsx +const useEnhancedDashboard = + import.meta.env.VITE_ENABLE_ENHANCED_DASHBOARD === 'true'; + +return ( +
+ {useEnhancedDashboard ? ( + <> + + + + ) : ( + + )} + {/* Rest of dashboard */} +
+); +``` + +#### Week 3-4: Opt-In Testing +- Add toggle in user settings: `Settings > Preferences > Beta Features` +- Invite bakery owner (you?) to test +- Collect feedback via in-app survey +- Track analytics: time on page, clicks, errors + +**User Setting:** +```typescript +// services/auth/app/schemas/users.py +class UserPreferences(BaseModel): + use_enhanced_dashboard: bool = False # NEW + # ... existing preferences +``` + +#### Week 5-6: Hybrid Mode +- Show enhanced widgets during specific times: + - Morning briefing: 6-10 AM (auto-enabled for all) + - Evening checklist: 5-8 PM (auto-enabled for all) +- Keep existing widgets for midday +- A/B test with 50% of users + +**Analytics to Track:** +- Morning briefing usage: % who interact +- Time saved: avg. time to complete daily review +- Errors prevented: P0 alerts resolved before escalation +- User satisfaction: weekly NPS survey + +#### Week 7-8: Default On +- Enhanced dashboard becomes default for all users +- Old view available as "Classic Mode" toggle +- Deprecation notice: "Classic mode will be removed in 2 weeks" + +#### Week 9-10: Full Migration +- Remove old components if new validated (>80% satisfaction) +- Cleanup feature flags +- Archive old code +- Update documentation + +--- + +## βœ… SUCCESS METRICS + +### Phase 1 Success Criteria +- [ ] Owner sees financial impact on all critical stats +- [ ] Alerts sorted by priority, not chronologically +- [ ] Daily briefing widget loads in <500ms +- [ ] 90% of users prefer enhanced stats over old + +### Phase 2 Success Criteria +- [ ] Daily profit visible (Revenue - COGS - Waste) +- [ ] All P0/P1 alerts show € impact +- [ ] Owner can explain profitability to investor in <2 minutes +- [ ] Financial badges render in <100ms + +### Phase 3 Success Criteria +- [ ] Morning briefing shows yesterday + today priorities +- [ ] Evening checklist shows tomorrow prep +- [ ] Owner saves 15+ minutes in morning routine (self-reported) +- [ ] 85% find time-based views helpful + +### Phase 4 Success Criteria +- [ ] Team workload visible at a glance +- [ ] Performance highlights shown weekly +- [ ] Owner can balance assignments without manual calculation +- [ ] Team morale improves (quarterly survey) + +### Phase 5 Success Criteria +- [ ] At least 3 actionable insights generated per week +- [ ] Win streaks visible and celebrated +- [ ] Owner reports "dashboard anticipates my questions" (interview) +- [ ] Reduction in reactive firefighting (tracked via alert resolution time) + +### Overall Success (End of Phase 5) +- [ ] Dashboard is #1 most-visited page (analytics) +- [ ] Owner satisfaction >4.5/5 (quarterly survey) +- [ ] 30% reduction in time spent on morning review +- [ ] 20% improvement in problem prevention (P0 alerts avoided) +- [ ] 3 month retention: 95% of users still use enhanced dashboard + +--- + +## πŸš€ IMMEDIATE NEXT STEPS + +### To Start Phase 1 (This Week): + +1. **Create feature branch:** + ```bash + git checkout -b feature/dashboard-enhancements-phase1 + ``` + +2. **Set up feature flag:** + ```bash + # frontend/.env.local + VITE_ENABLE_ENHANCED_DASHBOARD=true + ``` + +3. **Implement DailyBriefing component** (2-3 hours): + - Create `frontend/src/components/domain/dashboard/DailyBriefing.tsx` + - Reuse existing `useDashboardStats` hook + - Simple algorithm: compare today vs. yesterday + - Generate 2-3 bullet summary + +4. **Add priority to alerts** (3-4 hours): + - Create `frontend/src/hooks/business/useAlertPriority.ts` + - Create `frontend/src/utils/alertPriority.ts` (calculation logic) + - Modify `RealTimeAlerts` to support priority grouping + - Add priority badge to `AlertCard` + +5. **Add impact to StatsGrid** (1-2 hours): + - Extend `DashboardStats` type with optional `impact?: string` + - Modify stat card rendering to show impact subtitle + - Populate from existing data (no new API calls yet) + +6. **Test and validate** (1-2 hours): + - Unit tests for priority calculation + - Integration tests for DailyBriefing + - Visual regression tests for modified components + - Accessibility audit (keyboard navigation, screen readers) + +7. **Deploy to dev and gather feedback** (1 hour): + ```bash + npm run build + kubectl apply -f infrastructure/kubernetes/overlays/dev/ + ``` + +**Total Phase 1 Effort:** ~10-12 hours (1.5-2 days) + +--- + +## πŸ“š APPENDIX + +### A. Technology Stack + +**Frontend:** +- React 18 + TypeScript +- TanStack Query (React Query) for data fetching +- React Router for navigation +- i18next for translations +- Lucide React for icons + +**Backend:** +- FastAPI (Python 3.11+) +- PostgreSQL for persistence +- Redis for caching +- SQLAlchemy ORM + +**Infrastructure:** +- Kubernetes (local: k3d, prod: managed cluster) +- Nginx Ingress +- Prometheus + Grafana (monitoring) + +### B. Existing Dashboard APIs + +**Available Data Sources:** +- `GET /api/v1/tenants/{id}/dashboard/summary` - Inventory dashboard +- `GET /api/v1/tenants/{id}/alerts/analytics` - Alert analytics (7-30 days) +- `GET /api/v1/tenants/{id}/orders/dashboard-summary` - Orders metrics +- `GET /api/v1/tenants/{id}/sales/analytics` - Sales data +- `GET /api/v1/tenants/{id}/production/batches/active` - Production batches +- `GET /api/v1/tenants/{id}/sustainability/widget` - Sustainability metrics + +**Aggregated via:** +- `useDashboardStats()` hook - Combines all above into single dashboard snapshot + +### C. User Roles & Permissions + +**Relevant Roles:** +- `owner` (tenant role) - Full access, primary user of dashboard +- `admin` (tenant role) - Full access +- `manager` (global role) - Can view but limited editing +- `member` (tenant role) - Limited view access + +**Dashboard Visibility:** +- All roles can view dashboard +- Actions (approve PO, start batch) require `owner` or `admin` role +- Financial data visible to `owner`, `admin`, `manager` only + +### D. Performance Considerations + +**Current Dashboard Load Time:** +- Initial load: ~800ms (parallel API calls) +- Re-render: <100ms (React Query caching) +- SSE connection: Real-time alerts + +**Target Performance (Enhanced):** +- Initial load: <1000ms (same or better) +- Daily briefing: <500ms (cached server-side) +- Financial snapshot: <300ms (15-min cache) +- Win streak: <50ms (localStorage) + +**Optimization Strategies:** +- Server-side caching (Redis, 5-15 min TTL) +- Client-side caching (React Query, 30-60s stale time) +- Code splitting (lazy load evening/morning widgets) +- Incremental rendering (Suspense boundaries) + +### E. Accessibility Requirements + +**WCAG 2.1 AA Compliance:** +- [ ] Keyboard navigation (Tab, Enter, Esc) +- [ ] Screen reader support (ARIA labels) +- [ ] Color contrast >4.5:1 +- [ ] Focus indicators visible +- [ ] No content flash <3 times/sec +- [ ] Text scalable to 200% + +**Dashboard-Specific:** +- Alert priority communicated via text, not color only +- Financial impact readable by screen readers +- Time-based views announced on change +- All actions have keyboard shortcuts + +### F. Internationalization + +**Supported Languages:** +- Spanish (es) +- Basque (eu) +- English (en) + +**Translation Keys to Add:** +```json +// frontend/src/locales/en/dashboard.json +{ + "daily_briefing": { + "title": "Today's Briefing", + "yesterday_recap": "Yesterday's Performance", + "today_priorities": "Today's Priorities", + "quick_wins": "Quick Wins" + }, + "financial": { + "profit_snapshot": "Daily Profit", + "revenue": "Revenue", + "costs": "Costs", + "net_profit": "Net Profit", + "margin": "Margin" + }, + "priorities": { + "p0": "URGENT", + "p1": "Today", + "p2": "This Week", + "p3": "Backlog" + } + // ... etc +} +``` + +### G. References & Inspiration + +**JTBD Resources:** +- Bob Moesta - "Demand-Side Sales 101" +- Clayton Christensen - "Competing Against Luck" +- Alan Klement - "When Coffee and Kale Compete" + +**Dashboard Design Patterns:** +- Intercom Product Updates (narrative-driven) +- Shopify Home (financial context always) +- Linear Dashboard (time-based views) +- Notion Workspace (celebration moments) + +**Bakery Domain Expertise:** +- UN SDG 12.3 Food Waste Guidelines +- EU Sustainability Reporting Standards +- Local bakery owner interviews (recommended) + +--- + +## πŸ“ CONCLUSION + +This JTBD analysis transforms the dashboard from a **data display tool** into a **decision support companion** that: + +1. **Aligns with bakery workflows** (morning/midday/evening rhythms) +2. **Provides financial context** (every metric tied to profitability) +3. **Guides action** (prioritized by urgency Γ— impact) +4. **Celebrates progress** (wins, streaks, milestones) +5. **Anticipates needs** (time-based smart views) + +The incremental implementation approach ensures: +- No breaking changes to existing system +- Value delivered every 2 weeks +- User feedback incorporated continuously +- Low risk, high reward + +**Recommendation:** Start with Phase 1 this week to validate core assumptions before committing to full redesign. + +--- + +**Last Updated:** 2025-10-24 +**Next Review:** After Phase 1 completion (Week 2) +**Owner:** Development Team + Bakery Owner (Product Validation) diff --git a/docs/SUSTAINABILITY_COMPLETE_IMPLEMENTATION.md b/docs/SUSTAINABILITY_COMPLETE_IMPLEMENTATION.md index 125e2647..a76b86fc 100644 --- a/docs/SUSTAINABILITY_COMPLETE_IMPLEMENTATION.md +++ b/docs/SUSTAINABILITY_COMPLETE_IMPLEMENTATION.md @@ -2,11 +2,32 @@ ## Implementation Date **Completed:** October 21, 2025 +**Updated:** October 23, 2025 - Grant programs refined to reflect accurate, accessible EU opportunities for Spanish bakeries ## Overview The bakery-ia platform now has a **fully functional, production-ready sustainability tracking system** aligned with UN SDG 12.3 and EU Green Deal objectives. This feature enables grant applications, environmental impact reporting, and food waste reduction tracking. +### Recent Update (October 23, 2025) +The grant program assessment has been **updated and refined** based on comprehensive 2025 research to ensure all listed programs are: +- βœ… **Actually accessible** to Spanish bakeries and retail businesses +- βœ… **Currently open** or with rolling applications in 2025 +- βœ… **Real grant programs** (not strategies or policy frameworks) +- βœ… **Properly named** with correct requirements and funding amounts +- βœ… **Aligned with Spain's Law 1/2025** on food waste prevention + +**Programs Removed (Not Actual Grants):** +- ❌ "EU Farm to Fork" - This is a strategy, not a grant program +- ❌ "National Circular Economy" - Too vague, replaced with specific LIFE Programme + +**Programs Added:** +- βœ… **LIFE Programme - Circular Economy** (€73M, 15% reduction) +- βœ… **Fedima Sustainability Grant** (€20k, bakery-specific) +- βœ… **EIT Food - Retail Innovation** (€15-45k, retail-specific) + +**Programs Renamed:** +- "EU Horizon Europe" β†’ **"Horizon Europe Cluster 6"** (more specific) + --- ## 🎯 What Was Implemented @@ -195,13 +216,18 @@ The bakery-ia platform now has a **fully functional, production-ready sustainabi - **Progress** - % toward target - **Status** - sdg_compliant, on_track, progressing, baseline -### Grant Eligibility -| Program | Requirement | Eligible When | -|---------|-------------|---------------| -| **EU Horizon Europe** | 30% reduction | βœ… reduction >= 30% | -| **EU Farm to Fork** | 20% reduction | βœ… reduction >= 20% | -| **Circular Economy** | 15% reduction | βœ… reduction >= 15% | -| **UN SDG Certified** | 50% reduction | βœ… reduction >= 50% | +### Grant Eligibility (Updated October 2025 - Spanish Bakeries & Retail) +| Program | Requirement | Funding | Deadline | Sector | Eligible When | +|---------|-------------|---------|----------|--------|---------------| +| **LIFE Programme - Circular Economy** | 15% reduction | €73M available | Sept 23, 2025 | General | βœ… reduction >= 15% | +| **Horizon Europe Cluster 6** | 20% reduction | €880M+ annually | Rolling 2025 | Food Systems | βœ… reduction >= 20% | +| **Fedima Sustainability Grant** | 15% reduction | €20,000 per award | June 30, 2025 | Bakery-specific | βœ… reduction >= 15% | +| **EIT Food - Retail Innovation** | 20% reduction | €15-45k per project | Rolling | Retail-specific | βœ… reduction >= 20% | +| **UN SDG 12.3 Certification** | 50% reduction | Certification only | Ongoing | General | βœ… reduction >= 50% | + +**Spain-Specific Legislative Compliance:** +- βœ… **Spanish Law 1/2025** - Food Waste Prevention compliance +- βœ… **Spanish Circular Economy Strategy 2030** - National targets alignment ### Financial Impact - **Waste Cost** - Total waste Γ— €3.50/kg diff --git a/frontend/src/api/hooks/inventory.ts b/frontend/src/api/hooks/inventory.ts index 62e0c64e..b1f843a8 100644 --- a/frontend/src/api/hooks/inventory.ts +++ b/frontend/src/api/hooks/inventory.ts @@ -196,10 +196,24 @@ export const useStockMovements = ( offset: number = 0, options?: Omit, 'queryKey' | 'queryFn'> ) => { + // Validate UUID format if ingredientId is provided + const isValidUUID = (uuid?: string): boolean => { + if (!uuid) return true; // undefined/null is valid (means no filter) + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + return uuidRegex.test(uuid); + }; + + const validIngredientId = ingredientId && isValidUUID(ingredientId) ? ingredientId : undefined; + + // Log warning if ingredient ID is invalid + if (ingredientId && !isValidUUID(ingredientId)) { + console.warn('[useStockMovements] Invalid ingredient ID format:', ingredientId); + } + return useQuery({ - queryKey: inventoryKeys.stock.movements(tenantId, ingredientId), - queryFn: () => inventoryService.getStockMovements(tenantId, ingredientId, limit, offset), - enabled: !!tenantId, + queryKey: inventoryKeys.stock.movements(tenantId, validIngredientId), + queryFn: () => inventoryService.getStockMovements(tenantId, validIngredientId, limit, offset), + enabled: !!tenantId && (!ingredientId || isValidUUID(ingredientId)), staleTime: 1 * 60 * 1000, // 1 minute ...options, }); diff --git a/frontend/src/api/hooks/production.ts b/frontend/src/api/hooks/production.ts index eab81483..d4d39a34 100644 --- a/frontend/src/api/hooks/production.ts +++ b/frontend/src/api/hooks/production.ts @@ -230,7 +230,7 @@ export const useProductionPlanningData = (tenantId: string, date?: string) => { const schedule = useProductionSchedule(tenantId); const capacity = useCapacityStatus(tenantId, date); const requirements = useProductionRequirements(tenantId, date); - + return { schedule: schedule.data, capacity: capacity.data, @@ -243,4 +243,40 @@ export const useProductionPlanningData = (tenantId: string, date?: string) => { requirements.refetch(); }, }; +}; + +// ===== Scheduler Mutations ===== + +/** + * Hook to trigger production scheduler manually (for development/testing) + */ +export const useTriggerProductionScheduler = ( + options?: UseMutationOptions< + { success: boolean; message: string; tenant_id: string }, + ApiError, + string + > +) => { + const queryClient = useQueryClient(); + + return useMutation< + { success: boolean; message: string; tenant_id: string }, + ApiError, + string + >({ + mutationFn: (tenantId: string) => productionService.triggerProductionScheduler(tenantId), + onSuccess: (_, tenantId) => { + // Invalidate all production queries for this tenant + queryClient.invalidateQueries({ + queryKey: productionKeys.dashboard(tenantId), + }); + queryClient.invalidateQueries({ + queryKey: productionKeys.batches(tenantId), + }); + queryClient.invalidateQueries({ + queryKey: productionKeys.activeBatches(tenantId), + }); + }, + ...options, + }); }; \ No newline at end of file diff --git a/frontend/src/api/hooks/tenant.ts b/frontend/src/api/hooks/tenant.ts index 869f908c..0da44c0b 100644 --- a/frontend/src/api/hooks/tenant.ts +++ b/frontend/src/api/hooks/tenant.ts @@ -12,6 +12,7 @@ import { TenantStatistics, TenantSearchParams, TenantNearbyParams, + AddMemberWithUserCreate, } from '../types/tenant'; import { ApiError } from '../client'; @@ -247,16 +248,16 @@ export const useUpdateModelStatus = ( export const useAddTeamMember = ( options?: UseMutationOptions< - TenantMemberResponse, - ApiError, + TenantMemberResponse, + ApiError, { tenantId: string; userId: string; role: string } > ) => { const queryClient = useQueryClient(); - + return useMutation< - TenantMemberResponse, - ApiError, + TenantMemberResponse, + ApiError, { tenantId: string; userId: string; role: string } >({ mutationFn: ({ tenantId, userId, role }) => tenantService.addTeamMember(tenantId, userId, role), @@ -268,6 +269,30 @@ export const useAddTeamMember = ( }); }; +export const useAddTeamMemberWithUserCreation = ( + options?: UseMutationOptions< + TenantMemberResponse, + ApiError, + { tenantId: string; memberData: AddMemberWithUserCreate } + > +) => { + const queryClient = useQueryClient(); + + return useMutation< + TenantMemberResponse, + ApiError, + { tenantId: string; memberData: AddMemberWithUserCreate } + >({ + mutationFn: ({ tenantId, memberData }) => + tenantService.addTeamMemberWithUserCreation(tenantId, memberData), + onSuccess: (data, { tenantId }) => { + // Invalidate team members query + queryClient.invalidateQueries({ queryKey: tenantKeys.members(tenantId) }); + }, + ...options, + }); +}; + export const useUpdateMemberRole = ( options?: UseMutationOptions< TenantMemberResponse, diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index fe60692f..9bf47e41 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -655,6 +655,7 @@ export { useUpdateBatchStatus, useProductionDashboardData, useProductionPlanningData, + useTriggerProductionScheduler, productionKeys, } from './hooks/production'; diff --git a/frontend/src/api/services/production.ts b/frontend/src/api/services/production.ts index 7c1ace4d..c93b66cf 100644 --- a/frontend/src/api/services/production.ts +++ b/frontend/src/api/services/production.ts @@ -405,6 +405,25 @@ export class ProductionService { return apiClient.get(url); } + + // =================================================================== + // OPERATIONS: Scheduler + // =================================================================== + + /** + * Trigger production scheduler manually (for testing/development) + * POST /tenants/{tenant_id}/production/operations/scheduler/trigger + */ + static async triggerProductionScheduler(tenantId: string): Promise<{ + success: boolean; + message: string; + tenant_id: string + }> { + return apiClient.post( + `/tenants/${tenantId}/production/operations/scheduler/trigger`, + {} + ); + } } export const productionService = new ProductionService(); diff --git a/frontend/src/api/services/tenant.ts b/frontend/src/api/services/tenant.ts index 7881332c..d06ac088 100644 --- a/frontend/src/api/services/tenant.ts +++ b/frontend/src/api/services/tenant.ts @@ -21,6 +21,7 @@ import { TenantStatistics, TenantSearchParams, TenantNearbyParams, + AddMemberWithUserCreate, } from '../types/tenant'; export class TenantService { @@ -125,8 +126,8 @@ export class TenantService { // Backend: services/tenant/app/api/tenant_members.py // =================================================================== async addTeamMember( - tenantId: string, - userId: string, + tenantId: string, + userId: string, role: string ): Promise { return apiClient.post(`${this.baseUrl}/${tenantId}/members`, { @@ -135,6 +136,16 @@ export class TenantService { }); } + async addTeamMemberWithUserCreation( + tenantId: string, + memberData: AddMemberWithUserCreate + ): Promise { + return apiClient.post( + `${this.baseUrl}/${tenantId}/members/with-user`, + memberData + ); + } + async getTeamMembers(tenantId: string, activeOnly: boolean = true): Promise { const queryParams = new URLSearchParams(); queryParams.append('active_only', activeOnly.toString()); diff --git a/frontend/src/api/types/tenant.ts b/frontend/src/api/types/tenant.ts index d45604fc..85693b60 100644 --- a/frontend/src/api/types/tenant.ts +++ b/frontend/src/api/types/tenant.ts @@ -68,13 +68,34 @@ export interface TenantMemberInvitation { /** * Schema for updating tenant member - * Backend: TenantMemberUpdate in schemas/tenants.py (lines 132-135) + * Backend: TenantMemberUpdate in schemas/tenants.py (lines 137-140) */ export interface TenantMemberUpdate { role?: 'owner' | 'admin' | 'member' | 'viewer' | null; is_active?: boolean | null; } +/** + * Schema for adding member with optional user creation (pilot phase) + * Backend: AddMemberWithUserCreate in schemas/tenants.py (lines 142-174) + */ +export interface AddMemberWithUserCreate { + // For existing users + user_id?: string | null; + + // For new user creation + create_user?: boolean; // Default: false + email?: string | null; + full_name?: string | null; + password?: string | null; + phone?: string | null; + language?: 'es' | 'en' | 'eu'; // Default: "es" + timezone?: string; // Default: "Europe/Madrid" + + // Common fields + role: 'admin' | 'member' | 'viewer'; +} + /** * Schema for updating tenant subscription * Backend: TenantSubscriptionUpdate in schemas/tenants.py (lines 137-140) @@ -121,8 +142,8 @@ export interface TenantAccessResponse { } /** - * Tenant member response - FIXED VERSION - * Backend: TenantMemberResponse in schemas/tenants.py (lines 90-107) + * Tenant member response - FIXED VERSION with enriched user data + * Backend: TenantMemberResponse in schemas/tenants.py (lines 91-112) */ export interface TenantMemberResponse { id: string; @@ -130,6 +151,10 @@ export interface TenantMemberResponse { role: string; is_active: boolean; joined_at?: string | null; // ISO datetime string + // Enriched user fields (populated via service layer) + user_email?: string | null; + user_full_name?: string | null; + user?: any; // Full user object for compatibility } /** diff --git a/frontend/src/components/domain/auth/PasswordResetForm.tsx b/frontend/src/components/domain/auth/PasswordResetForm.tsx index 76e4905d..f5cbc783 100644 --- a/frontend/src/components/domain/auth/PasswordResetForm.tsx +++ b/frontend/src/components/domain/auth/PasswordResetForm.tsx @@ -3,6 +3,7 @@ import { Button, Input, Card } from '../../ui'; import { PasswordCriteria, validatePassword, getPasswordErrors } from '../../ui/PasswordCriteria'; import { useAuthActions } from '../../../stores/auth.store'; import { useToast } from '../../../hooks/ui/useToast'; +import { useResetPassword } from '../../../api/hooks/auth'; interface PasswordResetFormProps { token?: string; @@ -33,10 +34,10 @@ export const PasswordResetForm: React.FC = ({ const emailInputRef = useRef(null); const passwordInputRef = useRef(null); - - // TODO: Implement password reset in Zustand auth store - // const { requestPasswordReset, resetPassword, isLoading, error } = useAuth(); - const isLoading = false; + + // Password reset mutation hooks + const { mutateAsync: resetPasswordMutation, isPending: isResetting } = useResetPassword(); + const isLoading = isResetting; const error = null; const { showToast } = useToast(); @@ -150,23 +151,14 @@ export const PasswordResetForm: React.FC = ({ } try { - // TODO: Implement password reset request - // const success = await requestPasswordReset(email); - const success = false; // Placeholder - if (success) { - setIsEmailSent(true); - showToast({ - type: 'success', - title: 'Email enviado correctamente', - message: 'Te hemos enviado las instrucciones para restablecer tu contraseΓ±a' - }); - } else { - showToast({ - type: 'error', - title: 'Error al enviar email', - message: error || 'No se pudo enviar el email. Verifica que la direcciΓ³n sea correcta.' - }); - } + // Note: Password reset request functionality needs to be implemented in backend + // For now, show a message that the feature is coming soon + setIsEmailSent(true); + showToast({ + type: 'info', + title: 'FunciΓ³n en desarrollo', + message: 'La solicitud de restablecimiento de contraseΓ±a estarΓ‘ disponible prΓ³ximamente. Por favor, contacta al administrador.' + }); } catch (err) { showToast({ type: 'error', @@ -197,28 +189,24 @@ export const PasswordResetForm: React.FC = ({ } try { - // TODO: Implement password reset - // const success = await resetPassword(token, password); - const success = false; // Placeholder - if (success) { - showToast({ - type: 'success', - title: 'ContraseΓ±a actualizada', - message: 'Β‘Tu contraseΓ±a ha sido restablecida exitosamente! Ya puedes iniciar sesiΓ³n.' - }); - onSuccess?.(); - } else { - showToast({ - type: 'error', - title: 'Error al restablecer contraseΓ±a', - message: error || 'El enlace ha expirado o no es vΓ‘lido. Solicita un nuevo restablecimiento.' - }); - } - } catch (err) { + // Call the reset password API + await resetPasswordMutation({ + token: token, + new_password: password + }); + + showToast({ + type: 'success', + title: 'ContraseΓ±a actualizada', + message: 'Β‘Tu contraseΓ±a ha sido restablecida exitosamente! Ya puedes iniciar sesiΓ³n.' + }); + onSuccess?.(); + } catch (err: any) { + const errorMessage = err?.response?.data?.detail || err?.message || 'El enlace ha expirado o no es vΓ‘lido. Solicita un nuevo restablecimiento.'; showToast({ type: 'error', - title: 'Error de conexiΓ³n', - message: 'No se pudo conectar con el servidor. Verifica tu conexiΓ³n a internet.' + title: 'Error al restablecer contraseΓ±a', + message: errorMessage }); } }; diff --git a/frontend/src/components/domain/production/CreateQualityTemplateModal.tsx b/frontend/src/components/domain/production/CreateQualityTemplateModal.tsx index 8e47cae9..86173dd0 100644 --- a/frontend/src/components/domain/production/CreateQualityTemplateModal.tsx +++ b/frontend/src/components/domain/production/CreateQualityTemplateModal.tsx @@ -11,6 +11,7 @@ import { useQuery } from '@tanstack/react-query'; import { recipesService } from '../../../api/services/recipes'; import type { RecipeResponse } from '../../../api/types/recipes'; import { statusColors } from '../../../styles/colors'; +import { useTranslation } from 'react-i18next'; interface CreateQualityTemplateModalProps { isOpen: boolean; @@ -45,16 +46,23 @@ const PROCESS_STAGE_OPTIONS = [ { value: ProcessStage.FINISHING, label: 'Acabado' } ]; -const CATEGORY_OPTIONS = [ - { value: 'appearance', label: 'Apariencia' }, - { value: 'structure', label: 'Estructura' }, - { value: 'texture', label: 'Textura' }, - { value: 'flavor', label: 'Sabor' }, - { value: 'safety', label: 'Seguridad' }, - { value: 'packaging', label: 'Empaque' }, - { value: 'temperature', label: 'Temperatura' }, - { value: 'weight', label: 'Peso' }, - { value: 'dimensions', label: 'Dimensiones' } +const CATEGORY_OPTIONS_KEYS = [ + { value: 'appearance', key: 'appearance' }, + { value: 'structure', key: 'structure' }, + { value: 'texture', key: 'texture' }, + { value: 'flavor', key: 'flavor' }, + { value: 'safety', key: 'safety' }, + { value: 'packaging', key: 'packaging' }, + { value: 'temperature', key: 'temperature' }, + { value: 'weight', key: 'weight' }, + { value: 'dimensions', key: 'dimensions' }, + { value: 'weight_check', key: 'weight_check' }, + { value: 'temperature_check', key: 'temperature_check' }, + { value: 'moisture_check', key: 'moisture_check' }, + { value: 'volume_check', key: 'volume_check' }, + { value: 'time_check', key: 'time_check' }, + { value: 'chemical', key: 'chemical' }, + { value: 'hygiene', key: 'hygiene' } ]; export const CreateQualityTemplateModal: React.FC = ({ @@ -64,10 +72,27 @@ export const CreateQualityTemplateModal: React.FC { + const { t } = useTranslation(); const currentTenant = useCurrentTenant(); const [loading, setLoading] = useState(false); const [selectedStages, setSelectedStages] = useState([]); + // Helper function to get translated category label + const getCategoryLabel = (category: string | null | undefined): string => { + if (!category) return 'Sin categorΓ­a'; + const translationKey = `production.quality.categories.${category}`; + const translated = t(translationKey); + return translated === translationKey ? category : translated; + }; + + // Build category options with translations + const getCategoryOptions = () => { + return CATEGORY_OPTIONS_KEYS.map(option => ({ + value: option.value, + label: getCategoryLabel(option.key) + })); + }; + // Fetch available recipes for association const { data: recipes, isLoading: recipesLoading } = useQuery({ queryKey: ['recipes', currentTenant?.id], @@ -186,7 +211,7 @@ export const CreateQualityTemplateModal: React.FC void; +}> = ({ value, onChange }) => { + const handleToggle = (stage: ProcessStage) => { + const newStages = value.includes(stage) + ? value.filter(s => s !== stage) + : [...value, stage]; + onChange(newStages); + }; + + return ( +
+ {PROCESS_STAGE_OPTIONS.map(stage => ( + + ))} +
+ ); +}; + export const EditQualityTemplateModal: React.FC = ({ isOpen, onClose, @@ -62,84 +96,99 @@ export const EditQualityTemplateModal: React.FC = onUpdateTemplate, isLoading = false }) => { + const { t } = useTranslation(); + const [mode, setMode] = useState<'view' | 'edit'>('edit'); + const [editedTemplate, setEditedTemplate] = useState(template); const [selectedStages, setSelectedStages] = useState( template.applicable_stages || [] ); - const { - register, - control, - handleSubmit, - watch, - reset, - formState: { errors, isDirty } - } = useForm({ - defaultValues: { - name: template.name, - template_code: template.template_code || '', - check_type: template.check_type, - category: template.category || '', - description: template.description || '', - instructions: template.instructions || '', - is_active: template.is_active, - is_required: template.is_required, - is_critical: template.is_critical, - weight: template.weight, - min_value: template.min_value, - max_value: template.max_value, - target_value: template.target_value, - unit: template.unit || '', - tolerance_percentage: template.tolerance_percentage - } - }); + // Helper function to get translated category label + const getCategoryLabel = (category: string | null | undefined): string => { + if (!category) return 'Sin categoría'; + const translationKey = `production.quality.categories.${category}`; + const translated = t(translationKey); + // If translation is same as key, it means no translation exists, return the original + return translated === translationKey ? category : translated; + }; - const checkType = watch('check_type'); + // Build category options with translations + const getCategoryOptions = () => { + return CATEGORY_OPTIONS_KEYS.map(option => ({ + value: option.value, + label: option.value === '' ? 'Seleccionar categoría' : getCategoryLabel(option.key) + })); + }; + + // Update local state when template changes + useEffect(() => { + if (template) { + setEditedTemplate(template); + setSelectedStages(template.applicable_stages || []); + } + }, [template]); + + const checkType = editedTemplate.check_type; const showMeasurementFields = [ QualityCheckType.MEASUREMENT, QualityCheckType.TEMPERATURE, QualityCheckType.WEIGHT - ].includes(checkType || template.check_type); + ].includes(checkType); - // Update form when template changes - useEffect(() => { - if (template) { - reset({ - name: template.name, - template_code: template.template_code || '', - check_type: template.check_type, - category: template.category || '', - description: template.description || '', - instructions: template.instructions || '', - is_active: template.is_active, - is_required: template.is_required, - is_critical: template.is_critical, - weight: template.weight, - min_value: template.min_value, - max_value: template.max_value, - target_value: template.target_value, - unit: template.unit || '', - tolerance_percentage: template.tolerance_percentage - }); - setSelectedStages(template.applicable_stages || []); + const handleFieldChange = (sectionIndex: number, fieldIndex: number, value: string | number) => { + const sections = getSections(); + const field = sections[sectionIndex].fields[fieldIndex]; + const fieldLabel = field.label; + + // Map field labels to template properties + const fieldMap: Record = { + 'Nombre': 'name', + 'Código de Plantilla': 'template_code', + 'Tipo de Control': 'check_type', + 'Categoría': 'category', + 'Descripción': 'description', + 'Instrucciones para el Personal': 'instructions', + 'Valor Mínimo': 'min_value', + 'Valor MÑximo': 'max_value', + 'Valor Objetivo': 'target_value', + 'Unidad': 'unit', + 'Tolerancia (%)': 'tolerance_percentage', + 'Etapas Aplicables': 'stages', + 'Peso en Puntuación General': 'weight', + 'Plantilla Activa': 'is_active', + 'Control Requerido': 'is_required', + 'Control Crítico': 'is_critical' + }; + + const propertyName = fieldMap[fieldLabel]; + if (propertyName) { + if (propertyName === 'stages') { + // Handle stages separately through the custom component + return; + } + + // Convert string booleans to actual booleans + let processedValue: any = value; + if (propertyName === 'is_active' || propertyName === 'is_required' || propertyName === 'is_critical') { + processedValue = String(value) === 'true'; + } + + setEditedTemplate(prev => ({ + ...prev, + [propertyName]: processedValue + })); } - }, [template, reset]); - - const handleStageToggle = (stage: ProcessStage) => { - const newStages = selectedStages.includes(stage) - ? selectedStages.filter(s => s !== stage) - : [...selectedStages, stage]; - - setSelectedStages(newStages); }; - const onSubmit = async (data: QualityCheckTemplateUpdate) => { + const handleSave = async () => { try { - // Only include changed fields + // Build update object with only changed fields const updates: QualityCheckTemplateUpdate = {}; - Object.entries(data).forEach(([key, value]) => { + // Check each field for changes + Object.entries(editedTemplate).forEach(([key, value]) => { const originalValue = (template as any)[key]; - if (value !== originalValue) { + if (value !== originalValue && key !== 'id' && key !== 'created_at' && key !== 'updated_at' && key !== 'created_by') { (updates as any)[key] = value; } }); @@ -155,309 +204,275 @@ export const EditQualityTemplateModal: React.FC = // Only submit if there are actual changes if (Object.keys(updates).length > 0) { await onUpdateTemplate(updates); - } else { - onClose(); } + + setMode('view'); } catch (error) { console.error('Error updating template:', error); + throw error; } }; - const handleClose = () => { - reset(); + const handleCancel = () => { + // Reset to original values + setEditedTemplate(template); setSelectedStages(template.applicable_stages || []); - onClose(); + setMode('view'); + }; + + const getSections = (): EditViewModalSection[] => { + const sections: EditViewModalSection[] = [ + { + title: 'Información BÑsica', + icon: ClipboardCheck, + fields: [ + { + label: 'Nombre', + value: editedTemplate.name, + type: 'text', + highlight: true, + editable: true, + required: true, + placeholder: 'Ej: Control Visual de Pan' + }, + { + label: 'Código de Plantilla', + value: editedTemplate.template_code || '', + type: 'text', + editable: true, + placeholder: 'Ej: CV_PAN_01' + }, + { + label: 'Tipo de Control', + value: editedTemplate.check_type, + type: 'select', + editable: true, + required: true, + options: QUALITY_CHECK_TYPE_OPTIONS + }, + { + label: 'Categoría', + value: editedTemplate.category || '', + type: 'select', + editable: true, + options: getCategoryOptions() + }, + { + label: 'Descripción', + value: editedTemplate.description || '', + type: 'textarea', + editable: true, + placeholder: 'Describe qué evalúa esta plantilla de calidad', + span: 2 + }, + { + label: 'Instrucciones para el Personal', + value: editedTemplate.instructions || '', + type: 'textarea', + editable: true, + placeholder: 'Instrucciones detalladas para realizar este control de calidad', + span: 2, + helpText: 'Pasos específicos que debe seguir el operario' + } + ] + } + ]; + + // Add measurement configuration section if applicable + if (showMeasurementFields) { + sections.push({ + title: 'Configuración de Medición', + icon: Target, + fields: [ + { + label: 'Valor Mínimo', + value: editedTemplate.min_value || 0, + type: 'number', + editable: true, + placeholder: '0', + helpText: 'Valor mínimo aceptable para la medición' + }, + { + label: 'Valor MÑximo', + value: editedTemplate.max_value || 0, + type: 'number', + editable: true, + placeholder: '100', + helpText: 'Valor mÑximo aceptable para la medición' + }, + { + label: 'Valor Objetivo', + value: editedTemplate.target_value || 0, + type: 'number', + editable: true, + placeholder: '50', + helpText: 'Valor ideal que se busca alcanzar' + }, + { + label: 'Unidad', + value: editedTemplate.unit || '', + type: 'text', + editable: true, + placeholder: '°C / g / cm', + helpText: 'Unidad de medida (ej: °C para temperatura)' + }, + { + label: 'Tolerancia (%)', + value: editedTemplate.tolerance_percentage || 0, + type: 'number', + editable: true, + placeholder: '5', + helpText: 'Porcentaje de tolerancia permitido' + } + ] + }); + } + + // Process stages section with custom component + sections.push({ + title: 'Etapas del Proceso', + icon: Settings, + description: 'Selecciona las etapas donde se debe aplicar este control. Si no seleccionas ninguna, se aplicarÑ a todas las etapas.', + fields: [ + { + label: 'Etapas Aplicables', + value: selectedStages.length > 0 + ? selectedStages.map(s => PROCESS_STAGE_OPTIONS.find(opt => opt.value === s)?.label || s).join(', ') + : 'Todas las etapas', + type: 'component', + editable: true, + component: ProcessStagesSelector, + componentProps: { + value: selectedStages, + onChange: setSelectedStages + }, + span: 2 + } + ] + }); + + // Configuration section + sections.push({ + title: 'Configuración Avanzada', + icon: Cog, + fields: [ + { + label: 'Peso en Puntuación General', + value: editedTemplate.weight, + type: 'number', + editable: true, + placeholder: '1.0', + helpText: 'Mayor peso = mayor importancia en la puntuación final (0-10)', + validation: (value: string | number) => { + const num = Number(value); + return num < 0 || num > 10 ? 'El peso debe estar entre 0 y 10' : null; + } + }, + { + label: 'Plantilla Activa', + value: editedTemplate.is_active, + type: 'select', + editable: true, + options: [ + { value: 'true', label: 'Sí' }, + { value: 'false', label: 'No' } + ] + }, + { + label: 'Control Requerido', + value: editedTemplate.is_required, + type: 'select', + editable: true, + options: [ + { value: 'true', label: 'Sí' }, + { value: 'false', label: 'No' } + ], + helpText: 'Si es requerido, debe completarse obligatoriamente' + }, + { + label: 'Control Crítico', + value: editedTemplate.is_critical, + type: 'select', + editable: true, + options: [ + { value: 'true', label: 'Sí' }, + { value: 'false', label: 'No' } + ], + helpText: 'Si es crítico, bloquea la producción si falla' + } + ] + }); + + // Template metadata section + sections.push({ + title: 'Información de la Plantilla', + icon: ClipboardCheck, + collapsible: true, + collapsed: true, + fields: [ + { + label: 'ID', + value: editedTemplate.id, + type: 'text', + editable: false + }, + { + label: 'Creado', + value: editedTemplate.created_at, + type: 'datetime', + editable: false + }, + { + label: 'Última Actualización', + value: editedTemplate.updated_at, + type: 'datetime', + editable: false + } + ] + }); + + return sections; + }; + + const getStatusIndicator = () => { + const typeColors: Record = { + [QualityCheckType.VISUAL]: '#3B82F6', + [QualityCheckType.MEASUREMENT]: '#10B981', + [QualityCheckType.TEMPERATURE]: '#EF4444', + [QualityCheckType.WEIGHT]: '#A855F7', + [QualityCheckType.BOOLEAN]: '#6B7280', + [QualityCheckType.TIMING]: '#F59E0B', + [QualityCheckType.CHECKLIST]: '#6366F1' + }; + + return { + color: editedTemplate.is_active ? typeColors[editedTemplate.check_type] : '#6B7280', + text: editedTemplate.is_active ? 'Activa' : 'Inactiva', + icon: Edit, + isCritical: editedTemplate.is_critical, + isHighlight: editedTemplate.is_required + }; }; return ( - -
- {/* Basic Information */} - -

- InformaciΓ³n BΓ‘sica -

- -
-
- - -
- -
- - -
- -
- - ( - - )} - /> -
- -
- - ( - - )} - /> -
-
- -
- -