demo seed change 7

This commit is contained in:
Urtzi Alfaro
2025-12-15 13:39:33 +01:00
parent 46bd4f77b6
commit 5642b5a0c0
14 changed files with 5653 additions and 780 deletions

View File

@@ -1,6 +1,7 @@
/**
* Sustainability API Service
* Environmental impact, SDG compliance, and grant reporting
* Sustainability API Service - Microservices Architecture
* Fetches data from production and inventory services in parallel
* Performs client-side aggregation of sustainability metrics
*/
import apiClient from '../client/apiClient';
@@ -9,27 +10,406 @@ import type {
SustainabilityWidgetData,
SDGCompliance,
EnvironmentalImpact,
GrantReport
GrantReport,
WasteMetrics,
FinancialImpact,
AvoidedWaste,
GrantReadiness
} from '../types/sustainability';
const BASE_PATH = '/sustainability';
// ===== SERVICE-SPECIFIC API CALLS =====
/**
* Get comprehensive sustainability metrics
* Production Service: Get production waste metrics
*/
export async function getProductionWasteMetrics(
tenantId: string,
startDate?: string,
endDate?: string
): Promise<any> {
const params = new URLSearchParams();
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
const queryString = params.toString();
const url = `/tenants/${tenantId}/production/sustainability/waste-metrics${queryString ? `?${queryString}` : ''}`;
return await apiClient.get<any>(url);
}
/**
* Production Service: Get production baseline metrics
*/
export async function getProductionBaseline(
tenantId: string,
startDate?: string,
endDate?: string
): Promise<any> {
const params = new URLSearchParams();
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
const queryString = params.toString();
const url = `/tenants/${tenantId}/production/sustainability/baseline${queryString ? `?${queryString}` : ''}`;
return await apiClient.get<any>(url);
}
/**
* Production Service: Get AI impact on waste reduction
*/
export async function getProductionAIImpact(
tenantId: string,
startDate?: string,
endDate?: string
): Promise<any> {
const params = new URLSearchParams();
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
const queryString = params.toString();
const url = `/tenants/${tenantId}/production/sustainability/ai-impact${queryString ? `?${queryString}` : ''}`;
return await apiClient.get<any>(url);
}
/**
* Production Service: Get summary widget data
*/
export async function getProductionSummary(
tenantId: string,
days: number = 30
): Promise<any> {
return await apiClient.get<any>(
`/tenants/${tenantId}/production/sustainability/summary?days=${days}`
);
}
/**
* Inventory Service: Get inventory waste metrics
*/
export async function getInventoryWasteMetrics(
tenantId: string,
startDate?: string,
endDate?: string
): Promise<any> {
const params = new URLSearchParams();
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
const queryString = params.toString();
const url = `/tenants/${tenantId}/inventory/sustainability/waste-metrics${queryString ? `?${queryString}` : ''}`;
return await apiClient.get<any>(url);
}
/**
* Inventory Service: Get expiry alerts
*/
export async function getInventoryExpiryAlerts(
tenantId: string,
daysAhead: number = 7
): Promise<any> {
return await apiClient.get<any>(
`/tenants/${tenantId}/inventory/sustainability/expiry-alerts?days_ahead=${daysAhead}`
);
}
/**
* Inventory Service: Get waste events
*/
export async function getInventoryWasteEvents(
tenantId: string,
limit: number = 50,
offset: number = 0,
startDate?: string,
endDate?: string,
reasonCode?: string
): Promise<any> {
const params = new URLSearchParams();
params.append('limit', limit.toString());
params.append('offset', offset.toString());
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
if (reasonCode) params.append('reason_code', reasonCode);
const queryString = params.toString();
const url = `/tenants/${tenantId}/inventory/sustainability/waste-events?${queryString}`;
return await apiClient.get<any>(url);
}
/**
* Inventory Service: Get summary widget data
*/
export async function getInventorySummary(
tenantId: string,
days: number = 30
): Promise<any> {
return await apiClient.get<any>(
`/tenants/${tenantId}/inventory/sustainability/summary?days=${days}`
);
}
// ===== AGGREGATION FUNCTIONS =====
/**
* Environmental Constants for calculations
*/
const EnvironmentalConstants = {
CO2_PER_KG_WASTE: 1.9, // kg CO2e per kg waste
WATER_PER_KG: 1500, // liters per kg
LAND_USE_PER_KG: 3.4, // m² per kg
TREES_PER_TON_CO2: 50,
SDG_TARGET_REDUCTION: 0.50, // 50% reduction target
EU_BAKERY_BASELINE_WASTE: 0.25 // 25% baseline
};
/**
* Calculate environmental impact from total waste
*/
function calculateEnvironmentalImpact(totalWasteKg: number): EnvironmentalImpact {
const co2Kg = totalWasteKg * EnvironmentalConstants.CO2_PER_KG_WASTE;
const co2Tons = co2Kg / 1000;
const waterLiters = totalWasteKg * EnvironmentalConstants.WATER_PER_KG;
const landSqMeters = totalWasteKg * EnvironmentalConstants.LAND_USE_PER_KG;
return {
co2_emissions: {
kg: Math.round(co2Kg * 100) / 100,
tons: Math.round(co2Tons * 1000) / 1000,
trees_to_offset: Math.ceil(co2Tons * EnvironmentalConstants.TREES_PER_TON_CO2)
},
water_footprint: {
liters: Math.round(waterLiters),
cubic_meters: Math.round(waterLiters / 1000 * 100) / 100
},
land_use: {
square_meters: Math.round(landSqMeters * 100) / 100,
hectares: Math.round(landSqMeters / 10000 * 1000) / 1000
},
human_equivalents: {
car_km_equivalent: Math.round(co2Kg / 0.120), // 120g CO2 per km
smartphone_charges: Math.round(co2Kg * 1000 / 8), // 8g CO2 per charge
showers_equivalent: Math.round(waterLiters / 65), // 65L per shower
trees_planted: Math.ceil(co2Tons * EnvironmentalConstants.TREES_PER_TON_CO2)
}
};
}
/**
* Calculate SDG compliance status
*/
function calculateSDGCompliance(
currentWastePercentage: number,
baselineWastePercentage: number
): SDGCompliance {
const reductionAchieved = baselineWastePercentage > 0
? ((baselineWastePercentage - currentWastePercentage) / baselineWastePercentage) * 100
: 0;
const targetReduction = EnvironmentalConstants.SDG_TARGET_REDUCTION * 100; // 50%
const progressToTarget = Math.max(0, Math.min(100, (reductionAchieved / targetReduction) * 100));
let status: 'sdg_compliant' | 'on_track' | 'progressing' | 'baseline' | 'above_baseline' = 'baseline';
let statusLabel = 'Establishing Baseline';
if (reductionAchieved >= targetReduction) {
status = 'sdg_compliant';
statusLabel = 'SDG Compliant';
} else if (reductionAchieved >= 30) {
status = 'on_track';
statusLabel = 'On Track';
} else if (reductionAchieved >= 10) {
status = 'progressing';
statusLabel = 'Progressing';
} else if (reductionAchieved > 0) {
status = 'baseline';
statusLabel = 'Improving from Baseline';
} else if (reductionAchieved < 0) {
status = 'above_baseline';
statusLabel = 'Above Baseline';
}
return {
sdg_12_3: {
baseline_waste_percentage: Math.round(baselineWastePercentage * 100) / 100,
current_waste_percentage: Math.round(currentWastePercentage * 100) / 100,
reduction_achieved: Math.round(reductionAchieved * 100) / 100,
target_reduction: targetReduction,
progress_to_target: Math.round(progressToTarget * 100) / 100,
status,
status_label: statusLabel,
target_waste_percentage: baselineWastePercentage * (1 - EnvironmentalConstants.SDG_TARGET_REDUCTION)
},
baseline_period: 'first_90_days',
certification_ready: status === 'sdg_compliant',
improvement_areas: status === 'sdg_compliant' ? [] : ['reduce_waste_further']
};
}
/**
* Assess grant readiness based on SDG compliance
*/
function assessGrantReadiness(sdgCompliance: SDGCompliance): GrantReadiness {
const reductionAchieved = sdgCompliance.sdg_12_3.reduction_achieved;
const isSdgCompliant = sdgCompliance.certification_ready;
const grantPrograms: Record<string, any> = {
life_circular_economy: {
eligible: reductionAchieved >= 30,
confidence: reductionAchieved >= 40 ? 'high' : reductionAchieved >= 30 ? 'medium' : 'low',
requirements_met: reductionAchieved >= 30,
funding_eur: 73_000_000,
deadline: '2025-09-23',
program_type: 'grant'
},
horizon_europe_cluster_6: {
eligible: isSdgCompliant,
confidence: isSdgCompliant ? 'high' : 'low',
requirements_met: isSdgCompliant,
funding_eur: 880_000_000,
deadline: 'rolling_2025',
program_type: 'grant'
},
fedima_sustainability_grant: {
eligible: reductionAchieved >= 20,
confidence: reductionAchieved >= 25 ? 'high' : reductionAchieved >= 20 ? 'medium' : 'low',
requirements_met: reductionAchieved >= 20,
funding_eur: 20_000,
deadline: '2025-06-30',
program_type: 'grant',
sector_specific: 'bakery'
},
eit_food_retail: {
eligible: reductionAchieved >= 15,
confidence: reductionAchieved >= 20 ? 'high' : reductionAchieved >= 15 ? 'medium' : 'low',
requirements_met: reductionAchieved >= 15,
funding_eur: 45_000,
deadline: 'rolling',
program_type: 'grant',
sector_specific: 'retail'
},
un_sdg_certified: {
eligible: isSdgCompliant,
confidence: isSdgCompliant ? 'high' : 'low',
requirements_met: isSdgCompliant,
funding_eur: 0,
deadline: 'ongoing',
program_type: 'certification'
}
};
const recommendedApplications = Object.entries(grantPrograms)
.filter(([_, program]) => program.eligible && program.confidence !== 'low')
.map(([name, _]) => name);
const eligibleCount = Object.values(grantPrograms).filter(p => p.eligible).length;
const overallReadiness = (eligibleCount / Object.keys(grantPrograms).length) * 100;
return {
overall_readiness_percentage: Math.round(overallReadiness),
grant_programs: grantPrograms,
recommended_applications: recommendedApplications,
spain_compliance: {
law_1_2025: reductionAchieved >= 50,
circular_economy_strategy: reductionAchieved >= 30
}
};
}
// ===== MAIN AGGREGATION FUNCTION =====
/**
* Get comprehensive sustainability metrics by aggregating production and inventory data
*/
export async function getSustainabilityMetrics(
tenantId: string,
startDate?: string,
endDate?: string
): Promise<SustainabilityMetrics> {
const params = new URLSearchParams();
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
try {
// Fetch data from both services in parallel
const [productionData, inventoryData, productionBaseline, aiImpact] = await Promise.all([
getProductionWasteMetrics(tenantId, startDate, endDate),
getInventoryWasteMetrics(tenantId, startDate, endDate),
getProductionBaseline(tenantId, startDate, endDate),
getProductionAIImpact(tenantId, startDate, endDate)
]);
const queryString = params.toString();
const url = `/tenants/${tenantId}${BASE_PATH}/metrics${queryString ? `?${queryString}` : ''}`;
// Aggregate waste metrics
const productionWaste = (productionData.total_production_waste || 0) + (productionData.total_defects || 0);
const totalWasteKg = productionWaste + (inventoryData.inventory_waste_kg || 0);
return await apiClient.get<SustainabilityMetrics>(url);
const totalProductionKg = productionData.total_planned || 0;
const wastePercentage = totalProductionKg > 0
? (totalWasteKg / totalProductionKg) * 100
: 0;
const wasteMetrics: WasteMetrics = {
total_waste_kg: Math.round(totalWasteKg * 100) / 100,
production_waste_kg: Math.round(productionWaste * 100) / 100,
expired_waste_kg: Math.round((inventoryData.inventory_waste_kg || 0) * 100) / 100,
waste_percentage: Math.round(wastePercentage * 100) / 100,
waste_by_reason: {
...(productionData.waste_by_defect_type || {}),
...(inventoryData.waste_by_reason || {})
}
};
// Calculate environmental impact
const environmentalImpact = calculateEnvironmentalImpact(totalWasteKg);
// Calculate SDG compliance
const baselineWastePercentage = productionBaseline.waste_percentage ||
EnvironmentalConstants.EU_BAKERY_BASELINE_WASTE * 100;
const sdgCompliance = calculateSDGCompliance(wastePercentage, baselineWastePercentage);
// Calculate avoided waste from AI
const wasteAvoidedKg = aiImpact.impact?.waste_avoided_kg || 0;
const avoidedWaste: AvoidedWaste = {
waste_avoided_kg: Math.round(wasteAvoidedKg * 100) / 100,
ai_assisted_batches: aiImpact.ai_batches?.count || 0,
environmental_impact_avoided: {
co2_kg: Math.round(wasteAvoidedKg * EnvironmentalConstants.CO2_PER_KG_WASTE * 100) / 100,
water_liters: Math.round(wasteAvoidedKg * EnvironmentalConstants.WATER_PER_KG)
},
methodology: 'ai_vs_manual_comparison'
};
// Calculate financial impact
const inventoryCost = inventoryData.waste_cost_eur || 0;
const productionCost = productionWaste * 3.50; // €3.50/kg avg
const totalCost = inventoryCost + productionCost;
const financialImpact: FinancialImpact = {
waste_cost_eur: Math.round(totalCost * 100) / 100,
cost_per_kg: 3.50,
potential_monthly_savings: Math.round((aiImpact.impact?.cost_savings_eur || 0) * 100) / 100,
annual_projection: Math.round((aiImpact.impact?.cost_savings_eur || 0) * 12 * 100) / 100
};
// Assess grant readiness
const grantReadiness = assessGrantReadiness(sdgCompliance);
return {
period: productionData.period || {
start_date: startDate || new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
end_date: endDate || new Date().toISOString(),
days: 30
},
waste_metrics: wasteMetrics,
environmental_impact: environmentalImpact,
sdg_compliance: sdgCompliance,
avoided_waste: avoidedWaste,
financial_impact: financialImpact,
grant_readiness: grantReadiness
};
} catch (error) {
console.error('Error aggregating sustainability metrics:', error);
throw error;
}
}
/**
@@ -39,18 +419,57 @@ export async function getSustainabilityWidgetData(
tenantId: string,
days: number = 30
): Promise<SustainabilityWidgetData> {
return await apiClient.get<SustainabilityWidgetData>(
`/tenants/${tenantId}${BASE_PATH}/widget?days=${days}`
);
try {
// Fetch summaries from both services in parallel
const [productionSummary, inventorySummary] = await Promise.all([
getProductionSummary(tenantId, days),
getInventorySummary(tenantId, days)
]);
const productionWasteWidget = (productionSummary.total_production_waste || 0) + (productionSummary.total_defects || 0);
const totalWasteKg = productionWasteWidget + (inventorySummary.inventory_waste_kg || 0);
const totalProduction = productionSummary.total_planned || productionSummary.total_production_kg || 0;
const wastePercentage = totalProduction > 0 ? ((totalWasteKg / totalProduction) * 100) : 0;
const baselinePercentage = productionSummary.waste_percentage || 25;
const reductionPercentage = baselinePercentage > 0
? ((baselinePercentage - wastePercentage) / baselinePercentage) * 100
: 0;
const co2SavedKg = totalWasteKg * EnvironmentalConstants.CO2_PER_KG_WASTE;
const waterSavedLiters = totalWasteKg * EnvironmentalConstants.WATER_PER_KG;
return {
total_waste_kg: Math.round(totalWasteKg * 100) / 100,
waste_reduction_percentage: Math.round(reductionPercentage * 100) / 100,
co2_saved_kg: Math.round(co2SavedKg * 100) / 100,
water_saved_liters: Math.round(waterSavedLiters),
trees_equivalent: Math.ceil((co2SavedKg / 1000) * EnvironmentalConstants.TREES_PER_TON_CO2),
sdg_status: reductionPercentage >= 50 ? 'sdg_compliant' :
reductionPercentage >= 37.5 ? 'on_track' :
reductionPercentage >= 12.5 ? 'progressing' : 'baseline',
sdg_progress: Math.min(100, (reductionPercentage / 50) * 100),
grant_programs_ready: reductionPercentage >= 50 ? 5 :
reductionPercentage >= 30 ? 3 :
reductionPercentage >= 15 ? 2 : 0,
financial_savings_eur: Math.round(
((inventorySummary.waste_cost_eur || 0) + (productionWasteWidget * 3.50)) * 100
) / 100
};
} catch (error) {
console.error('Error fetching sustainability widget data:', error);
throw error;
}
}
/**
* Get SDG 12.3 compliance status
*/
export async function getSDGCompliance(tenantId: string): Promise<SDGCompliance> {
return await apiClient.get<SDGCompliance>(
`/tenants/${tenantId}${BASE_PATH}/sdg-compliance`
);
const metrics = await getSustainabilityMetrics(tenantId);
return metrics.sdg_compliance;
}
/**
@@ -60,13 +479,16 @@ export async function getEnvironmentalImpact(
tenantId: string,
days: number = 30
): Promise<EnvironmentalImpact> {
return await apiClient.get<EnvironmentalImpact>(
`/tenants/${tenantId}${BASE_PATH}/environmental-impact?days=${days}`
);
const endDate = new Date().toISOString();
const startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
const metrics = await getSustainabilityMetrics(tenantId, startDate, endDate);
return metrics.environmental_impact;
}
/**
* Export grant application report
* Note: This still uses the aggregated metrics approach
*/
export async function exportGrantReport(
tenantId: string,
@@ -74,12 +496,35 @@ export async function exportGrantReport(
startDate?: string,
endDate?: string
): Promise<GrantReport> {
const payload: any = { grant_type: grantType, format: 'json' };
if (startDate) payload.start_date = startDate;
if (endDate) payload.end_date = endDate;
const metrics = await getSustainabilityMetrics(tenantId, startDate, endDate);
return await apiClient.post<GrantReport>(
`/tenants/${tenantId}${BASE_PATH}/export/grant-report`,
payload
);
return {
report_metadata: {
generated_at: new Date().toISOString(),
report_type: grantType,
period: metrics.period,
tenant_id: tenantId
},
executive_summary: {
total_waste_reduced_kg: metrics.avoided_waste.waste_avoided_kg,
waste_reduction_percentage: metrics.sdg_compliance.sdg_12_3.reduction_achieved,
co2_emissions_avoided_kg: metrics.avoided_waste.environmental_impact_avoided.co2_kg,
financial_savings_eur: metrics.financial_impact.potential_monthly_savings,
sdg_compliance_status: metrics.sdg_compliance.sdg_12_3.status_label
},
detailed_metrics: metrics,
certifications: {
sdg_12_3_compliant: metrics.sdg_compliance.certification_ready,
grant_programs_eligible: metrics.grant_readiness.recommended_applications
},
supporting_data: {
baseline_comparison: {
baseline: metrics.sdg_compliance.sdg_12_3.baseline_waste_percentage,
current: metrics.sdg_compliance.sdg_12_3.current_waste_percentage,
improvement: metrics.sdg_compliance.sdg_12_3.reduction_achieved
},
environmental_benefits: metrics.environmental_impact,
financial_benefits: metrics.financial_impact
}
};
}

View File

@@ -53,7 +53,7 @@ export interface SDG123Metrics {
reduction_achieved: number;
target_reduction: number;
progress_to_target: number;
status: 'sdg_compliant' | 'on_track' | 'progressing' | 'baseline';
status: 'sdg_compliant' | 'on_track' | 'progressing' | 'baseline' | 'above_baseline';
status_label: string;
target_waste_percentage: number;
}