diff --git a/backend/app/models.py b/backend/app/models.py index a4e314f..dde01cf 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -1,9 +1,7 @@ from sqlalchemy import Column, Integer, String, Date, ForeignKey from sqlalchemy.orm import relationship -from app.database import Base # Ensure this imports correctly +from app.database import Base - -# SQLAlchemy ORM models class Shipment(Base): __tablename__ = "shipments" @@ -21,7 +19,6 @@ class Shipment(Base): proposal = relationship("Proposal", back_populates="shipments") dewars = relationship("Dewar", back_populates="shipment") - class ContactPerson(Base): __tablename__ = "contact_persons" @@ -33,7 +30,6 @@ class ContactPerson(Base): shipments = relationship("Shipment", back_populates="contact_person") - class Address(Base): __tablename__ = "addresses" @@ -45,7 +41,6 @@ class Address(Base): shipments = relationship("Shipment", back_populates="return_address") - class Dewar(Base): __tablename__ = "dewars" @@ -65,9 +60,8 @@ class Dewar(Base): contact_person_id = Column(Integer, ForeignKey("contact_persons.id")) # Added shipment = relationship("Shipment", back_populates="dewars") - return_address = relationship("Address") # Defines relationship with Address - contact_person = relationship("ContactPerson") # Defines relationship with ContactPerson - + return_address = relationship("Address") + contact_person = relationship("ContactPerson") class Proposal(Base): __tablename__ = "proposals" @@ -75,4 +69,4 @@ class Proposal(Base): id = Column(Integer, primary_key=True, index=True) number = Column(String) - shipments = relationship("Shipment", back_populates="proposal") + shipments = relationship("Shipment", back_populates="proposal") \ No newline at end of file diff --git a/backend/app/routers/shipment.py b/backend/app/routers/shipment.py index 81b19f4..b266048 100644 --- a/backend/app/routers/shipment.py +++ b/backend/app/routers/shipment.py @@ -2,17 +2,23 @@ from fastapi import APIRouter, HTTPException, status, Query, Depends from sqlalchemy.orm import Session from typing import List, Optional import uuid +import json +from datetime import date -from app.schemas import ShipmentCreate, Shipment as ShipmentSchema, ContactPerson as ContactPersonSchema +from app.models import Shipment as ShipmentModel, ContactPerson as ContactPersonModel, Address as AddressModel, Proposal as ProposalModel, Dewar as DewarModel +from app.schemas import ShipmentCreate, Shipment as ShipmentSchema, DewarUpdate, ContactPerson as ContactPersonSchema from app.database import get_db from app.crud import get_shipments, get_shipment_by_id router = APIRouter() +def default_serializer(obj): + if isinstance(obj, date): + return obj.isoformat() + raise TypeError(f"Type {type(obj)} not serializable") @router.get("", response_model=List[ShipmentSchema]) -async def fetch_shipments(shipment_id: Optional[str] = Query(None), - db: Session = Depends(get_db)): +async def fetch_shipments(shipment_id: Optional[str] = Query(None), db: Session = Depends(get_db)): if shipment_id: shipment = get_shipment_by_id(db, shipment_id) if not shipment: @@ -20,17 +26,13 @@ async def fetch_shipments(shipment_id: Optional[str] = Query(None), return [shipment] return get_shipments(db) - @router.post("", response_model=ShipmentSchema, status_code=status.HTTP_201_CREATED) async def create_shipment(shipment: ShipmentCreate, db: Session = Depends(get_db)): - from app.models import Shipment as ShipmentModel, ContactPerson as ContactPersonModel, Address as AddressModel, \ - Proposal as ProposalModel, Dewar as DewarModel - contact_person = db.query(ContactPersonModel).filter(ContactPersonModel.id == shipment.contact_person_id).first() return_address = db.query(AddressModel).filter(AddressModel.id == shipment.return_address_id).first() proposal = db.query(ProposalModel).filter(ProposalModel.id == shipment.proposal_id).first() - if not (contact_person and return_address and proposal): + if not (contact_person or return_address or proposal): raise HTTPException(status_code=404, detail="Associated entity not found") shipment_id = f'SHIP-{uuid.uuid4().hex[:8].upper()}' @@ -47,7 +49,8 @@ async def create_shipment(shipment: ShipmentCreate, db: Session = Depends(get_db # Handling dewars association if shipment.dewars: - dewars = db.query(DewarModel).filter(DewarModel.id.in_(shipment.dewars)).all() + dewar_ids = [dewar.dewar_id for dewar in shipment.dewars] + dewars = db.query(DewarModel).filter(DewarModel.id.in_(dewar_ids)).all() if len(dewars) != len(shipment.dewars): raise HTTPException(status_code=404, detail="One or more dewars not found") db_shipment.dewars.extend(dewars) @@ -58,10 +61,8 @@ async def create_shipment(shipment: ShipmentCreate, db: Session = Depends(get_db return db_shipment - @router.delete("/{shipment_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_shipment(shipment_id: str, db: Session = Depends(get_db)): - from app.models import Shipment as ShipmentModel shipment = db.query(ShipmentModel).filter(ShipmentModel.shipment_id == shipment_id).first() if not shipment: raise HTTPException(status_code=404, detail="Shipment not found") @@ -69,57 +70,58 @@ async def delete_shipment(shipment_id: str, db: Session = Depends(get_db)): db.commit() return - -@router.put("/shipments/{shipment_id}", response_model=ShipmentSchema) +@router.put("/{shipment_id}", response_model=ShipmentSchema) async def update_shipment(shipment_id: str, updated_shipment: ShipmentCreate, db: Session = Depends(get_db)): - from app.models import Shipment as ShipmentModel, ContactPerson as ContactPersonModel, Address as AddressModel, \ - Dewar as DewarModel + print("Received payload:", json.dumps(updated_shipment.dict(), indent=2, default=default_serializer)) - # Log incoming payload for detailed inspection - print("Received payload:", json.dumps(updated_shipment.dict(), indent=2)) + shipment = db.query(ShipmentModel).filter(ShipmentModel.shipment_id == shipment_id).first() + if not shipment: + raise HTTPException(status_code=404, detail="Shipment not found") - try: - shipment = db.query(ShipmentModel).filter(ShipmentModel.shipment_id == shipment_id).first() - if not shipment: - raise HTTPException(status_code=404, detail="Shipment not found") + # Validate relationships by IDs + contact_person = db.query(ContactPersonModel).filter(ContactPersonModel.id == updated_shipment.contact_person_id).first() + return_address = db.query(AddressModel).filter(AddressModel.id == updated_shipment.return_address_id).first() + if not contact_person: + raise HTTPException(status_code=404, detail="Contact person not found") + if not return_address: + raise HTTPException(status_code=404, detail="Return address not found") - # Validate relationships by IDs - contact_person = db.query(ContactPersonModel).filter( - ContactPersonModel.id == updated_shipment.contact_person_id).first() - return_address = db.query(AddressModel).filter(AddressModel.id == updated_shipment.return_address_id).first() - if not contact_person: - raise HTTPException(status_code=404, detail="Contact person not found") - if not return_address: - raise HTTPException(status_code=404, detail="Return address not found") + # Update shipment details + shipment.shipment_name = updated_shipment.shipment_name + shipment.shipment_date = updated_shipment.shipment_date + shipment.shipment_status = updated_shipment.shipment_status + shipment.comments = updated_shipment.comments + shipment.contact_person_id = updated_shipment.contact_person_id + shipment.return_address_id = updated_shipment.return_address_id - # Handling dewars association by IDs - dewars_ids = [d['id'] for d in updated_shipment.dewars] - dewars = db.query(DewarModel).filter(DewarModel.id.in_(dewars_ids)).all() - if len(dewars) != len(dewars_ids): - raise HTTPException(status_code=422, detail="One or more dewars not found") + # Process and update dewars' details + for dewar_data in updated_shipment.dewars: + dewar = db.query(DewarModel).filter(DewarModel.id == dewar_data.dewar_id).first() + if not dewar: + raise HTTPException(status_code=404, detail=f"Dewar with ID {dewar_data.dewar_id} not found") - # Update shipment details - shipment.shipment_name = updated_shipment.shipment_name - shipment.shipment_date = updated_shipment.shipment_date - shipment.shipment_status = updated_shipment.shipment_status - shipment.comments = updated_shipment.comments - shipment.contact_person_id = updated_shipment.contact_person_id - shipment.return_address_id = updated_shipment.return_address_id - shipment.dewars = dewars + # Dynamically update the dewar fields based on provided input + update_fields = dewar_data.dict(exclude_unset=True) + for key, value in update_fields.items(): + if key == 'contact_person_id': + contact_person = db.query(ContactPersonModel).filter(ContactPersonModel.id == value).first() + if not contact_person: + raise HTTPException(status_code=404, detail=f"Contact person with ID {value} for Dewar {dewar_data.dewar_id} not found") + if key == 'return_address_id': + address = db.query(AddressModel).filter(AddressModel.id == value).first() + if not address: + raise HTTPException(status_code=404, detail=f"Address with ID {value} for Dewar {dewar_data.dewar_id} not found") - db.commit() - db.refresh(shipment) - - return shipment - - except Exception as e: - print(f"Update failed with exception: {e}") - raise HTTPException(status_code=500, detail="Internal server error") + for key, value in update_fields.items(): + if key != 'dewar_id': + setattr(dewar, key, value) + db.commit() + db.refresh(shipment) + return shipment @router.post("/{shipment_id}/add_dewar", response_model=ShipmentSchema) async def add_dewar_to_shipment(shipment_id: str, dewar_id: str, db: Session = Depends(get_db)): - from app.models import Shipment as ShipmentModel, Dewar as DewarModel shipment = db.query(ShipmentModel).filter(ShipmentModel.shipment_id == shipment_id).first() if not shipment: raise HTTPException(status_code=404, detail="Shipment not found") @@ -133,10 +135,8 @@ async def add_dewar_to_shipment(shipment_id: str, dewar_id: str, db: Session = D db.refresh(shipment) return shipment - @router.delete("/{shipment_id}/remove_dewar/{dewar_id}", response_model=ShipmentSchema) async def remove_dewar_from_shipment(shipment_id: str, dewar_id: str, db: Session = Depends(get_db)): - from app.models import Shipment as ShipmentModel, Dewar as DewarModel shipment = db.query(ShipmentModel).filter(ShipmentModel.shipment_id == shipment_id).first() if not shipment: raise HTTPException(status_code=404, detail="Shipment not found") @@ -146,9 +146,7 @@ async def remove_dewar_from_shipment(shipment_id: str, dewar_id: str, db: Sessio db.refresh(shipment) return shipment - @router.get("/contact_persons", response_model=List[ContactPersonSchema]) async def get_shipment_contact_persons(db: Session = Depends(get_db)): - from app.models import ContactPerson as ContactPersonModel contact_persons = db.query(ContactPersonModel).all() return contact_persons \ No newline at end of file diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 86c5097..1d42c39 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -101,8 +101,21 @@ class Shipment(BaseModel): class Config: from_attributes = True +class DewarUpdate(BaseModel): + dewar_id: str + dewar_name: Optional[str] = None + tracking_number: Optional[str] = None + number_of_pucks: Optional[int] = None + number_of_samples: Optional[int] = None + status: Optional[str] = None + ready_date: Optional[date] = None + shipping_date: Optional[date] = None + arrival_date: Optional[date] = None + returning_date: Optional[date] = None + qrcode: Optional[str] = None + contact_person_id: Optional[int] = None + address_id: Optional[int] = None # Added -# Create schema for Shipment class ShipmentCreate(BaseModel): shipment_name: str shipment_date: date @@ -110,8 +123,8 @@ class ShipmentCreate(BaseModel): comments: Optional[str] = "" contact_person_id: int return_address_id: int - proposal_id: int # Change "proposal_number_id" to "proposal_id" - dewars: Optional[List[str]] = [] + proposal_id: int + dewars: Optional[List[DewarUpdate]] = [] class Config: from_attributes = True diff --git a/frontend/src/components/DewarDetails.tsx b/frontend/src/components/DewarDetails.tsx index eef9861..6d6a9dc 100644 --- a/frontend/src/components/DewarDetails.tsx +++ b/frontend/src/components/DewarDetails.tsx @@ -12,7 +12,7 @@ import QRCode from 'react-qr-code'; import { ContactPerson, Address, - Dewar, ContactsService, AddressesService, ShipmentsService, + Dewar, ContactsService, AddressesService, ShipmentsService, DewarsService, } from '../../openapi'; interface DewarDetailsProps { @@ -200,10 +200,10 @@ const DewarDetails: React.FC = ({ const handleSaveChanges = async () => { console.log('handleSaveChanges called'); - const formatDate = (dateString: string | undefined): string => { - if (!dateString) return ''; + const formatDate = (dateString: string | undefined): string | null => { + if (!dateString) return null; const date = new Date(dateString); - if (isNaN(date.getTime())) return ''; + if (isNaN(date.getTime())) return null; return date.toISOString().split('T')[0]; }; @@ -231,7 +231,7 @@ const DewarDetails: React.FC = ({ } const updatedDewar = { - id: dewar.id, + dewar_id: dewar.id, dewar_name: dewar.dewar_name, tracking_number: dewar.tracking_number, number_of_pucks: dewar.number_of_pucks, @@ -243,7 +243,7 @@ const DewarDetails: React.FC = ({ returning_date: dewar.returning_date, qrcode: dewar.qrcode, return_address_id: selectedReturnAddress, - contact_person_id: selectedContactPerson, + contact_person_id: selectedContactPerson, // Set dewar-specific contact person }; const payload = { @@ -252,26 +252,29 @@ const DewarDetails: React.FC = ({ shipment_date: existingShipment.shipment_date, shipment_status: existingShipment.shipment_status, comments: existingShipment.comments, - contact_person_id: selectedContactPerson, + contact_person_id: existingShipment.contact_person.id, // Keep main shipment contact person return_address_id: selectedReturnAddress, proposal_id: existingShipment.proposal?.id, - dewars: [ - updatedDewar - ], + dewars: [updatedDewar], // Updated dewars array }; console.log('Payload for update:', JSON.stringify(payload, null, 2)); try { - await ShipmentsService.updateShipmentShipmentsShipmentsShipmentIdPut(shipmentId, payload); + await ShipmentsService.updateShipmentShipmentsShipmentIdPut(shipmentId, payload); setFeedbackMessage('Changes saved successfully.'); setChangesMade(false); refreshShipments(); - } catch (error) { + } catch (error: any) { console.error('Update Shipment Error:', error); - setFeedbackMessage('Failed to save changes. Please try again later.'); + + if (error.response && error.response.data) { + setFeedbackMessage(`Failed to save shipment. Validation errors: ${JSON.stringify(error.response.data)}`); + } else { + setFeedbackMessage('Failed to save changes. Please try again later.'); + } + setOpenSnackbar(true); } - setOpenSnackbar(true); }; return (