added pucks and samples

This commit is contained in:
GotthardG 2024-11-04 11:34:14 +01:00
parent a9b8925be8
commit 23e7ebb819
17 changed files with 378 additions and 112 deletions

0
backend/app/__init__.py Normal file
View File

View File

@ -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)

View File

@ -0,0 +1 @@
from .data import contacts, return_addresses, dewars, proposals, shipments, pucks, samples

View File

@ -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 from datetime import datetime
import random
contacts = [ contacts = [
ContactPerson(id=1, firstname="Frodo", lastname="Baggins", phone_number="123-456-7890", ContactPerson(id=1, firstname="Frodo", lastname="Baggins", phone_number="123-456-7890",
@ -34,32 +35,34 @@ return_addresses = [
dewars = [ dewars = [
Dewar( 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', 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, ready_date=datetime.strptime('2023-09-30', '%Y-%m-%d'), shipping_date=None, arrival_date=None,
returning_date=None, qrcode='QR123DEWAR001', returning_date=None, qrcode='QR123DEWAR001',
), ),
Dewar( 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', 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', ready_date=None, shipping_date=None, arrival_date=None, returning_date=None, qrcode='QR123DEWAR002',
), ),
Dewar( 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', 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, ready_date=datetime.strptime('2024-01-01', '%Y-%m-%d'), shipping_date=None, arrival_date=None,
returning_date=None, qrcode='QR123DEWAR003', returning_date=None, qrcode='QR123DEWAR003',
), ),
Dewar( 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', 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', arrival_date=None, returning_date=None, qrcode='QR123DEWAR004',
), ),
Dewar( 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', 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', qrcode='QR123DEWAR005',
), ),
] ]
@ -97,3 +100,50 @@ shipments = [
proposal_id=5, return_address_id=1, comments='Contains the one ring', dewars=specific_dewars3 proposal_id=5, return_address_id=1, comments='Contains the one ring', dewars=specific_dewars3
), ),
] ]
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

View File

@ -3,7 +3,7 @@ from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker 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}) engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@ -21,19 +21,20 @@ def get_db():
def init_db(): def init_db():
# Import inside function to avoid circular dependency # Import models inside function to avoid circular dependency
from app import models from app import models
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
def load_sample_data(session: Session): def load_sample_data(session: Session):
# Import inside function to avoid circular dependency # Import models inside function to avoid circular dependency
from app.data import contacts, return_addresses, dewars, proposals, shipments 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(): if session.query(models.ContactPerson).first():
return return
session.add_all(contacts + return_addresses + dewars + proposals + shipments) session.add_all(contacts + return_addresses + dewars + proposals + shipments + pucks + samples)
session.commit() session.commit()

View File

@ -1,7 +1,9 @@
from app.database import init_db from app.database import init_db
def initialize_database(): def initialize_database():
init_db() init_db()
if __name__ == "__main__": if __name__ == "__main__":
initialize_database() initialize_database()

View File

@ -3,7 +3,7 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware 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 from app.database import Base, engine, SessionLocal, load_sample_data
app = FastAPI() 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(dewar.router, prefix="/dewars", tags=["dewars"])
app.include_router(shipment.router, prefix="/shipments", tags=["shipments"]) 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(upload.router, tags=["upload"]) # Removed the trailing '/' from the prefix
app.include_router(puck.router, prefix="/pucks", tags=["pucks"])
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn

View File

