Improve the frontend 2

This commit is contained in:
Urtzi Alfaro
2025-10-29 06:58:05 +01:00
parent 858d985c92
commit 36217a2729
98 changed files with 6652 additions and 4230 deletions

View File

@@ -50,7 +50,7 @@ async def create_food_safety_alert(
alert = await food_safety_service.create_food_safety_alert(
db,
alert_data,
user_id=UUID(current_user["sub"])
user_id=UUID(current_user["user_id"])
)
logger.info("Food safety alert created",
@@ -196,7 +196,7 @@ async def update_food_safety_alert(
set_clauses.append("updated_at = NOW()")
set_clauses.append("updated_by = :updated_by")
params["updated_by"] = UUID(current_user["sub"])
params["updated_by"] = UUID(current_user["user_id"])
update_query = f"""
UPDATE food_safety_alerts

View File

@@ -14,6 +14,7 @@ from shared.auth.access_control import require_user_role
from shared.routing import RouteBuilder
from app.core.database import get_db
from app.services.food_safety_service import FoodSafetyService
from app.models import AuditLog
from app.schemas.food_safety import (
FoodSafetyComplianceCreate,
FoodSafetyComplianceUpdate,
@@ -50,7 +51,7 @@ async def create_compliance_record(
compliance = await food_safety_service.create_compliance_record(
db,
compliance_data,
user_id=UUID(current_user["sub"])
user_id=UUID(current_user["user_id"])
)
logger.info("Compliance record created",
@@ -181,7 +182,7 @@ async def update_compliance_record(
compliance_id,
tenant_id,
compliance_data,
user_id=UUID(current_user["sub"])
user_id=UUID(current_user["user_id"])
)
if not compliance:
@@ -268,7 +269,7 @@ async def archive_compliance_record(
# Log audit event for archiving compliance record
try:
from shared.security import create_audit_logger, AuditSeverity, AuditAction
audit_logger = create_audit_logger("inventory-service")
audit_logger = create_audit_logger("inventory-service", AuditLog)
await audit_logger.log_event(
db_session=db,
tenant_id=str(tenant_id),

View File

@@ -56,7 +56,7 @@ async def acknowledge_alert(
result = await db.execute(update_query, {
"alert_id": alert_id,
"tenant_id": tenant_id,
"user_id": UUID(current_user["sub"]),
"user_id": UUID(current_user["user_id"]),
"notes": f"\nAcknowledged: {notes}" if notes else "\nAcknowledged"
})

View File

@@ -11,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.services.inventory_service import InventoryService
from app.models import AuditLog
from app.schemas.inventory import (
IngredientCreate,
IngredientUpdate,
@@ -30,7 +31,7 @@ route_builder = RouteBuilder('inventory')
router = APIRouter(tags=["ingredients"])
# Initialize audit logger
audit_logger = create_audit_logger("inventory-service")
audit_logger = create_audit_logger("inventory-service", AuditLog)
# Helper function to extract user ID from user object
def get_current_user_id(current_user: dict = Depends(get_current_user_dep)) -> UUID:

View File

@@ -51,7 +51,7 @@ async def log_temperature(
temp_log = await food_safety_service.log_temperature(
db,
temp_data,
user_id=UUID(current_user["sub"])
user_id=UUID(current_user["user_id"])
)
logger.info("Temperature logged",
@@ -89,7 +89,7 @@ async def bulk_log_temperatures(
temp_logs = await food_safety_service.bulk_log_temperatures(
db,
bulk_data.readings,
user_id=UUID(current_user["sub"])
user_id=UUID(current_user["user_id"])
)
logger.info("Bulk temperature logging completed",

View File

@@ -85,6 +85,22 @@ class SustainabilityService:
# Get waste data from production and inventory
waste_data = await self._get_waste_data(db, tenant_id, start_date, end_date)
# Check if there's sufficient data for meaningful calculations
# Minimum: 50kg production to avoid false metrics on empty accounts
total_production = waste_data['total_production_kg']
has_sufficient_data = total_production >= 50.0
logger.info(
"Checking data sufficiency for sustainability metrics",
tenant_id=str(tenant_id),
total_production=total_production,
has_sufficient_data=has_sufficient_data
)
# If insufficient data, return a special "collecting data" state
if not has_sufficient_data:
return self._get_insufficient_data_response(start_date, end_date, waste_data)
# Calculate environmental impact
environmental_impact = self._calculate_environmental_impact(waste_data)
@@ -118,7 +134,8 @@ class SustainabilityService:
'sdg_compliance': sdg_compliance,
'avoided_waste': avoided_waste,
'financial_impact': financial_impact,
'grant_readiness': self._assess_grant_readiness(sdg_compliance)
'grant_readiness': self._assess_grant_readiness(sdg_compliance),
'data_sufficient': True
}
except Exception as e:
@@ -126,6 +143,138 @@ class SustainabilityService:
tenant_id=str(tenant_id), error=str(e))
raise
def _get_insufficient_data_response(
self,
start_date: datetime,
end_date: datetime,
waste_data: Dict[str, Any]
) -> Dict[str, Any]:
"""
Return response for tenants with insufficient data
This prevents showing misleading "100% compliant" status for empty accounts
"""
return {
'period': {
'start_date': start_date.isoformat(),
'end_date': end_date.isoformat(),
'days': (end_date - start_date).days
},
'waste_metrics': {
'total_waste_kg': 0.0,
'production_waste_kg': 0.0,
'expired_waste_kg': 0.0,
'waste_percentage': 0.0,
'waste_by_reason': {}
},
'environmental_impact': {
'co2_emissions': {
'kg': 0.0,
'tons': 0.0,
'trees_to_offset': 0.0
},
'water_footprint': {
'liters': 0.0,
'cubic_meters': 0.0
},
'land_use': {
'square_meters': 0.0,
'hectares': 0.0
},
'human_equivalents': {
'car_km_equivalent': 0.0,
'smartphone_charges': 0.0,
'showers_equivalent': 0.0,
'trees_planted': 0.0
}
},
'sdg_compliance': {
'sdg_12_3': {
'baseline_waste_percentage': 0.0,
'current_waste_percentage': 0.0,
'reduction_achieved': 0.0,
'target_reduction': 50.0,
'progress_to_target': 0.0,
'status': 'insufficient_data',
'status_label': 'Collecting Baseline Data',
'target_waste_percentage': 0.0
},
'baseline_period': 'not_available',
'certification_ready': False,
'improvement_areas': ['start_production_tracking']
},
'avoided_waste': {
'waste_avoided_kg': 0.0,
'ai_assisted_batches': 0,
'environmental_impact_avoided': {
'co2_kg': 0.0,
'water_liters': 0.0
},
'methodology': 'insufficient_data'
},
'financial_impact': {
'waste_cost_eur': 0.0,
'cost_per_kg': 3.50,
'potential_monthly_savings': 0.0,
'annual_projection': 0.0
},
'grant_readiness': {
'overall_readiness_percentage': 0.0,
'grant_programs': {
'life_circular_economy': {
'eligible': False,
'confidence': 'low',
'requirements_met': False,
'funding_eur': 73_000_000,
'deadline': '2025-09-23',
'program_type': 'grant'
},
'horizon_europe_cluster_6': {
'eligible': False,
'confidence': 'low',
'requirements_met': False,
'funding_eur': 880_000_000,
'deadline': 'rolling_2025',
'program_type': 'grant'
},
'fedima_sustainability_grant': {
'eligible': False,
'confidence': 'low',
'requirements_met': False,
'funding_eur': 20_000,
'deadline': '2025-06-30',
'program_type': 'grant',
'sector_specific': 'bakery'
},
'eit_food_retail': {
'eligible': False,
'confidence': 'low',
'requirements_met': False,
'funding_eur': 45_000,
'deadline': 'rolling',
'program_type': 'grant',
'sector_specific': 'retail'
},
'un_sdg_certified': {
'eligible': False,
'confidence': 'low',
'requirements_met': False,
'funding_eur': 0,
'deadline': 'ongoing',
'program_type': 'certification'
}
},
'recommended_applications': [],
'spain_compliance': {
'law_1_2025': False,
'circular_economy_strategy': False
}
},
'data_sufficient': False,
'minimum_production_required_kg': 50.0,
'current_production_kg': waste_data['total_production_kg']
}
async def _get_waste_data(
self,
db: AsyncSession,
@@ -306,79 +455,104 @@ class SustainabilityService:
"""
Calculate compliance with UN SDG 12.3
Target: Halve per capita global food waste by 2030
IMPORTANT: This method assumes sufficient data validation was done upstream.
It should only be called when waste_data has meaningful production volumes.
"""
try:
# Get baseline (first 90 days of operation or industry average)
# Get baseline (first 90 days of operation)
baseline = await self._get_baseline_waste(db, tenant_id)
current_waste_percentage = waste_data['waste_percentage']
# Ensure baseline is at least the industry average if not available
baseline_percentage = baseline.get('waste_percentage', EnvironmentalConstants.EU_BAKERY_BASELINE_WASTE * 100)
# If baseline is too low (less than 1%), use industry average to prevent calculation errors
if baseline_percentage < 1.0:
baseline_percentage = EnvironmentalConstants.EU_BAKERY_BASELINE_WASTE * 100
total_production = waste_data['total_production_kg']
# Calculate reduction from baseline
# If current waste is higher than baseline, show negative reduction (worse than baseline)
# If current waste is lower than baseline, show positive reduction (better than baseline)
if baseline_percentage > 0:
reduction_percentage = ((baseline_percentage - current_waste_percentage) / baseline_percentage) * 100
else:
# Check if we have a real baseline from production history
has_real_baseline = baseline.get('data_available', False)
baseline_percentage = baseline.get('waste_percentage', 0.0)
# If no real baseline AND insufficient current production, we can't make comparisons
if not has_real_baseline and total_production < 50:
logger.warning(
"Cannot calculate SDG compliance without baseline or sufficient production",
tenant_id=str(tenant_id),
total_production=total_production
)
return self._get_insufficient_sdg_data()
# If we have no real baseline but have current production, use it as baseline
if not has_real_baseline:
logger.info(
"Using current period as baseline (no historical data available)",
tenant_id=str(tenant_id),
current_waste_percentage=current_waste_percentage
)
baseline_percentage = current_waste_percentage
# Set reduction to 0 since we're establishing baseline
reduction_percentage = 0
# Calculate progress toward 50% reduction target
# The target is to achieve 50% reduction from baseline
# So if baseline is 25%, target is to reach 12.5% (25% * 0.5)
target_reduction_percentage = 50.0
target_waste_percentage = baseline_percentage * (1 - (target_reduction_percentage / 100))
# Calculate progress: how much of the 50% target has been achieved
# If we've reduced from 25% to 19.28%, we've achieved (25-19.28)/(25-12.5) = 5.72/12.5 = 45.8% of target
if baseline_percentage > target_waste_percentage:
max_possible_reduction = baseline_percentage - target_waste_percentage
actual_reduction = baseline_percentage - current_waste_percentage
progress_to_target = (actual_reduction / max_possible_reduction) * 100 if max_possible_reduction > 0 else 0
else:
# If current is already better than target
progress_to_target = 100.0 if current_waste_percentage <= target_waste_percentage else 0.0
# Ensure progress doesn't exceed 100%
progress_to_target = min(progress_to_target, 100.0)
# Status assessment based on actual reduction achieved
if reduction_percentage >= 50:
status = 'sdg_compliant'
status_label = 'SDG 12.3 Compliant'
elif reduction_percentage >= 30:
status = 'on_track'
status_label = 'On Track to Compliance'
elif reduction_percentage >= 10:
status = 'progressing'
status_label = 'Making Progress'
elif reduction_percentage > 0:
status = 'improving'
status_label = 'Improving'
elif reduction_percentage < 0:
status = 'baseline'
status_label = 'Above Baseline'
else:
progress_to_target = 0
status = 'baseline'
status_label = 'Establishing Baseline'
else:
# We have a real baseline - calculate actual reduction
# If current waste is higher than baseline, show negative reduction (worse than baseline)
# If current waste is lower than baseline, show positive reduction (better than baseline)
if baseline_percentage > 0:
reduction_percentage = ((baseline_percentage - current_waste_percentage) / baseline_percentage) * 100
else:
reduction_percentage = 0
# Calculate progress toward 50% reduction target
# The target is to achieve 50% reduction from baseline
# So if baseline is 25%, target is to reach 12.5% (25% * 0.5)
target_reduction_percentage = 50.0
target_waste_percentage = baseline_percentage * (1 - (target_reduction_percentage / 100))
# Calculate progress: how much of the 50% target has been achieved
# If we've reduced from 25% to 19.28%, we've achieved (25-19.28)/(25-12.5) = 5.72/12.5 = 45.8% of target
if baseline_percentage > target_waste_percentage:
max_possible_reduction = baseline_percentage - target_waste_percentage
actual_reduction = baseline_percentage - current_waste_percentage
progress_to_target = (actual_reduction / max_possible_reduction) * 100 if max_possible_reduction > 0 else 0
else:
# If current is already better than target
progress_to_target = 100.0 if current_waste_percentage <= target_waste_percentage else 0.0
# Ensure progress doesn't exceed 100%
progress_to_target = min(progress_to_target, 100.0)
# Status assessment based on actual reduction achieved
if reduction_percentage >= 50:
status = 'sdg_compliant'
status_label = 'SDG 12.3 Compliant'
elif reduction_percentage >= 30:
status = 'on_track'
status_label = 'On Track to Compliance'
elif reduction_percentage >= 10:
status = 'progressing'
status_label = 'Making Progress'
elif reduction_percentage > 0:
status = 'improving'
status_label = 'Improving'
elif reduction_percentage < 0:
status = 'above_baseline'
status_label = 'Above Baseline'
else:
status = 'baseline'
status_label = 'At Baseline'
return {
'sdg_12_3': {
'baseline_waste_percentage': round(baseline_percentage, 2),
'current_waste_percentage': round(current_waste_percentage, 2),
'reduction_achieved': round(reduction_percentage, 2),
'target_reduction': target_reduction_percentage,
'target_reduction': 50.0,
'progress_to_target': round(max(progress_to_target, 0), 1), # Ensure non-negative
'status': status,
'status_label': status_label,
'target_waste_percentage': round(target_waste_percentage, 2)
'target_waste_percentage': round(baseline_percentage * 0.5, 2) if baseline_percentage > 0 else 0.0
},
'baseline_period': baseline.get('period', 'industry_average'),
'certification_ready': reduction_percentage >= 50,
'baseline_period': baseline.get('period', 'current_period'),
'certification_ready': reduction_percentage >= 50 if has_real_baseline else False,
'improvement_areas': self._identify_improvement_areas(waste_data)
}
@@ -386,6 +560,24 @@ class SustainabilityService:
logger.error("Failed to calculate SDG compliance", error=str(e))
raise
def _get_insufficient_sdg_data(self) -> Dict[str, Any]:
"""Return SDG compliance structure for insufficient data case"""
return {
'sdg_12_3': {
'baseline_waste_percentage': 0.0,
'current_waste_percentage': 0.0,
'reduction_achieved': 0.0,
'target_reduction': 50.0,
'progress_to_target': 0.0,
'status': 'insufficient_data',
'status_label': 'Collecting Baseline Data',
'target_waste_percentage': 0.0
},
'baseline_period': 'not_available',
'certification_ready': False,
'improvement_areas': ['start_production_tracking']
}
async def _get_baseline_waste(
self,
db: AsyncSession,
@@ -482,12 +674,24 @@ class SustainabilityService:
return {
'waste_avoided_kg': 0,
'ai_assisted_batches': 0,
'note': 'Insufficient data for avoided waste calculation'
'environmental_impact_avoided': {
'co2_kg': 0,
'water_liters': 0
},
'methodology': 'insufficient_data'
}
except Exception as e:
logger.error("Failed to calculate avoided waste", error=str(e))
return {'waste_avoided_kg': 0, 'error': str(e)}
return {
'waste_avoided_kg': 0,
'ai_assisted_batches': 0,
'environmental_impact_avoided': {
'co2_kg': 0,
'water_liters': 0
},
'methodology': 'error_occurred'
}
def _calculate_financial_impact(self, waste_data: Dict[str, Any]) -> Dict[str, Any]:
"""Calculate financial impact of food waste"""