Improve the frontend 2
This commit is contained in:
@@ -14,11 +14,13 @@ from shared.routing import RouteBuilder
|
||||
from shared.security import create_audit_logger, AuditSeverity, AuditAction
|
||||
from app.core.database import get_db
|
||||
from app.services.production_service import ProductionService
|
||||
from app.models import AuditLog
|
||||
from app.schemas.equipment import (
|
||||
EquipmentCreate,
|
||||
EquipmentUpdate,
|
||||
EquipmentResponse,
|
||||
EquipmentListResponse
|
||||
EquipmentListResponse,
|
||||
EquipmentDeletionSummary
|
||||
)
|
||||
from app.models.production import EquipmentStatus, EquipmentType
|
||||
from app.core.config import settings
|
||||
@@ -27,8 +29,8 @@ logger = structlog.get_logger()
|
||||
route_builder = RouteBuilder('production')
|
||||
router = APIRouter(tags=["production-equipment"])
|
||||
|
||||
# Initialize audit logger
|
||||
audit_logger = create_audit_logger("production-service")
|
||||
# Initialize audit logger with the production service's AuditLog model
|
||||
audit_logger = create_audit_logger("production-service", AuditLog)
|
||||
|
||||
|
||||
def get_production_service() -> ProductionService:
|
||||
@@ -80,7 +82,8 @@ async def create_equipment(
|
||||
equipment_data: EquipmentCreate,
|
||||
tenant_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
production_service: ProductionService = Depends(get_production_service)
|
||||
production_service: ProductionService = Depends(get_production_service),
|
||||
db = Depends(get_db)
|
||||
):
|
||||
"""Create a new equipment item"""
|
||||
try:
|
||||
@@ -89,15 +92,16 @@ async def create_equipment(
|
||||
logger.info("Created equipment",
|
||||
equipment_id=str(equipment.id), tenant_id=str(tenant_id))
|
||||
|
||||
# Audit log
|
||||
await audit_logger.log(
|
||||
action=AuditAction.CREATE,
|
||||
# Audit log the equipment creation
|
||||
await audit_logger.log_event(
|
||||
db_session=db,
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user.get('user_id'),
|
||||
action=AuditAction.CREATE.value,
|
||||
resource_type="equipment",
|
||||
resource_id=str(equipment.id),
|
||||
user_id=current_user.get('user_id'),
|
||||
tenant_id=str(tenant_id),
|
||||
severity=AuditSeverity.INFO,
|
||||
details={"equipment_name": equipment.name, "equipment_type": equipment.type.value}
|
||||
severity=AuditSeverity.INFO.value,
|
||||
audit_metadata={"equipment_name": equipment.name, "equipment_type": equipment.type.value}
|
||||
)
|
||||
|
||||
return EquipmentResponse.model_validate(equipment)
|
||||
@@ -152,7 +156,8 @@ async def update_equipment(
|
||||
tenant_id: UUID = Path(...),
|
||||
equipment_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
production_service: ProductionService = Depends(get_production_service)
|
||||
production_service: ProductionService = Depends(get_production_service),
|
||||
db = Depends(get_db)
|
||||
):
|
||||
"""Update an equipment item"""
|
||||
try:
|
||||
@@ -164,15 +169,16 @@ async def update_equipment(
|
||||
logger.info("Updated equipment",
|
||||
equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
||||
|
||||
# Audit log
|
||||
await audit_logger.log(
|
||||
action=AuditAction.UPDATE,
|
||||
# Audit log the equipment update
|
||||
await audit_logger.log_event(
|
||||
db_session=db,
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user.get('user_id'),
|
||||
action=AuditAction.UPDATE.value,
|
||||
resource_type="equipment",
|
||||
resource_id=str(equipment_id),
|
||||
user_id=current_user.get('user_id'),
|
||||
tenant_id=str(tenant_id),
|
||||
severity=AuditSeverity.INFO,
|
||||
details={"updates": equipment_data.model_dump(exclude_unset=True)}
|
||||
severity=AuditSeverity.INFO.value,
|
||||
audit_metadata={"updates": equipment_data.model_dump(exclude_unset=True)}
|
||||
)
|
||||
|
||||
return EquipmentResponse.model_validate(equipment)
|
||||
@@ -189,37 +195,80 @@ async def update_equipment(
|
||||
raise HTTPException(status_code=500, detail="Failed to update equipment")
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_base_route("equipment/{equipment_id}/deletion-summary"),
|
||||
response_model=EquipmentDeletionSummary
|
||||
)
|
||||
async def get_equipment_deletion_summary(
|
||||
tenant_id: UUID = Path(...),
|
||||
equipment_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
production_service: ProductionService = Depends(get_production_service)
|
||||
):
|
||||
"""Get deletion summary for equipment (dependency check)"""
|
||||
try:
|
||||
summary = await production_service.get_equipment_deletion_summary(tenant_id, equipment_id)
|
||||
|
||||
logger.info("Retrieved equipment deletion summary",
|
||||
equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
||||
|
||||
return EquipmentDeletionSummary(**summary)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting equipment deletion summary",
|
||||
error=str(e), equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
||||
raise HTTPException(status_code=500, detail="Failed to get deletion summary")
|
||||
|
||||
|
||||
@router.delete(
|
||||
route_builder.build_base_route("equipment/{equipment_id}")
|
||||
)
|
||||
async def delete_equipment(
|
||||
tenant_id: UUID = Path(...),
|
||||
equipment_id: UUID = Path(...),
|
||||
permanent: bool = Query(False, description="Permanent delete (hard delete) if true"),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
production_service: ProductionService = Depends(get_production_service)
|
||||
production_service: ProductionService = Depends(get_production_service),
|
||||
db = Depends(get_db)
|
||||
):
|
||||
"""Delete (soft delete) an equipment item"""
|
||||
"""Delete an equipment item. Use permanent=true for hard delete (requires admin role)"""
|
||||
try:
|
||||
success = await production_service.delete_equipment(tenant_id, equipment_id)
|
||||
# Hard delete requires admin role
|
||||
if permanent:
|
||||
user_role = current_user.get('role', '').lower()
|
||||
if user_role not in ['admin', 'owner']:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Hard delete requires admin or owner role"
|
||||
)
|
||||
|
||||
success = await production_service.hard_delete_equipment(tenant_id, equipment_id)
|
||||
delete_type = "hard_delete"
|
||||
severity = AuditSeverity.CRITICAL.value
|
||||
else:
|
||||
success = await production_service.delete_equipment(tenant_id, equipment_id)
|
||||
delete_type = "soft_delete"
|
||||
severity = AuditSeverity.WARNING.value
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Equipment not found")
|
||||
|
||||
logger.info("Deleted equipment",
|
||||
logger.info(f"{'Hard' if permanent else 'Soft'} deleted equipment",
|
||||
equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
||||
|
||||
# Audit log
|
||||
await audit_logger.log(
|
||||
action=AuditAction.DELETE,
|
||||
# Audit log the equipment deletion
|
||||
await audit_logger.log_event(
|
||||
db_session=db,
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user.get('user_id'),
|
||||
action=AuditAction.DELETE.value,
|
||||
resource_type="equipment",
|
||||
resource_id=str(equipment_id),
|
||||
user_id=current_user.get('user_id'),
|
||||
tenant_id=str(tenant_id),
|
||||
severity=AuditSeverity.WARNING,
|
||||
details={"action": "soft_delete"}
|
||||
severity=severity,
|
||||
audit_metadata={"action": delete_type, "permanent": permanent}
|
||||
)
|
||||
|
||||
return {"message": "Equipment deleted successfully"}
|
||||
return {"message": f"Equipment {'permanently deleted' if permanent else 'deleted'} successfully"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
@@ -15,6 +15,7 @@ from shared.routing import RouteBuilder
|
||||
from shared.security import create_audit_logger, AuditSeverity, AuditAction
|
||||
from app.core.database import get_db
|
||||
from app.services.production_service import ProductionService
|
||||
from app.models import AuditLog
|
||||
from app.schemas.production import (
|
||||
ProductionBatchCreate,
|
||||
ProductionBatchUpdate,
|
||||
@@ -29,8 +30,8 @@ logger = structlog.get_logger()
|
||||
route_builder = RouteBuilder('production')
|
||||
router = APIRouter(tags=["production-batches"])
|
||||
|
||||
# Initialize audit logger
|
||||
audit_logger = create_audit_logger("production-service")
|
||||
# Initialize audit logger with the production service's AuditLog model
|
||||
audit_logger = create_audit_logger("production-service", AuditLog)
|
||||
|
||||
|
||||
def get_production_service() -> ProductionService:
|
||||
|
||||
@@ -15,6 +15,7 @@ from shared.routing import RouteBuilder
|
||||
from shared.security import create_audit_logger, AuditSeverity, AuditAction
|
||||
from app.core.database import get_db
|
||||
from app.services.production_service import ProductionService
|
||||
from app.models import AuditLog
|
||||
from app.schemas.production import (
|
||||
ProductionScheduleCreate,
|
||||
ProductionScheduleUpdate,
|
||||
@@ -26,8 +27,8 @@ logger = structlog.get_logger()
|
||||
route_builder = RouteBuilder('production')
|
||||
router = APIRouter(tags=["production-schedules"])
|
||||
|
||||
# Initialize audit logger
|
||||
audit_logger = create_audit_logger("production-service")
|
||||
# Initialize audit logger with the production service's AuditLog model
|
||||
audit_logger = create_audit_logger("production-service", AuditLog)
|
||||
|
||||
|
||||
def get_production_service() -> ProductionService:
|
||||
|
||||
@@ -102,7 +102,7 @@ async def create_quality_template(
|
||||
|
||||
# Add created_by from current user
|
||||
template_dict = template_data.dict()
|
||||
template_dict['created_by'] = UUID(current_user["sub"])
|
||||
template_dict['created_by'] = UUID(current_user["user_id"])
|
||||
template_create = QualityCheckTemplateCreate(**template_dict)
|
||||
|
||||
# Create template via service (handles validation and business rules)
|
||||
@@ -111,6 +111,9 @@ async def create_quality_template(
|
||||
template_data=template_create
|
||||
)
|
||||
|
||||
# Commit the transaction to persist changes
|
||||
await db.commit()
|
||||
|
||||
logger.info("Created quality template",
|
||||
template_id=str(template.id),
|
||||
template_name=template.name,
|
||||
@@ -202,6 +205,9 @@ async def update_quality_template(
|
||||
detail="Quality template not found"
|
||||
)
|
||||
|
||||
# Commit the transaction to persist changes
|
||||
await db.commit()
|
||||
|
||||
logger.info("Updated quality template",
|
||||
template_id=str(template_id),
|
||||
tenant_id=str(tenant_id))
|
||||
@@ -259,6 +265,9 @@ async def delete_quality_template(
|
||||
detail="Quality template not found"
|
||||
)
|
||||
|
||||
# Commit the transaction to persist changes
|
||||
await db.commit()
|
||||
|
||||
logger.info("Deleted quality template",
|
||||
template_id=str(template_id),
|
||||
tenant_id=str(tenant_id))
|
||||
|
||||
@@ -150,3 +150,72 @@ class EquipmentRepository(ProductionBaseRepository):
|
||||
except Exception as e:
|
||||
logger.error("Error deleting equipment", error=str(e), equipment_id=str(equipment_id))
|
||||
raise
|
||||
|
||||
async def hard_delete_equipment(self, equipment_id: UUID) -> bool:
|
||||
"""Permanently delete equipment from database"""
|
||||
try:
|
||||
equipment = await self.get(equipment_id)
|
||||
if not equipment:
|
||||
return False
|
||||
|
||||
await self.session.delete(equipment)
|
||||
await self.session.flush()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error hard deleting equipment", error=str(e), equipment_id=str(equipment_id))
|
||||
raise
|
||||
|
||||
async def get_equipment_deletion_summary(self, tenant_id: UUID, equipment_id: UUID) -> Dict[str, Any]:
|
||||
"""Get summary of what will be affected by deleting equipment"""
|
||||
try:
|
||||
equipment = await self.get_equipment_by_id(tenant_id, equipment_id)
|
||||
if not equipment:
|
||||
return {
|
||||
"can_delete": False,
|
||||
"warnings": ["Equipment not found"],
|
||||
"production_batches_count": 0,
|
||||
"maintenance_records_count": 0,
|
||||
"temperature_logs_count": 0
|
||||
}
|
||||
|
||||
# Check for related production batches
|
||||
from app.models.production import ProductionBatch
|
||||
batch_query = select(func.count(ProductionBatch.id)).filter(
|
||||
and_(
|
||||
ProductionBatch.tenant_id == tenant_id,
|
||||
ProductionBatch.equipment_id == equipment_id
|
||||
)
|
||||
)
|
||||
batch_result = await self.session.execute(batch_query)
|
||||
batches_count = batch_result.scalar() or 0
|
||||
|
||||
# For now, we'll use placeholder counts for maintenance and temperature logs
|
||||
# These would need to be implemented based on your actual models
|
||||
maintenance_count = 0
|
||||
temperature_logs_count = 0
|
||||
|
||||
warnings = []
|
||||
if batches_count > 0:
|
||||
warnings.append(f"{batches_count} production batch(es) are using this equipment")
|
||||
|
||||
# Equipment can be deleted even with dependencies, but warn the user
|
||||
can_delete = True
|
||||
|
||||
return {
|
||||
"can_delete": can_delete,
|
||||
"warnings": warnings,
|
||||
"production_batches_count": batches_count,
|
||||
"maintenance_records_count": maintenance_count,
|
||||
"temperature_logs_count": temperature_logs_count,
|
||||
"equipment_name": equipment.name,
|
||||
"equipment_type": equipment.type.value,
|
||||
"equipment_location": equipment.location
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting equipment deletion summary",
|
||||
error=str(e),
|
||||
equipment_id=str(equipment_id),
|
||||
tenant_id=str(tenant_id))
|
||||
raise
|
||||
|
||||
@@ -132,6 +132,16 @@ class QualityTemplateRepository(ProductionBaseRepository):
|
||||
existing = await self.get_by_filters(and_(*filters))
|
||||
return existing is not None
|
||||
|
||||
async def get_by_filters(self, *filters):
|
||||
"""Get a single record by filters"""
|
||||
try:
|
||||
query = select(self.model).where(and_(*filters))
|
||||
result = await self.session.execute(query)
|
||||
return result.scalar_one_or_none()
|
||||
except Exception as e:
|
||||
logger.error("Error getting record by filters", error=str(e), filters=str(filters))
|
||||
raise
|
||||
|
||||
async def get_templates_by_ids(
|
||||
self,
|
||||
tenant_id: str,
|
||||
@@ -149,4 +159,4 @@ class QualityTemplateRepository(ProductionBaseRepository):
|
||||
QualityCheckTemplate.is_required.desc(),
|
||||
QualityCheckTemplate.weight.desc()
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -169,3 +169,30 @@ class EquipmentListResponse(BaseModel):
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class EquipmentDeletionSummary(BaseModel):
|
||||
"""Schema for equipment deletion summary"""
|
||||
can_delete: bool = Field(..., description="Whether the equipment can be deleted")
|
||||
warnings: List[str] = Field(default_factory=list, description="List of warnings about deletion")
|
||||
production_batches_count: int = Field(default=0, description="Number of production batches using this equipment")
|
||||
maintenance_records_count: int = Field(default=0, description="Number of maintenance records")
|
||||
temperature_logs_count: int = Field(default=0, description="Number of temperature logs")
|
||||
equipment_name: Optional[str] = Field(None, description="Equipment name")
|
||||
equipment_type: Optional[str] = Field(None, description="Equipment type")
|
||||
equipment_location: Optional[str] = Field(None, description="Equipment location")
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"can_delete": True,
|
||||
"warnings": ["3 production batch(es) are using this equipment"],
|
||||
"production_batches_count": 3,
|
||||
"maintenance_records_count": 5,
|
||||
"temperature_logs_count": 120,
|
||||
"equipment_name": "Horno Principal #1",
|
||||
"equipment_type": "oven",
|
||||
"equipment_location": "Área de Horneado"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1501,6 +1501,9 @@ class ProductionService:
|
||||
# Create equipment
|
||||
equipment = await equipment_repo.create_equipment(equipment_dict)
|
||||
|
||||
# Commit the transaction to persist changes
|
||||
await session.commit()
|
||||
|
||||
logger.info("Created equipment",
|
||||
equipment_id=str(equipment.id), tenant_id=str(tenant_id))
|
||||
|
||||
@@ -1529,6 +1532,9 @@ class ProductionService:
|
||||
equipment_update.model_dump(exclude_none=True)
|
||||
)
|
||||
|
||||
# Commit the transaction to persist changes
|
||||
await session.commit()
|
||||
|
||||
logger.info("Updated equipment",
|
||||
equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
||||
|
||||
@@ -1554,6 +1560,9 @@ class ProductionService:
|
||||
# Soft delete equipment
|
||||
success = await equipment_repo.delete_equipment(equipment_id)
|
||||
|
||||
# Commit the transaction to persist changes
|
||||
await session.commit()
|
||||
|
||||
logger.info("Deleted equipment",
|
||||
equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
||||
|
||||
@@ -1564,6 +1573,60 @@ class ProductionService:
|
||||
error=str(e), equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
||||
raise
|
||||
|
||||
async def hard_delete_equipment(self, tenant_id: UUID, equipment_id: UUID) -> bool:
|
||||
"""Permanently delete an equipment item from database"""
|
||||
try:
|
||||
async with self.database_manager.get_session() as session:
|
||||
from app.repositories.equipment_repository import EquipmentRepository
|
||||
equipment_repo = EquipmentRepository(session)
|
||||
|
||||
# First verify equipment belongs to tenant
|
||||
equipment = await equipment_repo.get_equipment_by_id(tenant_id, equipment_id)
|
||||
if not equipment:
|
||||
return False
|
||||
|
||||
# Get deletion summary first for logging
|
||||
summary = await equipment_repo.get_equipment_deletion_summary(tenant_id, equipment_id)
|
||||
|
||||
# Hard delete equipment
|
||||
success = await equipment_repo.hard_delete_equipment(equipment_id)
|
||||
|
||||
# Commit the transaction to persist changes
|
||||
await session.commit()
|
||||
|
||||
logger.info("Hard deleted equipment",
|
||||
equipment_id=str(equipment_id),
|
||||
tenant_id=str(tenant_id),
|
||||
affected_batches=summary.get("production_batches_count", 0))
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error hard deleting equipment",
|
||||
error=str(e), equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
||||
raise
|
||||
|
||||
async def get_equipment_deletion_summary(self, tenant_id: UUID, equipment_id: UUID) -> Dict[str, Any]:
|
||||
"""Get deletion summary for an equipment item"""
|
||||
try:
|
||||
async with self.database_manager.get_session() as session:
|
||||
from app.repositories.equipment_repository import EquipmentRepository
|
||||
equipment_repo = EquipmentRepository(session)
|
||||
|
||||
summary = await equipment_repo.get_equipment_deletion_summary(tenant_id, equipment_id)
|
||||
|
||||
logger.info("Retrieved equipment deletion summary",
|
||||
equipment_id=str(equipment_id),
|
||||
tenant_id=str(tenant_id),
|
||||
can_delete=summary.get("can_delete", False))
|
||||
|
||||
return summary
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting equipment deletion summary",
|
||||
error=str(e), equipment_id=str(equipment_id), tenant_id=str(tenant_id))
|
||||
raise
|
||||
|
||||
# ================================================================
|
||||
# SUSTAINABILITY / WASTE ANALYTICS
|
||||
# ================================================================
|
||||
|
||||
Reference in New Issue
Block a user