@ -1,6 +1,8 @@
from sqlalchemy import Column, Integer, String, Date, ForeignKey from sqlalchemy import Column, Integer, String, Date, ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.database import Base from app.database import Base
from app.calculations import calculate_number_of_pucks, calculate_number_of_samples
class Shipment(Base): class Shipment(Base):
__tablename__ = "shipments" __tablename__ = "shipments"
@ -19,6 +21,7 @@ class Shipment(Base):
proposal = relationship("Proposal", back_populates="shipments") proposal = relationship("Proposal", back_populates="shipments")
dewars = relationship("Dewar", back_populates="shipment") dewars = relationship("Dewar", back_populates="shipment")
class ContactPerson(Base): class ContactPerson(Base):
__tablename__ = "contact_persons" __tablename__ = "contact_persons"
@ -30,6 +33,7 @@ class ContactPerson(Base):
shipments = relationship("Shipment", back_populates="contact_person") shipments = relationship("Shipment", back_populates="contact_person")
class Address(Base): class Address(Base):
__tablename__ = "addresses" __tablename__ = "addresses"
@ -41,14 +45,13 @@ class Address(Base):
shipments = relationship("Shipment", back_populates="return_address") shipments = relationship("Shipment", back_populates="return_address")
class Dewar(Base): class Dewar(Base):
__tablename__ = "dewars" __tablename__ = "dewars"
id = Column(String, primary_key=True, index=True) id = Column(String, primary_key=True, index=True)
dewar_name = Column(String) dewar_name = Column(String)
tracking_number = Column(String) tracking_number = Column(String)
number_of_pucks = Column(Integer)
number_of_samples = Column(Integer)
status = Column(String) status = Column(String)
ready_date = Column(Date, nullable=True) ready_date = Column(Date, nullable=True)
shipping_date = Column(Date, nullable=True) shipping_date = Column(Date, nullable=True)
@ -56,12 +59,22 @@ class Dewar(Base):
returning_date = Column(Date, nullable=True) returning_date = Column(Date, nullable=True)
qrcode = Column(String) qrcode = Column(String)
shipment_id = Column(String, ForeignKey("shipments.shipment_id")) shipment_id = Column(String, ForeignKey("shipments.shipment_id"))
return_address_id = Column(Integer, ForeignKey("addresses.id")) # Added return_address_id = Column(Integer, ForeignKey("addresses.id"))
contact_person_id = Column(Integer, ForeignKey("contact_persons.id")) # Added contact_person_id = Column(Integer, ForeignKey("contact_persons.id"))
shipment = relationship("Shipment", back_populates="dewars") shipment = relationship("Shipment", back_populates="dewars")
return_address = relationship("Address") return_address = relationship("Address")
contact_person = relationship("ContactPerson") 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): class Proposal(Base):
__tablename__ = "proposals" __tablename__ = "proposals"
@ -70,3 +83,24 @@ class Proposal(Base):
number = Column(String) number = Column(String)
shipments = relationship("Shipment", back_populates="proposal") 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")

View File

@ -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"]

View File

@ -1,17 +1,19 @@
from fastapi import APIRouter, HTTPException, status, Depends from fastapi import APIRouter, HTTPException, status, Depends
from sqlalchemy.orm import Session from sqlalchemy.orm import Session, joinedload
from typing import List from typing import List
import uuid import uuid
from app.schemas import Dewar as DewarSchema, DewarCreate from app.schemas import Dewar as DewarSchema, DewarCreate, DewarUpdate, Sample as SampleSchema, Puck as PuckSchema
from app.models import Dewar as DewarModel from app.models import Dewar as DewarModel, Puck as PuckModel, Sample as SampleModel
from app.dependencies import get_db from app.dependencies import get_db
router = APIRouter() router = APIRouter()
@router.get("/", response_model=List[DewarSchema]) @router.get("/", response_model=List[DewarSchema])
async def get_dewars(db: Session = Depends(get_db)): async def get_dewars(db: Session = Depends(get_db)):
return db.query(DewarModel).all() return db.query(DewarModel).all()
@router.post("/", response_model=DewarSchema, status_code=status.HTTP_201_CREATED) @router.post("/", response_model=DewarSchema, status_code=status.HTTP_201_CREATED)
async def create_dewar(dewar: DewarCreate, db: Session = Depends(get_db)) -> DewarSchema: async def create_dewar(dewar: DewarCreate, db: Session = Depends(get_db)) -> DewarSchema:
dewar_id = f'DEWAR-{uuid.uuid4().hex[:8].upper()}' dewar_id = f'DEWAR-{uuid.uuid4().hex[:8].upper()}'
@ -36,3 +38,31 @@ async def create_dewar(dewar: DewarCreate, db: Session = Depends(get_db)) -> Dew
db.commit() db.commit()
db.refresh(db_dewar) db.refresh(db_dewar)
return db_dewar 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

