added dewar type, serial number, generate unique id, qr code and generate label

This commit is contained in:
GotthardG 2024-11-14 23:17:20 +01:00
parent ca11a359f9
commit 6083c72a1d
8 changed files with 684 additions and 181 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<DewarDetailsProps> = ({
const [isCreatingContactPerson, setIsCreatingContactPerson] = useState(false);
const [isCreatingReturnAddress, setIsCreatingReturnAddress] = useState(false);
const [puckStatuses, setPuckStatuses] = useState<string[][]>([]);
const [newContactPerson, setNewContactPerson] = useState<NewContactPerson>({ id: 0, firstName: '', lastName: '', phone_number: '', email: '' });
const [newReturnAddress, setNewReturnAddress] = useState<NewReturnAddress>({ id: 0, street: '', city: '', zipcode: '', country: '' });
const [newContactPerson, setNewContactPerson] = useState<NewContactPerson>({
id: 0,
firstName: '',
lastName: '',
phone_number: '',
email: '',
});
const [newReturnAddress, setNewReturnAddress] = useState<NewReturnAddress>({
id: 0,
street: '',
city: '',
zipcode: '',
country: '',
});
const [changesMade, setChangesMade] = useState(false);
const [feedbackMessage, setFeedbackMessage] = useState('');
const [openSnackbar, setOpenSnackbar] = useState(false);
const [feedbackMessage, setFeedbackMessage] = useState<string>('');
const [openSnackbar, setOpenSnackbar] = useState<boolean>(false);
const [newDewarType, setNewDewarType] = useState<string>('');
const [newDewarSerialNumber, setNewDewarSerialNumber] = useState<string>('');
const [selectedDewarType, setSelectedDewarType] = useState<string>(dewar.dewar_type_id?.toString() || '');
const [knownDewarTypes, setKnownDewarTypes] = useState<DewarType[]>([]);
const [knownSerialNumbers, setKnownSerialNumbers] = useState<DewarSerialNumber[]>([]);
const [selectedSerialNumber, setSelectedSerialNumber] = useState<string>('');
const [isQRCodeGenerated, setIsQRCodeGenerated] = useState(false);
const [qrCodeValue, setQrCodeValue] = useState(dewar.qrcode || '');
const qrCodeRef = useRef<HTMLCanvasElement>(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<DewarDetailsProps> = ({
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<DewarDetailsProps> = ({
setPuckStatuses(updatedPuckStatuses);
} catch (error) {
console.error("Error fetching samples:", error);
console.error('Error fetching samples:', error);
}
}
};
@ -135,15 +215,62 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
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 <Typography>No dewar selected.</Typography>;
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<DewarDetailsProps> = ({
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<DewarDetailsProps> = ({
};
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<DewarDetailsProps> = ({
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<DewarDetailsProps> = ({
};
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<DewarDetailsProps> = ({
<TextField
label="Tracking Number"
value={localTrackingNumber}
onChange={e => {
onChange={(e) => {
setLocalTrackingNumber(e.target.value);
setTrackingNumber(e.target.value);
setChangesMade(true);
@ -262,41 +446,121 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
variant="outlined"
sx={{ width: '300px', marginBottom: 2 }}
/>
<Box sx={{ display: 'flex', alignItems: 'center', marginBottom: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column' }}>
{dewar.qrcode ? (
<QRCode value={dewar.qrcode} size={70} />
<FormControl variant="outlined" sx={{ width: '300px', marginBottom: 2 }}>
<InputLabel id="dewar-serial-number-label">Dewar Serial Number</InputLabel>
<Select
labelId="dewar-serial-number-label"
label="Dewar Serial Number"
value={selectedSerialNumber}
onChange={(e) => handleSerialNumberChange(e.target.value as string)}
displayEmpty
>
{knownSerialNumbers.map((sn) => (
<MenuItem key={sn.id} value={sn.id.toString()}>
{sn.serial_number}
</MenuItem>
))}
<MenuItem value="add">Add New Serial Number</MenuItem>
</Select>
{selectedSerialNumber === 'add' && (
<Box>
<FormControl variant="outlined" sx={{ width: '300px', marginBottom: 2 }}>
<InputLabel id="dewar-type-label">Dewar Type</InputLabel>
<Select
labelId="dewar-type-label"
label="Dewar Type"
value={selectedDewarType}
onChange={(e) => {
const value = e.target.value as string;
setSelectedDewarType(value);
setChangesMade(true);
}}
displayEmpty
>
{knownDewarTypes.map((type) => (
<MenuItem key={type.id} value={type.id.toString()}>
{type.dewar_type}
</MenuItem>
))}
<MenuItem value="add">Add New Dewar Type</MenuItem>
</Select>
{selectedDewarType === 'add' && (
<Box>
<TextField
label="Add New Dewar Type"
value={newDewarType}
onChange={(e) => setNewDewarType(e.target.value)}
variant="outlined"
sx={{ width: '300px', marginBottom: 2 }}
/>
</Box>
)}
</FormControl>
<TextField
label="Add New Serial Number"
value={newDewarSerialNumber}
onChange={(e) => setNewDewarSerialNumber(e.target.value)}
variant="outlined"
sx={{ width: '300px', marginBottom: 2 }}
/>
<Button onClick={handleSaveNewDewarTypeAndSerialNumber} variant="contained">
Save Dewar Type and Serial Number
</Button>
</Box>
)}
</FormControl>
<Box sx={{ marginTop: 2 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: 2 }}>
{qrCodeValue ? (
<Box sx={{ textAlign: 'center', marginBottom: 2 }}>
<QRCode id="qrCodeCanvas" value={qrCodeValue} size={150} />
<Box sx={{ display: 'flex', alignItems: 'center', marginTop: 1 }}>
<Tooltip title="Download Label">
<IconButton onClick={handleDownloadLabel} sx={{ transform: 'scale(1.5)', margin: 1 }}>
<DownloadIcon />
</IconButton>
</Tooltip>
<Typography variant="body2">Label is ready for download</Typography>
</Box>
</Box>
) : (
<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={handleGenerateQRCode}
>
Generate QR Code
</Button>
</Box>
</Box>
<Box sx={{ marginTop: 2 }}>
<Typography variant="body1">Number of Pucks: {dewar.number_of_pucks}</Typography>
{(dewar.pucks ?? []).length > 0
? <Unipuck pucks={(dewar.pucks ?? []).length} samples={puckStatuses} />
: <Typography>No pucks attached to the dewar.</Typography>}
{(dewar.pucks ?? []).length > 0 ? (
<Unipuck pucks={(dewar.pucks ?? []).length} samples={puckStatuses} />
) : (
<Typography>No pucks attached to the dewar.</Typography>
)}
<Typography variant="body1">Number of Samples: {dewar.number_of_samples}</Typography>
</Box>
<Typography variant="body1">Current Contact Person:</Typography>
<Select
value={selectedContactPerson}
onChange={e => {
onChange={(e) => {
const value = e.target.value;
setSelectedContactPerson(value);
setIsCreatingContactPerson(value === 'add');
setChangesMade(true);
}}
fullWidth
sx={{ marginBottom: 2 }}
variant="outlined"
displayEmpty
sx={{ width: '300px', marginBottom: 2 }}
>
{contactPersons.map(person => (
<MenuItem key={person.id?.toString()} value={person.id?.toString() || ''}>
{contactPersons.map((person) => (
<MenuItem key={person.id} value={person.id?.toString()}>
{person.firstname} {person.lastname}
</MenuItem>
))}
@ -307,61 +571,72 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
<TextField
label="First Name"
value={newContactPerson.firstName}
onChange={e => setNewContactPerson({ ...newContactPerson, firstName: e.target.value })}
onChange={(e) =>
setNewContactPerson((prev) => ({
...prev,
firstName: e.target.value,
}))
}
variant="outlined"
fullWidth
sx={{ marginBottom: 1 }}
sx={{ width: '300px', marginBottom: 2 }}
/>
<TextField
label="Last Name"
value={newContactPerson.lastName}
onChange={e => setNewContactPerson({ ...newContactPerson, lastName: e.target.value })}
onChange={(e) =>
setNewContactPerson((prev) => ({
...prev,
lastName: e.target.value,
}))
}
variant="outlined"
fullWidth
sx={{ marginBottom: 1 }}
sx={{ width: '300px', marginBottom: 2 }}
/>
<TextField
label="Phone"
label="Phone Number"
value={newContactPerson.phone_number}
onChange={e => setNewContactPerson({ ...newContactPerson, phone_number: e.target.value })}
onChange={(e) =>
setNewContactPerson((prev) => ({
...prev,
phone_number: e.target.value,
}))
}
variant="outlined"
fullWidth
sx={{ marginBottom: 1 }}
error={!validatePhoneNumber(newContactPerson.phone_number)}
helperText={!validatePhoneNumber(newContactPerson.phone_number) ? "Invalid phone number" : ""}
sx={{ width: '300px', marginBottom: 2 }}
/>
<TextField
label="Email"
value={newContactPerson.email}
onChange={e => setNewContactPerson({ ...newContactPerson, email: e.target.value })}
onChange={(e) =>
setNewContactPerson((prev) => ({
...prev,
email: e.target.value,
}))
}
variant="outlined"
fullWidth
sx={{ marginBottom: 1 }}
error={!validateEmail(newContactPerson.email)}
helperText={!validateEmail(newContactPerson.email) ? "Invalid email" : ""}
sx={{ width: '300px', marginBottom: 2 }}
/>
<Button variant="contained" onClick={handleAddContact}>
Save Contact Person
<Button onClick={handleAddContact} variant="contained">
Save New Contact Person
</Button>
</Box>
)}
<Typography variant="body1">Current Return Address:</Typography>
<Select
value={selectedReturnAddress}
onChange={e => {
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 => (
<MenuItem key={address.id?.toString()} value={address.id?.toString() || ''}>
{address.street}, {address.city}
{returnAddresses.map((address) => (
<MenuItem key={address.id} value={address.id?.toString()}>
{address.street}, {address.city}, {address.zipcode}, {address.country}
</MenuItem>
))}
<MenuItem value="add">Add New Return Address</MenuItem>
@ -371,53 +646,77 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
<TextField
label="Street"
value={newReturnAddress.street}
onChange={e => 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 }}
/>
<TextField
label="City"
value={newReturnAddress.city}
onChange={e => 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 }}
/>
<TextField
label="Zip Code"
value={newReturnAddress.zipcode}
onChange={e => 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 }}
/>
<TextField
label="Country"
value={newReturnAddress.country}
onChange={e => 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 }}
/>
<Button variant="contained" onClick={handleAddAddress}>
Save Return Address
<Button onClick={handleAddAddress} variant="contained">
Save New Return Address
</Button>
</Box>
)}
{changesMade && (
<Button variant="contained" color="primary" onClick={handleSaveChanges} sx={{ marginTop: 2 }}>
<Box sx={{ marginTop: 2 }}>
<Button
variant="contained"
color="primary"
onClick={handleSaveChanges}
disabled={!changesMade}
>
Save Changes
</Button>
)}
</Box>
<Snackbar
open={openSnackbar}
autoHideDuration={6000}
onClose={() => setOpenSnackbar(false)}
message={feedbackMessage}
/>
>
<Alert onClose={() => setOpenSnackbar(false)} severity="info" sx={{ width: '100%' }}>
{feedbackMessage}
</Alert>
</Snackbar>
</Box>
);
};