From 6083c72a1d270969fc9253e8b03964704b9a793b Mon Sep 17 00:00:00 2001 From: GotthardG <51994228+GotthardG@users.noreply.github.com> Date: Thu, 14 Nov 2024 23:17:20 +0100 Subject: [PATCH] added dewar type, serial number, generate unique id, qr code and generate label --- backend/app/calculations.py | 8 - backend/app/data/__init__.py | 2 +- backend/app/data/data.py | 90 +++-- backend/app/database.py | 4 +- backend/app/models.py | 24 +- backend/app/routers/dewar.py | 186 +++++++-- backend/app/schemas.py | 64 ++- frontend/src/components/DewarDetails.tsx | 487 ++++++++++++++++++----- 8 files changed, 684 insertions(+), 181 deletions(-) delete mode 100644 backend/app/calculations.py diff --git a/backend/app/calculations.py b/backend/app/calculations.py deleted file mode 100644 index dbe2cf6..0000000 --- a/backend/app/calculations.py +++ /dev/null @@ -1,8 +0,0 @@ -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 index ec98eb9..8d85d5b 100644 --- a/backend/app/data/__init__.py +++ b/backend/app/data/__init__.py @@ -1 +1 @@ -from .data import contacts, return_addresses, dewars, proposals, shipments, pucks, samples +from .data import contacts, return_addresses, dewars, proposals, shipments, pucks, samples, dewar_types, serial_numbers diff --git a/backend/app/data/data.py b/backend/app/data/data.py index fd12fd5..92940c2 100644 --- a/backend/app/data/data.py +++ b/backend/app/data/data.py @@ -1,30 +1,40 @@ -from app.models import ContactPerson, Address, Dewar, Proposal, Shipment, Puck, Sample +from app.models import ContactPerson, Address, Dewar, Proposal, Shipment, Puck, Sample, DewarType, DewarSerialNumber from datetime import datetime import random +import uuid -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"), + +dewar_types = [ + DewarType(id=1, dewar_type="Type A"), + DewarType(id=2, dewar_type="Type B"), + DewarType(id=3, dewar_type="Type C"), ] +# Define Dewar serial numbers +serial_numbers = [ + DewarSerialNumber(id=1, serial_number="SN00001", dewar_type_id=1), + DewarSerialNumber(id=2, serial_number="SN00002", dewar_type_id=1), + DewarSerialNumber(id=3, serial_number="SN00003", dewar_type_id=2), + DewarSerialNumber(id=4, serial_number="SN00004", dewar_type_id=2), + DewarSerialNumber(id=5, serial_number="SN00005", dewar_type_id=3), + DewarSerialNumber(id=6, serial_number="SN00006", dewar_type_id=3), +] + +# Define contact persons +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"), +] + +# Define return addresses 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'), @@ -33,40 +43,51 @@ return_addresses = [ Address(id=5, street='654 Falgorn Pass', city='Rivendell', zipcode='11223', country='Middle Earth'), ] +# Utilize a function to generate unique IDs +def generate_unique_id(): + return str(uuid.uuid4()) + +# Define dewars with unique IDs dewars = [ Dewar( - id=1, dewar_name='Dewar One', tracking_number='TRACK123', + id=1, dewar_name='Dewar One', dewar_type_id=1, + dewar_serial_number_id=2, 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', + returning_date=None, qrcode=generate_unique_id() ), Dewar( - id=2, dewar_name='Dewar Two', tracking_number='TRACK124', + id=2, dewar_name='Dewar Two', dewar_type_id=3, + dewar_serial_number_id=1, 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', + ready_date=None, shipping_date=None, arrival_date=None, returning_date=None, qrcode=generate_unique_id() ), Dewar( - id=3, dewar_name='Dewar Three', tracking_number='TRACK125', + id=3, dewar_name='Dewar Three', dewar_type_id=2, + dewar_serial_number_id=3, 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', + returning_date=None, qrcode='' ), Dewar( - id=4, dewar_name='Dewar Four', tracking_number='', + id=4, dewar_name='Dewar Four', dewar_type_id=2, + dewar_serial_number_id=4, 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'), - arrival_date=None, returning_date=None, qrcode='QR123DEWAR004', + arrival_date=None, returning_date=None, qrcode='' ), Dewar( - id=5, dewar_name='Dewar Five', tracking_number='', + id=5, dewar_name='Dewar Five', dewar_type_id=1, + dewar_serial_number_id=1, 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'), - qrcode='QR123DEWAR005', + qrcode='' ), ] +# Define proposals proposals = [ Proposal(id=1, number="PROPOSAL-FRODO-001"), Proposal(id=2, number="PROPOSAL-GANDALF-002"), @@ -75,6 +96,7 @@ proposals = [ Proposal(id=5, number="PROPOSAL-MORDOR-005"), ] +# Define shipment specific dewars specific_dewar_ids1 = [5] specific_dewar_ids2 = [1, 2] specific_dewar_ids3 = [3, 4] @@ -83,6 +105,7 @@ specific_dewars1 = [dewar for dewar in dewars if dewar.id in specific_dewar_ids1 specific_dewars2 = [dewar for dewar in dewars if dewar.id in specific_dewar_ids2] specific_dewars3 = [dewar for dewar in dewars if dewar.id in specific_dewar_ids3] +# Define shipments shipments = [ Shipment( id=1, shipment_date=datetime.strptime('2024-10-10', '%Y-%m-%d'), @@ -101,6 +124,7 @@ shipments = [ ), ] +# Define pucks pucks = [ Puck(id=1, puck_name="PUCK001", puck_type="Unipuck", puck_location_in_dewar=1, dewar_id=1), Puck(id=2, puck_name="PUCK002", puck_type="Unipuck", puck_location_in_dewar=2, dewar_id=1), @@ -134,7 +158,7 @@ pucks = [ Puck(id=30, puck_name="PKK007", puck_type="Unipuck", puck_location_in_dewar=7, dewar_id=5) ] - +# Define samples samples = [] sample_id_counter = 1 diff --git a/backend/app/database.py b/backend/app/database.py index 220b3c0..c166339 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -28,7 +28,7 @@ def init_db(): def load_sample_data(session: Session): # Import models inside function to avoid circular dependency - from app.data import contacts, return_addresses, dewars, proposals, shipments, pucks, samples + from app.data import contacts, return_addresses, dewars, proposals, shipments, pucks, samples, dewar_types, serial_numbers from app import models @@ -36,5 +36,5 @@ def load_sample_data(session: Session): if session.query(models.ContactPerson).first(): return - session.add_all(contacts + return_addresses + dewars + proposals + shipments + pucks + samples) + session.add_all(contacts + return_addresses + dewars + proposals + shipments + pucks + samples + dewar_types + serial_numbers) session.commit() diff --git a/backend/app/models.py b/backend/app/models.py index ad1ebab..a82a47d 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -1,7 +1,7 @@ from sqlalchemy import Column, Integer, String, Date, ForeignKey, JSON from sqlalchemy.orm import relationship from app.database import Base -from app.calculations import calculate_number_of_pucks, calculate_number_of_samples +import uuid class Shipment(Base): @@ -45,19 +45,35 @@ class Address(Base): shipments = relationship("Shipment", back_populates="return_address") +class DewarType(Base): + __tablename__ = "dewar_types" + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + dewar_type = Column(String, unique=True, index=True) + serial_numbers = relationship("DewarSerialNumber", back_populates="dewar_type") + +class DewarSerialNumber(Base): + __tablename__ = "dewar_serial_numbers" + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + serial_number = Column(String, index=True) + dewar_type_id = Column(Integer, ForeignKey('dewar_types.id')) + dewar_type = relationship("DewarType", back_populates="serial_numbers") + class Dewar(Base): __tablename__ = "dewars" id = Column(Integer, primary_key=True, index=True, autoincrement=True) dewar_name = Column(String) + dewar_type_id = Column(Integer, ForeignKey("dewar_types.id"), nullable=True) + dewar_serial_number_id = Column(Integer, ForeignKey("dewar_serial_numbers.id"), nullable=True) tracking_number = Column(String) 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) + unique_id = Column(String(36), default=lambda: str(uuid.uuid4()), unique=True, index=True, nullable=True) + qrcode = Column(String, nullable=True) shipment_id = Column(Integer, ForeignKey("shipments.id")) return_address_id = Column(Integer, ForeignKey("addresses.id")) contact_person_id = Column(Integer, ForeignKey("contact_persons.id")) @@ -67,6 +83,9 @@ class Dewar(Base): contact_person = relationship("ContactPerson") pucks = relationship("Puck", back_populates="dewar") + dewar_type = relationship("DewarType") + dewar_serial_number = relationship("DewarSerialNumber") + @property def number_of_pucks(self) -> int: return len(self.pucks) if self.pucks else 0 @@ -77,7 +96,6 @@ class Dewar(Base): return 0 return sum(len(puck.samples) for puck in self.pucks) - class Proposal(Base): __tablename__ = "proposals" diff --git a/backend/app/routers/dewar.py b/backend/app/routers/dewar.py index ce20bbd..a37e68b 100644 --- a/backend/app/routers/dewar.py +++ b/backend/app/routers/dewar.py @@ -1,26 +1,50 @@ -from fastapi import APIRouter, HTTPException, status, Depends +from fastapi import APIRouter, HTTPException, status, Depends, Response from sqlalchemy.orm import Session, joinedload from typing import List import logging from sqlalchemy.exc import SQLAlchemyError from pydantic import ValidationError -from app.schemas import Dewar as DewarSchema, DewarCreate, DewarUpdate -from app.models import Dewar as DewarModel, Puck as PuckModel, \ - Sample as SampleModel # Assuming SampleModel is defined in models +from app.schemas import ( + Dewar as DewarSchema, + DewarCreate, + DewarUpdate, + DewarType as DewarTypeSchema, + DewarTypeCreate, + DewarSerialNumber as DewarSerialNumberSchema, + DewarSerialNumberCreate +) +from app.models import ( + Dewar as DewarModel, + Puck as PuckModel, + Sample as SampleModel, + DewarType as DewarTypeModel, + DewarSerialNumber as DewarSerialNumberModel +) from app.dependencies import get_db +import uuid +import qrcode +import io +from io import BytesIO +from PIL import Image +from reportlab.lib.pagesizes import A5 +from reportlab.lib.units import cm +from reportlab.pdfgen import canvas router = APIRouter() -@router.get("/", response_model=List[DewarSchema]) -async def get_dewars(db: Session = Depends(get_db)): - dewars = db.query(DewarModel).options(joinedload(DewarModel.pucks)).all() - return dewars - +def generate_unique_id(db: Session) -> str: + while True: + unique_id = str(uuid.uuid4()) + existing_dewar = db.query(DewarModel).filter(DewarModel.unique_id == unique_id).first() + if not existing_dewar: + break + return unique_id @router.post("/", response_model=DewarSchema, status_code=status.HTTP_201_CREATED) async def create_dewar(dewar: DewarCreate, db: Session = Depends(get_db)) -> DewarSchema: try: + unique_id = generate_unique_id(db) db_dewar = DewarModel( dewar_name=dewar.dewar_name, tracking_number=dewar.tracking_number, @@ -29,11 +53,10 @@ async def create_dewar(dewar: DewarCreate, db: Session = Depends(get_db)) -> Dew 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 + return_address_id=dewar.return_address_id, + unique_id=unique_id ) - db.add(db_dewar) db.commit() db.refresh(db_dewar) @@ -54,7 +77,6 @@ async def create_dewar(dewar: DewarCreate, db: Session = Depends(get_db)) -> Dew puck_id=puck.id, sample_name=sample_data.sample_name, position=sample_data.position, - # Ensure only valid attributes are set data_collection_parameters=sample_data.data_collection_parameters, ) db.add(sample) @@ -70,24 +92,142 @@ async def create_dewar(dewar: DewarCreate, db: Session = Depends(get_db)) -> Dew logging.error(f"Validation error occurred: {e}") raise HTTPException(status_code=400, detail="Validation error") +@router.post("/{dewar_id}/generate-qrcode") +async def generate_dewar_qrcode(dewar_id: int, db: Session = Depends(get_db)): + dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first() + if not dewar: + raise HTTPException(status_code=404, detail="Dewar not found") + + if not dewar.unique_id: + dewar.unique_id = generate_unique_id(db) + + qr = qrcode.QRCode(version=1, box_size=10, border=5) + qr.add_data(dewar.unique_id) + qr.make(fit=True) + img = qr.make_image(fill='black', back_color='white') + + buf = io.BytesIO() + img.save(buf) + buf.seek(0) + dewar.qrcode = dewar.unique_id + dewar.qrcode_image = buf.getvalue() + db.commit() + + return {"message": "QR Code generated", "qrcode": dewar.unique_id} + + +def generate_label(dewar): + buffer = BytesIO() + c = canvas.Canvas(buffer, pagesize=A5) + + # Draw header + c.setFont("Helvetica-Bold", 16) + c.drawCentredString(10.5 * cm, 14 * cm, "COMPANY LOGO / TITLE") + + # Draw details section + c.setFont("Helvetica", 12) + c.drawString(2 * cm, 12.5 * cm, f"Dewar Name: {dewar.dewar_name}") + c.drawString(2 * cm, 11.5 * cm, f"Unique ID: {dewar.unique_id}") + if dewar.dewar_type: + c.drawString(2 * cm, 10.5 * cm, f"Dewar Type: {dewar.dewar_type.dewar_type}") + else: + c.drawString(2 * cm, 10.5 * cm, "Dewar Type: Unknown") + c.drawString(2 * cm, 9.5 * cm, "Beamtime Information: Placeholder") + + # Generate QR code + qr = qrcode.QRCode(version=1, box_size=10, border=5) + qr.add_data(dewar.unique_id) + qr.make(fit=True) + img = qr.make_image(fill='black', back_color='white') + qr_io = BytesIO() + img.save(qr_io, format='PNG') + qr_io.seek(0) + qr_image = Image.open(qr_io) + + # Add QR code to PDF + c.drawInlineImage(qr_image, 8 * cm, 5 * cm, width=4 * cm, height=4 * cm) + + # Add footer text + c.setFont("Helvetica", 10) + c.drawCentredString(10.5 * cm, 4 * cm, "Scan for more information") + + # Draw border + c.rect(1 * cm, 3 * cm, 18 * cm, 12 * cm) + + # Finalize the canvas + c.showPage() + c.save() + + buffer.seek(0) + return buffer + +@router.get("/{dewar_id}/download-label", response_class=Response) +async def download_dewar_label(dewar_id: int, db: Session = Depends(get_db)): + dewar = db.query(DewarModel).options(joinedload(DewarModel.dewar_type)).filter(DewarModel.id == dewar_id).first() + if not dewar: + raise HTTPException(status_code=404, detail="Dewar not found") + if not dewar.unique_id: + raise HTTPException(status_code=404, detail="QR Code not generated for this dewar") + + buffer = generate_label(dewar) + + return Response(buffer.getvalue(), media_type="application/pdf", headers={ + "Content-Disposition": f"attachment; filename=dewar_label_{dewar.id}.pdf" + }) + +@router.get("/", response_model=List[DewarSchema]) +async def get_dewars(db: Session = Depends(get_db)): + try: + dewars = db.query(DewarModel).options(joinedload(DewarModel.pucks)).all() + return dewars + except SQLAlchemyError as e: + logging.error(f"Database error occurred: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.get("/dewar-types", response_model=List[DewarTypeSchema]) +def get_dewar_types(db: Session = Depends(get_db)): + return db.query(DewarTypeModel).all() + +@router.get("/dewar-types/{type_id}/serial-numbers", response_model=List[DewarSerialNumberSchema]) +def get_serial_numbers(type_id: int, db: Session = Depends(get_db)): + return db.query(DewarSerialNumberModel).filter(DewarSerialNumberModel.dewar_type_id == type_id).all() + +@router.post("/dewar-types", response_model=DewarTypeSchema) +def create_dewar_type(dewar_type: DewarTypeCreate, db: Session = Depends(get_db)): + db_type = DewarTypeModel(**dewar_type.dict()) + db.add(db_type) + db.commit() + db.refresh(db_type) + return db_type + +@router.post("/dewar-serial-numbers", response_model=DewarSerialNumberSchema) +def create_dewar_serial_number(serial_number: DewarSerialNumberCreate, db: Session = Depends(get_db)): + db_serial = DewarSerialNumberModel(**serial_number.dict()) + db.add(db_serial) + db.commit() + db.refresh(db_serial) + return db_serial + +@router.get("/dewar-serial-numbers", response_model=List[DewarSerialNumberSchema]) +def get_all_serial_numbers(db: Session = Depends(get_db)): + try: + serial_numbers = db.query(DewarSerialNumberModel).all() + return serial_numbers + except SQLAlchemyError as e: + logging.error(f"Database error occurred: {e}") + raise HTTPException(status_code=500, detail="Internal server error") @router.get("/{dewar_id}", response_model=DewarSchema) async def get_dewar(dewar_id: int, db: Session = Depends(get_db)): dewar = db.query(DewarModel).options( - joinedload(DewarModel.pucks).joinedload(PuckModel.positions) + joinedload(DewarModel.pucks).joinedload(PuckModel.samples) ).filter(DewarModel.id == dewar_id).first() if not dewar: raise HTTPException(status_code=404, detail="Dewar not found") - # Ensure dewar.pucks is an empty list if there are no pucks - dewar_dict = dewar.__dict__ - if dewar_dict.get("pucks") is None: - dewar_dict["pucks"] = [] - return DewarSchema.from_orm(dewar) - @router.put("/{dewar_id}", response_model=DewarSchema) async def update_dewar(dewar_id: int, dewar_update: DewarUpdate, db: Session = Depends(get_db)) -> DewarSchema: dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first() @@ -96,22 +236,20 @@ async def update_dewar(dewar_id: int, dewar_update: DewarUpdate, db: Session = D raise HTTPException(status_code=404, detail="Dewar not found") for key, value in dewar_update.dict(exclude_unset=True).items(): - # Ensure we're only setting directly settable attributes if hasattr(dewar, key): setattr(dewar, key, value) db.commit() db.refresh(dewar) - return dewar - @router.delete("/{dewar_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_dewar(dewar_id: int, db: Session = Depends(get_db)): dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first() + if not dewar: raise HTTPException(status_code=404, detail="Dewar not found") db.delete(dewar) db.commit() - return + return \ No newline at end of file diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 4b17553..69a9b51 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -3,6 +3,38 @@ from pydantic import BaseModel, EmailStr, constr, Field from datetime import date +class DewarTypeBase(BaseModel): + dewar_type: str + + +class DewarTypeCreate(DewarTypeBase): + pass + + +class DewarType(DewarTypeBase): + id: int + + class Config: + from_attributes = True + + +class DewarSerialNumberBase(BaseModel): + serial_number: str + dewar_type_id: int + + +class DewarSerialNumberCreate(DewarSerialNumberBase): + pass + + +class DewarSerialNumber(DewarSerialNumberBase): + id: int + dewar_type: DewarType + + class Config: + from_attributes = True + + class DataCollectionParameters(BaseModel): priority: Optional[int] = None comments: Optional[str] = None @@ -41,7 +73,6 @@ class Results(BaseModel): pass -# Contact Person schemas class ContactPersonBase(BaseModel): firstname: str lastname: str @@ -61,13 +92,12 @@ class ContactPerson(ContactPersonBase): class ContactPersonUpdate(BaseModel): - firstname: str | None = None - lastname: str | None = None - phone_number: str | None = None - email: EmailStr | None = None + firstname: Optional[str] = None + lastname: Optional[str] = None + phone_number: Optional[str] = None + email: Optional[EmailStr] = None -# Address schemas class AddressCreate(BaseModel): street: str city: str @@ -83,10 +113,10 @@ class Address(AddressCreate): class AddressUpdate(BaseModel): - street: str | None = None - city: str | None = None - zipcode: str | None = None - country: str | None = None + street: Optional[str] = None + city: Optional[str] = None + zipcode: Optional[str] = None + country: Optional[str] = None class Sample(BaseModel): @@ -108,8 +138,6 @@ class SampleCreate(BaseModel): populate_by_name = True - -# Puck schemas class PuckBase(BaseModel): puck_name: str puck_type: str @@ -142,9 +170,11 @@ class Puck(BaseModel): from_attributes = True -# Dewar schemas class DewarBase(BaseModel): dewar_name: str + dewar_type_id: Optional[int] = None + dewar_serial_number_id: Optional[int] = None + unique_id: Optional[str] = None tracking_number: str number_of_pucks: int number_of_samples: int @@ -176,6 +206,9 @@ class Dewar(DewarBase): class DewarUpdate(BaseModel): dewar_name: Optional[str] = None + dewar_type_id: Optional[int] = None + dewar_serial_number_id: Optional[int] = None + unique_id: Optional[str] = None tracking_number: Optional[str] = None status: Optional[str] = None ready_date: Optional[date] = None @@ -186,6 +219,7 @@ class DewarUpdate(BaseModel): contact_person_id: Optional[int] = None address_id: Optional[int] = None + class DewarSchema(BaseModel): id: int dewar_name: str @@ -198,7 +232,6 @@ class DewarSchema(BaseModel): from_attributes = True -# Proposal schemas class Proposal(BaseModel): id: int number: str @@ -207,7 +240,6 @@ class Proposal(BaseModel): from_attributes = True -# Shipment schemas class Shipment(BaseModel): id: int shipment_name: str @@ -238,4 +270,4 @@ class ShipmentCreate(BaseModel): class UpdateShipmentComments(BaseModel): - comments: str + comments: str \ No newline at end of file diff --git a/frontend/src/components/DewarDetails.tsx b/frontend/src/components/DewarDetails.tsx index ee928dc..0330963 100644 --- a/frontend/src/components/DewarDetails.tsx +++ b/frontend/src/components/DewarDetails.tsx @@ -1,9 +1,34 @@ -import React, { useState, useEffect } from 'react'; -import { Box, Typography, TextField, Button, Select, MenuItem, Snackbar } from '@mui/material'; +import React, { useRef, useState, useEffect } from 'react'; +import { + Box, + Typography, + TextField, + Button, + Select, + MenuItem, + Snackbar, + FormControl, + InputLabel, + IconButton, + Tooltip, + Alert, +} from '@mui/material'; + import QRCode from 'react-qr-code'; -import { ContactPerson, Address, Dewar, ContactsService, AddressesService, DewarsService, ShipmentsService } from '../../openapi'; // Adjust path if necessary -import Unipuck from '../components/Unipuck'; // This path should be checked and corrected if necessary -import { Shipment } from "../types.ts"; // Correct or adjust as needed +import { + Dewar, + DewarType, + DewarSerialNumber, + ContactPerson, + Address, + ContactsService, + AddressesService, + DewarsService, + ShipmentsService, +} from '../../openapi'; +import Unipuck from '../components/Unipuck'; +import { saveAs } from 'file-saver'; +import DownloadIcon from '@mui/icons-material/Download'; interface DewarDetailsProps { dewar: Dewar; @@ -14,7 +39,6 @@ interface DewarDetailsProps { defaultContactPerson?: ContactPerson; defaultReturnAddress?: Address; shipmentId: number; - selectedShipment?: Shipment; } interface NewContactPerson { @@ -51,32 +75,86 @@ const DewarDetails: React.FC = ({ const [isCreatingContactPerson, setIsCreatingContactPerson] = useState(false); const [isCreatingReturnAddress, setIsCreatingReturnAddress] = useState(false); const [puckStatuses, setPuckStatuses] = useState([]); - const [newContactPerson, setNewContactPerson] = useState({ id: 0, firstName: '', lastName: '', phone_number: '', email: '' }); - const [newReturnAddress, setNewReturnAddress] = useState({ id: 0, street: '', city: '', zipcode: '', country: '' }); + const [newContactPerson, setNewContactPerson] = useState({ + id: 0, + firstName: '', + lastName: '', + phone_number: '', + email: '', + }); + const [newReturnAddress, setNewReturnAddress] = useState({ + id: 0, + street: '', + city: '', + zipcode: '', + country: '', + }); const [changesMade, setChangesMade] = useState(false); - const [feedbackMessage, setFeedbackMessage] = useState(''); - const [openSnackbar, setOpenSnackbar] = useState(false); + const [feedbackMessage, setFeedbackMessage] = useState(''); + const [openSnackbar, setOpenSnackbar] = useState(false); + const [newDewarType, setNewDewarType] = useState(''); + const [newDewarSerialNumber, setNewDewarSerialNumber] = useState(''); + const [selectedDewarType, setSelectedDewarType] = useState(dewar.dewar_type_id?.toString() || ''); + const [knownDewarTypes, setKnownDewarTypes] = useState([]); + const [knownSerialNumbers, setKnownSerialNumbers] = useState([]); + const [selectedSerialNumber, setSelectedSerialNumber] = useState(''); + const [isQRCodeGenerated, setIsQRCodeGenerated] = useState(false); + const [qrCodeValue, setQrCodeValue] = useState(dewar.qrcode || ''); + const qrCodeRef = useRef(null); // useEffect(() => { + const fetchDewarTypes = async () => { + try { + const response = await DewarsService.getDewarTypesDewarsDewarTypesGet(); + setKnownDewarTypes(response ?? []); + } catch (error) { + setFeedbackMessage('Failed to fetch dewar types.'); + setOpenSnackbar(true); + console.error('Error fetching dewar types:', error); + } + }; + fetchDewarTypes(); + }, []); + + // Fetch known serial numbers + useEffect(() => { + const fetchSerialNumbers = async () => { + try { + const response = await DewarsService.getAllSerialNumbersDewarsDewarSerialNumbersGet(); + setKnownSerialNumbers(response ?? []); + } catch (error) { + setFeedbackMessage('Failed to fetch serial numbers.'); + setOpenSnackbar(true); + console.error('Error fetching serial numbers:', error); + } + }; + fetchSerialNumbers(); + }, []); + + useEffect(() => { + setLocalTrackingNumber(dewar.tracking_number || ''); + const setInitialContactPerson = () => { setSelectedContactPerson( - dewar.contact_person?.id?.toString() || - defaultContactPerson?.id?.toString() || - '' + dewar.contact_person?.id?.toString() || defaultContactPerson?.id?.toString() || '' ); }; const setInitialReturnAddress = () => { setSelectedReturnAddress( - dewar.return_address?.id?.toString() || - defaultReturnAddress?.id?.toString() || - '' + dewar.return_address?.id?.toString() || defaultReturnAddress?.id?.toString() || '' ); }; - setLocalTrackingNumber(dewar.tracking_number || ''); setInitialContactPerson(); setInitialReturnAddress(); + + if (dewar.dewar_type_id) { + setSelectedDewarType(dewar.dewar_type_id.toString()); + } + if (dewar.dewar_serial_number_id) { + setSelectedSerialNumber(dewar.dewar_serial_number_id.toString()); + } }, [dewar, defaultContactPerson, defaultReturnAddress]); useEffect(() => { @@ -108,17 +186,19 @@ const DewarDetails: React.FC = ({ const fetchSamples = async () => { if (dewar.id) { try { - const fetchedSamples = await ShipmentsService.getSamplesInDewarShipmentsShipmentsShipmentIdDewarsDewarIdSamplesGet(shipmentId, dewar.id); - console.log("Fetched Samples: ", fetchedSamples); + const fetchedSamples = await ShipmentsService.getSamplesInDewarShipmentsShipmentsShipmentIdDewarsDewarIdSamplesGet( + shipmentId, + dewar.id + ); - const updatedPuckStatuses = (dewar.pucks ?? []).map(puck => { - const puckSamples = fetchedSamples.filter(sample => sample.puck_id === puck.id); + const updatedPuckStatuses = (dewar.pucks ?? []).map((puck) => { + const puckSamples = fetchedSamples.filter((sample) => sample.puck_id === puck.id); const statusArray = Array(16).fill('empty'); - puckSamples.forEach(sample => { + puckSamples.forEach((sample) => { if (sample.position >= 1 && sample.position <= 16) { - statusArray[sample.position - 1] = 'filled'; // Corrected line + statusArray[sample.position - 1] = 'filled'; } }); @@ -127,7 +207,7 @@ const DewarDetails: React.FC = ({ setPuckStatuses(updatedPuckStatuses); } catch (error) { - console.error("Error fetching samples:", error); + console.error('Error fetching samples:', error); } } }; @@ -135,15 +215,62 @@ const DewarDetails: React.FC = ({ fetchSamples(); }, [dewar, shipmentId]); + useEffect(() => { + setSelectedDewarType( + knownDewarTypes.find((type) => type.id === dewar.dewar_type_id)?.id.toString() || '' + ); + }, [knownDewarTypes, dewar.dewar_type_id]); + + useEffect(() => { + setSelectedSerialNumber( + knownSerialNumbers.find((sn) => sn.id === dewar.dewar_serial_number_id)?.id.toString() || '' + ); + }, [knownSerialNumbers, dewar.dewar_serial_number_id]); + 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); if (!dewar) return No dewar selected.; + const handleSaveNewDewarTypeAndSerialNumber = async () => { + if (newDewarType) { + try { + const typeResponse = await DewarsService.createDewarTypeDewarsDewarTypesPost({ dewar_type: newDewarType }); + const serialResponse = await DewarsService.createDewarSerialNumberDewarsDewarSerialNumbersPost({ + serial_number: newDewarSerialNumber, + dewar_type_id: typeResponse.id, + }); + setKnownDewarTypes([...knownDewarTypes, typeResponse]); + setKnownSerialNumbers([...knownSerialNumbers, serialResponse]); + setSelectedDewarType(typeResponse.id.toString()); + setSelectedSerialNumber(serialResponse.serial_number); + setNewDewarType(''); + setNewDewarSerialNumber(''); + setChangesMade(true); + } catch (error) { + setFeedbackMessage('Failed to save new dewar type and serial number.'); + setOpenSnackbar(true); + } + } else if (newDewarSerialNumber && selectedDewarType) { + try { + const response = await DewarsService.createDewarSerialNumberDewarsDewarSerialNumbersPost({ + serial_number: newDewarSerialNumber, + dewar_type_id: parseInt(selectedDewarType, 10), + }); + setKnownSerialNumbers([...knownSerialNumbers, response]); + setSelectedSerialNumber(response.serial_number); + setNewDewarSerialNumber(''); + setChangesMade(true); + } catch (error) { + setFeedbackMessage('Failed to save new serial number.'); + setOpenSnackbar(true); + } + } + }; + const handleAddContact = async () => { - if (!validateEmail(newContactPerson.email) || !validatePhoneNumber(newContactPerson.phone_number) || - !newContactPerson.firstName || !newContactPerson.lastName) { + if (!validateEmail(newContactPerson.email) || !validatePhoneNumber(newContactPerson.phone_number) || !newContactPerson.firstName || !newContactPerson.lastName) { setFeedbackMessage('Please fill in all new contact person fields correctly.'); setOpenSnackbar(true); return; @@ -189,7 +316,13 @@ const DewarDetails: React.FC = ({ const a = await AddressesService.createReturnAddressAddressesPost(payload); setReturnAddresses([...returnAddresses, a]); setFeedbackMessage('Return address added successfully.'); - setNewReturnAddress({ id: 0, 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.'); @@ -209,7 +342,7 @@ const DewarDetails: React.FC = ({ }; if (!selectedContactPerson || !selectedReturnAddress) { - setFeedbackMessage("Please ensure all required fields are filled."); + setFeedbackMessage('Please ensure all required fields are filled.'); setOpenSnackbar(true); return; } @@ -217,15 +350,16 @@ const DewarDetails: React.FC = ({ const dewarId = dewar.id; if (!dewarId) { - setFeedbackMessage("Invalid Dewar ID. Please ensure Dewar ID is provided."); + setFeedbackMessage('Invalid Dewar ID. Please ensure Dewar ID is provided.'); setOpenSnackbar(true); return; } try { const payload = { - dewar_id: dewarId, dewar_name: dewar.dewar_name, + dewar_type_id: parseInt(selectedDewarType, 10), + dewar_serial_number_id: parseInt(selectedSerialNumber, 10), tracking_number: localTrackingNumber, number_of_pucks: dewar.number_of_pucks, number_of_samples: dewar.number_of_samples, @@ -240,10 +374,60 @@ const DewarDetails: React.FC = ({ }; await DewarsService.updateDewarDewarsDewarIdPut(dewarId, payload); - setFeedbackMessage("Changes saved successfully."); + setFeedbackMessage('Changes saved successfully.'); setChangesMade(false); } catch (error) { - setFeedbackMessage("Failed to save changes. Please try again later."); + setFeedbackMessage('Failed to save changes. Please try again later.'); + setOpenSnackbar(true); + } + }; + + const handleSerialNumberChange = (value: string) => { + setSelectedSerialNumber(value); + const serialNumber = knownSerialNumbers.find((sn) => sn.id.toString() === value); + if (serialNumber) { + setSelectedDewarType(serialNumber.dewar_type_id.toString()); + } + setChangesMade(true); + }; + + const handleGenerateQRCode = async () => { + if (!dewar) return; + + try { + const response = await DewarsService.generateDewarQrcodeDewarsDewarIdGenerateQrcodePost(dewar.id); + setQrCodeValue(response.qrcode); // assuming the backend returns the QR code value + setIsQRCodeGenerated(true); // to track the state if the QR code is generated + setFeedbackMessage("QR Code generated successfully"); + setOpenSnackbar(true); + } catch (error) { + console.error("Failed to generate QR code:", error); + setFeedbackMessage("QR Code generation failed"); + setOpenSnackbar(true); + } + }; + + const handleDownloadLabel = async () => { + if (!dewar) return; + + try { + const response = await DewarsService.downloadDewarLabelDewarsDewarIdDownloadLabelGet(dewar.id); + + // The response object might need parsing + const blob = new Blob([response as any], { type: 'application/pdf' }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `dewar_label_${dewar.id}.pdf`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + setFeedbackMessage("Label downloaded successfully"); + setOpenSnackbar(true); + } catch (error) { + console.error("Failed to download label:", error); + setFeedbackMessage("Label download failed"); setOpenSnackbar(true); } }; @@ -254,7 +438,7 @@ const DewarDetails: React.FC = ({ { + onChange={(e) => { setLocalTrackingNumber(e.target.value); setTrackingNumber(e.target.value); setChangesMade(true); @@ -262,41 +446,121 @@ const DewarDetails: React.FC = ({ variant="outlined" sx={{ width: '300px', marginBottom: 2 }} /> - - - {dewar.qrcode ? ( - + + Dewar Serial Number + + {selectedSerialNumber === 'add' && ( + + + Dewar Type + + {selectedDewarType === 'add' && ( + + setNewDewarType(e.target.value)} + variant="outlined" + sx={{ width: '300px', marginBottom: 2 }} + /> + + )} + + setNewDewarSerialNumber(e.target.value)} + variant="outlined" + sx={{ width: '300px', marginBottom: 2 }} + /> + + + )} + + + + + {qrCodeValue ? ( + + + + + + + + + Label is ready for download + + ) : ( No QR code available )} - + Number of Pucks: {dewar.number_of_pucks} - {(dewar.pucks ?? []).length > 0 - ? - : No pucks attached to the dewar.} + {(dewar.pucks ?? []).length > 0 ? ( + + ) : ( + No pucks attached to the dewar. + )} Number of Samples: {dewar.number_of_samples} + Current Contact Person: { + onChange={(e) => { const value = e.target.value; setSelectedReturnAddress(value); setIsCreatingReturnAddress(value === 'add'); setChangesMade(true); }} - fullWidth - sx={{ marginBottom: 2 }} - variant="outlined" displayEmpty + sx={{ width: '300px', marginBottom: 2 }} > - {returnAddresses.map(address => ( - - {address.street}, {address.city} + {returnAddresses.map((address) => ( + + {address.street}, {address.city}, {address.zipcode}, {address.country} ))} Add New Return Address @@ -371,53 +646,77 @@ const DewarDetails: React.FC = ({ setNewReturnAddress({ ...newReturnAddress, street: e.target.value })} + onChange={(e) => + setNewReturnAddress((prev) => ({ + ...prev, + street: e.target.value, + })) + } variant="outlined" - fullWidth - sx={{ marginBottom: 1 }} + sx={{ width: '300px', marginBottom: 2 }} /> setNewReturnAddress({ ...newReturnAddress, city: e.target.value })} + onChange={(e) => + setNewReturnAddress((prev) => ({ + ...prev, + city: e.target.value, + })) + } variant="outlined" - fullWidth - sx={{ marginBottom: 1 }} + sx={{ width: '300px', marginBottom: 2 }} /> setNewReturnAddress({ ...newReturnAddress, zipcode: e.target.value })} + onChange={(e) => + setNewReturnAddress((prev) => ({ + ...prev, + zipcode: e.target.value, + })) + } variant="outlined" - fullWidth - sx={{ marginBottom: 1 }} - error={!validateZipCode(newReturnAddress.zipcode)} - helperText={!validateZipCode(newReturnAddress.zipcode) ? "Invalid zip code" : ""} + sx={{ width: '300px', marginBottom: 2 }} /> setNewReturnAddress({ ...newReturnAddress, country: e.target.value })} + onChange={(e) => + setNewReturnAddress((prev) => ({ + ...prev, + country: e.target.value, + })) + } variant="outlined" - fullWidth - sx={{ marginBottom: 1 }} + sx={{ width: '300px', marginBottom: 2 }} /> - )} - {changesMade && ( - - )} + + setOpenSnackbar(false)} - message={feedbackMessage} - /> + > + setOpenSnackbar(false)} severity="info" sx={{ width: '100%' }}> + {feedbackMessage} + + ); };