View File

@ -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

View File

@ -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.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 ShipmentCreate, Shipment as ShipmentSchema, DewarUpdate, ContactPerson as ContactPersonSchema
from app.schemas import Sample as SampleSchema
from app.database import get_db from app.database import get_db
from app.crud import get_shipments, get_shipment_by_id from app.crud import get_shipments, get_shipment_by_id
@ -150,3 +151,32 @@ async def remove_dewar_from_shipment(shipment_id: str, dewar_id: str, db: Sessio
async def get_shipment_contact_persons(db: Session = Depends(get_db)): async def get_shipment_contact_persons(db: Session = Depends(get_db)):
contact_persons = db.query(ContactPersonModel).all() contact_persons = db.query(ContactPersonModel).all()
return contact_persons 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

View File

@ -3,7 +3,7 @@ from pydantic import BaseModel, EmailStr, constr
from datetime import date from datetime import date
# Base class for Contact Person # Contact Person schemas
class ContactPersonBase(BaseModel): class ContactPersonBase(BaseModel):
firstname: str firstname: str
lastname: str lastname: str
@ -11,12 +11,10 @@ class ContactPersonBase(BaseModel):
email: EmailStr email: EmailStr
# Create schema for Contact Person
class ContactPersonCreate(ContactPersonBase): class ContactPersonCreate(ContactPersonBase):
pass pass
# Response schema for Contact Person with ID
class ContactPerson(ContactPersonBase): class ContactPerson(ContactPersonBase):
id: int id: int
@ -24,7 +22,7 @@ class ContactPerson(ContactPersonBase):
from_attributes = True from_attributes = True
# Create schema for Address # Address schemas
class AddressCreate(BaseModel): class AddressCreate(BaseModel):
street: str street: str
city: str city: str
@ -32,7 +30,6 @@ class AddressCreate(BaseModel):
country: str country: str
# Response schema for Address with ID
class Address(AddressCreate): class Address(AddressCreate):
id: int id: int
@ -40,8 +37,44 @@ class Address(AddressCreate):
from_attributes = True from_attributes = True
# Create schema for Dewar # Sample schemas
class DewarCreate(BaseModel): 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 dewar_name: str
tracking_number: str tracking_number: str
number_of_pucks: int number_of_pucks: int
@ -56,28 +89,37 @@ class DewarCreate(BaseModel):
return_address_id: Optional[int] return_address_id: Optional[int]
# Response schema for Dewar class DewarCreate(DewarBase):
class Dewar(BaseModel): pass
class Dewar(DewarBase):
id: str 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] shipment_id: Optional[str]
contact_person: Optional[ContactPerson] contact_person: Optional[ContactPerson]
return_address: Optional[Address] return_address: Optional[Address]
pucks: Optional[List[Puck]] = []
class Config: class Config:
from_attributes = True 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): class Proposal(BaseModel):
id: int id: int
number: str number: str
@ -86,7 +128,7 @@ class Proposal(BaseModel):
from_attributes = True from_attributes = True
# Response schema for Shipment # Shipment schemas
class Shipment(BaseModel): class Shipment(BaseModel):
shipment_id: str shipment_id: str
shipment_name: str shipment_name: str
@ -101,20 +143,6 @@ class Shipment(BaseModel):
class Config: class Config:
from_attributes = True 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): class ShipmentCreate(BaseModel):
shipment_name: str shipment_name: str

11
backend/app/utils.py Normal file
View File

@ -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)

BIN
backend/test.db Normal file

Binary file not shown.

View File

