diff --git a/backend/app/routers/dewar.py b/backend/app/routers/dewar.py index 662318f..15ab1ad 100644 --- a/backend/app/routers/dewar.py +++ b/backend/app/routers/dewar.py @@ -30,6 +30,9 @@ from app.models import ( Sample as SampleModel, DewarType as DewarTypeModel, DewarSerialNumber as DewarSerialNumberModel, + LogisticsEvent, + PuckEvent, + SampleEvent, ) from app.dependencies import get_db import qrcode @@ -505,12 +508,55 @@ async def update_dewar( @router.delete("/{dewar_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_dewar(dewar_id: int, db: Session = Depends(get_db)): + # Fetch the Dewar from the database dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first() - if not dewar: raise HTTPException(status_code=404, detail="Dewar not found") - db.delete(dewar) + # Check for associated logistics events + logistics_event_exists = ( + db.query(LogisticsEvent).filter(LogisticsEvent.dewar_id == dewar_id).first() + ) + if logistics_event_exists: + raise HTTPException( + status_code=400, + detail=f"Dewar {dewar_id} has associated logistics events." + f"Deletion not allowed.", + ) + + # Check associated pucks and their events + for puck in dewar.pucks: + puck_event_exists = ( + db.query(PuckEvent).filter(PuckEvent.puck_id == puck.id).first() + ) + if puck_event_exists: + raise HTTPException( + status_code=400, + detail=f"Puck {puck.id} " + f"associated with this Dewar has events." + f"Deletion not allowed.", + ) + + # Check associated samples and their events + for sample in puck.samples: + sample_event_exists = ( + db.query(SampleEvent).filter(SampleEvent.sample_id == sample.id).first() + ) + if sample_event_exists: + raise HTTPException( + status_code=400, + detail=f"Sample {sample.id} " + f"associated with Puck {puck.id} has events." + f" Deletion not allowed.", + ) + + # Perform cascade deletion: Delete samples, pucks, and the dewar + for puck in dewar.pucks: + for sample in puck.samples: + db.delete(sample) # Delete associated samples + db.delete(puck) # Delete associated puck + db.delete(dewar) # Finally, delete the dewar itself + db.commit() return diff --git a/backend/app/routers/shipment.py b/backend/app/routers/shipment.py index bbbd718..37d6bce 100644 --- a/backend/app/routers/shipment.py +++ b/backend/app/routers/shipment.py @@ -15,6 +15,9 @@ from app.models import ( Dewar as DewarModel, Puck as PuckModel, Sample as SampleModel, + LogisticsEvent, + SampleEvent, + PuckEvent, ) from app.schemas import ( ShipmentCreate, @@ -117,9 +120,60 @@ async def create_shipment(shipment: ShipmentCreate, db: Session = Depends(get_db @router.delete("/{shipment_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_shipment(shipment_id: int, db: Session = Depends(get_db)): + # Fetch the shipment shipment = db.query(ShipmentModel).filter(ShipmentModel.id == shipment_id).first() if not shipment: raise HTTPException(status_code=404, detail="Shipment not found") + + # Check associated dewars + dewars = db.query(DewarModel).filter(DewarModel.shipment_id == shipment_id).all() + if dewars: + # Ensure dewars, pucks, and samples have no associated events + for dewar in dewars: + if ( + db.query(LogisticsEvent) + .filter(LogisticsEvent.dewar_id == dewar.id) + .first() + ): + raise HTTPException( + status_code=400, + detail=f"Dewar {dewar.id} has associated logistics events." + f"Shipment cannot be deleted.", + ) + + for puck in dewar.pucks: + if db.query(PuckEvent).filter(PuckEvent.puck_id == puck.id).first(): + raise HTTPException( + status_code=400, + detail=f"Puck {puck.id}" + f" in Dewar {dewar.id}" + f" has associated puck events." + f"Shipment cannot be deleted.", + ) + + for sample in puck.samples: + if ( + db.query(SampleEvent) + .filter(SampleEvent.sample_id == sample.id) + .first() + ): + raise HTTPException( + status_code=400, + detail=f"Sample {sample.id}" + f" in Puck {puck.id}" + f" has associated sample events." + f"Shipment cannot be deleted.", + ) + + # If no events are found, proceed to delete the shipment + for dewar in dewars: + for puck in dewar.pucks: + for sample in puck.samples: + db.delete(sample) # Delete associated samples + db.delete(puck) # Delete associated pucks + db.delete(dewar) # Delete the dewar itself + + # Finally, delete the shipment db.delete(shipment) db.commit() return @@ -228,19 +282,63 @@ async def add_dewar_to_shipment( async def remove_dewar_from_shipment( shipment_id: int, dewar_id: int, db: Session = Depends(get_db) ): + # Fetch the shipment shipment = db.query(ShipmentModel).filter(ShipmentModel.id == shipment_id).first() if not shipment: raise HTTPException(status_code=404, detail="Shipment not found") - dewar_exists = any(dw.id == dewar_id for dw in shipment.dewars) - if not dewar_exists: + # Check if the dewar belongs to the shipment + dewar = ( + db.query(DewarModel) + .filter(DewarModel.id == dewar_id, DewarModel.shipment_id == shipment_id) + .first() + ) + if not dewar: raise HTTPException( - status_code=404, detail=f"Dewar with ID {dewar_id} not found in shipment" + status_code=404, + detail=f"Dewar with ID {dewar_id} not found in shipment {shipment_id}", ) + # Check for logistics events associated with the dewar + logistics_event_exists = ( + db.query(LogisticsEvent).filter(LogisticsEvent.dewar_id == dewar_id).first() + ) + if logistics_event_exists: + raise HTTPException( + status_code=400, + detail=f"Dewar {dewar_id} has " f" logistics events. Removal not allowed.", + ) + + # Check associated pucks and their events + for puck in dewar.pucks: + puck_event_exists = ( + db.query(PuckEvent).filter(PuckEvent.puck_id == puck.id).first() + ) + if puck_event_exists: + raise HTTPException( + status_code=400, + detail=f"Puck {puck.id} in Dewar {dewar_id}" + f" has associated events. Removal not allowed.", + ) + + # Check associated samples and their events + for sample in puck.samples: + sample_event_exists = ( + db.query(SampleEvent).filter(SampleEvent.sample_id == sample.id).first() + ) + if sample_event_exists: + raise HTTPException( + status_code=400, + detail=f"Sample {sample.id} " + f"in Puck {puck.id} " + f"has associated events. Removal not allowed.", + ) + + # Unlink the dewar from the shipment shipment.dewars = [dw for dw in shipment.dewars if dw.id != dewar_id] db.commit() db.refresh(shipment) + return shipment diff --git a/frontend/src/components/ShipmentDetails.tsx b/frontend/src/components/ShipmentDetails.tsx index 57cd41d..759ad17 100644 --- a/frontend/src/components/ShipmentDetails.tsx +++ b/frontend/src/components/ShipmentDetails.tsx @@ -78,13 +78,27 @@ const ShipmentDetails: React.FC = ({ const confirmed = window.confirm('Are you sure you want to delete this dewar?'); if (confirmed && selectedShipment) { try { - const updatedShipment = await ShipmentsService.removeDewarFromShipmentShipmentsShipmentIdRemoveDewarDewarIdDelete(selectedShipment.id, dewarId); + const updatedShipment = await ShipmentsService.removeDewarFromShipmentShipmentsShipmentIdRemoveDewarDewarIdDelete( + selectedShipment.id, + dewarId + ); setSelectedShipment(updatedShipment); setLocalSelectedDewar(null); refreshShipments(); - } catch (error) { - console.error('Failed to delete dewar:', error); - alert('Failed to delete dewar. Please try again.'); + alert('Dewar deleted successfully!'); + } catch (error: any) { + console.error('Full error object:', error); + + let errorMessage = 'Failed to delete dewar. Please try again.'; + + if (error instanceof ApiError && error.body) { + console.error('API error body:', error.body); + errorMessage = error.body.detail || errorMessage; + } else if (error.message) { + errorMessage = error.message; + } + + alert(`Failed to delete dewar: ${errorMessage}`); } } }; diff --git a/frontend/src/components/ShipmentPanel.tsx b/frontend/src/components/ShipmentPanel.tsx index c319ff6..fd133c6 100644 --- a/frontend/src/components/ShipmentPanel.tsx +++ b/frontend/src/components/ShipmentPanel.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { Button, Box, Typography, IconButton } from '@mui/material'; import { Add as AddIcon, Delete as DeleteIcon, UploadFile as UploadFileIcon, Refresh as RefreshIcon } from '@mui/icons-material'; import UploadDialog from './UploadDialog'; -import { Dewar, Shipment, ShipmentsService } from '../../openapi'; +import {ApiError, Dewar, Shipment, ShipmentsService} from '../../openapi'; import { SxProps } from '@mui/material'; import bottleGrey from '/src/assets/icons/bottle-svgrepo-com-grey.svg'; import bottleYellow from '/src/assets/icons/bottle-svgrepo-com-yellow.svg'; @@ -39,21 +39,34 @@ const ShipmentPanel: React.FC = ({ const handleDeleteShipment = async () => { if (selectedShipment) { - const confirmed = window.confirm(`Are you sure you want to delete the shipment: ${selectedShipment.shipment_name}?`); - if (confirmed) { - await deleteShipment(selectedShipment.id); - } + const confirmDelete = window.confirm( + `Are you sure you want to delete the shipment: ${selectedShipment.shipment_name}?` + ); + + if (!confirmDelete) return; + + // Try to delete the shipment + await deleteShipment(selectedShipment.id); } }; const deleteShipment = async (shipmentId: number) => { if (!shipmentId) return; + try { await ShipmentsService.deleteShipmentShipmentsShipmentIdDelete(shipmentId); refreshShipments(); selectShipment(null); - } catch (error) { - console.error('Failed to delete shipment:', error); + alert("Shipment deleted successfully."); + } catch (error: any) { + console.error("Failed to delete shipment:", error); + + let errorMessage = "Failed to delete shipment."; + if (error instanceof ApiError && error.body) { + errorMessage = error.body.detail || errorMessage; + } + + alert(errorMessage); } };