diff --git a/MVP_GAP_ANALYSIS_REPORT.md b/MVP_GAP_ANALYSIS_REPORT.md new file mode 100644 index 00000000..8007aab5 --- /dev/null +++ b/MVP_GAP_ANALYSIS_REPORT.md @@ -0,0 +1,216 @@ +# Bakery AI Platform - MVP Gap Analysis Report + +## Executive Summary + +Based on the detailed bakery research report and analysis of the current platform, this document identifies critical missing features that are preventing the platform from delivering value to Madrid's small bakery owners. While the platform has a solid technical foundation with microservices architecture and AI forecasting capabilities, it lacks several core operational features that are essential for day-to-day bakery management. + +## Current Platform Status + +### ✅ **Implemented Features** + +#### Backend Services (Functional) +- **Authentication Service**: Complete user registration, login, JWT tokens, role-based access +- **Tenant Service**: Multi-tenant architecture, subscription management, team member access +- **Training Service**: ML model training using Prophet for demand forecasting +- **Forecasting Service**: AI-powered demand predictions and alerts +- **Data Service**: Weather data integration (AEMET), traffic data, external data processing +- **Notification Service**: Email and WhatsApp notifications +- **API Gateway**: Centralized routing, rate limiting, service discovery + +#### Frontend Features (Functional) +- **Dashboard**: Revenue metrics, weather display, production overview +- **Authentication**: Login/registration pages with proper validation +- **Forecasting**: Demand prediction visualizations, forecast charts +- **Production Planning**: Basic production scheduling interface +- **Order Management**: Mock order display with supplier information +- **Settings**: User profile and basic configuration + +#### Technical Infrastructure +- Microservices architecture with Docker containerization +- PostgreSQL databases per service with proper migrations +- RabbitMQ message queuing for inter-service communication +- Monitoring with Prometheus and Grafana +- Comprehensive error handling and logging + +### ❌ **Critical Missing Features for MVP Launch** + +## 1. **INVENTORY MANAGEMENT SYSTEM** 🚨 **HIGHEST PRIORITY** + +### **Problem Identified**: +According to the bakery research, manual inventory tracking is described as "too cumbersome," "time-consuming," and highly susceptible to "mistakes." This leads to: +- 1.5% to 20% losses due to spoilage and waste +- Production delays during peak hours +- Quality inconsistencies +- Lost sales opportunities + +### **Missing Components**: +- **Ingredient tracking**: Real-time stock levels for flour, yeast, dairy products +- **Automatic reordering**: FIFO/FEFO expiration date management +- **Spoilage monitoring**: Track and predict ingredient expiration +- **Stock alerts**: Low stock warnings integrated with production planning +- **Barcode/QR scanning**: Easy inventory updates without manual entry +- **Supplier integration**: Automated ordering from suppliers like Harinas Castellana + +### **Required Implementation**: +``` +Backend Services Needed: +- Inventory Service (new microservice) +- Supplier Service (new microservice) +- Integration with existing Forecasting Service + +Frontend Components Needed: +- Real-time inventory dashboard +- Mobile-friendly inventory scanning +- Automated reorder interface +- Expiration date tracking +``` + +## 2. **RECIPE & PRODUCTION MANAGEMENT** 🚨 **HIGH PRIORITY** + +### **Problem Identified**: +Individual bakeries struggle with production planning complexity due to: +- Wide variety of products with different preparation times +- Manual calculation of ingredient quantities +- Lack of standardized recipes affecting quality consistency + +### **Missing Components**: +- **Digital recipe management**: Store recipes with exact measurements +- **Bill of Materials (BOM)**: Automatic ingredient calculation based on production volume +- **Yield tracking**: Compare actual vs. expected production output +- **Cost calculation**: Real-time cost per product based on current ingredient prices +- **Production workflow**: Step-by-step production guidance +- **Quality control**: Track temperature, humidity, timing parameters + +## 3. **SUPPLIER & PROCUREMENT SYSTEM** 🚨 **HIGH PRIORITY** + +### **Problem Identified**: +Research shows small bakeries face "low buyer power" and struggle with: +- Manual ordering processes via phone/WhatsApp +- Difficulty tracking supplier performance +- Limited negotiation power with suppliers + +### **Missing Components**: +- **Supplier database**: Contact information, lead times, reliability ratings +- **Purchase order system**: Digital ordering with approval workflows +- **Price comparison**: Compare prices across multiple suppliers +- **Delivery tracking**: Monitor order status and delivery reliability +- **Payment terms**: Track payment schedules and supplier agreements +- **Performance analytics**: Supplier reliability and cost analysis + +## 4. **SALES DATA INTEGRATION** 🚨 **HIGH PRIORITY** + +### **Problem Identified**: +Current forecasting relies on manual data entry. Research shows bakeries need: +- Integration with POS systems +- Historical sales pattern analysis +- External factor correlation (weather, events, holidays) + +### **Missing Components**: +- **POS Integration**: Automatic sales data import from common Spanish POS systems +- **Manual sales entry**: Simple interface for bakeries without POS +- **Product categorization**: Organize sales by bread types, pastries, seasonal items +- **Customer analytics**: Track popular products and buying patterns +- **Seasonal adjustments**: Account for holidays, local events, weather impacts + +## 5. **WASTE TRACKING & REDUCTION** 🚨 **MEDIUM PRIORITY** + +### **Problem Identified**: +Research indicates waste reduction potential of 20-40% through AI optimization: +- Unsold products (1.5% of production) +- Ingredient spoilage +- Production errors + +### **Missing Components**: +- **Daily waste logging**: Track unsold products, spoiled ingredients +- **Waste analytics**: Identify patterns in waste generation +- **Dynamic pricing**: Reduce prices on items approaching expiration +- **Donation tracking**: Manage food donations to reduce total waste +- **Cost impact analysis**: Calculate financial impact of waste reduction + +## 6. **MOBILE-FIRST INTERFACE** 🚨 **MEDIUM PRIORITY** + +### **Problem Identified**: +Research emphasizes bakery owners work demanding schedules starting at 4:30 AM and need "mobile accessibility" for on-the-go management. + +### **Missing Components**: +- **Mobile-responsive design**: Current frontend is not optimized for mobile +- **Offline capabilities**: Work without internet connection +- **Quick actions**: Fast inventory checks, order placement +- **Voice input**: Hands-free operation in production environment +- **QR code scanning**: For inventory and product management + +## 7. **FINANCIAL MANAGEMENT** 🚨 **LOW PRIORITY** + +### **Problem Identified**: +With 75-85% of revenue consumed by operating costs and 4-9% profit margins, bakeries need precise cost control. + +### **Missing Components**: +- **Cost tracking**: Monitor food costs (25-35% of sales) and labor costs (24-40% of sales) +- **Profit analysis**: Real-time profit margins per product +- **Budget planning**: Monthly expense forecasting +- **Tax preparation**: VAT calculations, expense categorization +- **Financial reporting**: P&L statements, cash flow analysis + +## Implementation Priority Matrix + +| Feature | Business Impact | Technical Complexity | Implementation Time | Priority | +|---------|----------------|---------------------|-------------------|----------| +| Inventory Management | Very High | Medium | 6-8 weeks | 1 | +| Recipe & BOM System | Very High | Medium | 4-6 weeks | 2 | +| Supplier Management | High | Low-Medium | 4-5 weeks | 3 | +| Sales Data Integration | High | Medium | 3-4 weeks | 4 | +| Waste Tracking | Medium | Low | 2-3 weeks | 5 | +| Mobile Optimization | Medium | Medium | 4-6 weeks | 6 | +| Financial Management | Low | High | 8-10 weeks | 7 | + +## Technical Architecture Requirements + +### New Microservices Needed: +1. **Inventory Service** - Real-time stock management, expiration tracking +2. **Recipe Service** - Digital recipes, BOM calculations, cost management +3. **Supplier Service** - Supplier database, purchase orders, performance tracking +4. **Integration Service** - POS system connectors, external data feeds + +### Database Schema Extensions: +- Products table with recipes and ingredient relationships +- Inventory transactions with batch/lot tracking +- Supplier master data with performance metrics +- Purchase orders with approval workflows + +### Frontend Components Required: +- Mobile-responsive inventory management interface +- Recipe editor with drag-drop ingredient addition +- Supplier portal for order placement and tracking +- Real-time dashboard with critical alerts + +## MVP Launch Recommendations + +### Phase 1 (8-10 weeks): Core Operations +- Implement Inventory Management System +- Build Recipe & BOM functionality +- Create Supplier Management portal +- Mobile UI optimization + +### Phase 2 (4-6 weeks): Data Integration +- POS system integrations +- Enhanced sales data processing +- Waste tracking implementation + +### Phase 3 (6-8 weeks): Advanced Features +- Financial management tools +- Advanced analytics and reporting +- Performance optimization + +## Conclusion + +The current platform has excellent technical foundations but lacks the core operational features that small Madrid bakeries desperately need. The research clearly shows that **inventory management inefficiencies are the #1 pain point**, causing 1.5-20% losses and significant operational stress. + +**Without implementing inventory management, recipe management, and supplier systems, the platform cannot deliver the value proposition of waste reduction and cost savings that bakeries require for survival.** + +The recommended approach is to focus on the top 4 priority features for MVP launch, which will provide immediate tangible value to bakery owners and justify the platform subscription costs. + +--- + +**Report Generated**: January 2025 +**Status**: MVP Gap Analysis Complete +**Next Actions**: Begin Phase 1 implementation planning \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 9d74f267..03f9f6ed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,8 @@ volumes: auth_db_data: training_db_data: forecasting_db_data: - data_db_data: + sales_db_data: + external_db_data: tenant_db_data: notification_db_data: redis_data: @@ -153,23 +154,44 @@ services: timeout: 5s retries: 5 - data-db: + sales-db: image: postgres:15-alpine - container_name: bakery-data-db + container_name: bakery-sales-db restart: unless-stopped environment: - - POSTGRES_DB=${DATA_DB_NAME} - - POSTGRES_USER=${DATA_DB_USER} - - POSTGRES_PASSWORD=${DATA_DB_PASSWORD} + - POSTGRES_DB=${SALES_DB_NAME} + - POSTGRES_USER=${SALES_DB_USER} + - POSTGRES_PASSWORD=${SALES_DB_PASSWORD} - POSTGRES_INITDB_ARGS=${POSTGRES_INITDB_ARGS} - PGDATA=/var/lib/postgresql/data/pgdata volumes: - - data_db_data:/var/lib/postgresql/data + - sales_db_data:/var/lib/postgresql/data networks: bakery-network: ipv4_address: 172.20.0.23 healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DATA_DB_USER} -d ${DATA_DB_NAME}"] + test: ["CMD-SHELL", "pg_isready -U ${SALES_DB_USER} -d ${SALES_DB_NAME}"] + interval: 10s + timeout: 5s + retries: 5 + + external-db: + image: postgres:15-alpine + container_name: bakery-external-db + restart: unless-stopped + environment: + - POSTGRES_DB=${EXTERNAL_DB_NAME} + - POSTGRES_USER=${EXTERNAL_DB_USER} + - POSTGRES_PASSWORD=${EXTERNAL_DB_PASSWORD} + - POSTGRES_INITDB_ARGS=${POSTGRES_INITDB_ARGS} + - PGDATA=/var/lib/postgresql/data/pgdata + volumes: + - external_db_data:/var/lib/postgresql/data + networks: + bakery-network: + ipv4_address: 172.20.0.26 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${EXTERNAL_DB_USER} -d ${EXTERNAL_DB_NAME}"] interval: 10s timeout: 5s retries: 5 @@ -409,7 +431,9 @@ services: condition: service_healthy auth-service: condition: service_healthy - data-service: + sales-service: + condition: service_healthy + external-service: condition: service_healthy networks: bakery-network: @@ -468,21 +492,21 @@ services: timeout: 10s retries: 3 - data-service: + sales-service: build: context: . - dockerfile: ./services/data/Dockerfile + dockerfile: ./services/sales/Dockerfile args: - ENVIRONMENT=${ENVIRONMENT} - BUILD_DATE=${BUILD_DATE} - image: bakery/data-service:${IMAGE_TAG} - container_name: bakery-data-service + image: bakery/sales-service:${IMAGE_TAG} + container_name: bakery-sales-service restart: unless-stopped env_file: .env ports: - - "${DATA_SERVICE_PORT}:8000" + - "${SALES_SERVICE_PORT}:8000" depends_on: - data-db: + sales-db: condition: service_healthy redis: condition: service_healthy @@ -495,7 +519,42 @@ services: ipv4_address: 172.20.0.104 volumes: - log_storage:/app/logs - - ./services/data:/app + - ./services/sales:/app + - ./shared:/app/shared + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health').read()"] + interval: 30s + timeout: 10s + retries: 3 + + external-service: + build: + context: . + dockerfile: ./services/external/Dockerfile + args: + - ENVIRONMENT=${ENVIRONMENT} + - BUILD_DATE=${BUILD_DATE} + image: bakery/external-service:${IMAGE_TAG} + container_name: bakery-external-service + restart: unless-stopped + env_file: .env + ports: + - "${EXTERNAL_SERVICE_PORT}:8000" + depends_on: + external-db: + condition: service_healthy + redis: + condition: service_healthy + rabbitmq: + condition: service_healthy + auth-service: + condition: service_healthy + networks: + bakery-network: + ipv4_address: 172.20.0.107 + volumes: + - log_storage:/app/logs + - ./services/external:/app - ./shared:/app/shared healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8c47370b..18d00bd7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -193,7 +193,8 @@ const App: React.FC = () => { isAuthenticated: false, isLoading: false, user: null, - currentPage: 'landing' // 👈 Return to landing page after logout + currentPage: 'landing', // 👈 Return to landing page after logout + routingDecision: null }); }; diff --git a/frontend/src/api/client/index.ts b/frontend/src/api/client/index.ts index cf903a3c..533ccc88 100644 --- a/frontend/src/api/client/index.ts +++ b/frontend/src/api/client/index.ts @@ -333,7 +333,12 @@ private buildURL(endpoint: string): string { size: JSON.stringify(result).length, }); - return result; + // Handle both wrapped and unwrapped responses + // If result has a 'data' property, return it; otherwise return the result itself + if (result && typeof result === 'object' && 'data' in result) { + return result.data as T; + } + return result as T; } catch (error) { // Record error metrics this.recordMetrics({ diff --git a/frontend/src/api/client/types.ts b/frontend/src/api/client/types.ts index 89f1af8e..2b54af5f 100644 --- a/frontend/src/api/client/types.ts +++ b/frontend/src/api/client/types.ts @@ -7,12 +7,15 @@ export interface RequestConfig { method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; headers?: Record; params?: Record; + body?: any; + url?: string; timeout?: number; retries?: number; cache?: boolean; cacheTTL?: number; optimistic?: boolean; background?: boolean; + metadata?: any; } export interface ApiResponse { @@ -20,12 +23,14 @@ export interface ApiResponse { message?: string; status: string; timestamp?: string; + metadata?: any; meta?: { page?: number; limit?: number; total?: number; hasNext?: boolean; hasPrev?: boolean; + requestId?: string; }; } diff --git a/frontend/src/api/hooks/index.ts b/frontend/src/api/hooks/index.ts index 60cdfa0c..81b210d9 100644 --- a/frontend/src/api/hooks/index.ts +++ b/frontend/src/api/hooks/index.ts @@ -5,7 +5,8 @@ export { useAuth, useAuthHeaders } from './useAuth'; export { useTenant } from './useTenant'; -export { useData } from './useData'; +export { useSales } from './useSales'; +export { useExternal } from './useExternal'; export { useTraining } from './useTraining'; export { useForecast } from './useForecast'; export { useNotification } from './useNotification'; @@ -14,7 +15,8 @@ export { useOnboarding, useOnboardingStep } from './useOnboarding'; // Import hooks for combined usage import { useAuth } from './useAuth'; import { useTenant } from './useTenant'; -import { useData } from './useData'; +import { useSales } from './useSales'; +import { useExternal } from './useExternal'; import { useTraining } from './useTraining'; import { useForecast } from './useForecast'; import { useNotification } from './useNotification'; @@ -24,7 +26,8 @@ import { useOnboarding } from './useOnboarding'; export const useApiHooks = () => { const auth = useAuth(); const tenant = useTenant(); - const data = useData(); + const sales = useSales(); + const external = useExternal(); const training = useTraining(); const forecast = useForecast(); const notification = useNotification(); @@ -33,7 +36,8 @@ export const useApiHooks = () => { return { auth, tenant, - data, + sales, + external, training, forecast, notification, diff --git a/frontend/src/api/hooks/useAuth.ts b/frontend/src/api/hooks/useAuth.ts index 9d8a48e3..6cb6ba15 100644 --- a/frontend/src/api/hooks/useAuth.ts +++ b/frontend/src/api/hooks/useAuth.ts @@ -99,7 +99,11 @@ export const useAuth = () => { const response = await authService.register(data); // Auto-login after successful registration - if (response.user) { + if (response && response.user) { + await login({ email: data.email, password: data.password }); + } else { + // If response doesn't have user property, registration might still be successful + // Try to login anyway in case the user was created but response format is different await login({ email: data.email, password: data.password }); } } catch (error) { diff --git a/frontend/src/api/hooks/useExternal.ts b/frontend/src/api/hooks/useExternal.ts new file mode 100644 index 00000000..9b3950eb --- /dev/null +++ b/frontend/src/api/hooks/useExternal.ts @@ -0,0 +1,209 @@ +// frontend/src/api/hooks/useExternal.ts +/** + * External Data Management Hooks + * Handles weather and traffic data operations + */ + +import { useState, useCallback } from 'react'; +import { externalService } from '../services/external.service'; +import type { WeatherData, TrafficData, WeatherForecast } from '../services/external.service'; + +export const useExternal = () => { + const [weatherData, setWeatherData] = useState(null); + const [trafficData, setTrafficData] = useState(null); + const [weatherForecast, setWeatherForecast] = useState([]); + const [trafficForecast, setTrafficForecast] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + /** + * Get Current Weather + */ + const getCurrentWeather = useCallback(async ( + tenantId: string, + lat: number, + lon: number + ): Promise => { + try { + setIsLoading(true); + setError(null); + + const weather = await externalService.getCurrentWeather(tenantId, lat, lon); + setWeatherData(weather); + + return weather; + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to get weather data'; + setError(message); + throw error; + } finally { + setIsLoading(false); + } + }, []); + + /** + * Get Weather Forecast + */ + const getWeatherForecast = useCallback(async ( + tenantId: string, + lat: number, + lon: number, + days: number = 7 + ): Promise => { + try { + setIsLoading(true); + setError(null); + + const forecast = await externalService.getWeatherForecast(tenantId, lat, lon, days); + setWeatherForecast(forecast); + + return forecast; + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to get weather forecast'; + setError(message); + throw error; + } finally { + setIsLoading(false); + } + }, []); + + /** + * Get Historical Weather Data + */ + const getHistoricalWeather = useCallback(async ( + tenantId: string, + lat: number, + lon: number, + startDate: string, + endDate: string + ): Promise => { + try { + setIsLoading(true); + setError(null); + + const data = await externalService.getHistoricalWeather(tenantId, lat, lon, startDate, endDate); + + return data; + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to get historical weather'; + setError(message); + throw error; + } finally { + setIsLoading(false); + } + }, []); + + /** + * Get Current Traffic + */ + const getCurrentTraffic = useCallback(async ( + tenantId: string, + lat: number, + lon: number + ): Promise => { + try { + setIsLoading(true); + setError(null); + + const traffic = await externalService.getCurrentTraffic(tenantId, lat, lon); + setTrafficData(traffic); + + return traffic; + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to get traffic data'; + setError(message); + throw error; + } finally { + setIsLoading(false); + } + }, []); + + /** + * Get Traffic Forecast + */ + const getTrafficForecast = useCallback(async ( + tenantId: string, + lat: number, + lon: number, + hours: number = 24 + ): Promise => { + try { + setIsLoading(true); + setError(null); + + const forecast = await externalService.getTrafficForecast(tenantId, lat, lon, hours); + setTrafficForecast(forecast); + + return forecast; + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to get traffic forecast'; + setError(message); + throw error; + } finally { + setIsLoading(false); + } + }, []); + + /** + * Get Historical Traffic Data + */ + const getHistoricalTraffic = useCallback(async ( + tenantId: string, + lat: number, + lon: number, + startDate: string, + endDate: string + ): Promise => { + try { + setIsLoading(true); + setError(null); + + const data = await externalService.getHistoricalTraffic(tenantId, lat, lon, startDate, endDate); + + return data; + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to get historical traffic'; + setError(message); + throw error; + } finally { + setIsLoading(false); + } + }, []); + + /** + * Test External Services Connectivity + */ + const testConnectivity = useCallback(async (tenantId: string) => { + try { + setIsLoading(true); + setError(null); + + const results = await externalService.testConnectivity(tenantId); + + return results; + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to test connectivity'; + setError(message); + throw error; + } finally { + setIsLoading(false); + } + }, []); + + return { + weatherData, + trafficData, + weatherForecast, + trafficForecast, + isLoading, + error, + getCurrentWeather, + getWeatherForecast, + getHistoricalWeather, + getCurrentTraffic, + getTrafficForecast, + getHistoricalTraffic, + testConnectivity, + clearError: () => setError(null), + }; +}; \ No newline at end of file diff --git a/frontend/src/api/hooks/useData.ts b/frontend/src/api/hooks/useSales.ts similarity index 78% rename from frontend/src/api/hooks/useData.ts rename to frontend/src/api/hooks/useSales.ts index 6c8c1572..0913a46c 100644 --- a/frontend/src/api/hooks/useData.ts +++ b/frontend/src/api/hooks/useSales.ts @@ -1,20 +1,21 @@ -// frontend/src/api/hooks/useData.ts +// frontend/src/api/hooks/useSales.ts /** - * Data Management Hooks + * Sales Data Management Hooks */ import { useState, useCallback } from 'react'; -import { dataService } from '../services'; +import { salesService } from '../services/sales.service'; import type { SalesData, SalesValidationResult, SalesDataQuery, + SalesDataImport, SalesImportResult, DashboardStats, ActivityItem, } from '../types'; -export const useData = () => { +export const useSales = () => { const [salesData, setSalesData] = useState([]); const [dashboardStats, setDashboardStats] = useState(null); const [recentActivity, setRecentActivity] = useState([]); @@ -32,7 +33,7 @@ export const useData = () => { setError(null); setUploadProgress(0); - const result = await dataService.uploadSalesHistory(tenantId, file, { + const result = await salesService.uploadSalesHistory(tenantId, file, { ...additionalData, onProgress: (progress) => { setUploadProgress(progress.percentage); @@ -58,7 +59,7 @@ export const useData = () => { setIsLoading(true); setError(null); - const result = await dataService.validateSalesData(tenantId, file); + const result = await salesService.validateSalesData(tenantId, file); return result; } catch (error) { const message = error instanceof Error ? error.message : 'Validation failed'; @@ -77,7 +78,7 @@ export const useData = () => { setIsLoading(true); setError(null); - const response = await dataService.getSalesData(tenantId, query); + const response = await salesService.getSalesData(tenantId, query); setSalesData(response.data); return response.data; @@ -95,7 +96,7 @@ export const useData = () => { setIsLoading(true); setError(null); - const stats = await dataService.getDashboardStats(tenantId); + const stats = await salesService.getDashboardStats(tenantId); setDashboardStats(stats); return stats; @@ -113,7 +114,7 @@ export const useData = () => { setIsLoading(true); setError(null); - const activity = await dataService.getRecentActivity(tenantId, limit); + const activity = await salesService.getRecentActivity(tenantId, limit); setRecentActivity(activity); return activity; @@ -135,7 +136,7 @@ export const useData = () => { setIsLoading(true); setError(null); - const blob = await dataService.exportSalesData(tenantId, format, query); + const blob = await salesService.exportSalesData(tenantId, format, query); // Create download link const url = window.URL.createObjectURL(blob); @@ -157,14 +158,13 @@ export const useData = () => { /** * Get Products List - * Add this method to the useData hook */ const getProductsList = useCallback(async (tenantId: string): Promise => { try { setIsLoading(true); setError(null); - const products = await dataService.getProductsList(tenantId); + const products = await salesService.getProductsList(tenantId); return products; } catch (error) { @@ -176,30 +176,8 @@ export const useData = () => { } }, []); - /** - * Get Current Weather - * Add this method to the useData hook - */ - const getCurrentWeather = useCallback(async (tenantId: string, lat: number, lon: number) => { - try { - setIsLoading(true); - setError(null); - - const weather = await dataService.getCurrentWeather(tenantId, lat, lon); - - return weather; - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get weather data'; - setError(message); - throw error; - } finally { - setIsLoading(false); - } - }, []); - /** * Get Sales Analytics - * Add this method to the useData hook */ const getSalesAnalytics = useCallback(async ( tenantId: string, @@ -210,7 +188,7 @@ export const useData = () => { setIsLoading(true); setError(null); - const analytics = await dataService.getSalesAnalytics(tenantId, startDate, endDate); + const analytics = await salesService.getSalesAnalytics(tenantId, startDate, endDate); return analytics; } catch (error) { @@ -236,8 +214,7 @@ export const useData = () => { getRecentActivity, exportSalesData, getProductsList, - getCurrentWeather, getSalesAnalytics, clearError: () => setError(null), }; -}; +}; \ No newline at end of file diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 8867fa3e..94d47554 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -11,7 +11,8 @@ export { apiClient } from './client'; export { authService, tenantService, - dataService, + salesService, + externalService, trainingService, forecastingService, notificationService, @@ -23,7 +24,8 @@ export { useAuth, useAuthHeaders, useTenant, - useData, + useSales, + useExternal, useTraining, useForecast, useNotification, diff --git a/frontend/src/api/services/auth.service.ts b/frontend/src/api/services/auth.service.ts index d9f7d162..dd2a8c2d 100644 --- a/frontend/src/api/services/auth.service.ts +++ b/frontend/src/api/services/auth.service.ts @@ -24,7 +24,7 @@ export class AuthService { /** * User Registration */ - async register(data: RegisterRequest): Promise<{ user: UserResponse }> { + async register(data: RegisterRequest): Promise { return apiClient.post(`${this.baseEndpoint}/register`, data); } diff --git a/frontend/src/api/services/external.service.ts b/frontend/src/api/services/external.service.ts new file mode 100644 index 00000000..96169c9f --- /dev/null +++ b/frontend/src/api/services/external.service.ts @@ -0,0 +1,264 @@ +// frontend/src/api/services/external.service.ts +/** + * External Data Service + * Handles weather and traffic data operations for the external microservice + */ + +import { apiClient } from '../client'; +import { RequestTimeouts } from '../client/config'; + +// Align with backend WeatherDataResponse schema +export interface WeatherData { + date: string; + temperature?: number; + precipitation?: number; + humidity?: number; + wind_speed?: number; + pressure?: number; + description?: string; + source: string; +} + +// Align with backend TrafficDataResponse schema +export interface TrafficData { + date: string; + traffic_volume?: number; + pedestrian_count?: number; + congestion_level?: string; + average_speed?: number; + source: string; +} + +export interface WeatherForecast { + date: string; + temperature_min: number; + temperature_max: number; + temperature_avg: number; + precipitation: number; + description: string; + humidity?: number; + wind_speed?: number; +} + +export class ExternalService { + /** + * Get Current Weather Data + */ + async getCurrentWeather( + tenantId: string, + lat: number, + lon: number + ): Promise { + try { + // ✅ FIX 1: Correct endpoint path with tenant ID + const endpoint = `/tenants/${tenantId}/weather/current`; + + // ✅ FIX 2: Correct parameter names (latitude/longitude, not lat/lon) + const response = await apiClient.get(endpoint, { + params: { + latitude: lat, // Backend expects 'latitude' + longitude: lon // Backend expects 'longitude' + } + }); + + console.log('Weather API response:', response); + + // Return backend response directly (matches WeatherData interface) + return response; + + } catch (error) { + console.error('Failed to fetch weather from backend:', error); + + // Fallback weather for Madrid (matching WeatherData schema) + return { + date: new Date().toISOString(), + temperature: 18, + description: 'Parcialmente nublado', + precipitation: 0, + humidity: 65, + wind_speed: 10, + source: 'fallback' + }; + } + } + + /** + * Get Weather Forecast + */ + async getWeatherForecast( + tenantId: string, + lat: number, + lon: number, + days: number = 7 + ): Promise { + try { + // Fix: Use POST with JSON body as expected by backend + const response = await apiClient.post(`/tenants/${tenantId}/weather/forecast`, { + latitude: lat, + longitude: lon, + days: days + }); + + // Handle response format + if (Array.isArray(response)) { + return response; + } else if (response && response.forecasts) { + return response.forecasts; + } else { + console.warn('Unexpected weather forecast response format:', response); + return []; + } + } catch (error) { + console.error('Failed to fetch weather forecast:', error); + return []; + } + } + + /** + * Get Historical Weather Data + */ + async getHistoricalWeather( + tenantId: string, + lat: number, + lon: number, + startDate: string, + endDate: string + ): Promise { + try { + // Fix: Use POST with JSON body as expected by backend + const response = await apiClient.post(`/tenants/${tenantId}/weather/historical`, { + latitude: lat, + longitude: lon, + start_date: startDate, + end_date: endDate + }); + + // Return backend response directly (matches WeatherData interface) + return Array.isArray(response) ? response : response.data || []; + } catch (error) { + console.error('Failed to fetch historical weather:', error); + return []; + } + } + + /** + * Get Current Traffic Data + */ + async getCurrentTraffic( + tenantId: string, + lat: number, + lon: number + ): Promise { + try { + const response = await apiClient.get(`/tenants/${tenantId}/traffic/current`, { + params: { + latitude: lat, + longitude: lon + } + }); + + // Return backend response directly (matches TrafficData interface) + return response; + } catch (error) { + console.error('Failed to fetch traffic data:', error); + + // Fallback traffic data (matching TrafficData schema) + return { + date: new Date().toISOString(), + traffic_volume: 50, + pedestrian_count: 25, + congestion_level: 'medium', + average_speed: 30, + source: 'fallback' + }; + } + } + + /** + * Get Traffic Forecast + */ + async getTrafficForecast( + tenantId: string, + lat: number, + lon: number, + hours: number = 24 + ): Promise { + try { + // Fix: Use POST with JSON body as expected by backend + const response = await apiClient.post(`/tenants/${tenantId}/traffic/forecast`, { + latitude: lat, + longitude: lon, + hours: hours + }); + + // Return backend response directly (matches TrafficData interface) + return Array.isArray(response) ? response : response.data || []; + } catch (error) { + console.error('Failed to fetch traffic forecast:', error); + return []; + } + } + + /** + * Get Historical Traffic Data + */ + async getHistoricalTraffic( + tenantId: string, + lat: number, + lon: number, + startDate: string, + endDate: string + ): Promise { + try { + // Fix: Use POST with JSON body as expected by backend + const response = await apiClient.post(`/tenants/${tenantId}/traffic/historical`, { + latitude: lat, + longitude: lon, + start_date: startDate, + end_date: endDate + }); + + // Return backend response directly (matches TrafficData interface) + return Array.isArray(response) ? response : response.data || []; + } catch (error) { + console.error('Failed to fetch historical traffic:', error); + return []; + } + } + + /** + * Test External Service Connectivity + */ + async testConnectivity(tenantId: string): Promise<{ + weather: boolean; + traffic: boolean; + overall: boolean; + }> { + const results = { + weather: false, + traffic: false, + overall: false + }; + + try { + // Test weather service + await this.getCurrentWeather(tenantId, 40.4168, -3.7038); // Madrid coordinates + results.weather = true; + } catch (error) { + console.warn('Weather service connectivity test failed:', error); + } + + try { + // Test traffic service + await this.getCurrentTraffic(tenantId, 40.4168, -3.7038); // Madrid coordinates + results.traffic = true; + } catch (error) { + console.warn('Traffic service connectivity test failed:', error); + } + + results.overall = results.weather && results.traffic; + return results; + } +} + +export const externalService = new ExternalService(); \ No newline at end of file diff --git a/frontend/src/api/services/index.ts b/frontend/src/api/services/index.ts index 8a5076ce..af13f71b 100644 --- a/frontend/src/api/services/index.ts +++ b/frontend/src/api/services/index.ts @@ -7,7 +7,8 @@ // Import and export individual services import { AuthService } from './auth.service'; import { TenantService } from './tenant.service'; -import { DataService } from './data.service'; +import { SalesService } from './sales.service'; +import { ExternalService } from './external.service'; import { TrainingService } from './training.service'; import { ForecastingService } from './forecasting.service'; import { NotificationService } from './notification.service'; @@ -16,17 +17,28 @@ import { OnboardingService } from './onboarding.service'; // Create service instances export const authService = new AuthService(); export const tenantService = new TenantService(); -export const dataService = new DataService(); +export const salesService = new SalesService(); +export const externalService = new ExternalService(); export const trainingService = new TrainingService(); export const forecastingService = new ForecastingService(); export const notificationService = new NotificationService(); export const onboardingService = new OnboardingService(); // Export the classes as well -export { AuthService, TenantService, DataService, TrainingService, ForecastingService, NotificationService, OnboardingService }; +export { + AuthService, + TenantService, + SalesService, + ExternalService, + TrainingService, + ForecastingService, + NotificationService, + OnboardingService +}; // Import base client -export { apiClient } from '../client'; +import { apiClient } from '../client'; +export { apiClient }; // Re-export all types export * from '../types'; @@ -35,7 +47,8 @@ export * from '../types'; export const api = { auth: authService, tenant: tenantService, - data: dataService, + sales: salesService, + external: externalService, training: trainingService, forecasting: forecastingService, notification: notificationService, @@ -56,7 +69,8 @@ export class HealthService { const services = [ { name: 'Auth', endpoint: '/auth/health' }, { name: 'Tenant', endpoint: '/tenants/health' }, - { name: 'Data', endpoint: '/data/health' }, + { name: 'Sales', endpoint: '/sales/health' }, + { name: 'External', endpoint: '/external/health' }, { name: 'Training', endpoint: '/training/health' }, { name: 'Forecasting', endpoint: '/forecasting/health' }, { name: 'Notification', endpoint: '/notifications/health' }, diff --git a/frontend/src/api/services/data.service.ts b/frontend/src/api/services/sales.service.ts similarity index 73% rename from frontend/src/api/services/data.service.ts rename to frontend/src/api/services/sales.service.ts index 72cc8ba9..e6af97ed 100644 --- a/frontend/src/api/services/data.service.ts +++ b/frontend/src/api/services/sales.service.ts @@ -1,7 +1,7 @@ -// frontend/src/api/services/data.service.ts +// frontend/src/api/services/sales.service.ts /** - * Data Management Service - * Handles sales data operations + * Sales Data Service + * Handles sales data operations for the sales microservice */ import { apiClient } from '../client'; @@ -17,7 +17,7 @@ import type { ActivityItem, } from '../types'; -export class DataService { +export class SalesService { /** * Upload Sales History File */ @@ -143,7 +143,7 @@ export class DataService { metrics?: string[]; } ): Promise { - return apiClient.get(`/tenants/${tenantId}/analytics`, { params }); + return apiClient.get(`/tenants/${tenantId}/sales/analytics`, { params }); } /** @@ -175,14 +175,13 @@ export class DataService { * Get Recent Activity */ async getRecentActivity(tenantId: string, limit?: number): Promise { - return apiClient.get(`/tenants/${tenantId}/activity`, { + return apiClient.get(`/tenants/${tenantId}/sales/activity`, { params: { limit }, }); } /** * Get Products List from Sales Data - * This should be added to the DataService class */ async getProductsList(tenantId: string): Promise { try { @@ -261,90 +260,8 @@ export class DataService { } } - - /** - * Get Current Weather Data - * This should be added to the DataService class - */ - async getCurrentWeather( - tenantId: string, - lat: number, - lon: number - ): Promise<{ - temperature: number; - description: string; - precipitation: number; - humidity?: number; - wind_speed?: number; - }> { - try { - // ✅ FIX 1: Correct endpoint path with tenant ID - const endpoint = `/tenants/${tenantId}/weather/current`; - - // ✅ FIX 2: Correct parameter names (latitude/longitude, not lat/lon) - const response = await apiClient.get(endpoint, { - params: { - latitude: lat, // Backend expects 'latitude' - longitude: lon // Backend expects 'longitude' - } - }); - - // ✅ FIX 3: Handle the actual backend response structure - // Backend returns WeatherDataResponse: - // { - // "date": "2025-08-04T12:00:00Z", - // "temperature": 25.5, - // "precipitation": 0.0, - // "humidity": 65.0, - // "wind_speed": 10.2, - // "pressure": 1013.2, - // "description": "Partly cloudy", - // "source": "aemet" - // } - - console.log('Weather API response:', response); - - // Map backend response to expected frontend format - return { - temperature: response.temperature || 18, - description: response.description || 'Parcialmente nublado', - precipitation: response.precipitation || 0, - humidity: response.humidity || 65, - wind_speed: response.wind_speed || 10 - }; - - } catch (error) { - console.error('Failed to fetch weather from backend:', error); - - // Fallback weather for Madrid - return { - temperature: 18, - description: 'Parcialmente nublado', - precipitation: 0, - humidity: 65, - wind_speed: 10 - }; - } - } - - - /** - * Get Weather Forecast - * This should be added to the DataService class - */ - async getWeatherForecast( - lat: number, - lon: number, - days: number = 7 - ): Promise { - return apiClient.get(`/data/weather/forecast`, { - params: { lat, lon, days } - }); - } - /** * Get Sales Summary by Period - * This should be added to the DataService class */ async getSalesSummary( tenantId: string, @@ -357,7 +274,6 @@ export class DataService { /** * Get Sales Analytics - * This should be added to the DataService class */ async getSalesAnalytics( tenantId: string, @@ -369,14 +285,13 @@ export class DataService { forecast_accuracy?: number; stockout_events?: number; }> { - return apiClient.get(`/tenants/${tenantId}/sales/analytics`, { + return apiClient.get(`/tenants/${tenantId}/sales/analytics/summary`, { params: { start_date: startDate, end_date: endDate } }); } - } -export const dataService = new DataService(); +export const salesService = new SalesService(); \ No newline at end of file diff --git a/frontend/src/api/types/data.ts b/frontend/src/api/types/data.ts index cb16d3b9..9ebc816b 100644 --- a/frontend/src/api/types/data.ts +++ b/frontend/src/api/types/data.ts @@ -3,6 +3,8 @@ * Data Management Types */ +import { BaseQueryParams } from './common'; + export interface SalesData { id: string; tenant_id: string; @@ -26,6 +28,8 @@ export interface SalesValidationResult { errors: ValidationError[]; warnings: ValidationError[]; summary: Record; + message?: string; + details?: Record; } export interface ExternalFactors { @@ -63,6 +67,7 @@ export interface SalesImportResult { success: boolean; message: string; imported_count: number; + records_imported: number; skipped_count: number; error_count: number; validation_errors?: ValidationError[]; diff --git a/frontend/src/api/types/forecasting.ts b/frontend/src/api/types/forecasting.ts index 72c80e5c..9a157391 100644 --- a/frontend/src/api/types/forecasting.ts +++ b/frontend/src/api/types/forecasting.ts @@ -3,6 +3,8 @@ * Forecasting Service Types */ +import { ExternalFactors } from './data'; + export interface SingleForecastRequest { product_name: string; forecast_date: string; diff --git a/frontend/src/api/types/tenant.ts b/frontend/src/api/types/tenant.ts index 16340ee8..0283f73b 100644 --- a/frontend/src/api/types/tenant.ts +++ b/frontend/src/api/types/tenant.ts @@ -61,11 +61,16 @@ export interface TenantSubscription { export interface TenantCreate { name: string; + address?: string; + business_type?: 'individual' | 'central_workshop'; postal_code: string; phone: string; description?: string; settings?: Partial; location?: TenantLocation; + coordinates?: { lat: number; lng: number }; + products?: string[]; + has_historical_data?: boolean; } export interface TenantUpdate { diff --git a/frontend/src/api/types/training.ts b/frontend/src/api/types/training.ts index 809f05b1..9d795b40 100644 --- a/frontend/src/api/types/training.ts +++ b/frontend/src/api/types/training.ts @@ -7,6 +7,10 @@ export interface TrainingJobRequest { config?: TrainingJobConfig; priority?: number; schedule_time?: string; + include_weather?: boolean; + include_traffic?: boolean; + min_data_points?: number; + use_default_data?: boolean; } export interface SingleProductTrainingRequest { diff --git a/frontend/src/api/utils/error.ts b/frontend/src/api/utils/error.ts index 45d5411d..cf576ed7 100644 --- a/frontend/src/api/utils/error.ts +++ b/frontend/src/api/utils/error.ts @@ -3,7 +3,7 @@ * Error Handling Utilities */ -import type { ApiError } from '../types'; +import type { ApiError } from '../client/types'; export class ApiErrorHandler { static formatError(error: any): string { diff --git a/frontend/src/hooks/useDashboard.ts b/frontend/src/hooks/useDashboard.ts index 7c7899ea..814c3926 100644 --- a/frontend/src/hooks/useDashboard.ts +++ b/frontend/src/hooks/useDashboard.ts @@ -2,7 +2,7 @@ // Complete dashboard hook using your API infrastructure import { useState, useEffect, useCallback } from 'react'; -import { useAuth, useData, useForecast } from '../api'; +import { useAuth, useSales, useExternal, useForecast } from '../api'; import { useTenantId } from './useTenantId'; @@ -31,12 +31,16 @@ export const useDashboard = () => { const { user } = useAuth(); const { getProductsList, - getCurrentWeather, getSalesAnalytics, getDashboardStats, - isLoading: dataLoading, - error: dataError - } = useData(); + isLoading: salesLoading, + error: salesError + } = useSales(); + const { + getCurrentWeather, + isLoading: externalLoading, + error: externalError + } = useExternal(); const { createSingleForecast, @@ -236,8 +240,8 @@ export const useDashboard = () => { return { ...dashboardData, - isLoading: isLoading || dataLoading || forecastLoading, - error: error || dataError || forecastError, + isLoading: isLoading || salesLoading || externalLoading || forecastLoading, + error: error || salesError || externalError || forecastError, reload: () => tenantId ? loadDashboardData(tenantId) : Promise.resolve(), clearError: () => setError(null) }; diff --git a/frontend/src/hooks/useOrderSuggestions.ts b/frontend/src/hooks/useOrderSuggestions.ts index c2f99c8c..c13dc20e 100644 --- a/frontend/src/hooks/useOrderSuggestions.ts +++ b/frontend/src/hooks/useOrderSuggestions.ts @@ -1,6 +1,6 @@ // Real API hook for Order Suggestions using backend data import { useState, useCallback, useEffect } from 'react'; -import { useData, useForecast } from '../api'; +import { useSales, useExternal, useForecast } from '../api'; import { useTenantId } from './useTenantId'; import type { DailyOrderItem, WeeklyOrderItem } from '../components/simple/OrderSuggestions'; @@ -44,9 +44,11 @@ export const useOrderSuggestions = () => { const { getProductsList, getSalesAnalytics, - getDashboardStats, + getDashboardStats + } = useSales(); + const { getCurrentWeather - } = useData(); + } = useExternal(); const { createSingleForecast, getQuickForecasts, @@ -158,8 +160,8 @@ export const useOrderSuggestions = () => { console.log('📊 OrderSuggestions: Generating weekly suggestions for tenant:', tenantId); // Get sales analytics for the past month - const endDate = new Date().toISOString().split('T')[0]; - const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + const endDate = new Date().toISOString(); + const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(); let analytics: any = null; try { diff --git a/frontend/src/pages/auth/RegisterPage.tsx b/frontend/src/pages/auth/RegisterPage.tsx index 1a3ba348..35088f4b 100644 --- a/frontend/src/pages/auth/RegisterPage.tsx +++ b/frontend/src/pages/auth/RegisterPage.tsx @@ -58,6 +58,16 @@ interface RegisterForm { paymentCompleted: boolean; } +interface RegisterFormErrors { + fullName?: string; + email?: string; + confirmEmail?: string; + password?: string; + confirmPassword?: string; + acceptTerms?: string; + paymentCompleted?: string; +} + const RegisterPage: React.FC = ({ onLogin, onNavigateToLogin }) => { const { register, isLoading } = useAuth(); @@ -73,7 +83,7 @@ const RegisterPage: React.FC = ({ onLogin, onNavigateToLogin const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); - const [errors, setErrors] = useState>({}); + const [errors, setErrors] = useState({}); const [passwordStrength, setPasswordStrength] = useState<{ score: number; checks: { [key: string]: boolean }; @@ -246,7 +256,7 @@ const RegisterPage: React.FC = ({ onLogin, onNavigateToLogin e.preventDefault(); // Validate form but exclude payment requirement for first step - const newErrors: Partial = {}; + const newErrors: RegisterFormErrors = {}; if (!formData.fullName.trim()) { newErrors.fullName = 'El nombre completo es obligatorio'; @@ -346,7 +356,7 @@ const RegisterPage: React.FC = ({ onLogin, onNavigateToLogin })); // Clear error when user starts typing - if (errors[name as keyof RegisterForm]) { + if (errors[name as keyof RegisterFormErrors]) { setErrors(prev => ({ ...prev, [name]: undefined diff --git a/frontend/src/pages/dashboard/DashboardPage.tsx b/frontend/src/pages/dashboard/DashboardPage.tsx index 7d158768..6fd16474 100644 --- a/frontend/src/pages/dashboard/DashboardPage.tsx +++ b/frontend/src/pages/dashboard/DashboardPage.tsx @@ -219,9 +219,6 @@ const DashboardPage: React.FC = ({ onUpdateStatus={(itemId: string, status: any) => { console.log('Update status:', itemId, status); }} - onViewDetails={() => { - onNavigateToProduction?.(); - }} /> {/* Quick Overview - Supporting Information */} diff --git a/frontend/src/pages/onboarding/OnboardingPage.tsx b/frontend/src/pages/onboarding/OnboardingPage.tsx index 651aedc0..95694e99 100644 --- a/frontend/src/pages/onboarding/OnboardingPage.tsx +++ b/frontend/src/pages/onboarding/OnboardingPage.tsx @@ -7,7 +7,7 @@ import SimplifiedTrainingProgress from '../../components/SimplifiedTrainingProgr import { useTenant, useTraining, - useData, + useSales, useTrainingWebSocket, useOnboarding, TenantCreate, @@ -75,7 +75,7 @@ const OnboardingPage: React.FC = ({ user, onComplete }) => const [trainingJobId, setTrainingJobId] = useState(''); const { createTenant, getUserTenants, isLoading: tenantLoading } = useTenant(); const { startTrainingJob } = useTraining({ disablePolling: true }); - const { uploadSalesHistory, validateSalesData } = useData(); + const { uploadSalesHistory, validateSalesData } = useSales(); const steps = [ { id: 1, title: 'Datos de Panadería', icon: Store }, @@ -315,7 +315,7 @@ const OnboardingPage: React.FC = ({ user, onComplete }) => const tenantData: TenantCreate = { name: bakeryData.name, address: bakeryData.address, - business_type: "bakery", + business_type: "individual", postal_code: "28010", phone: "+34655334455", coordinates: bakeryData.coordinates, diff --git a/frontend/src/pages/orders/OrdersPage.tsx b/frontend/src/pages/orders/OrdersPage.tsx index 0b3306d2..ebadda5a 100644 --- a/frontend/src/pages/orders/OrdersPage.tsx +++ b/frontend/src/pages/orders/OrdersPage.tsx @@ -355,9 +355,9 @@ const OrdersPage: React.FC = () => {

No hay pedidos

- {activeTab === 'all' + {activeTab === 'orders' ? 'Aún no has creado ningún pedido' - : `No hay pedidos ${activeTab === 'pending' ? 'pendientes' : 'entregados'}` + : 'No hay datos disponibles para esta sección' }