Improve the frontend 2
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"
|
||||
})
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"""
|
||||
|
||||
Reference in New Issue
Block a user