@ -1,19 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { import { Box, Typography, TextField, Button, Select, MenuItem, Snackbar } from '@mui/material';
Box,
Typography,
TextField,
Button,
Select,
MenuItem,
Snackbar
} from '@mui/material';
import QRCode from 'react-qr-code'; import QRCode from 'react-qr-code';
import { import { ContactPerson, Address, Dewar, ContactsService, AddressesService, ShipmentsService, Puck, Sample } from '../../openapi';
ContactPerson,
Address,
Dewar, ContactsService, AddressesService, ShipmentsService,
} from '../../openapi';
import Unipuck from '../components/Unipuck'; import Unipuck from '../components/Unipuck';
interface DewarDetailsProps { interface DewarDetailsProps {
@ -48,6 +36,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
const [selectedReturnAddress, setSelectedReturnAddress] = useState<string>(''); const [selectedReturnAddress, setSelectedReturnAddress] = useState<string>('');
const [isCreatingContactPerson, setIsCreatingContactPerson] = useState(false); const [isCreatingContactPerson, setIsCreatingContactPerson] = useState(false);
const [isCreatingReturnAddress, setIsCreatingReturnAddress] = useState(false); const [isCreatingReturnAddress, setIsCreatingReturnAddress] = useState(false);
const [puckStatuses, setPuckStatuses] = useState<string[][]>(dewar.pucks.map(() => Array(16).fill('empty')));
const [newContactPerson, setNewContactPerson] = useState({ const [newContactPerson, setNewContactPerson] = useState({
id: 0, id: 0,
firstName: '', firstName: '',
@ -114,6 +103,28 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
getReturnAddresses(); 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 validateEmail = (email: string) => /\S+@\S+\.\S+/.test(email);
const validatePhoneNumber = (phone: string) => /^\+?[1-9]\d{1,14}$/.test(phone); const validatePhoneNumber = (phone: string) => /^\+?[1-9]\d{1,14}$/.test(phone);
const validateZipCode = (zipcode: string) => /^\d{5}(?:[-\s]\d{4})?$/.test(zipcode); const validateZipCode = (zipcode: string) => /^\d{5}(?:[-\s]\d{4})?$/.test(zipcode);
@ -123,7 +134,6 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
} }
const handleAddContact = async () => { const handleAddContact = async () => {
console.log('handleAddContact called');
if (!validateEmail(newContactPerson.email) || !validatePhoneNumber(newContactPerson.phone_number) || if (!validateEmail(newContactPerson.email) || !validatePhoneNumber(newContactPerson.phone_number) ||
!newContactPerson.firstName || !newContactPerson.lastName) { !newContactPerson.firstName || !newContactPerson.lastName) {
setFeedbackMessage('Please fill in all new contact person fields correctly.'); setFeedbackMessage('Please fill in all new contact person fields correctly.');
@ -154,9 +164,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
}; };
const handleAddAddress = async () => { 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.'); setFeedbackMessage('Please fill in all new return address fields correctly.');
setOpenSnackbar(true); setOpenSnackbar(true);
return; return;
@ -185,7 +193,6 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
}; };
const getShipmentById = async (shipmentId: string) => { const getShipmentById = async (shipmentId: string) => {
console.log(`Fetching shipment with ID: ${shipmentId}`);
try { try {
const response = await ShipmentsService.fetchShipmentsShipmentsGet(shipmentId); const response = await ShipmentsService.fetchShipmentsShipmentsGet(shipmentId);
if (response && response.length > 0) { if (response && response.length > 0) {
@ -193,14 +200,11 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
} }
throw new Error('Shipment not found'); throw new Error('Shipment not found');
} catch (error) { } catch (error) {
console.error('Error fetching shipment:', error);
throw error; throw error;
} }
}; };
const handleSaveChanges = async () => { const handleSaveChanges = async () => {
console.log('handleSaveChanges called');
const formatDate = (dateString: string | undefined): string | null => { const formatDate = (dateString: string | undefined): string | null => {
if (!dateString) return null; if (!dateString) return null;
const date = new Date(dateString); const date = new Date(dateString);
@ -208,23 +212,15 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
return date.toISOString().split('T')[0]; 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) { if (!selectedContactPerson || !selectedReturnAddress) {
setFeedbackMessage('Please ensure all required fields are filled.'); setFeedbackMessage('Please ensure all required fields are filled.');
setOpenSnackbar(true); setOpenSnackbar(true);
return; return;
} }
console.log('Saving changes...');
console.log('Current Dewar:', dewar);
let existingShipment; let existingShipment;
try { try {
existingShipment = await getShipmentById(shipmentId); existingShipment = await getShipmentById(shipmentId);
console.log('Existing Shipment:', existingShipment);
} catch { } catch {
setFeedbackMessage('Failed to fetch existing shipment data. Please try again later.'); setFeedbackMessage('Failed to fetch existing shipment data. Please try again later.');
setOpenSnackbar(true); setOpenSnackbar(true);
@ -234,7 +230,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
const updatedDewar = { const updatedDewar = {
dewar_id: dewar.id, dewar_id: dewar.id,
dewar_name: dewar.dewar_name, dewar_name: dewar.dewar_name,
tracking_number: dewar.tracking_number, tracking_number: localTrackingNumber,
number_of_pucks: dewar.number_of_pucks, number_of_pucks: dewar.number_of_pucks,
number_of_samples: dewar.number_of_samples, number_of_samples: dewar.number_of_samples,
status: dewar.status, status: dewar.status,
@ -244,7 +240,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
returning_date: dewar.returning_date, returning_date: dewar.returning_date,
qrcode: dewar.qrcode, qrcode: dewar.qrcode,
return_address_id: selectedReturnAddress, return_address_id: selectedReturnAddress,
contact_person_id: selectedContactPerson, // Set dewar-specific contact person contact_person_id: selectedContactPerson,
}; };
const payload = { const payload = {
@ -259,16 +255,12 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
dewars: [updatedDewar], dewars: [updatedDewar],
}; };
console.log('Payload for update:', JSON.stringify(payload, null, 2));
try { try {
await ShipmentsService.updateShipmentShipmentsShipmentIdPut(shipmentId, payload); await ShipmentsService.updateShipmentShipmentsShipmentIdPut(shipmentId, payload);
setFeedbackMessage('Changes saved successfully.'); setFeedbackMessage('Changes saved successfully.');
setChangesMade(false); setChangesMade(false);
refreshShipments(); refreshShipments();
} catch (error: any) { } catch (error: any) {
console.error('Update Shipment Error:', error);
if (error.response && error.response.data) { if (error.response && error.response.data) {
setFeedbackMessage(`Failed to save shipment. Validation errors: ${JSON.stringify(error.response.data)}`); setFeedbackMessage(`Failed to save shipment. Validation errors: ${JSON.stringify(error.response.data)}`);
} else { } else {
@ -286,7 +278,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
value={localTrackingNumber} value={localTrackingNumber}
onChange={(e) => { onChange={(e) => {
setLocalTrackingNumber(e.target.value); setLocalTrackingNumber(e.target.value);
setTrackingNumber(e.target.value); // Ensure parent state is updated if applicable setTrackingNumber(e.target.value);
setChangesMade(true); setChangesMade(true);
}} }}
variant="outlined" variant="outlined"
@ -299,20 +291,29 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
) : ( ) : (
<Typography>No QR code available</Typography> <Typography>No QR code available</Typography>
)} )}
<Button variant="contained" sx={{ marginTop: 1 }} onClick={() => { /** Add logic to generate QR Code */ }}> <Button variant="contained" sx={{ marginTop: 1 }} onClick={() => { /* Add logic to generate QR Code */ }}>
Generate QR Code Generate QR Code
</Button> </Button>
</Box> </Box>
</Box> </Box>
<Typography variant="body1">Number of Pucks: {dewar.number_of_pucks}</Typography> <Typography variant="body1">Number of Pucks: {dewar.number_of_pucks}</Typography>
<Unipuck pucks={dewar.number_of_pucks ?? 0} /> <Box sx={{ marginTop: 2 }}>
{/* Other inputs and elements */}
<Typography variant="body1">Number of Pucks: {dewar.number_of_pucks}</Typography>
{/* Here we integrate the Unipuck component with puck data */}
{puckStatuses && <Unipuck pucks={puckStatuses.length} samples={puckStatuses} />}
<Typography variant="body1">Number of Samples: {dewar.number_of_samples}</Typography>
{/* Rest of DewarDetails component */}
</Box>
<Typography variant="body1">Number of Samples: {dewar.number_of_samples}</Typography> <Typography variant="body1">Number of Samples: {dewar.number_of_samples}</Typography>
<Typography variant="body1">Current Contact Person:</Typography> <Typography variant="body1">Current Contact Person:</Typography>
<Select <Select
value={selectedContactPerson} value={selectedContactPerson}
onChange={(e) => { onChange={(e) => {
const value = e.target.value; const value = e.target.value;
console.log('Contact Person Selected:', value);
setSelectedContactPerson(value); setSelectedContactPerson(value);
setIsCreatingContactPerson(value === 'add'); setIsCreatingContactPerson(value === 'add');
setChangesMade(true); setChangesMade(true);
@ -377,7 +378,6 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
value={selectedReturnAddress} value={selectedReturnAddress}
onChange={(e) => { onChange={(e) => {
const value = e.target.value; const value = e.target.value;
console.log('Return Address Selected:', value);
setSelectedReturnAddress(value); setSelectedReturnAddress(value);
setIsCreatingReturnAddress(value === 'add'); setIsCreatingReturnAddress(value === 'add');
setChangesMade(true); setChangesMade(true);

View File

@ -1,13 +1,13 @@
// app/components/Unipuck.tsx
import React from 'react'; import React from 'react';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
interface UnipuckProps { interface UnipuckProps {
pucks: number; // Number of pucks, assuming each puck follows the same layout pucks: number; // Number of pucks
samples?: string[][]; // Array of sample arrays for each puck
} }
const Unipuck: React.FC<UnipuckProps> = ({ pucks }) => { const Unipuck: React.FC<UnipuckProps> = ({ pucks, samples }) => {
const renderPuck = () => { const renderPuck = (sampleStatus: string[]) => {
const puckSVG = ( const puckSVG = (
<svg width="100" height="100" viewBox="0 0 100 100"> <svg width="100" height="100" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" stroke="black" strokeWidth="2" fill="none" /> <circle cx="50" cy="50" r="45" stroke="black" strokeWidth="2" fill="none" />
@ -15,13 +15,13 @@ const Unipuck: React.FC<UnipuckProps> = ({ pucks }) => {
const angle = (index * (360 / 11)) * (Math.PI / 180); const angle = (index * (360 / 11)) * (Math.PI / 180);
const x = 50 + 35 * Math.cos(angle); const x = 50 + 35 * Math.cos(angle);
const y = 50 + 35 * Math.sin(angle); const y = 50 + 35 * Math.sin(angle);
return <circle key={index} cx={x} cy={y} r="5" fill="black" />; return <circle key={index} cx={x} cy={y} r="5" fill={sampleStatus[index] === 'filled' ? 'black' : 'none'} stroke="black" />;
})} })}
{[...Array(5)].map((_, index) => { {[...Array(5)].map((_, index) => {
const angle = (index * (360 / 5) + 36) * (Math.PI / 180); const angle = (index * (360 / 5) + 36) * (Math.PI / 180);
const x = 50 + 15 * Math.cos(angle); const x = 50 + 15 * Math.cos(angle);
const y = 50 + 15 * Math.sin(angle); const y = 50 + 15 * Math.sin(angle);
return <circle key={index} cx={x} cy={y} r="5" fill="black" />; return <circle key={index + 11} cx={x} cy={y} r="5" fill={sampleStatus[index + 11] === 'filled' ? 'black' : 'none'} stroke="black" />;
})} })}
</svg> </svg>
); );
@ -32,7 +32,7 @@ const Unipuck: React.FC<UnipuckProps> = ({ pucks }) => {
<Box sx={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 2 }}> <Box sx={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 2 }}>
{[...Array(pucks)].map((_, index) => ( {[...Array(pucks)].map((_, index) => (
<Box key={index} sx={{ margin: 1 }}> <Box key={index} sx={{ margin: 1 }}>
{renderPuck()} {renderPuck(samples ? samples[index] : Array(16).fill('empty'))}
</Box> </Box>
))} ))}
</Box> </Box>