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