From a01114a17872354bf9637147419d4f7d99f93c55 Mon Sep 17 00:00:00 2001 From: GotthardG <51994228+GotthardG@users.noreply.github.com> Date: Sat, 2 Nov 2024 00:54:37 +0100 Subject: [PATCH] Better integration of sqlite3 database --- backend/app/crud.py | 20 ++ backend/app/data/data.py | 97 +++++---- backend/app/database.py | 27 ++- backend/app/dependencies.py | 9 + backend/app/init_db.py | 7 + backend/app/main.py | 22 +- backend/app/models.py | 119 +++++----- backend/app/routers/address.py | 42 ++-- backend/app/routers/contact.py | 35 +-- backend/app/routers/dewar.py | 45 ++-- backend/app/routers/proposal.py | 18 +- backend/app/routers/shipment.py | 189 ++++++++++------ backend/app/schemas.py | 116 +++++++--- frontend/src/components/DewarDetails.tsx | 154 +++++++------ frontend/src/components/ShipmentDetails.tsx | 153 +++++++------ frontend/src/components/ShipmentForm.tsx | 227 ++++++++------------ frontend/src/components/ShipmentPanel.tsx | 108 +++++----- frontend/src/pages/ShipmentView.tsx | 29 ++- 18 files changed, 835 insertions(+), 582 deletions(-) create mode 100644 backend/app/crud.py create mode 100644 backend/app/dependencies.py create mode 100644 backend/app/init_db.py diff --git a/backend/app/crud.py b/backend/app/crud.py new file mode 100644 index 0000000..bad83cb --- /dev/null +++ b/backend/app/crud.py @@ -0,0 +1,20 @@ +from sqlalchemy.orm import Session, joinedload + +def get_shipments(db: Session): + from app.models import Shipment + return db.query(Shipment).options( + joinedload(Shipment.contact_person), + joinedload(Shipment.return_address), + joinedload(Shipment.proposal), + joinedload(Shipment.dewars) + ).all() + +def get_shipment_by_id(db: Session, shipment_id: str): + from app.models import Shipment + shipment = db.query(Shipment).options( + joinedload(Shipment.contact_person), + joinedload(Shipment.return_address), + joinedload(Shipment.proposal), + joinedload(Shipment.dewars) + ).filter(Shipment.shipment_id == shipment_id).first() + return shipment \ No newline at end of file diff --git a/backend/app/data/data.py b/backend/app/data/data.py index 2788947..5a7ab65 100644 --- a/backend/app/data/data.py +++ b/backend/app/data/data.py @@ -1,62 +1,75 @@ -from typing import List from app.models import ContactPerson, Address, Dewar, Proposal, Shipment +from datetime import datetime -contacts: List[ContactPerson] = [ - ContactPerson(id=1, firstname="Frodo", lastname="Baggins", phone_number="123-456-7890", email="frodo.baggins@lotr.com"), - ContactPerson(id=1, firstname="Frodo", lastname="Baggins", phone_number="123-456-7890", email="frodo.baggins@lotr.com"), - ContactPerson(id=2, firstname="Samwise", lastname="Gamgee", phone_number="987-654-3210", email="samwise.gamgee@lotr.com"), - ContactPerson(id=3, firstname="Aragorn", lastname="Elessar", phone_number="123-333-4444", email="aragorn.elessar@lotr.com"), - ContactPerson(id=4, firstname="Legolas", lastname="Greenleaf", phone_number="555-666-7777", email="legolas.greenleaf@lotr.com"), - ContactPerson(id=5, firstname="Gimli", lastname="Son of Gloin", phone_number="888-999-0000", email="gimli.sonofgloin@lotr.com"), - ContactPerson(id=6, firstname="Gandalf", lastname="The Grey", phone_number="222-333-4444", email="gandalf.thegrey@lotr.com"), - ContactPerson(id=7, firstname="Boromir", lastname="Son of Denethor", phone_number="111-222-3333", email="boromir.sonofdenethor@lotr.com"), - ContactPerson(id=8, firstname="Galadriel", lastname="Lady of Lothlórien", phone_number="444-555-6666", email="galadriel.lothlorien@lotr.com"), - ContactPerson(id=9, firstname="Elrond", lastname="Half-elven", phone_number="777-888-9999", email="elrond.halfelven@lotr.com"), - ContactPerson(id=10, firstname="Eowyn", lastname="Shieldmaiden of Rohan", phone_number="000-111-2222", email="eowyn.rohan@lotr.com"), +contacts = [ + ContactPerson(id=1, firstname="Frodo", lastname="Baggins", phone_number="123-456-7890", + email="frodo.baggins@lotr.com"), + ContactPerson(id=2, firstname="Samwise", lastname="Gamgee", phone_number="987-654-3210", + email="samwise.gamgee@lotr.com"), + ContactPerson(id=3, firstname="Aragorn", lastname="Elessar", phone_number="123-333-4444", + email="aragorn.elessar@lotr.com"), + ContactPerson(id=4, firstname="Legolas", lastname="Greenleaf", phone_number="555-666-7777", + email="legolas.greenleaf@lotr.com"), + ContactPerson(id=5, firstname="Gimli", lastname="Son of Gloin", phone_number="888-999-0000", + email="gimli.sonofgloin@lotr.com"), + ContactPerson(id=6, firstname="Gandalf", lastname="The Grey", phone_number="222-333-4444", + email="gandalf.thegrey@lotr.com"), + ContactPerson(id=7, firstname="Boromir", lastname="Son of Denethor", phone_number="111-222-3333", + email="boromir.sonofdenethor@lotr.com"), + ContactPerson(id=8, firstname="Galadriel", lastname="Lady of Lothlórien", phone_number="444-555-6666", + email="galadriel.lothlorien@lotr.com"), + ContactPerson(id=9, firstname="Elrond", lastname="Half-elven", phone_number="777-888-9999", + email="elrond.halfelven@lotr.com"), + ContactPerson(id=10, firstname="Eowyn", lastname="Shieldmaiden of Rohan", phone_number="000-111-2222", + email="eowyn.rohan@lotr.com"), ] -return_addresses: List[Address] = [ +return_addresses = [ Address(id=1, street='123 Hobbiton St', city='Shire', zipcode='12345', country='Middle Earth'), Address(id=2, street='456 Rohan Rd', city='Edoras', zipcode='67890', country='Middle Earth'), Address(id=3, street='789 Greenwood Dr', city='Mirkwood', zipcode='13579', country='Middle Earth'), Address(id=4, street='321 Gondor Ave', city='Minas Tirith', zipcode='24680', country='Middle Earth'), - Address(id=5, street='654 Falgorn Pass', city='Rivendell', zipcode='11223', country='Middle Earth') + Address(id=5, street='654 Falgorn Pass', city='Rivendell', zipcode='11223', country='Middle Earth'), ] -dewars: List[Dewar] = [ +dewars = [ Dewar( id='DEWAR001', dewar_name='Dewar One', tracking_number='TRACK123', number_of_pucks=7, number_of_samples=70, - return_address=[return_addresses[0]], contact_person=[contacts[0]], status='Ready for Shipping', - ready_date='2023-09-30', shipping_date='', arrival_date='', returning_date='', qrcode='QR123DEWAR001' + return_address_id=1, contact_person_id=1, status='Ready for Shipping', + ready_date=datetime.strptime('2023-09-30', '%Y-%m-%d'), shipping_date=None, arrival_date=None, + returning_date=None, qrcode='QR123DEWAR001', ), Dewar( id='DEWAR002', dewar_name='Dewar Two', tracking_number='TRACK124', number_of_pucks=3, number_of_samples=33, - return_address=[return_addresses[1]], contact_person=[contacts[1]], status='In Preparation', - ready_date='', shipping_date='', arrival_date='', returning_date='', qrcode='QR123DEWAR002' + return_address_id=2, contact_person_id=2, status='In Preparation', + ready_date=None, shipping_date=None, arrival_date=None, returning_date=None, qrcode='QR123DEWAR002', ), Dewar( id='DEWAR003', dewar_name='Dewar Three', tracking_number='TRACK125', number_of_pucks=7, number_of_samples=72, - return_address=[return_addresses[0]], contact_person=[contacts[2]], status='Not Shipped', ready_date='2024.01.01', - shipping_date='', arrival_date='', returning_date='', qrcode='QR123DEWAR003' + return_address_id=1, contact_person_id=3, status='Not Shipped', + ready_date=datetime.strptime('2024-01-01', '%Y-%m-%d'), shipping_date=None, arrival_date=None, + returning_date=None, qrcode='QR123DEWAR003', ), Dewar( id='DEWAR004', dewar_name='Dewar Four', tracking_number='', number_of_pucks=7, number_of_samples=70, - return_address=[return_addresses[0]], contact_person=[contacts[2]], status='Delayed', ready_date='2024.01.01', - shipping_date='2024.01.02', arrival_date='', returning_date='', qrcode='QR123DEWAR003' + return_address_id=1, contact_person_id=3, status='Delayed', + ready_date=datetime.strptime('2024-01-01', '%Y-%m-%d'), shipping_date=datetime.strptime('2024-01-02', '%Y-%m-%d'), + arrival_date=None, returning_date=None, qrcode='QR123DEWAR004', ), Dewar( id='DEWAR005', dewar_name='Dewar Five', tracking_number='', number_of_pucks=3, number_of_samples=30, - return_address=[return_addresses[0]], contact_person=[contacts[2]], status='Returned', ready_date='2024.01.01', - shipping_date='2024.01.02', arrival_date='2024.01.03', returning_date='2024.01.07', qrcode='QR123DEWAR003' + return_address_id=1, contact_person_id=3, status='Returned', + arrival_date=datetime.strptime('2024-01-03', '%Y-%m-%d'), returning_date=datetime.strptime('2024-01-07', '%Y-%m-%d'), + qrcode='QR123DEWAR005', ), ] -proposals: List[Proposal] = [ - Proposal(id=1, number="PROPOSAL-FRODO-001"), # "The Quest for the Ring" - Proposal(id=2, number="PROPOSAL-GANDALF-002"), # "The Fellowship's Journey" - Proposal(id=3, number="PROPOSAL-ARAGORN-003"), # "Return of the King" - Proposal(id=4, number="PROPOSAL-SAURON-004"), # "The Dark Lord's Plot" - Proposal(id=5, number="PROPOSAL-MORDOR-005"), # "The Road to Mount Doom" +proposals = [ + Proposal(id=1, number="PROPOSAL-FRODO-001"), + Proposal(id=2, number="PROPOSAL-GANDALF-002"), + Proposal(id=3, number="PROPOSAL-ARAGORN-003"), + Proposal(id=4, number="PROPOSAL-SAURON-004"), + Proposal(id=5, number="PROPOSAL-MORDOR-005"), ] specific_dewar_ids1 = ['DEWAR003'] @@ -69,18 +82,18 @@ specific_dewars3 = [dewar for dewar in dewars if dewar.id in specific_dewar_ids3 shipments = [ Shipment( - shipment_id='SHIPMORDOR', shipment_date='2024-10-10', shipment_name='Shipment from Mordor', - shipment_status='Delivered', contact_person=[contacts[1]], proposal_number=[proposals[1]], - return_address=[return_addresses[0]], comments='Handle with care', dewars=specific_dewars1 + shipment_id="SHIPMENT001", shipment_date=datetime.strptime('2024-10-10', '%Y-%m-%d'), + shipment_name='Shipment from Mordor', shipment_status='Delivered', contact_person_id=2, + proposal_id=3, return_address_id=1, comments='Handle with care', dewars=specific_dewars1 ), Shipment( - shipment_id='SHIPMORDOR2', shipment_date='2024-10-24', shipment_name='Shipment from Mordor', - shipment_status='In Transit', contact_person=[contacts[3]], proposal_number=[proposals[2]], - return_address=[return_addresses[1]], comments='Contains the one ring', dewars=specific_dewars2 + shipment_id="SHIPMENT002", shipment_date=datetime.strptime('2024-10-24', '%Y-%m-%d'), + shipment_name='Shipment from Mordor', shipment_status='In Transit', contact_person_id=4, + proposal_id=4, return_address_id=2, comments='Contains the one ring', dewars=specific_dewars2 ), Shipment( - shipment_id='SHIPMORDOR3', shipment_date='2024-10-28', shipment_name='Shipment from Mordor', - shipment_status='In Transit', contact_person=[contacts[4]], proposal_number=[proposals[3]], - return_address=[return_addresses[0]], comments='Contains the one ring', dewars=specific_dewars3 - ) + shipment_id="SHIPMENT003", shipment_date=datetime.strptime('2024-10-28', '%Y-%m-%d'), + shipment_name='Shipment from Mordor', shipment_status='In Transit', contact_person_id=5, + proposal_id=5, return_address_id=1, comments='Contains the one ring', dewars=specific_dewars3 + ), ] \ No newline at end of file diff --git a/backend/app/database.py b/backend/app/database.py index 06a7aec..99e3560 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -1,15 +1,16 @@ +from sqlalchemy.orm import Session from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -DATABASE_URL = "sqlite:///./test.db" +SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" -# Database setup -engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) +engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() + # Dependency def get_db(): db = SessionLocal() @@ -18,7 +19,21 @@ def get_db(): finally: db.close() -# Initialize the database + def init_db(): - import app.models # Import all models here for metadata.create_all() to recognize them - Base.metadata.create_all(bind=engine) \ No newline at end of file + # Import inside function to avoid circular dependency + from app import models + Base.metadata.create_all(bind=engine) + + +def load_sample_data(session: Session): + # Import inside function to avoid circular dependency + from app.data import contacts, return_addresses, dewars, proposals, shipments + + from app import models # Ensure these imports are correct + + if session.query(models.ContactPerson).first(): + return + + session.add_all(contacts + return_addresses + dewars + proposals + shipments) + session.commit() \ No newline at end of file diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py new file mode 100644 index 0000000..5dfd7a8 --- /dev/null +++ b/backend/app/dependencies.py @@ -0,0 +1,9 @@ +# app/dependencies.py +from app.database import SessionLocal # Import SessionLocal from database.py + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/backend/app/init_db.py b/backend/app/init_db.py new file mode 100644 index 0000000..7beb8a2 --- /dev/null +++ b/backend/app/init_db.py @@ -0,0 +1,7 @@ +from app.database import init_db + +def initialize_database(): + init_db() + +if __name__ == "__main__": + initialize_database() \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index b058b30..45495fa 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,21 +1,37 @@ +# app/main.py + from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware + from app.routers import address, contact, proposal, dewar, shipment +from app.database import Base, engine, SessionLocal, load_sample_data app = FastAPI() # Apply CORS middleware app.add_middleware( CORSMiddleware, - allow_origins=["*"], # Enable CORS for all origins for now + allow_origins=["*"], # Enable CORS for all origins allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) -# Include your routers +@app.on_event("startup") +def on_startup(): + # Drop and recreate database schema + Base.metadata.drop_all(bind=engine) + Base.metadata.create_all(bind=engine) + + db = SessionLocal() + try: + load_sample_data(db) + finally: + db.close() + +# Include routers with correct configuration app.include_router(contact.router, prefix="/contacts", tags=["contacts"]) -app.include_router(address.router, prefix="/return_addresses", tags=["return_addresses"]) +app.include_router(address.router, prefix="/addresses", tags=["addresses"]) app.include_router(proposal.router, prefix="/proposals", tags=["proposals"]) app.include_router(dewar.router, prefix="/dewars", tags=["dewars"]) app.include_router(shipment.router, prefix="/shipments", tags=["shipments"]) diff --git a/backend/app/models.py b/backend/app/models.py index 5b13c1f..a4e314f 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -1,61 +1,78 @@ -from pydantic import BaseModel -from typing import List, Optional +from sqlalchemy import Column, Integer, String, Date, ForeignKey +from sqlalchemy.orm import relationship +from app.database import Base # Ensure this imports correctly -class ContactPerson(BaseModel): - id: Optional[int] = None - firstname: str - lastname: str - phone_number: str - email: str -class Address(BaseModel): - id: Optional[int] = None - street: str - city: str - zipcode: str - country: str +# SQLAlchemy ORM models +class Shipment(Base): + __tablename__ = "shipments" -class Proposal(BaseModel): - id: Optional[int] = None - number: str + shipment_id = Column(String, primary_key=True, index=True) + shipment_name = Column(String, index=True) + shipment_date = Column(Date) + shipment_status = Column(String) + comments = Column(String, nullable=True) + contact_person_id = Column(Integer, ForeignKey("contact_persons.id")) + return_address_id = Column(Integer, ForeignKey("addresses.id")) + proposal_id = Column(Integer, ForeignKey("proposals.id")) -class Dewar(BaseModel): - id: Optional[str] = None - dewar_name: str - tracking_number: Optional[str] = None - number_of_pucks: int - number_of_samples: int - return_address: List[Address] - contact_person: List[ContactPerson] - status: str - ready_date: Optional[str] = None - shipping_date: Optional[str] = None - arrival_date: Optional[str] = None - returning_date: Optional[str] = None - qrcode: str + contact_person = relationship("ContactPerson", back_populates="shipments") + return_address = relationship("Address", back_populates="shipments") + proposal = relationship("Proposal", back_populates="shipments") + dewars = relationship("Dewar", back_populates="shipment") -class Shipment(BaseModel): - shipment_id: Optional[str] = None - shipment_name: str - shipment_date: str - shipment_status: str - contact_person: List[ContactPerson] - proposal_number: List[Proposal] - return_address: List[Address] - comments: Optional[str] = None - dewars: List[Dewar] - def get_number_of_dewars(self) -> int: - return len(self.dewars) +class ContactPerson(Base): + __tablename__ = "contact_persons" - def get_shipment_contact_persons(self) -> List[ContactPerson]: - return self.contact_person + id = Column(Integer, primary_key=True, index=True) + firstname = Column(String) + lastname = Column(String) + phone_number = Column(String) + email = Column(String) - def get_shipment_return_addresses(self) -> List[Address]: - return self.return_address + shipments = relationship("Shipment", back_populates="contact_person") - def get_proposals(self) -> List[Proposal]: - return self.proposal_number - class Config: - from_attributes = True \ No newline at end of file +class Address(Base): + __tablename__ = "addresses" + + id = Column(Integer, primary_key=True, index=True) + street = Column(String) + city = Column(String) + zipcode = Column(String) + country = Column(String) + + shipments = relationship("Shipment", back_populates="return_address") + + +class Dewar(Base): + __tablename__ = "dewars" + + id = Column(String, primary_key=True, index=True) + dewar_name = Column(String) + tracking_number = Column(String) + number_of_pucks = Column(Integer) + number_of_samples = Column(Integer) + status = Column(String) + ready_date = Column(Date, nullable=True) + shipping_date = Column(Date, nullable=True) + arrival_date = Column(Date, nullable=True) + returning_date = Column(Date, nullable=True) + qrcode = Column(String) + shipment_id = Column(String, ForeignKey("shipments.shipment_id")) + return_address_id = Column(Integer, ForeignKey("addresses.id")) # Added + 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 + + +class Proposal(Base): + __tablename__ = "proposals" + + id = Column(Integer, primary_key=True, index=True) + number = Column(String) + + shipments = relationship("Shipment", back_populates="proposal") diff --git a/backend/app/routers/address.py b/backend/app/routers/address.py index 7c36513..c765d0c 100644 --- a/backend/app/routers/address.py +++ b/backend/app/routers/address.py @@ -1,26 +1,32 @@ -from fastapi import APIRouter, HTTPException, status -from typing import List, Optional -from app.data.data import return_addresses -from app.models import Address # Import the Address model - +from fastapi import APIRouter, HTTPException, status, Depends +from sqlalchemy.orm import Session +from typing import List +from app.schemas import Address as AddressSchema, AddressCreate +from app.models import Address as AddressModel +from app.dependencies import get_db router = APIRouter() -@router.get("/", response_model=List[Address]) -async def get_return_addresses(): - return return_addresses +@router.get("/", response_model=List[AddressSchema]) +async def get_return_addresses(db: Session = Depends(get_db)): + return db.query(AddressModel).all() -@router.post("/", response_model=Address, status_code=status.HTTP_201_CREATED) -async def create_return_address(address: Address): - if any(a.city == address.city for a in return_addresses): +@router.post("/", response_model=AddressSchema, status_code=status.HTTP_201_CREATED) +async def create_return_address(address: AddressCreate, db: Session = Depends(get_db)): + if db.query(AddressModel).filter(AddressModel.city == address.city).first(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Address in this city already exists." ) - if return_addresses: - max_id = max(a.id for a in return_addresses) - address.id = max_id + 1 if address.id is None else address.id - else: - address.id = 1 if address.id is None else address.id - return_addresses.append(address) - return address \ No newline at end of file + + db_address = AddressModel( + street=address.street, + city=address.city, + zipcode=address.zipcode, + country=address.country + ) + + db.add(db_address) + db.commit() + db.refresh(db_address) + return db_address \ No newline at end of file diff --git a/backend/app/routers/contact.py b/backend/app/routers/contact.py index 14e72d6..1296c99 100644 --- a/backend/app/routers/contact.py +++ b/backend/app/routers/contact.py @@ -1,26 +1,31 @@ -# app/routers/contact.py -from fastapi import APIRouter, HTTPException, status +from fastapi import APIRouter, HTTPException, status, Depends +from sqlalchemy.orm import Session from typing import List -from app.data.data import contacts -from app.models import ContactPerson +from app.schemas import ContactPerson, ContactPersonCreate +from app.models import ContactPerson as ContactPersonModel +from app.dependencies import get_db router = APIRouter() @router.get("/", response_model=List[ContactPerson]) -async def get_contacts(): - return contacts +async def get_contacts(db: Session = Depends(get_db)): + return db.query(ContactPersonModel).all() @router.post("/", response_model=ContactPerson, status_code=status.HTTP_201_CREATED) -async def create_contact(contact: ContactPerson): - if any(c.email == contact.email for c in contacts): +async def create_contact(contact: ContactPersonCreate, db: Session = Depends(get_db)): + if db.query(ContactPersonModel).filter(ContactPersonModel.email == contact.email).first(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="This contact already exists." ) - if contacts: - max_id = max(c.id for c in contacts) - contact.id = max_id + 1 if contact.id is None else contact.id - else: - contact.id = 1 if contact.id is None else contact.id - contacts.append(contact) - return contact \ No newline at end of file + + db_contact = ContactPersonModel( + firstname=contact.firstname, + lastname=contact.lastname, + phone_number=contact.phone_number, + email=contact.email + ) + db.add(db_contact) + db.commit() + db.refresh(db_contact) + return db_contact \ No newline at end of file diff --git a/backend/app/routers/dewar.py b/backend/app/routers/dewar.py index 9dac9aa..bda6c5c 100644 --- a/backend/app/routers/dewar.py +++ b/backend/app/routers/dewar.py @@ -1,19 +1,38 @@ -from fastapi import APIRouter, HTTPException, status -from typing import List, Optional +from fastapi import APIRouter, HTTPException, status, Depends +from sqlalchemy.orm import Session +from typing import List import uuid -from app.data.data import dewars, contacts, return_addresses -from app.models import Dewar, ContactPerson, Address - +from app.schemas import Dewar as DewarSchema, DewarCreate +from app.models import Dewar as DewarModel +from app.dependencies import get_db router = APIRouter() -@router.get("/", response_model=List[Dewar]) -async def get_dewars(): - return dewars +@router.get("/", response_model=List[DewarSchema]) +async def get_dewars(db: Session = Depends(get_db)): + return db.query(DewarModel).all() -@router.post("/", response_model=Dewar, status_code=status.HTTP_201_CREATED) -async def create_dewar(dewar: Dewar) -> Dewar: +@router.post("/", response_model=DewarSchema, status_code=status.HTTP_201_CREATED) +async def create_dewar(dewar: DewarCreate, db: Session = Depends(get_db)) -> DewarSchema: dewar_id = f'DEWAR-{uuid.uuid4().hex[:8].upper()}' - dewar.id = dewar_id - dewars.append(dewar) - return dewar \ No newline at end of file + + db_dewar = DewarModel( + id=dewar_id, + dewar_name=dewar.dewar_name, + tracking_number=dewar.tracking_number, + number_of_pucks=dewar.number_of_pucks, + number_of_samples=dewar.number_of_samples, + status=dewar.status, + ready_date=dewar.ready_date, + shipping_date=dewar.shipping_date, + arrival_date=dewar.arrival_date, + returning_date=dewar.returning_date, + qrcode=dewar.qrcode, + contact_person_id=dewar.contact_person_id, + return_address_id=dewar.return_address_id + ) + + db.add(db_dewar) + db.commit() + db.refresh(db_dewar) + return db_dewar \ No newline at end of file diff --git a/backend/app/routers/proposal.py b/backend/app/routers/proposal.py index 49a3239..9d42224 100644 --- a/backend/app/routers/proposal.py +++ b/backend/app/routers/proposal.py @@ -1,11 +1,13 @@ -from fastapi import APIRouter -from typing import List, Optional -from app.data.data import proposals -from app.models import Proposal # Import the Address model - +# app/routers/proposal.py +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from typing import List +from app.schemas import Proposal as ProposalSchema +from app.models import Proposal as ProposalModel +from app.dependencies import get_db router = APIRouter() -@router.get("/", response_model=List[Proposal]) -async def get_proposals(): - return proposals \ No newline at end of file +@router.get("/", response_model=List[ProposalSchema]) +async def get_proposals(db: Session = Depends(get_db)): + return db.query(ProposalModel).all() \ No newline at end of file diff --git a/backend/app/routers/shipment.py b/backend/app/routers/shipment.py index d517398..81b19f4 100644 --- a/backend/app/routers/shipment.py +++ b/backend/app/routers/shipment.py @@ -1,85 +1,154 @@ -from fastapi import APIRouter, HTTPException, status, Query +from fastapi import APIRouter, HTTPException, status, Query, Depends +from sqlalchemy.orm import Session from typing import List, Optional import uuid -from app.data.data import shipments, dewars -from app.models import Shipment, Dewar, ContactPerson, Address, Proposal + +from app.schemas import ShipmentCreate, Shipment as ShipmentSchema, ContactPerson as ContactPersonSchema +from app.database import get_db +from app.crud import get_shipments, get_shipment_by_id router = APIRouter() -@router.get("/", response_model=List[Shipment]) -async def get_shipments(shipment_id: Optional[str] = Query(None, description="ID of the specific shipment to retrieve")): + +@router.get("", response_model=List[ShipmentSchema]) +async def fetch_shipments(shipment_id: Optional[str] = Query(None), + db: Session = Depends(get_db)): if shipment_id: - shipment = next((sh for sh in shipments if sh.shipment_id == shipment_id), None) + shipment = get_shipment_by_id(db, shipment_id) if not shipment: raise HTTPException(status_code=404, detail="Shipment not found") return [shipment] - return shipments + 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): + raise HTTPException(status_code=404, detail="Associated entity not found") + + shipment_id = f'SHIP-{uuid.uuid4().hex[:8].upper()}' + db_shipment = ShipmentModel( + shipment_id=shipment_id, + shipment_name=shipment.shipment_name, + shipment_date=shipment.shipment_date, + shipment_status=shipment.shipment_status, + comments=shipment.comments, + contact_person_id=contact_person.id, + return_address_id=return_address.id, + proposal_id=proposal.id, + ) + + # Handling dewars association + if shipment.dewars: + dewars = db.query(DewarModel).filter(DewarModel.id.in_(shipment.dewars)).all() + if len(dewars) != len(shipment.dewars): + raise HTTPException(status_code=404, detail="One or more dewars not found") + db_shipment.dewars.extend(dewars) + + db.add(db_shipment) + db.commit() + db.refresh(db_shipment) + + return db_shipment + @router.delete("/{shipment_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_shipment(shipment_id: str): - global shipments - shipments = [shipment for shipment in shipments if shipment.shipment_id != shipment_id] - -@router.post("/{shipment_id}/add_dewar", response_model=Shipment) -async def add_dewar_to_shipment(shipment_id: str, dewar_id: str): - shipment = next((sh for sh in shipments if sh.shipment_id == shipment_id), None) +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") - dewar = next((dw for dw in dewars if dw.id == dewar_id), None) + db.delete(shipment) + db.commit() + return + + +@router.put("/shipments/{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 + + # Log incoming payload for detailed inspection + print("Received payload:", json.dumps(updated_shipment.dict(), indent=2)) + + 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") + + # 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") + + # 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 + + 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") + + +@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") + dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first() if not dewar: raise HTTPException(status_code=404, detail="Dewar not found") + if dewar not in shipment.dewars: shipment.dewars.append(dewar) + db.commit() + db.refresh(shipment) return shipment -@router.put("/{shipment_id}", response_model=Shipment) -async def update_shipment(shipment_id: str, updated_shipment: Shipment): - global shipments - shipment = next((sh for sh in shipments if sh.shipment_id == shipment_id), None) + +@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") - shipment.shipment_name = updated_shipment.shipment_name - shipment.shipment_date = updated_shipment.shipment_date - shipment.shipment_status = updated_shipment.shipment_status - shipment.contact_person = updated_shipment.contact_person - shipment.proposal_number = updated_shipment.proposal_number - shipment.return_address = updated_shipment.return_address - shipment.comments = updated_shipment.comments - existing_dewar_dict = {dewar.id: dewar for dewar in shipment.dewars} - for updated_dewar in updated_shipment.dewars: - if updated_dewar.id in existing_dewar_dict: - existing_dewar_dict[updated_dewar.id].dewar_name = updated_dewar.dewar_name - existing_dewar_dict[updated_dewar.id].tracking_number = updated_dewar.tracking_number - existing_dewar_dict[updated_dewar.id].number_of_pucks = updated_dewar.number_of_pucks - existing_dewar_dict[updated_dewar.id].number_of_samples = updated_dewar.number_of_samples - existing_dewar_dict[updated_dewar.id].return_address = updated_dewar.return_address - existing_dewar_dict[updated_dewar.id].contact_person = updated_dewar.contact_person - existing_dewar_dict[updated_dewar.id].status = updated_dewar.status - existing_dewar_dict[updated_dewar.id].ready_date = updated_dewar.ready_date - existing_dewar_dict[updated_dewar.id].shipping_date = updated_dewar.shipping_date - existing_dewar_dict[updated_dewar.id].arrival_date = updated_dewar.arrival_date - existing_dewar_dict[updated_dewar.id].returning_date = updated_dewar.returning_date - existing_dewar_dict[updated_dewar.id].qrcode = updated_dewar.qrcode - else: - shipment.dewars.append(updated_dewar) - return shipment - -@router.post("/", response_model=Shipment, status_code=status.HTTP_201_CREATED) -async def create_shipment(shipment: Shipment): - shipment_id = f'SHIP-{uuid.uuid4().hex[:8].upper()}' - shipment.shipment_id = shipment_id - shipments.append(shipment) - return shipment - -@router.delete("/{shipment_id}/remove_dewar/{dewar_id}", response_model=Shipment) -async def remove_dewar_from_shipment(shipment_id: str, dewar_id: str): - shipment = next((sh for sh in shipments if sh.shipment_id == shipment_id), None) - if not shipment: - raise HTTPException(status_code=404, detail="Shipment not found") shipment.dewars = [dw for dw in shipment.dewars if dw.id != dewar_id] + db.commit() + db.refresh(shipment) return shipment -@router.get("/contact_persons", response_model=List[ContactPerson]) -async def get_shipment_contact_persons(): - return [{"shipment_id": shipment.shipment_id, "contact_person": shipment.get_shipment_contact_persons()} for shipment in shipments] \ No newline at end of file + +@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 2047e8a..86c5097 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -1,59 +1,117 @@ -from pydantic import BaseModel from typing import List, Optional +from pydantic import BaseModel, EmailStr +from datetime import date -class ContactPersonSchema(BaseModel): - id: Optional[int] + +# Base class for Contact Person +class ContactPersonBase(BaseModel): firstname: str lastname: str phone_number: str - email: str + email: EmailStr + + +# Create schema for Contact Person +class ContactPersonCreate(ContactPersonBase): + pass + + +# Response schema for Contact Person with ID +class ContactPerson(ContactPersonBase): + id: int class Config: - from_attributes = True # Update here + from_attributes = True -class AddressSchema(BaseModel): - id: Optional[int] + +# Create schema for Address +class AddressCreate(BaseModel): street: str city: str zipcode: str country: str - class Config: - from_attributes = True # Update here -class DewarSchema(BaseModel): - id: Optional[str] +# Response schema for Address with ID +class Address(AddressCreate): + id: int + + class Config: + from_attributes = True + + +# Create schema for Dewar +class DewarCreate(BaseModel): dewar_name: str - tracking_number: Optional[str] + tracking_number: str number_of_pucks: int number_of_samples: int status: str - ready_date: Optional[str] - shipping_date: Optional[str] - arrival_date: Optional[str] - returning_date: Optional[str] + ready_date: Optional[date] + shipping_date: Optional[date] + arrival_date: Optional[date] + returning_date: Optional[date] qrcode: str + contact_person_id: Optional[int] + return_address_id: Optional[int] + + +# Response schema for Dewar +class Dewar(BaseModel): + id: str + dewar_name: str + tracking_number: str + number_of_pucks: int + number_of_samples: int + status: str + ready_date: Optional[date] + shipping_date: Optional[date] + arrival_date: Optional[date] + returning_date: Optional[date] + qrcode: str + shipment_id: Optional[str] + contact_person: Optional[ContactPerson] + return_address: Optional[Address] class Config: - from_attributes = True # Update here + from_attributes = True -class ProposalSchema(BaseModel): - id: Optional[int] + +# Proposal schema +class Proposal(BaseModel): + id: int number: str class Config: - from_attributes = True # Update here + from_attributes = True -class ShipmentSchema(BaseModel): - shipment_id: Optional[str] + +# Response schema for Shipment +class Shipment(BaseModel): + shipment_id: str shipment_name: str - shipment_date: str + shipment_date: date shipment_status: str - contact_person: List[ContactPersonSchema] - proposal_number: List[ProposalSchema] - return_address: List[AddressSchema] - comments: Optional[str] = None - dewars: List[DewarSchema] + comments: Optional[str] + contact_person: Optional[ContactPerson] + return_address: Optional[Address] + proposal: Optional[Proposal] + dewars: Optional[List[Dewar]] = [] class Config: - from_attributes = True # Update here \ No newline at end of file + from_attributes = True + + +# Create schema for Shipment +class ShipmentCreate(BaseModel): + shipment_name: str + shipment_date: date + shipment_status: str + 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]] = [] + + class Config: + from_attributes = True diff --git a/frontend/src/components/DewarDetails.tsx b/frontend/src/components/DewarDetails.tsx index 89795fc..eef9861 100644 --- a/frontend/src/components/DewarDetails.tsx +++ b/frontend/src/components/DewarDetails.tsx @@ -12,8 +12,7 @@ import QRCode from 'react-qr-code'; import { ContactPerson, Address, - Dewar, - DefaultService + Dewar, ContactsService, AddressesService, ShipmentsService, } from '../../openapi'; interface DewarDetailsProps { @@ -26,18 +25,20 @@ interface DewarDetailsProps { defaultReturnAddress?: Address; shipmentId: string; refreshShipments: () => void; + selectedShipment: any; } const DewarDetails: React.FC = ({ dewar, trackingNumber, - //setTrackingNumber, + setTrackingNumber, initialContactPersons = [], initialReturnAddresses = [], defaultContactPerson, defaultReturnAddress, shipmentId, refreshShipments, + selectedShipment }) => { const [localTrackingNumber, setLocalTrackingNumber] = useState(trackingNumber); const [contactPersons, setContactPersons] = useState(initialContactPersons); @@ -47,12 +48,14 @@ const DewarDetails: React.FC = ({ const [isCreatingContactPerson, setIsCreatingContactPerson] = useState(false); const [isCreatingReturnAddress, setIsCreatingReturnAddress] = useState(false); const [newContactPerson, setNewContactPerson] = useState({ + id: 0, firstName: '', lastName: '', phone_number: '', email: '', }); const [newReturnAddress, setNewReturnAddress] = useState
({ + id: 0, street: '', city: '', zipcode: '', @@ -61,26 +64,34 @@ const DewarDetails: React.FC = ({ const [changesMade, setChangesMade] = useState(false); const [feedbackMessage, setFeedbackMessage] = useState(''); const [openSnackbar, setOpenSnackbar] = useState(false); - const [updatedDewar, setUpdatedDewar] = useState(dewar); useEffect(() => { - setSelectedContactPerson( - (dewar.contact_person?.[0]?.id?.toString() || defaultContactPerson?.id?.toString() || '') - ); - setSelectedReturnAddress( - (dewar.return_address?.[0]?.id?.toString() || defaultReturnAddress?.id?.toString() || '') - ); + const setInitialContactPerson = () => { + const contactPersonId = + selectedShipment?.contact_person?.id?.toString() || + dewar.contact_person?.id?.toString() || + defaultContactPerson?.id?.toString() || + ''; + setSelectedContactPerson(contactPersonId); + }; + + const setInitialReturnAddress = () => { + const returnAddressId = + dewar.return_address?.id?.toString() || + defaultReturnAddress?.id?.toString() || + ''; + setSelectedReturnAddress(returnAddressId); + }; + setLocalTrackingNumber(dewar.tracking_number || ''); - }, [dewar, defaultContactPerson, defaultReturnAddress]); - - useEffect(() => { - console.log('DewarDetails - dewar updated:', dewar); - }, [dewar]); + setInitialContactPerson(); + setInitialReturnAddress(); + }, [dewar, defaultContactPerson, defaultReturnAddress, selectedShipment]); useEffect(() => { const getContacts = async () => { try { - const c: ContactPerson[] = await DefaultService.getContactsContactsGet(); + const c: ContactPerson[] = await ContactsService.getContactsContactsGet(); setContactPersons(c); } catch { setFeedbackMessage('Failed to load contact persons. Please try again later.'); @@ -90,7 +101,7 @@ const DewarDetails: React.FC = ({ const getReturnAddresses = async () => { try { - const a: Address[] = await DefaultService.getReturnAddressesReturnAddressesGet(); + const a: Address[] = await AddressesService.getReturnAddressesAddressesGet(); setReturnAddresses(a); } catch { setFeedbackMessage('Failed to load return addresses. Please try again later.'); @@ -111,6 +122,7 @@ const DewarDetails: React.FC = ({ } const handleAddContact = async () => { + console.log('handleAddContact called'); if (!validateEmail(newContactPerson.email) || !validatePhoneNumber(newContactPerson.phone_number) || !newContactPerson.firstName || !newContactPerson.lastName) { setFeedbackMessage('Please fill in all new contact person fields correctly.'); @@ -126,10 +138,10 @@ const DewarDetails: React.FC = ({ }; try { - const c: ContactPerson = await DefaultService.createContactContactsPost(payload); + const c: ContactPerson = await ContactsService.createContactContactsPost(payload); setContactPersons([...contactPersons, c]); setFeedbackMessage('Contact person added successfully.'); - setNewContactPerson({ firstName: '', lastName: '', phone_number: '', email: '' }); + setNewContactPerson({ id: 0, firstName: '', lastName: '', phone_number: '', email: '' }); setSelectedContactPerson(c.id?.toString() || ''); } catch { setFeedbackMessage('Failed to create a new contact person. Please try again later.'); @@ -141,6 +153,7 @@ const DewarDetails: React.FC = ({ }; const handleAddAddress = async () => { + console.log('handleAddAddress called'); if (!validateZipCode(newReturnAddress.zipcode) || !newReturnAddress.street || !newReturnAddress.city || !newReturnAddress.country) { setFeedbackMessage('Please fill in all new return address fields correctly.'); @@ -156,10 +169,10 @@ const DewarDetails: React.FC = ({ }; try { - const a: Address = await DefaultService.createReturnAddressReturnAddressesPost(payload); + const a: Address = await AddressesService.createReturnAddressAddressesPost(payload); setReturnAddresses([...returnAddresses, a]); setFeedbackMessage('Return address added successfully.'); - setNewReturnAddress({ street: '', city: '', zipcode: '', country: '' }); + setNewReturnAddress({ id: 0, street: '', city: '', zipcode: '', country: '' }); setSelectedReturnAddress(a.id?.toString() || ''); } catch { setFeedbackMessage('Failed to create a new return address. Please try again later.'); @@ -171,10 +184,11 @@ const DewarDetails: React.FC = ({ }; const getShipmentById = async (shipmentId: string) => { + console.log(`Fetching shipment with ID: ${shipmentId}`); try { - const response = await DefaultService.getShipmentsShipmentsGet(shipmentId); + const response = await ShipmentsService.fetchShipmentsShipmentsGet(shipmentId); if (response && response.length > 0) { - return response[0]; // Since the result is an array, we take the first element + return response[0]; } throw new Error('Shipment not found'); } catch (error) { @@ -184,22 +198,32 @@ const DewarDetails: React.FC = ({ }; const handleSaveChanges = async () => { + console.log('handleSaveChanges called'); + const formatDate = (dateString: string | undefined): string => { - if (!dateString) return '2024-01-01'; // Default date if undefined + if (!dateString) return ''; const date = new Date(dateString); - if (isNaN(date.getTime())) return '2024-01-01'; // Default date if invalid + if (isNaN(date.getTime())) return ''; return date.toISOString().split('T')[0]; }; - if (!dewar.dewar_name || !selectedContactPerson || !selectedReturnAddress || !trackingNumber) { + console.log('Selected Contact Person:', selectedContactPerson); + console.log('Selected Return Address:', selectedReturnAddress); + + // Check if required fields are filled + if (!selectedContactPerson || !selectedReturnAddress) { setFeedbackMessage('Please ensure all required fields are filled.'); setOpenSnackbar(true); return; } + console.log('Saving changes...'); + console.log('Current Dewar:', dewar); + let existingShipment; try { existingShipment = await getShipmentById(shipmentId); + console.log('Existing Shipment:', existingShipment); } catch { setFeedbackMessage('Failed to fetch existing shipment data. Please try again later.'); setOpenSnackbar(true); @@ -207,56 +231,58 @@ const DewarDetails: React.FC = ({ } const updatedDewar = { - id: dewar.id, // Ensure dewar ID is included + id: dewar.id, dewar_name: dewar.dewar_name, - return_address: returnAddresses.find((a) => a.id?.toString() === selectedReturnAddress) - ? [returnAddresses.find((a) => a.id?.toString() === selectedReturnAddress)] - : [], - contact_person: contactPersons.find((c) => c.id?.toString() === selectedContactPerson) - ? [contactPersons.find((c) => c.id?.toString() === selectedContactPerson)] - : [], + tracking_number: dewar.tracking_number, number_of_pucks: dewar.number_of_pucks, number_of_samples: dewar.number_of_samples, - qrcode: dewar.qrcode, - ready_date: formatDate(dewar.ready_date), - shipping_date: formatDate(dewar.shipping_date), status: dewar.status, - tracking_number: trackingNumber, + ready_date: formatDate(dewar.ready_date ?? undefined), + shipping_date: formatDate(dewar.shipping_date ?? undefined), + arrival_date: dewar.arrival_date, + returning_date: dewar.returning_date, + qrcode: dewar.qrcode, + return_address_id: selectedReturnAddress, + contact_person_id: selectedContactPerson, }; const payload = { - ...existingShipment, - dewars: existingShipment.dewars?.map(d => d.id === dewar.id ? updatedDewar : d) || [], // Update specific dewar in the dewars array + shipment_id: existingShipment.shipment_id, + shipment_name: existingShipment.shipment_name, + shipment_date: existingShipment.shipment_date, + shipment_status: existingShipment.shipment_status, + comments: existingShipment.comments, + contact_person_id: selectedContactPerson, + return_address_id: selectedReturnAddress, + proposal_id: existingShipment.proposal?.id, + dewars: [ + updatedDewar + ], }; + console.log('Payload for update:', JSON.stringify(payload, null, 2)); + try { - await DefaultService.updateShipmentShipmentsShipmentIdPut(shipmentId, payload); + await ShipmentsService.updateShipmentShipmentsShipmentsShipmentIdPut(shipmentId, payload); setFeedbackMessage('Changes saved successfully.'); setChangesMade(false); - setUpdatedDewar(updatedDewar); - console.log('Calling refreshShipments'); - refreshShipments(); // Trigger refresh shipments after saving changes + refreshShipments(); } catch (error) { - if (error.response) { - console.error('Server Response:', error.response.data); - } else { - console.error('Update Shipment Error:', error); - setFeedbackMessage('Failed to save changes. Please try again later.'); - } - setOpenSnackbar(true); - return; + console.error('Update Shipment Error:', error); + setFeedbackMessage('Failed to save changes. Please try again later.'); } setOpenSnackbar(true); }; return ( - Selected Dewar: {updatedDewar.dewar_name} + Selected Dewar: {dewar.dewar_name} { setLocalTrackingNumber(e.target.value); + setTrackingNumber(e.target.value); // Ensure parent state is updated if applicable setChangesMade(true); }} variant="outlined" @@ -264,8 +290,8 @@ const DewarDetails: React.FC = ({ /> - {updatedDewar.qrcode ? ( - + {dewar.qrcode ? ( + ) : ( No QR code available )} @@ -274,14 +300,16 @@ const DewarDetails: React.FC = ({ Generate QR Code - Number of Pucks: {updatedDewar.number_of_pucks} - Number of Samples: {updatedDewar.number_of_samples} + Number of Pucks: {dewar.number_of_pucks} + Number of Samples: {dewar.number_of_samples} Current Contact Person: { - setSelectedReturnAddress(e.target.value); - setIsCreatingReturnAddress(e.target.value === 'add'); + const value = e.target.value; + console.log('Return Address Selected:', value); + setSelectedReturnAddress(value); + setIsCreatingReturnAddress(value === 'add'); setChangesMade(true); }} fullWidth @@ -352,7 +382,7 @@ const DewarDetails: React.FC = ({ variant="outlined" displayEmpty > - {returnAddresses?.map((address) => ( + {Array.isArray(returnAddresses) && returnAddresses.map((address) => ( {address.street}, {address.city} diff --git a/frontend/src/components/ShipmentDetails.tsx b/frontend/src/components/ShipmentDetails.tsx index a768fbc..07c20dd 100644 --- a/frontend/src/components/ShipmentDetails.tsx +++ b/frontend/src/components/ShipmentDetails.tsx @@ -2,35 +2,50 @@ import React from 'react'; import { Box, Typography, Button, Stack, TextField } from '@mui/material'; import QRCode from 'react-qr-code'; import DeleteIcon from "@mui/icons-material/Delete"; -import { Dewar, Shipment_Input, DefaultService } from "../../openapi"; +import { Dewar, DewarsService, ShipmentsService, ContactPerson, ApiError } from "../../openapi"; // Ensure ApiError is imported here import { SxProps } from "@mui/system"; import CustomStepper from "./DewarStepper"; import DewarDetails from './DewarDetails'; interface ShipmentDetailsProps { isCreatingShipment: boolean; - selectedShipment: Shipment_Input; + sx?: SxProps; + selectedShipment: ShipmentsService | null; selectedDewar: Dewar | null; setSelectedDewar: React.Dispatch>; - setSelectedShipment: React.Dispatch>; - sx?: SxProps; + setSelectedShipment: React.Dispatch>; refreshShipments: () => void; + defaultContactPerson?: ContactPerson; } const ShipmentDetails: React.FC = ({ + isCreatingShipment, + sx, selectedShipment, + selectedDewar, setSelectedDewar, setSelectedShipment, - sx = {}, refreshShipments, + defaultContactPerson }) => { const [localSelectedDewar, setLocalSelectedDewar] = React.useState(null); const [isAddingDewar, setIsAddingDewar] = React.useState(false); - const [newDewar, setNewDewar] = React.useState>({ - dewar_name: '', - }); - // To reset localSelectedDewar when selectedShipment changes + const initialNewDewarState: Partial = { + dewar_name: '', + tracking_number: '', + number_of_pucks: 0, + number_of_samples: 0, + status: 'In preparation', + ready_date: null, + shipping_date: null, + arrival_date: null, + returning_date: null, + qrcode: 'N/A' + }; + + const [newDewar, setNewDewar] = React.useState>(initialNewDewarState); + React.useEffect(() => { setLocalSelectedDewar(null); }, [selectedShipment]); @@ -39,8 +54,8 @@ const ShipmentDetails: React.FC = ({ console.log('ShipmentDetails - selectedShipment updated:', selectedShipment); }, [selectedShipment]); - const totalPucks = selectedShipment.dewars.reduce((acc, dewar) => acc + (dewar.number_of_pucks || 0), 0); - const totalSamples = selectedShipment.dewars.reduce((acc, dewar) => acc + (dewar.number_of_samples || 0), 0); + const totalPucks = selectedShipment?.dewars?.reduce((acc, dewar) => acc + (dewar.number_of_pucks || 0), 0) || 0; + const totalSamples = selectedShipment?.dewars?.reduce((acc, dewar) => acc + (dewar.number_of_samples || 0), 0) || 0; const handleDewarSelection = (dewar: Dewar) => { const newSelection = localSelectedDewar?.id === dewar.id ? null : dewar; @@ -50,18 +65,12 @@ const ShipmentDetails: React.FC = ({ const handleDeleteDewar = async (dewarId: string) => { const confirmed = window.confirm('Are you sure you want to delete this dewar?'); - if (confirmed) { + if (confirmed && selectedShipment) { try { - console.log('Selected Shipment ID:', selectedShipment.shipment_id); - console.log('Dewar ID to be deleted:', dewarId); - - const updatedShipment = await DefaultService.removeDewarFromShipmentShipmentsShipmentIdRemoveDewarDewarIdDelete( - selectedShipment.shipment_id, dewarId - ); - - // Ensure state is updated with server response + const updatedShipment = await ShipmentsService.removeDewarFromShipmentShipmentsShipmentIdRemoveDewarDewarIdDelete(selectedShipment.shipment_id, dewarId); setSelectedShipment(updatedShipment); setLocalSelectedDewar(null); + refreshShipments(); } catch (error) { console.error('Failed to delete dewar:', error); alert('Failed to delete dewar. Please try again.'); @@ -78,55 +87,42 @@ const ShipmentDetails: React.FC = ({ }; const handleAddDewar = async () => { - if (selectedShipment && newDewar.dewar_name) { + if (newDewar.dewar_name?.trim()) { try { const newDewarToPost: Dewar = { - ...newDewar as Dewar, - dewar_name: newDewar.dewar_name.trim() || 'Unnamed Dewar', - number_of_pucks: newDewar.number_of_pucks ?? 0, - number_of_samples: newDewar.number_of_samples ?? 0, - return_address: selectedShipment.return_address, - contact_person: selectedShipment.contact_person, - status: 'In preparation', - qrcode: newDewar.qrcode || 'N/A', - }; + ...initialNewDewarState, + ...newDewar, + dewar_name: newDewar.dewar_name.trim(), + contact_person: selectedShipment?.contact_person, + contact_person_id: selectedShipment?.contact_person?.id, // Adding contact_person_id + return_address: selectedShipment?.return_address, + return_address_id: selectedShipment?.return_address?.id, // Adding return_address_id + } as Dewar; - // Create a new dewar - const createdDewar = await DefaultService.createDewarDewarsPost(newDewarToPost); - - console.log('Created Dewar:', createdDewar); - - // Check IDs before calling backend - console.log('Adding dewar to shipment:', { - shipment_id: selectedShipment.shipment_id, - dewar_id: createdDewar.id, - }); - - // Make an API call to associate the dewar with the shipment - const updatedShipment = await DefaultService.addDewarToShipmentShipmentsShipmentIdAddDewarPost( - selectedShipment.shipment_id, - createdDewar.id - ); - - if (updatedShipment) { + const createdDewar = await DewarsService.createDewarDewarsPost(newDewarToPost); + if (createdDewar && selectedShipment) { + const updatedShipment = await ShipmentsService.addDewarToShipmentShipmentsShipmentIdAddDewarPost(selectedShipment.shipment_id, createdDewar.id); setSelectedShipment(updatedShipment); - } else { - throw new Error('Failed to update shipment with new dewar'); + setIsAddingDewar(false); + setNewDewar(initialNewDewarState); + refreshShipments(); } - - setIsAddingDewar(false); - setNewDewar({ dewar_name: '', tracking_number: '' }); - refreshShipments() - } catch (error) { - alert('Failed to add dewar or update shipment. Please try again.'); console.error('Error adding dewar or updating shipment:', error); + if (error instanceof ApiError && error.body) { + console.error('Validation errors:', error.body.detail); // Log specific validation errors + } else { + console.error('Unexpected error:', error); + } + alert('Failed to add dewar or update shipment. Please check the data and try again.'); } } else { alert('Please fill in the Dewar Name'); } }; + const contactPerson = selectedShipment?.contact_person; + return ( {!localSelectedDewar && !isAddingDewar && ( @@ -159,11 +155,19 @@ const ShipmentDetails: React.FC = ({ )} - {selectedShipment.shipment_name} - Main contact person: {`${selectedShipment.contact_person[0].firstname} ${selectedShipment.contact_person[0].lastname}`} - Number of Pucks: {totalPucks} - Number of Samples: {totalSamples} - Shipment Date: {selectedShipment.shipment_date} + {selectedShipment ? ( + <> + {selectedShipment.shipment_name} + + Main contact person: {contactPerson ? `${contactPerson.firstname} ${contactPerson.lastname}` : 'N/A'} + + Number of Pucks: {totalPucks} + Number of Samples: {totalSamples} + Shipment Date: {selectedShipment.shipment_date} + + ) : ( + No shipment selected + )} {localSelectedDewar && !isAddingDewar && ( = ({ setTrackingNumber={(value) => { setLocalSelectedDewar((prev) => (prev ? { ...prev, tracking_number: value as string } : prev)); }} - initialContactPersons={selectedShipment.contact_person} - initialReturnAddresses={selectedShipment.return_address} - defaultContactPerson={selectedShipment.contact_person[0]} - defaultReturnAddress={selectedShipment.return_address[0]} - shipmentId={selectedShipment.shipment_id} + initialContactPersons={selectedShipment?.contact_person ? [selectedShipment.contact_person] : []} + initialReturnAddresses={selectedShipment?.return_address ? [selectedShipment.return_address] : []} + defaultContactPerson={contactPerson} + defaultReturnAddress={selectedShipment?.return_address} + shipmentId={selectedShipment?.shipment_id || ''} refreshShipments={refreshShipments} /> )} - {selectedShipment.dewars.map((dewar) => ( + {selectedShipment?.dewars?.map((dewar) => (