diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/calculations.py b/backend/app/calculations.py new file mode 100644 index 0000000..dbe2cf6 --- /dev/null +++ b/backend/app/calculations.py @@ -0,0 +1,8 @@ +def calculate_number_of_pucks(dewar): + return len(dewar.pucks) if dewar.pucks else 0 + + +def calculate_number_of_samples(dewar): + if not dewar.pucks: + return 0 + return sum(len(puck.positions) for puck in dewar.pucks) \ No newline at end of file diff --git a/backend/app/data/__init__.py b/backend/app/data/__init__.py new file mode 100644 index 0000000..ec98eb9 --- /dev/null +++ b/backend/app/data/__init__.py @@ -0,0 +1 @@ +from .data import contacts, return_addresses, dewars, proposals, shipments, pucks, samples diff --git a/backend/app/data/data.py b/backend/app/data/data.py index 5a7ab65..de3942c 100644 --- a/backend/app/data/data.py +++ b/backend/app/data/data.py @@ -1,5 +1,6 @@ -from app.models import ContactPerson, Address, Dewar, Proposal, Shipment +from app.models import ContactPerson, Address, Dewar, Proposal, Shipment, Puck, Sample from datetime import datetime +import random contacts = [ ContactPerson(id=1, firstname="Frodo", lastname="Baggins", phone_number="123-456-7890", @@ -34,32 +35,34 @@ return_addresses = [ dewars = [ Dewar( - id='DEWAR001', dewar_name='Dewar One', tracking_number='TRACK123', number_of_pucks=7, number_of_samples=70, + id='DEWAR001', dewar_name='Dewar One', tracking_number='TRACK123', 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, + id='DEWAR002', dewar_name='Dewar Two', tracking_number='TRACK124', 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, + id='DEWAR003', dewar_name='Dewar Three', tracking_number='TRACK125', 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, + id='DEWAR004', dewar_name='Dewar Four', tracking_number='', 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'), + 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, + id='DEWAR005', dewar_name='Dewar Five', tracking_number='', 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'), + arrival_date=datetime.strptime('2024-01-03', '%Y-%m-%d'), + returning_date=datetime.strptime('2024-01-07', '%Y-%m-%d'), qrcode='QR123DEWAR005', ), ] @@ -96,4 +99,51 @@ shipments = [ 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 +] + +pucks = [ + Puck(id=1, puck_name="PUCK001", puck_type="Unipuck", puck_location_in_dewar=1, positions=[], dewar_id='DEWAR001'), + Puck(id=2, puck_name="PUCK002", puck_type="Unipuck", puck_location_in_dewar=2, positions=[], dewar_id='DEWAR001'), + Puck(id=3, puck_name="PUCK003", puck_type="Unipuck", puck_location_in_dewar=3, positions=[], dewar_id='DEWAR001'), + Puck(id=4, puck_name="PUCK004", puck_type="Unipuck", puck_location_in_dewar=4, positions=[], dewar_id='DEWAR001'), + Puck(id=5, puck_name="PUCK005", puck_type="Unipuck", puck_location_in_dewar=5, positions=[], dewar_id='DEWAR001'), + Puck(id=6, puck_name="PUCK006", puck_type="Unipuck", puck_location_in_dewar=6, positions=[], dewar_id='DEWAR001'), + Puck(id=7, puck_name="PUCK007", puck_type="Unipuck", puck_location_in_dewar=7, positions=[], dewar_id='DEWAR001'), + Puck(id=8, puck_name="PK001", puck_type="Unipuck", puck_location_in_dewar=1, positions=[], dewar_id='DEWAR002'), + Puck(id=9, puck_name="PK002", puck_type="Unipuck", puck_location_in_dewar=2, positions=[], dewar_id='DEWAR002'), + Puck(id=10, puck_name="PK003", puck_type="Unipuck", puck_location_in_dewar=3, positions=[], dewar_id='DEWAR002'), + Puck(id=11, puck_name="PK004", puck_type="Unipuck", puck_location_in_dewar=4, positions=[], dewar_id='DEWAR002'), + Puck(id=12, puck_name="PK005", puck_type="Unipuck", puck_location_in_dewar=5, positions=[], dewar_id='DEWAR002'), + Puck(id=13, puck_name="PK006", puck_type="Unipuck", puck_location_in_dewar=6, positions=[], dewar_id='DEWAR002'), + Puck(id=14, puck_name="P001", puck_type="Unipuck", puck_location_in_dewar=1, positions=[], dewar_id='DEWAR003'), + Puck(id=15, puck_name="P002", puck_type="Unipuck", puck_location_in_dewar=2, positions=[], dewar_id='DEWAR003'), + Puck(id=16, puck_name="P003", puck_type="Unipuck", puck_location_in_dewar=3, positions=[], dewar_id='DEWAR003'), + Puck(id=17, puck_name="P004", puck_type="Unipuck", puck_location_in_dewar=4, positions=[], dewar_id='DEWAR003'), + Puck(id=18, puck_name="P005", puck_type="Unipuck", puck_location_in_dewar=5, positions=[], dewar_id='DEWAR003'), + Puck(id=19, puck_name="P006", puck_type="Unipuck", puck_location_in_dewar=6, positions=[], dewar_id='DEWAR003'), + Puck(id=20, puck_name="P007", puck_type="Unipuck", puck_location_in_dewar=7, positions=[], dewar_id='DEWAR003'), + Puck(id=21, puck_name="PC002", puck_type="Unipuck", puck_location_in_dewar=2, positions=[], dewar_id='DEWAR004'), + Puck(id=22, puck_name="PC003", puck_type="Unipuck", puck_location_in_dewar=3, positions=[], dewar_id='DEWAR004'), + Puck(id=23, puck_name="PC004", puck_type="Unipuck", puck_location_in_dewar=4, positions=[], dewar_id='DEWAR004'), + Puck(id=24, puck_name="PC005", puck_type="Unipuck", puck_location_in_dewar=5, positions=[], dewar_id='DEWAR004'), + Puck(id=25, puck_name="PC006", puck_type="Unipuck", puck_location_in_dewar=6, positions=[], dewar_id='DEWAR004'), + Puck(id=26, puck_name="PC007", puck_type="Unipuck", puck_location_in_dewar=7, positions=[], dewar_id='DEWAR004'), + Puck(id=27, puck_name="PKK004", puck_type="Unipuck", puck_location_in_dewar=4, positions=[], dewar_id='DEWAR005'), + Puck(id=28, puck_name="PKK005", puck_type="Unipuck", puck_location_in_dewar=5, positions=[], dewar_id='DEWAR005'), + Puck(id=29, puck_name="PKK006", puck_type="Unipuck", puck_location_in_dewar=6, positions=[], dewar_id='DEWAR005'), + Puck(id=30, puck_name="PKK007", puck_type="Unipuck", puck_location_in_dewar=7, positions=[], dewar_id='DEWAR005') +] + +samples = [] +sample_id_counter = 1 + +for puck in pucks: + positions_with_samples = random.randint(1, 16) + occupied_positions = random.sample(range(1, 17), positions_with_samples) + + for pos in range(1, 17): + if pos in occupied_positions: + sample = Sample(id=sample_id_counter, sample_name=f"Sample{sample_id_counter:03}", puck_id=puck.id) + puck.positions.append(sample) + samples.append(sample) + sample_id_counter += 1 diff --git a/backend/app/database.py b/backend/app/database.py index 99e3560..220b3c0 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -3,7 +3,7 @@ from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" +SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" # Use appropriate path or database URL engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) @@ -21,19 +21,20 @@ def get_db(): def init_db(): - # Import inside function to avoid circular dependency - from app import models + # Import models 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 + # Import models inside function to avoid circular dependency + from app.data import contacts, return_addresses, dewars, proposals, shipments, pucks, samples - from app import models # Ensure these imports are correct + from app import models + # If any data already exists, skip seeding if session.query(models.ContactPerson).first(): return - session.add_all(contacts + return_addresses + dewars + proposals + shipments) - session.commit() \ No newline at end of file + session.add_all(contacts + return_addresses + dewars + proposals + shipments + pucks + samples) + session.commit() diff --git a/backend/app/init_db.py b/backend/app/init_db.py index 7beb8a2..c2311fe 100644 --- a/backend/app/init_db.py +++ b/backend/app/init_db.py @@ -1,7 +1,9 @@ from app.database import init_db + def initialize_database(): init_db() + if __name__ == "__main__": - initialize_database() \ No newline at end of file + initialize_database() diff --git a/backend/app/main.py b/backend/app/main.py index 5097c68..c23fd08 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -3,7 +3,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.routers import address, contact, proposal, dewar, shipment, upload +from app.routers import address, contact, proposal, dewar, shipment, upload, puck from app.database import Base, engine, SessionLocal, load_sample_data app = FastAPI() @@ -36,6 +36,7 @@ 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"]) app.include_router(upload.router, tags=["upload"]) # Removed the trailing '/' from the prefix +app.include_router(puck.router, prefix="/pucks", tags=["pucks"]) if __name__ == "__main__": import uvicorn diff --git a/backend/app/models.py b/backend/app/models.py index dde01cf..8e7d8db 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -1,6 +1,8 @@ from sqlalchemy import Column, Integer, String, Date, ForeignKey from sqlalchemy.orm import relationship from app.database import Base +from app.calculations import calculate_number_of_pucks, calculate_number_of_samples + class Shipment(Base): __tablename__ = "shipments" @@ -19,6 +21,7 @@ class Shipment(Base): proposal = relationship("Proposal", back_populates="shipments") dewars = relationship("Dewar", back_populates="shipment") + class ContactPerson(Base): __tablename__ = "contact_persons" @@ -30,6 +33,7 @@ class ContactPerson(Base): shipments = relationship("Shipment", back_populates="contact_person") + class Address(Base): __tablename__ = "addresses" @@ -41,14 +45,13 @@ class Address(Base): 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) @@ -56,12 +59,22 @@ class Dewar(Base): 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 + return_address_id = Column(Integer, ForeignKey("addresses.id")) + contact_person_id = Column(Integer, ForeignKey("contact_persons.id")) shipment = relationship("Shipment", back_populates="dewars") return_address = relationship("Address") contact_person = relationship("ContactPerson") + pucks = relationship("Puck", back_populates="dewar") + + @property + def number_of_pucks(self) -> int: + return calculate_number_of_pucks(self) + + @property + def number_of_samples(self) -> int: + return calculate_number_of_samples(self) + class Proposal(Base): __tablename__ = "proposals" @@ -69,4 +82,25 @@ class Proposal(Base): id = Column(Integer, primary_key=True, index=True) number = Column(String) - shipments = relationship("Shipment", back_populates="proposal") \ No newline at end of file + shipments = relationship("Shipment", back_populates="proposal") + + +class Puck(Base): + __tablename__ = 'pucks' + + id = Column(String, primary_key=True) + puck_name = Column(String) + puck_type = Column(String) + puck_location_in_dewar = Column(Integer) + dewar_id = Column(String, ForeignKey('dewars.id')) # Note: changed to String + + positions = relationship("Sample", back_populates="puck") + dewar = relationship("Dewar", back_populates="pucks") + +class Sample(Base): + __tablename__ = 'samples' + + id = Column(Integer, primary_key=True) + sample_name = Column(String) + puck_id = Column(Integer, ForeignKey('pucks.id')) + puck = relationship("Puck", back_populates="positions") diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py new file mode 100644 index 0000000..b6653e3 --- /dev/null +++ b/backend/app/routers/__init__.py @@ -0,0 +1,7 @@ +from .address import router as address_router +from .contact import router as contact_router +from .proposal import router as proposal_router +from .dewar import router as dewar_router +from .shipment import router as shipment_router + +__all__ = ["address_router", "contact_router", "proposal_router", "dewar_router", "shipment_router"] \ No newline at end of file diff --git a/backend/app/routers/dewar.py b/backend/app/routers/dewar.py index bda6c5c..78cacfb 100644 --- a/backend/app/routers/dewar.py +++ b/backend/app/routers/dewar.py @@ -1,17 +1,19 @@ from fastapi import APIRouter, HTTPException, status, Depends -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, joinedload from typing import List import uuid -from app.schemas import Dewar as DewarSchema, DewarCreate -from app.models import Dewar as DewarModel +from app.schemas import Dewar as DewarSchema, DewarCreate, DewarUpdate, Sample as SampleSchema, Puck as PuckSchema +from app.models import Dewar as DewarModel, Puck as PuckModel, Sample as SampleModel from app.dependencies import get_db router = APIRouter() + @router.get("/", response_model=List[DewarSchema]) async def get_dewars(db: Session = Depends(get_db)): return db.query(DewarModel).all() + @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()}' @@ -35,4 +37,32 @@ async def create_dewar(dewar: DewarCreate, db: Session = Depends(get_db)) -> Dew db.add(db_dewar) db.commit() db.refresh(db_dewar) - return db_dewar \ No newline at end of file + return db_dewar + + +@router.get("/{dewar_id}", response_model=DewarSchema) +async def get_dewar(dewar_id: str, db: Session = Depends(get_db)): + dewar = db.query(DewarModel).options( + joinedload(DewarModel.pucks).joinedload(PuckModel.positions) + ).filter(DewarModel.id == dewar_id).first() + + if not dewar: + raise HTTPException(status_code=404, detail="Dewar not found") + + return dewar + + +@router.put("/{dewar_id}", response_model=DewarSchema) +async def update_dewar(dewar_id: str, dewar_update: DewarUpdate, db: Session = Depends(get_db)) -> DewarSchema: + dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first() + + if not dewar: + raise HTTPException(status_code=404, detail="Dewar not found") + + for key, value in dewar_update.dict(exclude_unset=True).items(): + setattr(dewar, key, value) + + db.commit() + db.refresh(dewar) + + return dewar diff --git a/backend/app/routers/puck.py b/backend/app/routers/puck.py new file mode 100644 index 0000000..91e7c39 --- /dev/null +++ b/backend/app/routers/puck.py @@ -0,0 +1,63 @@ +from fastapi import APIRouter, HTTPException, status, Depends +from sqlalchemy.orm import Session +from typing import List +import uuid +from app.schemas import Puck as PuckSchema, PuckCreate, PuckUpdate +from app.models import Puck as PuckModel, Sample as SampleModel +from app.dependencies import get_db + +router = APIRouter() + + +@router.get("/", response_model=List[PuckSchema]) +async def get_pucks(db: Session = Depends(get_db)): + return db.query(PuckModel).all() + + +@router.get("/{puck_id}", response_model=PuckSchema) +async def get_puck(puck_id: str, db: Session = Depends(get_db)): + puck = db.query(PuckModel).filter(PuckModel.id == puck_id).first() + if not puck: + raise HTTPException(status_code=404, detail="Puck not found") + return puck + + +@router.post("/", response_model=PuckSchema, status_code=status.HTTP_201_CREATED) +async def create_puck(puck: PuckCreate, db: Session = Depends(get_db)) -> PuckSchema: + puck_id = f'PUCK-{uuid.uuid4().hex[:8].upper()}' + db_puck = PuckModel( + id=puck_id, + puck_name=puck.puck_name, + puck_type=puck.puck_type, + puck_location_in_dewar=puck.puck_location_in_dewar, + dewar_id=puck.dewar_id + ) + db.add(db_puck) + db.commit() + db.refresh(db_puck) + return db_puck + + +@router.put("/{puck_id}", response_model=PuckSchema) +async def update_puck(puck_id: str, updated_puck: PuckUpdate, db: Session = Depends(get_db)): + puck = db.query(PuckModel).filter(PuckModel.id == puck_id).first() + if not puck: + raise HTTPException(status_code=404, detail="Puck not found") + + for key, value in updated_puck.dict(exclude_unset=True).items(): + setattr(puck, key, value) + + db.commit() + db.refresh(puck) + return puck + + +@router.delete("/{puck_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_puck(puck_id: str, db: Session = Depends(get_db)): + puck = db.query(PuckModel).filter(PuckModel.id == puck_id).first() + if not puck: + raise HTTPException(status_code=404, detail="Puck not found") + + db.delete(puck) + db.commit() + return diff --git a/backend/app/routers/shipment.py b/backend/app/routers/shipment.py index b266048..d769c8b 100644 --- a/backend/app/routers/shipment.py +++ b/backend/app/routers/shipment.py @@ -7,6 +7,7 @@ from datetime import date 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.schemas import Sample as SampleSchema from app.database import get_db from app.crud import get_shipments, get_shipment_by_id @@ -149,4 +150,33 @@ async def remove_dewar_from_shipment(shipment_id: str, dewar_id: str, db: Sessio @router.get("/contact_persons", response_model=List[ContactPersonSchema]) async def get_shipment_contact_persons(db: Session = Depends(get_db)): contact_persons = db.query(ContactPersonModel).all() - return contact_persons \ No newline at end of file + return contact_persons + +@router.get("/{shipment_id}/samples", response_model=List[SampleSchema]) +def get_samples_in_shipment(shipment_id: str, db: Session = Depends(get_db)): + shipment = db.query(ShipmentModel).filter(ShipmentModel.shipment_id == shipment_id).first() + if shipment is None: + raise HTTPException(status_code=404, detail="Shipment not found") + + samples = [] + for dewar in shipment.dewars: + for puck in dewar.pucks: + samples.extend(puck.positions) + + return samples + +@router.get("/{shipment_id}/dewars/{dewar_id}/samples", response_model=List[SampleSchema]) +def get_samples_in_dewar(shipment_id: str, dewar_id: str, db: Session = Depends(get_db)): + shipment = db.query(ShipmentModel).filter(ShipmentModel.shipment_id == shipment_id).first() + if shipment is None: + raise HTTPException(status_code=404, detail="Shipment not found") + + dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id, DewarModel.shipment_id == shipment_id).first() + if dewar is None: + raise HTTPException(status_code=404, detail="Dewar not found in shipment") + + samples = [] + for puck in dewar.pucks: + samples.extend(puck.positions) + + return samples \ No newline at end of file diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 679ae68..91913cb 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -3,7 +3,7 @@ from pydantic import BaseModel, EmailStr, constr from datetime import date -# Base class for Contact Person +# Contact Person schemas class ContactPersonBase(BaseModel): firstname: str lastname: str @@ -11,12 +11,10 @@ class ContactPersonBase(BaseModel): email: EmailStr -# Create schema for Contact Person class ContactPersonCreate(ContactPersonBase): pass -# Response schema for Contact Person with ID class ContactPerson(ContactPersonBase): id: int @@ -24,7 +22,7 @@ class ContactPerson(ContactPersonBase): from_attributes = True -# Create schema for Address +# Address schemas class AddressCreate(BaseModel): street: str city: str @@ -32,7 +30,6 @@ class AddressCreate(BaseModel): country: str -# Response schema for Address with ID class Address(AddressCreate): id: int @@ -40,8 +37,44 @@ class Address(AddressCreate): from_attributes = True -# Create schema for Dewar -class DewarCreate(BaseModel): +# Sample schemas +class Sample(BaseModel): + id: int + sample_name: str + + class Config: + from_attributes = True + + +# Puck schemas +class PuckBase(BaseModel): + puck_name: str + puck_type: str + puck_location_in_dewar: int + + +class PuckCreate(PuckBase): + positions: List[int] = [] + + +class PuckUpdate(BaseModel): + puck_name: Optional[str] = None + puck_type: Optional[str] = None + puck_location_in_dewar: Optional[int] = None + dewar_id: Optional[int] = None + positions: Optional[List[int]] = None + + +class Puck(PuckBase): + id: int + positions: List[Sample] = [] + + class Config: + from_attributes = True + + +# Dewar schemas +class DewarBase(BaseModel): dewar_name: str tracking_number: str number_of_pucks: int @@ -56,28 +89,37 @@ class DewarCreate(BaseModel): return_address_id: Optional[int] -# Response schema for Dewar -class Dewar(BaseModel): +class DewarCreate(DewarBase): + pass + + +class Dewar(DewarBase): 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] + pucks: Optional[List[Puck]] = [] class Config: from_attributes = True -# Proposal schema +class DewarUpdate(BaseModel): + 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 + + +# Proposal schemas class Proposal(BaseModel): id: int number: str @@ -86,7 +128,7 @@ class Proposal(BaseModel): from_attributes = True -# Response schema for Shipment +# Shipment schemas class Shipment(BaseModel): shipment_id: str shipment_name: str @@ -101,20 +143,6 @@ 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 class ShipmentCreate(BaseModel): shipment_name: str diff --git a/backend/app/utils.py b/backend/app/utils.py new file mode 100644 index 0000000..2fadc2f --- /dev/null +++ b/backend/app/utils.py @@ -0,0 +1,11 @@ +from app.models import Dewar + + +def calculate_number_of_pucks(dewar: Dewar) -> int: + return len(dewar.pucks) if dewar.pucks else 0 + + +def calculate_number_of_samples(dewar: Dewar) -> int: + if not dewar.pucks: + return 0 + return sum(len(puck.positions) for puck in dewar.pucks) diff --git a/backend/test.db b/backend/test.db new file mode 100644 index 0000000..faf9a5a Binary files /dev/null and b/backend/test.db differ diff --git a/frontend/src/components/DewarDetails.tsx b/frontend/src/components/DewarDetails.tsx index d773c3c..9fae560 100644 --- a/frontend/src/components/DewarDetails.tsx +++ b/frontend/src/components/DewarDetails.tsx @@ -1,19 +1,7 @@ import React, { useState, useEffect } from 'react'; -import { - Box, - Typography, - TextField, - Button, - Select, - MenuItem, - Snackbar -} from '@mui/material'; +import { Box, Typography, TextField, Button, Select, MenuItem, Snackbar } from '@mui/material'; import QRCode from 'react-qr-code'; -import { - ContactPerson, - Address, - Dewar, ContactsService, AddressesService, ShipmentsService, -} from '../../openapi'; +import { ContactPerson, Address, Dewar, ContactsService, AddressesService, ShipmentsService, Puck, Sample } from '../../openapi'; import Unipuck from '../components/Unipuck'; interface DewarDetailsProps { @@ -48,6 +36,7 @@ const DewarDetails: React.FC = ({ const [selectedReturnAddress, setSelectedReturnAddress] = useState(''); const [isCreatingContactPerson, setIsCreatingContactPerson] = useState(false); const [isCreatingReturnAddress, setIsCreatingReturnAddress] = useState(false); + const [puckStatuses, setPuckStatuses] = useState(dewar.pucks.map(() => Array(16).fill('empty'))); const [newContactPerson, setNewContactPerson] = useState({ id: 0, firstName: '', @@ -114,6 +103,28 @@ const DewarDetails: React.FC = ({ getReturnAddresses(); }, []); + useEffect(() => { + const fetchSamples = async () => { + if (dewar.id) { + try { + const samples: Sample[] = await ShipmentsService.getSamplesInDewarShipmentsShipmentIdDewarsDewarIdSamplesGet(shipmentId, dewar.id); + const updatedPuckStatuses = dewar.pucks.map(puck => { + return puck.positions.map(position => { + const isOccupied = samples.some(sample => sample.id === position.id); + return isOccupied ? 'filled' : 'empty'; + }); + }); + setPuckStatuses(updatedPuckStatuses); + } catch { + setFeedbackMessage('Failed to load samples. Please try again later.'); + setOpenSnackbar(true); + } + } + }; + + fetchSamples(); + }, [dewar, shipmentId]); + const validateEmail = (email: string) => /\S+@\S+\.\S+/.test(email); const validatePhoneNumber = (phone: string) => /^\+?[1-9]\d{1,14}$/.test(phone); const validateZipCode = (zipcode: string) => /^\d{5}(?:[-\s]\d{4})?$/.test(zipcode); @@ -123,7 +134,6 @@ 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.'); @@ -154,9 +164,7 @@ const DewarDetails: React.FC = ({ }; const handleAddAddress = async () => { - console.log('handleAddAddress called'); - if (!validateZipCode(newReturnAddress.zipcode) || !newReturnAddress.street || !newReturnAddress.city || - !newReturnAddress.country) { + if (!validateZipCode(newReturnAddress.zipcode) || !newReturnAddress.street || !newReturnAddress.city || !newReturnAddress.country) { setFeedbackMessage('Please fill in all new return address fields correctly.'); setOpenSnackbar(true); return; @@ -185,7 +193,6 @@ const DewarDetails: React.FC = ({ }; const getShipmentById = async (shipmentId: string) => { - console.log(`Fetching shipment with ID: ${shipmentId}`); try { const response = await ShipmentsService.fetchShipmentsShipmentsGet(shipmentId); if (response && response.length > 0) { @@ -193,14 +200,11 @@ const DewarDetails: React.FC = ({ } throw new Error('Shipment not found'); } catch (error) { - console.error('Error fetching shipment:', error); throw error; } }; const handleSaveChanges = async () => { - console.log('handleSaveChanges called'); - const formatDate = (dateString: string | undefined): string | null => { if (!dateString) return null; const date = new Date(dateString); @@ -208,23 +212,15 @@ const DewarDetails: React.FC = ({ return date.toISOString().split('T')[0]; }; - 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); @@ -234,7 +230,7 @@ const DewarDetails: React.FC = ({ const updatedDewar = { dewar_id: dewar.id, dewar_name: dewar.dewar_name, - tracking_number: dewar.tracking_number, + tracking_number: localTrackingNumber, number_of_pucks: dewar.number_of_pucks, number_of_samples: dewar.number_of_samples, status: dewar.status, @@ -244,7 +240,7 @@ const DewarDetails: React.FC = ({ returning_date: dewar.returning_date, qrcode: dewar.qrcode, return_address_id: selectedReturnAddress, - contact_person_id: selectedContactPerson, // Set dewar-specific contact person + contact_person_id: selectedContactPerson, }; const payload = { @@ -259,16 +255,12 @@ const DewarDetails: React.FC = ({ dewars: [updatedDewar], }; - console.log('Payload for update:', JSON.stringify(payload, null, 2)); - try { await ShipmentsService.updateShipmentShipmentsShipmentIdPut(shipmentId, payload); setFeedbackMessage('Changes saved successfully.'); setChangesMade(false); refreshShipments(); } catch (error: any) { - console.error('Update Shipment Error:', error); - if (error.response && error.response.data) { setFeedbackMessage(`Failed to save shipment. Validation errors: ${JSON.stringify(error.response.data)}`); } else { @@ -286,7 +278,7 @@ const DewarDetails: React.FC = ({ value={localTrackingNumber} onChange={(e) => { setLocalTrackingNumber(e.target.value); - setTrackingNumber(e.target.value); // Ensure parent state is updated if applicable + setTrackingNumber(e.target.value); setChangesMade(true); }} variant="outlined" @@ -299,20 +291,29 @@ const DewarDetails: React.FC = ({ ) : ( No QR code available )} - Number of Pucks: {dewar.number_of_pucks} - + + {/* Other inputs and elements */} + + Number of Pucks: {dewar.number_of_pucks} + + {/* Here we integrate the Unipuck component with puck data */} + {puckStatuses && } + + Number of Samples: {dewar.number_of_samples} + {/* Rest of DewarDetails component */} + Number of Samples: {dewar.number_of_samples} Current Contact Person: