Improve the dahboard with the weather info

This commit is contained in:
Urtzi Alfaro
2025-08-18 13:36:37 +02:00
parent 355c0080cc
commit afca94dadd
12 changed files with 747 additions and 302 deletions

View File

@@ -9,9 +9,14 @@ import { useTenantId } from './useTenantId';
interface DashboardData {
weather: {
temperature: number;
description: string;
precipitation: number;
temperature?: number;
description?: string;
precipitation?: number;
humidity?: number;
wind_speed?: number;
pressure?: number;
source: string;
date: string;
} | null;
todayForecasts: Array<{
product: string;
@@ -91,18 +96,16 @@ export const useDashboard = () => {
try {
// 1. Get available products from inventory service
const products = await getProductsList(tenantId);
// 2. Get weather data (Madrid coordinates)
// 2. Get complete weather data from AEMET (Madrid coordinates)
let weather = null;
try {
weather = await getCurrentWeather(tenantId, 40.4168, -3.7038);
const weatherResponse = await getCurrentWeather(tenantId, 40.4168, -3.7038);
// Use the full WeatherData interface instead of limited subset
weather = weatherResponse;
} catch (error) {
console.warn('Failed to fetch weather:', error);
// Fallback weather
weather = {
temperature: 18,
description: 'Parcialmente nublado',
precipitation: 0
};
console.error('AEMET weather data unavailable for dashboard:', error);
// No fallback - weather remains null if AEMET API fails
weather = null;
}
// 3. Generate forecasts for each product

View File

@@ -1,6 +1,6 @@
// Real API hook for Order Suggestions using backend data
import { useState, useCallback, useEffect } from 'react';
import { useSales, useExternal, useForecast, useInventoryProducts } from '../api';
import { useExternal, useForecast, useInventoryProducts } from '../api';
import { useTenantId } from './useTenantId';
import type { DailyOrderItem, WeeklyOrderItem } from '../components/simple/OrderSuggestions';
@@ -44,10 +44,6 @@ export const useOrderSuggestions = (providedTenantId?: string | null) => {
const tenantId = providedTenantId !== undefined ? providedTenantId : hookTenantId;
console.log('🏢 OrderSuggestions: Tenant info:', { tenantId, tenantLoading, tenantError });
const {
getSalesAnalytics,
getDashboardStats
} = useSales();
const {
getProductsList
} = useInventoryProducts();
@@ -55,159 +51,213 @@ export const useOrderSuggestions = (providedTenantId?: string | null) => {
getCurrentWeather
} = useExternal();
const {
createSingleForecast,
getQuickForecasts,
getForecastAlerts
createSingleForecast
} = useForecast();
// Generate daily order suggestions based on real forecast data
// Generate daily order suggestions - creates next day forecasts for all products
const generateDailyOrderSuggestions = useCallback(async (): Promise<DailyOrderItem[]> => {
if (!tenantId) return [];
try {
console.log('📊 OrderSuggestions: Generating daily suggestions for tenant:', tenantId);
// Get products list from backend
// Get all products from backend
const productsList = await getProductsList(tenantId);
const products = productsList.map(p => p.name);
console.log('📋 OrderSuggestions: Products list:', products);
console.log('📋 OrderSuggestions: Products list:', productsList);
// Filter for daily bakery products (case insensitive)
const dailyProductKeywords = ['pan', 'baguette', 'croissant', 'magdalena'];
const dailyProducts = products.filter(p =>
// Filter for daily products (bakery items that need daily forecasting)
const dailyProductKeywords = ['pan', 'baguette', 'croissant', 'magdalena', 'tarta', 'bollo', 'empanada'];
const dailyProducts = productsList.filter(p =>
dailyProductKeywords.some(keyword =>
p.toLowerCase().includes(keyword.toLowerCase())
p.name.toLowerCase().includes(keyword.toLowerCase())
)
);
console.log('🥖 OrderSuggestions: Daily products:', dailyProducts);
// Get quick forecasts for these products
const quickForecasts = await getQuickForecasts(tenantId);
console.log('🔮 OrderSuggestions: Quick forecasts:', quickForecasts);
// Get weather data to determine urgency
const weather = await getCurrentWeather(tenantId, 40.4168, -3.7038);
console.log('🌤️ OrderSuggestions: Weather data:', weather);
// Create individual forecasts for each daily product for next day
const suggestions: DailyOrderItem[] = [];
console.log('🔄 OrderSuggestions: Processing daily products:', dailyProducts);
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().split('T')[0];
// Get weather data to determine urgency factors
let weather = null;
try {
weather = await getCurrentWeather(tenantId, 40.4168, -3.7038);
console.log('🌤️ OrderSuggestions: Weather data:', weather);
} catch (error) {
console.warn('⚠️ OrderSuggestions: AEMET weather data unavailable, continuing without weather factors:', error);
weather = null;
}
for (const product of dailyProducts) {
console.log('🔄 OrderSuggestions: Processing product:', product);
// Find forecast for this product
const forecast = quickForecasts.find(f =>
f.product_name === product || f.inventory_product_id === product
);
console.log('🔄 OrderSuggestions: Creating forecast for product:', product.name);
if (forecast) {
// Calculate suggested quantity based on prediction
const suggestedQuantity = Math.max(forecast.next_day_prediction, 10);
// Determine urgency based on confidence and trend
let urgency: 'high' | 'medium' | 'low' = 'medium';
if (forecast.confidence_score > 0.9 && forecast.trend_direction === 'up') {
urgency = 'high';
} else if (forecast.confidence_score < 0.7) {
urgency = 'low';
}
// Generate reason based on forecast data
let reason = `Predicción: ${forecast.next_day_prediction} unidades`;
if (forecast.trend_direction === 'up') {
reason += ' (tendencia al alza)';
}
if (weather && weather.precipitation > 0) {
reason += ', lluvia prevista';
urgency = urgency === 'low' ? 'medium' : 'high';
}
const orderItem: DailyOrderItem = {
id: `daily-${product.toLowerCase().replace(/\s+/g, '-')}`,
product,
emoji: getProductEmoji(product),
suggestedQuantity: Math.round(suggestedQuantity),
currentQuantity: Math.round(suggestedQuantity * 0.2), // Assume 20% current stock
unit: 'unidades',
urgency,
reason,
confidence: Math.round(forecast.confidence_score * 100),
supplier: SUPPLIERS[product] || 'Proveedor General',
estimatedCost: Math.round(suggestedQuantity * (PRODUCT_PRICES[product] || 2.5) * 100) / 100,
lastOrderDate: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0]
try {
// Create single forecast for next day for this specific product
const forecastRequest = {
inventory_product_id: product.inventory_product_id,
forecast_date: tomorrow,
forecast_days: 1,
location: 'Madrid',
include_external_factors: true,
confidence_intervals: true
};
const forecasts = await createSingleForecast(tenantId, forecastRequest);
if (forecasts.length > 0) {
const forecast = forecasts[0];
const suggestedQuantity = Math.max(Math.round(forecast.predicted_demand), 5);
// Determine urgency based on confidence and external factors
let urgency: 'high' | 'medium' | 'low' = 'medium';
const confidence = forecast.confidence_level || 0.8;
if (confidence > 0.9) {
urgency = 'low'; // High confidence = low urgency
} else if (confidence < 0.6) {
urgency = 'high'; // Low confidence = high urgency, need more buffer
}
// Adjust based on weather
if (weather && weather.precipitation > 0) {
urgency = urgency === 'low' ? 'medium' : 'high';
}
suggestions.push(orderItem);
console.log(' OrderSuggestions: Added daily suggestion:', orderItem);
// Generate reason based on forecast data
let reason = `Predicción: ${suggestedQuantity} unidades (demanda para mañana)`;
if (weather && weather.precipitation > 0) {
reason += ', lluvia prevista';
}
const orderItem: DailyOrderItem = {
id: `daily-${product.name.toLowerCase().replace(/\s+/g, '-')}`,
product: product.name,
emoji: getProductEmoji(product.name),
suggestedQuantity,
currentQuantity: Math.round(suggestedQuantity * (0.1 + Math.random() * 0.3)), // Random current stock 10-40%
unit: getProductUnit(product.name),
urgency,
reason,
confidence: Math.round(confidence * 100),
supplier: SUPPLIERS[product.name] || 'Proveedor General',
estimatedCost: Math.round(suggestedQuantity * (PRODUCT_PRICES[product.name] || 2.5) * 100) / 100,
lastOrderDate: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0]
};
suggestions.push(orderItem);
console.log(' OrderSuggestions: Added daily suggestion:', orderItem);
}
} catch (error) {
console.warn(`⚠️ OrderSuggestions: Failed to create forecast for ${product.name}:`, error);
// Continue with other products even if one fails
}
}
console.log('🎯 OrderSuggestions: Final daily suggestions:', suggestions);
return suggestions;
return suggestions.sort((a, b) => {
// Sort by urgency (high first) then by suggested quantity
const urgencyOrder = { high: 3, medium: 2, low: 1 };
if (urgencyOrder[a.urgency] !== urgencyOrder[b.urgency]) {
return urgencyOrder[b.urgency] - urgencyOrder[a.urgency];
}
return b.suggestedQuantity - a.suggestedQuantity;
});
} catch (error) {
console.error('❌ OrderSuggestions: Error in generateDailyOrderSuggestions:', error);
return [];
}
}, [tenantId, getProductsList, getQuickForecasts, getCurrentWeather]);
}, [tenantId, getProductsList, createSingleForecast, getCurrentWeather]);
// Generate weekly order suggestions based on sales analytics
// Generate weekly order suggestions - creates 7-day forecasts for weekly order products
const generateWeeklyOrderSuggestions = useCallback(async (): Promise<WeeklyOrderItem[]> => {
if (!tenantId) return [];
console.log('📊 OrderSuggestions: Generating weekly suggestions for tenant:', tenantId);
try {
console.log('📊 OrderSuggestions: Generating weekly suggestions for tenant:', tenantId);
// Get sales analytics for the past month
const endDate = new Date().toISOString();
const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
// Get all products from backend
const productsList = await getProductsList(tenantId);
console.log('📋 OrderSuggestions: Products list for weekly:', productsList);
const analytics = await getSalesAnalytics(tenantId, startDate, endDate);
console.log('📈 OrderSuggestions: Sales analytics:', analytics);
// Weekly products (ingredients and supplies)
const weeklyProducts = [
'Café en Grano',
'Leche Entera',
'Mantequilla',
'Vasos de Café',
'Servilletas',
'Bolsas papel'
];
// Filter for weekly products (ingredients, supplies, drinks that need weekly forecasting)
const weeklyProductKeywords = ['café', 'leche', 'mantequilla', 'vaso', 'servilleta', 'bolsa', 'bebida', 'refresco', 'agua'];
const weeklyProducts = productsList.filter(p =>
weeklyProductKeywords.some(keyword =>
p.name.toLowerCase().includes(keyword.toLowerCase())
)
);
console.log('📦 OrderSuggestions: Weekly products:', weeklyProducts);
const suggestions: WeeklyOrderItem[] = [];
const startDate = new Date().toISOString().split('T')[0];
for (const product of weeklyProducts) {
// Calculate weekly consumption based on analytics
const weeklyConsumption = calculateWeeklyConsumption(product, analytics);
const currentStock = Math.round(weeklyConsumption * (0.3 + Math.random() * 0.4)); // Random stock between 30-70% of weekly need
const stockDays = Math.max(1, Math.round((currentStock / weeklyConsumption) * 7));
console.log('🔄 OrderSuggestions: Creating 7-day forecast for product:', product.name);
try {
// Create single forecast for 7 days for this specific product
const forecastRequest = {
inventory_product_id: product.inventory_product_id,
forecast_date: startDate,
forecast_days: 7,
location: 'Madrid',
include_external_factors: true,
confidence_intervals: true
};
const forecasts = await createSingleForecast(tenantId, forecastRequest);
if (forecasts.length > 0) {
// Calculate total weekly demand from all forecast days
const totalWeeklyDemand = forecasts.reduce((sum, forecast) => sum + forecast.predicted_demand, 0);
const averageConfidence = forecasts.reduce((sum, forecast) => sum + (forecast.confidence_level || 0.8), 0) / forecasts.length;
// Determine frequency based on product type
const highFrequencyKeywords = ['café', 'leche', 'mantequilla', 'bebida'];
const frequency: 'weekly' | 'biweekly' =
highFrequencyKeywords.some(keyword => product.name.toLowerCase().includes(keyword)) ? 'weekly' : 'biweekly';
// Calculate suggested quantity with buffer
const suggestedQuantity = frequency === 'weekly' ?
Math.round(totalWeeklyDemand * 1.15) : // 15% buffer for weekly
Math.round(totalWeeklyDemand * 2.3); // 2 weeks + 15% buffer
// Simulate current stock and calculate stock days
const dailyConsumption = totalWeeklyDemand / 7;
const currentStock = Math.round(dailyConsumption * (2 + Math.random() * 8)); // 2-10 days worth
const stockDays = Math.max(1, Math.round(currentStock / dailyConsumption));
// Determine frequency
const frequency: 'weekly' | 'biweekly' =
['Café en Grano', 'Leche Entera', 'Mantequilla'].includes(product) ? 'weekly' : 'biweekly';
const orderItem: WeeklyOrderItem = {
id: `weekly-${product.name.toLowerCase().replace(/\s+/g, '-')}`,
product: product.name,
emoji: getProductEmoji(product.name),
suggestedQuantity,
currentStock,
unit: getProductUnit(product.name),
frequency,
nextOrderDate: getNextOrderDate(frequency, stockDays),
supplier: SUPPLIERS[product.name] || 'Proveedor General',
estimatedCost: Math.round(suggestedQuantity * (PRODUCT_PRICES[product.name] || 1.0) * 100) / 100,
stockDays,
confidence: Math.round(averageConfidence * 100)
};
const suggestedQuantity = frequency === 'weekly' ?
Math.round(weeklyConsumption * 1.1) : // 10% buffer for weekly
Math.round(weeklyConsumption * 2.2); // 2 weeks + 10% buffer
const orderItem: WeeklyOrderItem = {
id: `weekly-${product.toLowerCase().replace(/\s+/g, '-')}`,
product,
emoji: getProductEmoji(product),
suggestedQuantity,
currentStock,
unit: getProductUnit(product),
frequency,
nextOrderDate: getNextOrderDate(frequency, stockDays),
supplier: SUPPLIERS[product] || 'Proveedor General',
estimatedCost: Math.round(suggestedQuantity * (PRODUCT_PRICES[product] || 1.0) * 100) / 100,
stockDays,
confidence: stockDays <= 2 ? 95 : stockDays <= 5 ? 85 : 75
};
suggestions.push(orderItem);
suggestions.push(orderItem);
console.log(' OrderSuggestions: Added weekly suggestion:', orderItem);
}
} catch (error) {
console.warn(`⚠️ OrderSuggestions: Failed to create weekly forecast for ${product.name}:`, error);
// Continue with other products even if one fails
}
}
return suggestions.sort((a, b) => a.stockDays - b.stockDays); // Sort by urgency
}, [tenantId, getSalesAnalytics]);
console.log('🎯 OrderSuggestions: Final weekly suggestions:', suggestions);
return suggestions.sort((a, b) => a.stockDays - b.stockDays); // Sort by urgency (lowest stock days first)
} catch (error) {
console.error('❌ OrderSuggestions: Error in generateWeeklyOrderSuggestions:', error);
return [];
}
}, [tenantId, getProductsList, createSingleForecast]);
// Load order suggestions
const loadOrderSuggestions = useCallback(async () => {
@@ -282,7 +332,13 @@ function getProductEmoji(product: string): string {
'Mantequilla': '🧈',
'Vasos de Café': '🥤',
'Servilletas': '🧻',
'Bolsas papel': '🛍️'
'Bolsas papel': '🛍️',
'Bebida': '🥤',
'Refresco': '🥤',
'Agua': '💧',
'Tarta': '🎂',
'Bollo': '🥖',
'Empanada': '🥟'
};
return emojiMap[product] || '📦';
}
@@ -298,25 +354,17 @@ function getProductUnit(product: string): string {
'Mantequilla': 'kg',
'Vasos de Café': 'unidades',
'Servilletas': 'paquetes',
'Bolsas papel': 'unidades'
'Bolsas papel': 'unidades',
'Bebida': 'litros',
'Refresco': 'litros',
'Agua': 'litros',
'Tarta': 'unidades',
'Bollo': 'unidades',
'Empanada': 'unidades'
};
return unitMap[product] || 'unidades';
}
function calculateWeeklyConsumption(product: string, analytics: any): number {
// This would ideally come from sales analytics
// For now, use realistic estimates based on product type
const weeklyEstimates: Record<string, number> = {
'Café en Grano': 5, // 5kg per week
'Leche Entera': 25, // 25L per week
'Mantequilla': 3, // 3kg per week
'Vasos de Café': 500, // 500 cups per week
'Servilletas': 10, // 10 packs per week
'Bolsas papel': 200 // 200 bags per week
};
return weeklyEstimates[product] || 10;
}
function getNextOrderDate(frequency: 'weekly' | 'biweekly', stockDays: number): string {
const today = new Date();
@@ -333,97 +381,3 @@ function getNextOrderDate(frequency: 'weekly' | 'biweekly', stockDays: number):
return nextDate.toISOString().split('T')[0];
}
// Mock data functions for when tenant ID is not available
function getMockDailyOrders(): DailyOrderItem[] {
return [
{
id: 'daily-pan-molde',
product: 'Pan de Molde',
emoji: '🍞',
suggestedQuantity: 25,
currentQuantity: 5,
unit: 'unidades',
urgency: 'medium' as const,
reason: 'Predicción: 25 unidades (tendencia estable)',
confidence: 85,
supplier: 'Panadería Central Madrid',
estimatedCost: 45.00,
lastOrderDate: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0]
},
{
id: 'daily-baguettes',
product: 'Baguettes',
emoji: '🥖',
suggestedQuantity: 20,
currentQuantity: 3,
unit: 'unidades',
urgency: 'high' as const,
reason: 'Predicción: 20 unidades (tendencia al alza)',
confidence: 92,
supplier: 'Panadería Central Madrid',
estimatedCost: 56.00,
lastOrderDate: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0]
},
{
id: 'daily-croissants',
product: 'Croissants',
emoji: '🥐',
suggestedQuantity: 15,
currentQuantity: 8,
unit: 'unidades',
urgency: 'low' as const,
reason: 'Predicción: 15 unidades (demanda regular)',
confidence: 78,
supplier: 'Panadería Central Madrid',
estimatedCost: 37.50,
lastOrderDate: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0]
}
];
}
function getMockWeeklyOrders(): WeeklyOrderItem[] {
return [
{
id: 'weekly-cafe-grano',
product: 'Café en Grano',
emoji: '☕',
suggestedQuantity: 5,
currentStock: 2,
unit: 'kg',
frequency: 'weekly' as const,
nextOrderDate: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
supplier: 'Cafés Premium',
estimatedCost: 87.50,
stockDays: 3,
confidence: 95
},
{
id: 'weekly-leche-entera',
product: 'Leche Entera',
emoji: '🥛',
suggestedQuantity: 25,
currentStock: 15,
unit: 'litros',
frequency: 'weekly' as const,
nextOrderDate: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
supplier: 'Lácteos Frescos SA',
estimatedCost: 23.75,
stockDays: 6,
confidence: 88
},
{
id: 'weekly-vasos-cafe',
product: 'Vasos de Café',
emoji: '🥤',
suggestedQuantity: 500,
currentStock: 100,
unit: 'unidades',
frequency: 'biweekly' as const,
nextOrderDate: new Date(Date.now() + 1 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
supplier: 'Suministros Hostelería',
estimatedCost: 40.00,
stockDays: 2,
confidence: 90
}
];
}