Improve the frontend modals

This commit is contained in:
Urtzi Alfaro
2025-10-27 16:33:26 +01:00
parent 61376b7a9f
commit 858d985c92
143 changed files with 9289 additions and 2306 deletions

View File

@@ -132,7 +132,6 @@ async def clone_demo_data(
instructions=recipe.instructions,
preparation_notes=recipe.preparation_notes,
storage_instructions=recipe.storage_instructions,
quality_standards=recipe.quality_standards,
serves_count=recipe.serves_count,
nutritional_info=recipe.nutritional_info,
allergen_info=recipe.allergen_info,
@@ -142,9 +141,7 @@ async def clone_demo_data(
maximum_batch_size=recipe.maximum_batch_size,
optimal_production_temperature=recipe.optimal_production_temperature,
optimal_humidity=recipe.optimal_humidity,
quality_check_points=recipe.quality_check_points,
quality_check_configuration=recipe.quality_check_configuration,
common_issues=recipe.common_issues,
status=recipe.status,
is_seasonal=recipe.is_seasonal,
season_start_month=recipe.season_start_month,

View File

@@ -3,7 +3,7 @@
Recipes API - Atomic CRUD operations on Recipe model
"""
from fastapi import APIRouter, Depends, HTTPException, Header, Query
from fastapi import APIRouter, Depends, HTTPException, Header, Query, Request
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List, Optional
from uuid import UUID
@@ -18,6 +18,7 @@ from ..schemas.recipes import (
)
from shared.routing import RouteBuilder, RouteCategory
from shared.auth.access_control import require_user_role
from shared.auth.decorators import get_current_user_dep
from shared.security import create_audit_logger, AuditSeverity, AuditAction
route_builder = RouteBuilder('recipes')
@@ -43,6 +44,7 @@ async def create_recipe(
tenant_id: UUID,
recipe_data: RecipeCreate,
user_id: UUID = Depends(get_user_id),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Create a new recipe"""
@@ -86,6 +88,7 @@ async def search_recipes(
difficulty_level: Optional[int] = Query(None, ge=1, le=5),
limit: int = Query(100, ge=1, le=1000),
offset: int = Query(0, ge=0),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Search recipes with filters"""
@@ -135,7 +138,7 @@ async def count_recipes(
return {"count": count}
except Exception as e:
logger.error(f"Error counting recipes for tenant {tenant_id}: {e}")
logger.error(f"Error counting recipes for tenant: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@@ -151,6 +154,7 @@ async def get_recipe(
"""Get recipe by ID with ingredients"""
try:
recipe_service = RecipeService(db)
recipe = await recipe_service.get_recipe_with_ingredients(recipe_id)
if not recipe:
@@ -178,6 +182,7 @@ async def update_recipe(
recipe_id: UUID,
recipe_data: RecipeUpdate,
user_id: UUID = Depends(get_user_id),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Update an existing recipe"""
@@ -224,6 +229,7 @@ async def delete_recipe(
tenant_id: UUID,
recipe_id: UUID,
user_id: UUID = Depends(get_user_id),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Delete a recipe (Admin+ only)"""
@@ -237,6 +243,20 @@ async def delete_recipe(
if existing_recipe["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
# Check if deletion is safe
summary = await recipe_service.get_deletion_summary(recipe_id)
if not summary["success"]:
raise HTTPException(status_code=500, detail=summary["error"])
if not summary["data"]["can_delete"]:
raise HTTPException(
status_code=400,
detail={
"message": "Cannot delete recipe with active dependencies",
"warnings": summary["data"]["warnings"]
}
)
# Capture recipe data before deletion
recipe_data = {
"recipe_name": existing_recipe.get("name"),
@@ -281,3 +301,91 @@ async def delete_recipe(
except Exception as e:
logger.error(f"Error deleting recipe {recipe_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.patch(
route_builder.build_custom_route(RouteCategory.OPERATIONS, ["{recipe_id}", "archive"])
)
@require_user_role(['admin', 'owner'])
async def archive_recipe(
tenant_id: UUID,
recipe_id: UUID,
user_id: UUID = Depends(get_user_id),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Archive (soft delete) a recipe by setting status to ARCHIVED"""
try:
recipe_service = RecipeService(db)
existing_recipe = await recipe_service.get_recipe_with_ingredients(recipe_id)
if not existing_recipe:
raise HTTPException(status_code=404, detail="Recipe not found")
if existing_recipe["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Not authorized")
# Check status transitions (business rule)
current_status = existing_recipe.get("status")
if current_status == "DISCONTINUED":
raise HTTPException(
status_code=400,
detail="Cannot archive a discontinued recipe. Use hard delete instead."
)
# Update status to ARCHIVED
from ..schemas.recipes import RecipeUpdate, RecipeStatus
update_data = RecipeUpdate(status=RecipeStatus.ARCHIVED)
updated_recipe = await recipe_service.update_recipe(
recipe_id,
update_data.dict(exclude_unset=True),
user_id
)
if not updated_recipe["success"]:
raise HTTPException(status_code=400, detail=updated_recipe["error"])
logger.info(f"Archived recipe {recipe_id} by user {user_id}")
return RecipeResponse(**updated_recipe["data"])
except HTTPException:
raise
except Exception as e:
logger.error(f"Error archiving recipe: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get(
route_builder.build_custom_route(RouteCategory.OPERATIONS, ["{recipe_id}", "deletion-summary"])
)
@require_user_role(['admin', 'owner'])
async def get_recipe_deletion_summary(
tenant_id: UUID,
recipe_id: UUID,
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get summary of what will be affected by deleting this recipe"""
try:
recipe_service = RecipeService(db)
existing_recipe = await recipe_service.get_recipe_with_ingredients(recipe_id)
if not existing_recipe:
raise HTTPException(status_code=404, detail="Recipe not found")
if existing_recipe["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Not authorized")
summary = await recipe_service.get_deletion_summary(recipe_id)
if not summary["success"]:
raise HTTPException(status_code=500, detail=summary["error"])
return summary["data"]
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting deletion summary: {e}")
raise HTTPException(status_code=500, detail="Internal server error")