Better integration of sqlite3 database

This commit is contained in:
GotthardG 2024-11-02 00:54:37 +01:00
parent 48cd233231
commit a01114a178
18 changed files with 835 additions and 582 deletions

20
backend/app/crud.py Normal file
View File

@ -0,0 +1,20 @@
from sqlalchemy.orm import Session, joinedload
def get_shipments(db: Session):
from app.models import Shipment
return db.query(Shipment).options(
joinedload(Shipment.contact_person),
joinedload(Shipment.return_address),
joinedload(Shipment.proposal),
joinedload(Shipment.dewars)
).all()
def get_shipment_by_id(db: Session, shipment_id: str):
from app.models import Shipment
shipment = db.query(Shipment).options(
joinedload(Shipment.contact_person),
joinedload(Shipment.return_address),
joinedload(Shipment.proposal),
joinedload(Shipment.dewars)
).filter(Shipment.shipment_id == shipment_id).first()
return shipment

View File

@ -1,62 +1,75 @@
from typing import List
from app.models import ContactPerson, Address, Dewar, Proposal, Shipment
from datetime import datetime
contacts: List[ContactPerson] = [
ContactPerson(id=1, firstname="Frodo", lastname="Baggins", phone_number="123-456-7890", email="frodo.baggins@lotr.com"),
ContactPerson(id=1, firstname="Frodo", lastname="Baggins", phone_number="123-456-7890", email="frodo.baggins@lotr.com"),
ContactPerson(id=2, firstname="Samwise", lastname="Gamgee", phone_number="987-654-3210", email="samwise.gamgee@lotr.com"),
ContactPerson(id=3, firstname="Aragorn", lastname="Elessar", phone_number="123-333-4444", email="aragorn.elessar@lotr.com"),
ContactPerson(id=4, firstname="Legolas", lastname="Greenleaf", phone_number="555-666-7777", email="legolas.greenleaf@lotr.com"),
ContactPerson(id=5, firstname="Gimli", lastname="Son of Gloin", phone_number="888-999-0000", email="gimli.sonofgloin@lotr.com"),
ContactPerson(id=6, firstname="Gandalf", lastname="The Grey", phone_number="222-333-4444", email="gandalf.thegrey@lotr.com"),
ContactPerson(id=7, firstname="Boromir", lastname="Son of Denethor", phone_number="111-222-3333", email="boromir.sonofdenethor@lotr.com"),
ContactPerson(id=8, firstname="Galadriel", lastname="Lady of Lothlórien", phone_number="444-555-6666", email="galadriel.lothlorien@lotr.com"),
ContactPerson(id=9, firstname="Elrond", lastname="Half-elven", phone_number="777-888-9999", email="elrond.halfelven@lotr.com"),
ContactPerson(id=10, firstname="Eowyn", lastname="Shieldmaiden of Rohan", phone_number="000-111-2222", email="eowyn.rohan@lotr.com"),
contacts = [
ContactPerson(id=1, firstname="Frodo", lastname="Baggins", phone_number="123-456-7890",
email="frodo.baggins@lotr.com"),
ContactPerson(id=2, firstname="Samwise", lastname="Gamgee", phone_number="987-654-3210",
email="samwise.gamgee@lotr.com"),
ContactPerson(id=3, firstname="Aragorn", lastname="Elessar", phone_number="123-333-4444",
email="aragorn.elessar@lotr.com"),
ContactPerson(id=4, firstname="Legolas", lastname="Greenleaf", phone_number="555-666-7777",
email="legolas.greenleaf@lotr.com"),
ContactPerson(id=5, firstname="Gimli", lastname="Son of Gloin", phone_number="888-999-0000",
email="gimli.sonofgloin@lotr.com"),
ContactPerson(id=6, firstname="Gandalf", lastname="The Grey", phone_number="222-333-4444",
email="gandalf.thegrey@lotr.com"),
ContactPerson(id=7, firstname="Boromir", lastname="Son of Denethor", phone_number="111-222-3333",
email="boromir.sonofdenethor@lotr.com"),
ContactPerson(id=8, firstname="Galadriel", lastname="Lady of Lothlórien", phone_number="444-555-6666",
email="galadriel.lothlorien@lotr.com"),
ContactPerson(id=9, firstname="Elrond", lastname="Half-elven", phone_number="777-888-9999",
email="elrond.halfelven@lotr.com"),
ContactPerson(id=10, firstname="Eowyn", lastname="Shieldmaiden of Rohan", phone_number="000-111-2222",
email="eowyn.rohan@lotr.com"),
]
return_addresses: List[Address] = [
return_addresses = [
Address(id=1, street='123 Hobbiton St', city='Shire', zipcode='12345', country='Middle Earth'),
Address(id=2, street='456 Rohan Rd', city='Edoras', zipcode='67890', country='Middle Earth'),
Address(id=3, street='789 Greenwood Dr', city='Mirkwood', zipcode='13579', country='Middle Earth'),
Address(id=4, street='321 Gondor Ave', city='Minas Tirith', zipcode='24680', country='Middle Earth'),
Address(id=5, street='654 Falgorn Pass', city='Rivendell', zipcode='11223', country='Middle Earth')
Address(id=5, street='654 Falgorn Pass', city='Rivendell', zipcode='11223', country='Middle Earth'),
]
dewars: List[Dewar] = [
dewars = [
Dewar(
id='DEWAR001', dewar_name='Dewar One', tracking_number='TRACK123', number_of_pucks=7, number_of_samples=70,
return_address=[return_addresses[0]], contact_person=[contacts[0]], status='Ready for Shipping',
ready_date='2023-09-30', shipping_date='', arrival_date='', returning_date='', qrcode='QR123DEWAR001'
return_address_id=1, contact_person_id=1, status='Ready for Shipping',
ready_date=datetime.strptime('2023-09-30', '%Y-%m-%d'), shipping_date=None, arrival_date=None,
returning_date=None, qrcode='QR123DEWAR001',
),
Dewar(
id='DEWAR002', dewar_name='Dewar Two', tracking_number='TRACK124', number_of_pucks=3, number_of_samples=33,
return_address=[return_addresses[1]], contact_person=[contacts[1]], status='In Preparation',
ready_date='', shipping_date='', arrival_date='', returning_date='', qrcode='QR123DEWAR002'
return_address_id=2, contact_person_id=2, status='In Preparation',
ready_date=None, shipping_date=None, arrival_date=None, returning_date=None, qrcode='QR123DEWAR002',
),
Dewar(
id='DEWAR003', dewar_name='Dewar Three', tracking_number='TRACK125', number_of_pucks=7, number_of_samples=72,
return_address=[return_addresses[0]], contact_person=[contacts[2]], status='Not Shipped', ready_date='2024.01.01',
shipping_date='', arrival_date='', returning_date='', qrcode='QR123DEWAR003'
return_address_id=1, contact_person_id=3, status='Not Shipped',
ready_date=datetime.strptime('2024-01-01', '%Y-%m-%d'), shipping_date=None, arrival_date=None,
returning_date=None, qrcode='QR123DEWAR003',
),
Dewar(
id='DEWAR004', dewar_name='Dewar Four', tracking_number='', number_of_pucks=7, number_of_samples=70,
return_address=[return_addresses[0]], contact_person=[contacts[2]], status='Delayed', ready_date='2024.01.01',
shipping_date='2024.01.02', arrival_date='', returning_date='', qrcode='QR123DEWAR003'
return_address_id=1, contact_person_id=3, status='Delayed',
ready_date=datetime.strptime('2024-01-01', '%Y-%m-%d'), shipping_date=datetime.strptime('2024-01-02', '%Y-%m-%d'),
arrival_date=None, returning_date=None, qrcode='QR123DEWAR004',
),
Dewar(
id='DEWAR005', dewar_name='Dewar Five', tracking_number='', number_of_pucks=3, number_of_samples=30,
return_address=[return_addresses[0]], contact_person=[contacts[2]], status='Returned', ready_date='2024.01.01',
shipping_date='2024.01.02', arrival_date='2024.01.03', returning_date='2024.01.07', qrcode='QR123DEWAR003'
return_address_id=1, contact_person_id=3, status='Returned',
arrival_date=datetime.strptime('2024-01-03', '%Y-%m-%d'), returning_date=datetime.strptime('2024-01-07', '%Y-%m-%d'),
qrcode='QR123DEWAR005',
),
]
proposals: List[Proposal] = [
Proposal(id=1, number="PROPOSAL-FRODO-001"), # "The Quest for the Ring"
Proposal(id=2, number="PROPOSAL-GANDALF-002"), # "The Fellowship's Journey"
Proposal(id=3, number="PROPOSAL-ARAGORN-003"), # "Return of the King"
Proposal(id=4, number="PROPOSAL-SAURON-004"), # "The Dark Lord's Plot"
Proposal(id=5, number="PROPOSAL-MORDOR-005"), # "The Road to Mount Doom"
proposals = [
Proposal(id=1, number="PROPOSAL-FRODO-001"),
Proposal(id=2, number="PROPOSAL-GANDALF-002"),
Proposal(id=3, number="PROPOSAL-ARAGORN-003"),
Proposal(id=4, number="PROPOSAL-SAURON-004"),
Proposal(id=5, number="PROPOSAL-MORDOR-005"),
]
specific_dewar_ids1 = ['DEWAR003']
@ -69,18 +82,18 @@ specific_dewars3 = [dewar for dewar in dewars if dewar.id in specific_dewar_ids3
shipments = [
Shipment(
shipment_id='SHIPMORDOR', shipment_date='2024-10-10', shipment_name='Shipment from Mordor',
shipment_status='Delivered', contact_person=[contacts[1]], proposal_number=[proposals[1]],
return_address=[return_addresses[0]], comments='Handle with care', dewars=specific_dewars1
shipment_id="SHIPMENT001", shipment_date=datetime.strptime('2024-10-10', '%Y-%m-%d'),
shipment_name='Shipment from Mordor', shipment_status='Delivered', contact_person_id=2,
proposal_id=3, return_address_id=1, comments='Handle with care', dewars=specific_dewars1
),
Shipment(
shipment_id='SHIPMORDOR2', shipment_date='2024-10-24', shipment_name='Shipment from Mordor',
shipment_status='In Transit', contact_person=[contacts[3]], proposal_number=[proposals[2]],
return_address=[return_addresses[1]], comments='Contains the one ring', dewars=specific_dewars2
shipment_id="SHIPMENT002", shipment_date=datetime.strptime('2024-10-24', '%Y-%m-%d'),
shipment_name='Shipment from Mordor', shipment_status='In Transit', contact_person_id=4,
proposal_id=4, return_address_id=2, comments='Contains the one ring', dewars=specific_dewars2
),
Shipment(
shipment_id='SHIPMORDOR3', shipment_date='2024-10-28', shipment_name='Shipment from Mordor',
shipment_status='In Transit', contact_person=[contacts[4]], proposal_number=[proposals[3]],
return_address=[return_addresses[0]], comments='Contains the one ring', dewars=specific_dewars3
)
shipment_id="SHIPMENT003", shipment_date=datetime.strptime('2024-10-28', '%Y-%m-%d'),
shipment_name='Shipment from Mordor', shipment_status='In Transit', contact_person_id=5,
proposal_id=5, return_address_id=1, comments='Contains the one ring', dewars=specific_dewars3
),
]

View File

@ -1,15 +1,16 @@
from sqlalchemy.orm import Session
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "sqlite:///./test.db"
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
# Database setup
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Dependency
def get_db():
db = SessionLocal()
@ -18,7 +19,21 @@ def get_db():
finally:
db.close()
# Initialize the database
def init_db():
import app.models # Import all models here for metadata.create_all() to recognize them
# Import inside function to avoid circular dependency
from app import models
Base.metadata.create_all(bind=engine)
def load_sample_data(session: Session):
# Import inside function to avoid circular dependency
from app.data import contacts, return_addresses, dewars, proposals, shipments
from app import models # Ensure these imports are correct
if session.query(models.ContactPerson).first():
return
session.add_all(contacts + return_addresses + dewars + proposals + shipments)
session.commit()

View File

@ -0,0 +1,9 @@
# app/dependencies.py
from app.database import SessionLocal # Import SessionLocal from database.py
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

7
backend/app/init_db.py Normal file
View File

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

View File

@ -1,21 +1,37 @@
# app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.routers import address, contact, proposal, dewar, shipment
from app.database import Base, engine, SessionLocal, load_sample_data
app = FastAPI()
# Apply CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Enable CORS for all origins for now
allow_origins=["*"], # Enable CORS for all origins
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include your routers
@app.on_event("startup")
def on_startup():
# Drop and recreate database schema
Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine)
db = SessionLocal()
try:
load_sample_data(db)
finally:
db.close()
# Include routers with correct configuration
app.include_router(contact.router, prefix="/contacts", tags=["contacts"])
app.include_router(address.router, prefix="/return_addresses", tags=["return_addresses"])
app.include_router(address.router, prefix="/addresses", tags=["addresses"])
app.include_router(proposal.router, prefix="/proposals", tags=["proposals"])
app.include_router(dewar.router, prefix="/dewars", tags=["dewars"])
app.include_router(shipment.router, prefix="/shipments", tags=["shipments"])

View File

@ -1,61 +1,78 @@
from pydantic import BaseModel
from typing import List, Optional
from sqlalchemy import Column, Integer, String, Date, ForeignKey
from sqlalchemy.orm import relationship
from app.database import Base # Ensure this imports correctly
class ContactPerson(BaseModel):
id: Optional[int] = None
firstname: str
lastname: str
phone_number: str
email: str
class Address(BaseModel):
id: Optional[int] = None
street: str
city: str
zipcode: str
country: str
# SQLAlchemy ORM models
class Shipment(Base):
__tablename__ = "shipments"
class Proposal(BaseModel):
id: Optional[int] = None
number: str
shipment_id = Column(String, primary_key=True, index=True)
shipment_name = Column(String, index=True)
shipment_date = Column(Date)
shipment_status = Column(String)
comments = Column(String, nullable=True)
contact_person_id = Column(Integer, ForeignKey("contact_persons.id"))
return_address_id = Column(Integer, ForeignKey("addresses.id"))
proposal_id = Column(Integer, ForeignKey("proposals.id"))
class Dewar(BaseModel):
id: Optional[str] = None
dewar_name: str
tracking_number: Optional[str] = None
number_of_pucks: int
number_of_samples: int
return_address: List[Address]
contact_person: List[ContactPerson]
status: str
ready_date: Optional[str] = None
shipping_date: Optional[str] = None
arrival_date: Optional[str] = None
returning_date: Optional[str] = None
qrcode: str
contact_person = relationship("ContactPerson", back_populates="shipments")
return_address = relationship("Address", back_populates="shipments")
proposal = relationship("Proposal", back_populates="shipments")
dewars = relationship("Dewar", back_populates="shipment")
class Shipment(BaseModel):
shipment_id: Optional[str] = None
shipment_name: str
shipment_date: str
shipment_status: str
contact_person: List[ContactPerson]
proposal_number: List[Proposal]
return_address: List[Address]
comments: Optional[str] = None
dewars: List[Dewar]
def get_number_of_dewars(self) -> int:
return len(self.dewars)
class ContactPerson(Base):
__tablename__ = "contact_persons"
def get_shipment_contact_persons(self) -> List[ContactPerson]:
return self.contact_person
id = Column(Integer, primary_key=True, index=True)
firstname = Column(String)
lastname = Column(String)
phone_number = Column(String)
email = Column(String)
def get_shipment_return_addresses(self) -> List[Address]:
return self.return_address
shipments = relationship("Shipment", back_populates="contact_person")
def get_proposals(self) -> List[Proposal]:
return self.proposal_number
class Config:
from_attributes = True
class Address(Base):
__tablename__ = "addresses"
id = Column(Integer, primary_key=True, index=True)
street = Column(String)
city = Column(String)
zipcode = Column(String)
country = Column(String)
shipments = relationship("Shipment", back_populates="return_address")
class Dewar(Base):
__tablename__ = "dewars"
id = Column(String, primary_key=True, index=True)
dewar_name = Column(String)
tracking_number = Column(String)
number_of_pucks = Column(Integer)
number_of_samples = Column(Integer)
status = Column(String)
ready_date = Column(Date, nullable=True)
shipping_date = Column(Date, nullable=True)
arrival_date = Column(Date, nullable=True)
returning_date = Column(Date, nullable=True)
qrcode = Column(String)
shipment_id = Column(String, ForeignKey("shipments.shipment_id"))
return_address_id = Column(Integer, ForeignKey("addresses.id")) # Added
contact_person_id = Column(Integer, ForeignKey("contact_persons.id")) # Added
shipment = relationship("Shipment", back_populates="dewars")
return_address = relationship("Address") # Defines relationship with Address
contact_person = relationship("ContactPerson") # Defines relationship with ContactPerson
class Proposal(Base):
__tablename__ = "proposals"
id = Column(Integer, primary_key=True, index=True)
number = Column(String)
shipments = relationship("Shipment", back_populates="proposal")

View File

@ -1,26 +1,32 @@
from fastapi import APIRouter, HTTPException, status
from typing import List, Optional
from app.data.data import return_addresses
from app.models import Address # Import the Address model
from fastapi import APIRouter, HTTPException, status, Depends
from sqlalchemy.orm import Session
from typing import List
from app.schemas import Address as AddressSchema, AddressCreate
from app.models import Address as AddressModel
from app.dependencies import get_db
router = APIRouter()
@router.get("/", response_model=List[Address])
async def get_return_addresses():
return return_addresses
@router.get("/", response_model=List[AddressSchema])
async def get_return_addresses(db: Session = Depends(get_db)):
return db.query(AddressModel).all()
@router.post("/", response_model=Address, status_code=status.HTTP_201_CREATED)
async def create_return_address(address: Address):
if any(a.city == address.city for a in return_addresses):
@router.post("/", response_model=AddressSchema, status_code=status.HTTP_201_CREATED)
async def create_return_address(address: AddressCreate, db: Session = Depends(get_db)):
if db.query(AddressModel).filter(AddressModel.city == address.city).first():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Address in this city already exists."
)
if return_addresses:
max_id = max(a.id for a in return_addresses)
address.id = max_id + 1 if address.id is None else address.id
else:
address.id = 1 if address.id is None else address.id
return_addresses.append(address)
return address
db_address = AddressModel(
street=address.street,
city=address.city,
zipcode=address.zipcode,
country=address.country
)
db.add(db_address)
db.commit()
db.refresh(db_address)
return db_address

View File

@ -1,26 +1,31 @@
# app/routers/contact.py
from fastapi import APIRouter, HTTPException, status
from fastapi import APIRouter, HTTPException, status, Depends
from sqlalchemy.orm import Session
from typing import List
from app.data.data import contacts
from app.models import ContactPerson
from app.schemas import ContactPerson, ContactPersonCreate
from app.models import ContactPerson as ContactPersonModel
from app.dependencies import get_db
router = APIRouter()
@router.get("/", response_model=List[ContactPerson])
async def get_contacts():
return contacts
async def get_contacts(db: Session = Depends(get_db)):
return db.query(ContactPersonModel).all()
@router.post("/", response_model=ContactPerson, status_code=status.HTTP_201_CREATED)
async def create_contact(contact: ContactPerson):
if any(c.email == contact.email for c in contacts):
async def create_contact(contact: ContactPersonCreate, db: Session = Depends(get_db)):
if db.query(ContactPersonModel).filter(ContactPersonModel.email == contact.email).first():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="This contact already exists."
)
if contacts:
max_id = max(c.id for c in contacts)
contact.id = max_id + 1 if contact.id is None else contact.id
else:
contact.id = 1 if contact.id is None else contact.id
contacts.append(contact)
return contact
db_contact = ContactPersonModel(
firstname=contact.firstname,
lastname=contact.lastname,
phone_number=contact.phone_number,
email=contact.email
)
db.add(db_contact)
db.commit()
db.refresh(db_contact)
return db_contact

View File

@ -1,19 +1,38 @@
from fastapi import APIRouter, HTTPException, status
from typing import List, Optional
from fastapi import APIRouter, HTTPException, status, Depends
from sqlalchemy.orm import Session
from typing import List
import uuid
from app.data.data import dewars, contacts, return_addresses
from app.models import Dewar, ContactPerson, Address
from app.schemas import Dewar as DewarSchema, DewarCreate
from app.models import Dewar as DewarModel
from app.dependencies import get_db
router = APIRouter()
@router.get("/", response_model=List[Dewar])
async def get_dewars():
return dewars
@router.get("/", response_model=List[DewarSchema])
async def get_dewars(db: Session = Depends(get_db)):
return db.query(DewarModel).all()
@router.post("/", response_model=Dewar, status_code=status.HTTP_201_CREATED)
async def create_dewar(dewar: Dewar) -> Dewar:
@router.post("/", response_model=DewarSchema, status_code=status.HTTP_201_CREATED)
async def create_dewar(dewar: DewarCreate, db: Session = Depends(get_db)) -> DewarSchema:
dewar_id = f'DEWAR-{uuid.uuid4().hex[:8].upper()}'
dewar.id = dewar_id
dewars.append(dewar)
return dewar
db_dewar = DewarModel(
id=dewar_id,
dewar_name=dewar.dewar_name,
tracking_number=dewar.tracking_number,
number_of_pucks=dewar.number_of_pucks,
number_of_samples=dewar.number_of_samples,
status=dewar.status,
ready_date=dewar.ready_date,
shipping_date=dewar.shipping_date,
arrival_date=dewar.arrival_date,
returning_date=dewar.returning_date,
qrcode=dewar.qrcode,
contact_person_id=dewar.contact_person_id,
return_address_id=dewar.return_address_id
)
db.add(db_dewar)
db.commit()
db.refresh(db_dewar)
return db_dewar

View File

@ -1,11 +1,13 @@
from fastapi import APIRouter
from typing import List, Optional
from app.data.data import proposals
from app.models import Proposal # Import the Address model
# app/routers/proposal.py
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from typing import List
from app.schemas import Proposal as ProposalSchema
from app.models import Proposal as ProposalModel
from app.dependencies import get_db
router = APIRouter()
@router.get("/", response_model=List[Proposal])
async def get_proposals():
return proposals
@router.get("/", response_model=List[ProposalSchema])
async def get_proposals(db: Session = Depends(get_db)):
return db.query(ProposalModel).all()

View File

@ -1,85 +1,154 @@
from fastapi import APIRouter, HTTPException, status, Query
from fastapi import APIRouter, HTTPException, status, Query, Depends
from sqlalchemy.orm import Session
from typing import List, Optional
import uuid
from app.data.data import shipments, dewars
from app.models import Shipment, Dewar, ContactPerson, Address, Proposal
from app.schemas import ShipmentCreate, Shipment as ShipmentSchema, ContactPerson as ContactPersonSchema
from app.database import get_db
from app.crud import get_shipments, get_shipment_by_id
router = APIRouter()
@router.get("/", response_model=List[Shipment])
async def get_shipments(shipment_id: Optional[str] = Query(None, description="ID of the specific shipment to retrieve")):
@router.get("", response_model=List[ShipmentSchema])
async def fetch_shipments(shipment_id: Optional[str] = Query(None),
db: Session = Depends(get_db)):
if shipment_id:
shipment = next((sh for sh in shipments if sh.shipment_id == shipment_id), None)
shipment = get_shipment_by_id(db, shipment_id)
if not shipment:
raise HTTPException(status_code=404, detail="Shipment not found")
return [shipment]
return shipments
return get_shipments(db)
@router.post("", response_model=ShipmentSchema, status_code=status.HTTP_201_CREATED)
async def create_shipment(shipment: ShipmentCreate, db: Session = Depends(get_db)):
from app.models import Shipment as ShipmentModel, ContactPerson as ContactPersonModel, Address as AddressModel, \
Proposal as ProposalModel, Dewar as DewarModel
contact_person = db.query(ContactPersonModel).filter(ContactPersonModel.id == shipment.contact_person_id).first()
return_address = db.query(AddressModel).filter(AddressModel.id == shipment.return_address_id).first()
proposal = db.query(ProposalModel).filter(ProposalModel.id == shipment.proposal_id).first()
if not (contact_person and return_address and proposal):
raise HTTPException(status_code=404, detail="Associated entity not found")
shipment_id = f'SHIP-{uuid.uuid4().hex[:8].upper()}'
db_shipment = ShipmentModel(
shipment_id=shipment_id,
shipment_name=shipment.shipment_name,
shipment_date=shipment.shipment_date,
shipment_status=shipment.shipment_status,
comments=shipment.comments,
contact_person_id=contact_person.id,
return_address_id=return_address.id,
proposal_id=proposal.id,
)
# Handling dewars association
if shipment.dewars:
dewars = db.query(DewarModel).filter(DewarModel.id.in_(shipment.dewars)).all()
if len(dewars) != len(shipment.dewars):
raise HTTPException(status_code=404, detail="One or more dewars not found")
db_shipment.dewars.extend(dewars)
db.add(db_shipment)
db.commit()
db.refresh(db_shipment)
return db_shipment
@router.delete("/{shipment_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_shipment(shipment_id: str):
global shipments
shipments = [shipment for shipment in shipments if shipment.shipment_id != shipment_id]
@router.post("/{shipment_id}/add_dewar", response_model=Shipment)
async def add_dewar_to_shipment(shipment_id: str, dewar_id: str):
shipment = next((sh for sh in shipments if sh.shipment_id == shipment_id), None)
async def delete_shipment(shipment_id: str, db: Session = Depends(get_db)):
from app.models import Shipment as ShipmentModel
shipment = db.query(ShipmentModel).filter(ShipmentModel.shipment_id == shipment_id).first()
if not shipment:
raise HTTPException(status_code=404, detail="Shipment not found")
dewar = next((dw for dw in dewars if dw.id == dewar_id), None)
db.delete(shipment)
db.commit()
return
@router.put("/shipments/{shipment_id}", response_model=ShipmentSchema)
async def update_shipment(shipment_id: str, updated_shipment: ShipmentCreate, db: Session = Depends(get_db)):
from app.models import Shipment as ShipmentModel, ContactPerson as ContactPersonModel, Address as AddressModel, \
Dewar as DewarModel
# Log incoming payload for detailed inspection
print("Received payload:", json.dumps(updated_shipment.dict(), indent=2))
try:
shipment = db.query(ShipmentModel).filter(ShipmentModel.shipment_id == shipment_id).first()
if not shipment:
raise HTTPException(status_code=404, detail="Shipment not found")
# Validate relationships by IDs
contact_person = db.query(ContactPersonModel).filter(
ContactPersonModel.id == updated_shipment.contact_person_id).first()
return_address = db.query(AddressModel).filter(AddressModel.id == updated_shipment.return_address_id).first()
if not contact_person:
raise HTTPException(status_code=404, detail="Contact person not found")
if not return_address:
raise HTTPException(status_code=404, detail="Return address not found")
# Handling dewars association by IDs
dewars_ids = [d['id'] for d in updated_shipment.dewars]
dewars = db.query(DewarModel).filter(DewarModel.id.in_(dewars_ids)).all()
if len(dewars) != len(dewars_ids):
raise HTTPException(status_code=422, detail="One or more dewars not found")
# Update shipment details
shipment.shipment_name = updated_shipment.shipment_name
shipment.shipment_date = updated_shipment.shipment_date
shipment.shipment_status = updated_shipment.shipment_status
shipment.comments = updated_shipment.comments
shipment.contact_person_id = updated_shipment.contact_person_id
shipment.return_address_id = updated_shipment.return_address_id
shipment.dewars = dewars
db.commit()
db.refresh(shipment)
return shipment
except Exception as e:
print(f"Update failed with exception: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/{shipment_id}/add_dewar", response_model=ShipmentSchema)
async def add_dewar_to_shipment(shipment_id: str, dewar_id: str, db: Session = Depends(get_db)):
from app.models import Shipment as ShipmentModel, Dewar as DewarModel
shipment = db.query(ShipmentModel).filter(ShipmentModel.shipment_id == shipment_id).first()
if not shipment:
raise HTTPException(status_code=404, detail="Shipment not found")
dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first()
if not dewar:
raise HTTPException(status_code=404, detail="Dewar not found")
if dewar not in shipment.dewars:
shipment.dewars.append(dewar)
db.commit()
db.refresh(shipment)
return shipment
@router.put("/{shipment_id}", response_model=Shipment)
async def update_shipment(shipment_id: str, updated_shipment: Shipment):
global shipments
shipment = next((sh for sh in shipments if sh.shipment_id == shipment_id), None)
@router.delete("/{shipment_id}/remove_dewar/{dewar_id}", response_model=ShipmentSchema)
async def remove_dewar_from_shipment(shipment_id: str, dewar_id: str, db: Session = Depends(get_db)):
from app.models import Shipment as ShipmentModel, Dewar as DewarModel
shipment = db.query(ShipmentModel).filter(ShipmentModel.shipment_id == shipment_id).first()
if not shipment:
raise HTTPException(status_code=404, detail="Shipment not found")
shipment.shipment_name = updated_shipment.shipment_name
shipment.shipment_date = updated_shipment.shipment_date
shipment.shipment_status = updated_shipment.shipment_status
shipment.contact_person = updated_shipment.contact_person
shipment.proposal_number = updated_shipment.proposal_number
shipment.return_address = updated_shipment.return_address
shipment.comments = updated_shipment.comments
existing_dewar_dict = {dewar.id: dewar for dewar in shipment.dewars}
for updated_dewar in updated_shipment.dewars:
if updated_dewar.id in existing_dewar_dict:
existing_dewar_dict[updated_dewar.id].dewar_name = updated_dewar.dewar_name
existing_dewar_dict[updated_dewar.id].tracking_number = updated_dewar.tracking_number
existing_dewar_dict[updated_dewar.id].number_of_pucks = updated_dewar.number_of_pucks
existing_dewar_dict[updated_dewar.id].number_of_samples = updated_dewar.number_of_samples
existing_dewar_dict[updated_dewar.id].return_address = updated_dewar.return_address
existing_dewar_dict[updated_dewar.id].contact_person = updated_dewar.contact_person
existing_dewar_dict[updated_dewar.id].status = updated_dewar.status
existing_dewar_dict[updated_dewar.id].ready_date = updated_dewar.ready_date
existing_dewar_dict[updated_dewar.id].shipping_date = updated_dewar.shipping_date
existing_dewar_dict[updated_dewar.id].arrival_date = updated_dewar.arrival_date
existing_dewar_dict[updated_dewar.id].returning_date = updated_dewar.returning_date
existing_dewar_dict[updated_dewar.id].qrcode = updated_dewar.qrcode
else:
shipment.dewars.append(updated_dewar)
return shipment
@router.post("/", response_model=Shipment, status_code=status.HTTP_201_CREATED)
async def create_shipment(shipment: Shipment):
shipment_id = f'SHIP-{uuid.uuid4().hex[:8].upper()}'
shipment.shipment_id = shipment_id
shipments.append(shipment)
return shipment
@router.delete("/{shipment_id}/remove_dewar/{dewar_id}", response_model=Shipment)
async def remove_dewar_from_shipment(shipment_id: str, dewar_id: str):
shipment = next((sh for sh in shipments if sh.shipment_id == shipment_id), None)
if not shipment:
raise HTTPException(status_code=404, detail="Shipment not found")
shipment.dewars = [dw for dw in shipment.dewars if dw.id != dewar_id]
db.commit()
db.refresh(shipment)
return shipment
@router.get("/contact_persons", response_model=List[ContactPerson])
async def get_shipment_contact_persons():
return [{"shipment_id": shipment.shipment_id, "contact_person": shipment.get_shipment_contact_persons()} for shipment in shipments]
@router.get("/contact_persons", response_model=List[ContactPersonSchema])
async def get_shipment_contact_persons(db: Session = Depends(get_db)):
from app.models import ContactPerson as ContactPersonModel
contact_persons = db.query(ContactPersonModel).all()
return contact_persons

View File

@ -1,59 +1,117 @@
from pydantic import BaseModel
from typing import List, Optional
from pydantic import BaseModel, EmailStr
from datetime import date
class ContactPersonSchema(BaseModel):
id: Optional[int]
# Base class for Contact Person
class ContactPersonBase(BaseModel):
firstname: str
lastname: str
phone_number: str
email: str
email: EmailStr
# Create schema for Contact Person
class ContactPersonCreate(ContactPersonBase):
pass
# Response schema for Contact Person with ID
class ContactPerson(ContactPersonBase):
id: int
class Config:
from_attributes = True # Update here
from_attributes = True
class AddressSchema(BaseModel):
id: Optional[int]
# Create schema for Address
class AddressCreate(BaseModel):
street: str
city: str
zipcode: str
country: str
class Config:
from_attributes = True # Update here
class DewarSchema(BaseModel):
id: Optional[str]
# Response schema for Address with ID
class Address(AddressCreate):
id: int
class Config:
from_attributes = True
# Create schema for Dewar
class DewarCreate(BaseModel):
dewar_name: str
tracking_number: Optional[str]
tracking_number: str
number_of_pucks: int
number_of_samples: int
status: str
ready_date: Optional[str]
shipping_date: Optional[str]
arrival_date: Optional[str]
returning_date: Optional[str]
ready_date: Optional[date]
shipping_date: Optional[date]
arrival_date: Optional[date]
returning_date: Optional[date]
qrcode: str
contact_person_id: Optional[int]
return_address_id: Optional[int]
# Response schema for Dewar
class Dewar(BaseModel):
id: str
dewar_name: str
tracking_number: str
number_of_pucks: int
number_of_samples: int
status: str
ready_date: Optional[date]
shipping_date: Optional[date]
arrival_date: Optional[date]
returning_date: Optional[date]
qrcode: str
shipment_id: Optional[str]
contact_person: Optional[ContactPerson]
return_address: Optional[Address]
class Config:
from_attributes = True # Update here
from_attributes = True
class ProposalSchema(BaseModel):
id: Optional[int]
# Proposal schema
class Proposal(BaseModel):
id: int
number: str
class Config:
from_attributes = True # Update here
from_attributes = True
class ShipmentSchema(BaseModel):
shipment_id: Optional[str]
# Response schema for Shipment
class Shipment(BaseModel):
shipment_id: str
shipment_name: str
shipment_date: str
shipment_date: date
shipment_status: str
contact_person: List[ContactPersonSchema]
proposal_number: List[ProposalSchema]
return_address: List[AddressSchema]
comments: Optional[str] = None
dewars: List[DewarSchema]
comments: Optional[str]
contact_person: Optional[ContactPerson]
return_address: Optional[Address]
proposal: Optional[Proposal]
dewars: Optional[List[Dewar]] = []
class Config:
from_attributes = True # Update here
from_attributes = True
# Create schema for Shipment
class ShipmentCreate(BaseModel):
shipment_name: str
shipment_date: date
shipment_status: str
comments: Optional[str] = ""
contact_person_id: int
return_address_id: int
proposal_id: int # Change "proposal_number_id" to "proposal_id"
dewars: Optional[List[str]] = []
class Config:
from_attributes = True

View File

@ -12,8 +12,7 @@ import QRCode from 'react-qr-code';
import {
ContactPerson,
Address,
Dewar,
DefaultService
Dewar, ContactsService, AddressesService, ShipmentsService,
} from '../../openapi';
interface DewarDetailsProps {
@ -26,18 +25,20 @@ interface DewarDetailsProps {
defaultReturnAddress?: Address;
shipmentId: string;
refreshShipments: () => void;
selectedShipment: any;
}
const DewarDetails: React.FC<DewarDetailsProps> = ({
dewar,
trackingNumber,
//setTrackingNumber,
setTrackingNumber,
initialContactPersons = [],
initialReturnAddresses = [],
defaultContactPerson,
defaultReturnAddress,
shipmentId,
refreshShipments,
selectedShipment
}) => {
const [localTrackingNumber, setLocalTrackingNumber] = useState(trackingNumber);
const [contactPersons, setContactPersons] = useState<ContactPerson[]>(initialContactPersons);
@ -47,12 +48,14 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
const [isCreatingContactPerson, setIsCreatingContactPerson] = useState(false);
const [isCreatingReturnAddress, setIsCreatingReturnAddress] = useState(false);
const [newContactPerson, setNewContactPerson] = useState({
id: 0,
firstName: '',
lastName: '',
phone_number: '',
email: '',
});
const [newReturnAddress, setNewReturnAddress] = useState<Address>({
id: 0,
street: '',
city: '',
zipcode: '',
@ -61,26 +64,34 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
const [changesMade, setChangesMade] = useState<boolean>(false);
const [feedbackMessage, setFeedbackMessage] = useState<string>('');
const [openSnackbar, setOpenSnackbar] = useState<boolean>(false);
const [updatedDewar, setUpdatedDewar] = useState<Dewar>(dewar);
useEffect(() => {
setSelectedContactPerson(
(dewar.contact_person?.[0]?.id?.toString() || defaultContactPerson?.id?.toString() || '')
);
setSelectedReturnAddress(
(dewar.return_address?.[0]?.id?.toString() || defaultReturnAddress?.id?.toString() || '')
);
const setInitialContactPerson = () => {
const contactPersonId =
selectedShipment?.contact_person?.id?.toString() ||
dewar.contact_person?.id?.toString() ||
defaultContactPerson?.id?.toString() ||
'';
setSelectedContactPerson(contactPersonId);
};
const setInitialReturnAddress = () => {
const returnAddressId =
dewar.return_address?.id?.toString() ||
defaultReturnAddress?.id?.toString() ||
'';
setSelectedReturnAddress(returnAddressId);
};
setLocalTrackingNumber(dewar.tracking_number || '');
}, [dewar, defaultContactPerson, defaultReturnAddress]);
useEffect(() => {
console.log('DewarDetails - dewar updated:', dewar);
}, [dewar]);
setInitialContactPerson();
setInitialReturnAddress();
}, [dewar, defaultContactPerson, defaultReturnAddress, selectedShipment]);
useEffect(() => {
const getContacts = async () => {
try {
const c: ContactPerson[] = await DefaultService.getContactsContactsGet();
const c: ContactPerson[] = await ContactsService.getContactsContactsGet();
setContactPersons(c);
} catch {
setFeedbackMessage('Failed to load contact persons. Please try again later.');
@ -90,7 +101,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
const getReturnAddresses = async () => {
try {
const a: Address[] = await DefaultService.getReturnAddressesReturnAddressesGet();
const a: Address[] = await AddressesService.getReturnAddressesAddressesGet();
setReturnAddresses(a);
} catch {
setFeedbackMessage('Failed to load return addresses. Please try again later.');
@ -111,6 +122,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
}
const handleAddContact = async () => {
console.log('handleAddContact called');
if (!validateEmail(newContactPerson.email) || !validatePhoneNumber(newContactPerson.phone_number) ||
!newContactPerson.firstName || !newContactPerson.lastName) {
setFeedbackMessage('Please fill in all new contact person fields correctly.');
@ -126,10 +138,10 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
};
try {
const c: ContactPerson = await DefaultService.createContactContactsPost(payload);
const c: ContactPerson = await ContactsService.createContactContactsPost(payload);
setContactPersons([...contactPersons, c]);
setFeedbackMessage('Contact person added successfully.');
setNewContactPerson({ firstName: '', lastName: '', phone_number: '', email: '' });
setNewContactPerson({ id: 0, firstName: '', lastName: '', phone_number: '', email: '' });
setSelectedContactPerson(c.id?.toString() || '');
} catch {
setFeedbackMessage('Failed to create a new contact person. Please try again later.');
@ -141,6 +153,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
};
const handleAddAddress = async () => {
console.log('handleAddAddress called');
if (!validateZipCode(newReturnAddress.zipcode) || !newReturnAddress.street || !newReturnAddress.city ||
!newReturnAddress.country) {
setFeedbackMessage('Please fill in all new return address fields correctly.');
@ -156,10 +169,10 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
};
try {
const a: Address = await DefaultService.createReturnAddressReturnAddressesPost(payload);
const a: Address = await AddressesService.createReturnAddressAddressesPost(payload);
setReturnAddresses([...returnAddresses, a]);
setFeedbackMessage('Return address added successfully.');
setNewReturnAddress({ street: '', city: '', zipcode: '', country: '' });
setNewReturnAddress({ id: 0, street: '', city: '', zipcode: '', country: '' });
setSelectedReturnAddress(a.id?.toString() || '');
} catch {
setFeedbackMessage('Failed to create a new return address. Please try again later.');
@ -171,10 +184,11 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
};
const getShipmentById = async (shipmentId: string) => {
console.log(`Fetching shipment with ID: ${shipmentId}`);
try {
const response = await DefaultService.getShipmentsShipmentsGet(shipmentId);
const response = await ShipmentsService.fetchShipmentsShipmentsGet(shipmentId);
if (response && response.length > 0) {
return response[0]; // Since the result is an array, we take the first element
return response[0];
}
throw new Error('Shipment not found');
} catch (error) {
@ -184,22 +198,32 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
};
const handleSaveChanges = async () => {
console.log('handleSaveChanges called');
const formatDate = (dateString: string | undefined): string => {
if (!dateString) return '2024-01-01'; // Default date if undefined
if (!dateString) return '';
const date = new Date(dateString);
if (isNaN(date.getTime())) return '2024-01-01'; // Default date if invalid
if (isNaN(date.getTime())) return '';
return date.toISOString().split('T')[0];
};
if (!dewar.dewar_name || !selectedContactPerson || !selectedReturnAddress || !trackingNumber) {
console.log('Selected Contact Person:', selectedContactPerson);
console.log('Selected Return Address:', selectedReturnAddress);
// Check if required fields are filled
if (!selectedContactPerson || !selectedReturnAddress) {
setFeedbackMessage('Please ensure all required fields are filled.');
setOpenSnackbar(true);
return;
}
console.log('Saving changes...');
console.log('Current Dewar:', dewar);
let existingShipment;
try {
existingShipment = await getShipmentById(shipmentId);
console.log('Existing Shipment:', existingShipment);
} catch {
setFeedbackMessage('Failed to fetch existing shipment data. Please try again later.');
setOpenSnackbar(true);
@ -207,56 +231,58 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
}
const updatedDewar = {
id: dewar.id, // Ensure dewar ID is included
id: dewar.id,
dewar_name: dewar.dewar_name,
return_address: returnAddresses.find((a) => a.id?.toString() === selectedReturnAddress)
? [returnAddresses.find((a) => a.id?.toString() === selectedReturnAddress)]
: [],
contact_person: contactPersons.find((c) => c.id?.toString() === selectedContactPerson)
? [contactPersons.find((c) => c.id?.toString() === selectedContactPerson)]
: [],
tracking_number: dewar.tracking_number,
number_of_pucks: dewar.number_of_pucks,
number_of_samples: dewar.number_of_samples,
qrcode: dewar.qrcode,
ready_date: formatDate(dewar.ready_date),
shipping_date: formatDate(dewar.shipping_date),
status: dewar.status,
tracking_number: trackingNumber,
ready_date: formatDate(dewar.ready_date ?? undefined),
shipping_date: formatDate(dewar.shipping_date ?? undefined),
arrival_date: dewar.arrival_date,
returning_date: dewar.returning_date,
qrcode: dewar.qrcode,
return_address_id: selectedReturnAddress,
contact_person_id: selectedContactPerson,
};
const payload = {
...existingShipment,
dewars: existingShipment.dewars?.map(d => d.id === dewar.id ? updatedDewar : d) || [], // Update specific dewar in the dewars array
shipment_id: existingShipment.shipment_id,
shipment_name: existingShipment.shipment_name,
shipment_date: existingShipment.shipment_date,
shipment_status: existingShipment.shipment_status,
comments: existingShipment.comments,
contact_person_id: selectedContactPerson,
return_address_id: selectedReturnAddress,
proposal_id: existingShipment.proposal?.id,
dewars: [
updatedDewar
],
};
console.log('Payload for update:', JSON.stringify(payload, null, 2));
try {
await DefaultService.updateShipmentShipmentsShipmentIdPut(shipmentId, payload);
await ShipmentsService.updateShipmentShipmentsShipmentsShipmentIdPut(shipmentId, payload);
setFeedbackMessage('Changes saved successfully.');
setChangesMade(false);
setUpdatedDewar(updatedDewar);
console.log('Calling refreshShipments');
refreshShipments(); // Trigger refresh shipments after saving changes
refreshShipments();
} catch (error) {
if (error.response) {
console.error('Server Response:', error.response.data);
} else {
console.error('Update Shipment Error:', error);
setFeedbackMessage('Failed to save changes. Please try again later.');
}
setOpenSnackbar(true);
return;
console.error('Update Shipment Error:', error);
setFeedbackMessage('Failed to save changes. Please try again later.');
}
setOpenSnackbar(true);
};
return (
<Box sx={{ marginTop: 2 }}>
<Typography variant="h6">Selected Dewar: {updatedDewar.dewar_name}</Typography>
<Typography variant="h6">Selected Dewar: {dewar.dewar_name}</Typography>
<TextField
label="Tracking Number"
value={localTrackingNumber}
onChange={(e) => {
setLocalTrackingNumber(e.target.value);
setTrackingNumber(e.target.value); // Ensure parent state is updated if applicable
setChangesMade(true);
}}
variant="outlined"
@ -264,8 +290,8 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
/>
<Box sx={{ display: 'flex', alignItems: 'center', marginBottom: 2 }}>
<Box sx={{ width: 80, height: 80, backgroundColor: '#e0e0e0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{updatedDewar.qrcode ? (
<QRCode value={updatedDewar.qrcode} size={70} />
{dewar.qrcode ? (
<QRCode value={dewar.qrcode} size={70} />
) : (
<Typography>No QR code available</Typography>
)}
@ -274,14 +300,16 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
Generate QR Code
</Button>
</Box>
<Typography variant="body1">Number of Pucks: {updatedDewar.number_of_pucks}</Typography>
<Typography variant="body1">Number of Samples: {updatedDewar.number_of_samples}</Typography>
<Typography variant="body1">Number of Pucks: {dewar.number_of_pucks}</Typography>
<Typography variant="body1">Number of Samples: {dewar.number_of_samples}</Typography>
<Typography variant="body1">Current Contact Person:</Typography>
<Select
value={selectedContactPerson}
onChange={(e) => {
setSelectedContactPerson(e.target.value);
setIsCreatingContactPerson(e.target.value === 'add');
const value = e.target.value;
console.log('Contact Person Selected:', value);
setSelectedContactPerson(value);
setIsCreatingContactPerson(value === 'add');
setChangesMade(true);
}}
fullWidth
@ -289,7 +317,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
variant="outlined"
displayEmpty
>
{contactPersons?.map((person) => (
{Array.isArray(contactPersons) && contactPersons.map((person) => (
<MenuItem key={person.id?.toString()} value={person.id?.toString() || ''}>
{person.firstname} {person.lastname}
</MenuItem>
@ -343,8 +371,10 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
<Select
value={selectedReturnAddress}
onChange={(e) => {
setSelectedReturnAddress(e.target.value);
setIsCreatingReturnAddress(e.target.value === 'add');
const value = e.target.value;
console.log('Return Address Selected:', value);
setSelectedReturnAddress(value);
setIsCreatingReturnAddress(value === 'add');
setChangesMade(true);
}}
fullWidth
@ -352,7 +382,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
variant="outlined"
displayEmpty
>
{returnAddresses?.map((address) => (
{Array.isArray(returnAddresses) && returnAddresses.map((address) => (
<MenuItem key={address.id?.toString()} value={address.id?.toString() || ''}>
{address.street}, {address.city}
</MenuItem>

View File

@ -2,35 +2,50 @@ import React from 'react';
import { Box, Typography, Button, Stack, TextField } from '@mui/material';
import QRCode from 'react-qr-code';
import DeleteIcon from "@mui/icons-material/Delete";
import { Dewar, Shipment_Input, DefaultService } from "../../openapi";
import { Dewar, DewarsService, ShipmentsService, ContactPerson, ApiError } from "../../openapi"; // Ensure ApiError is imported here
import { SxProps } from "@mui/system";
import CustomStepper from "./DewarStepper";
import DewarDetails from './DewarDetails';
interface ShipmentDetailsProps {
isCreatingShipment: boolean;
selectedShipment: Shipment_Input;
sx?: SxProps;
selectedShipment: ShipmentsService | null;
selectedDewar: Dewar | null;
setSelectedDewar: React.Dispatch<React.SetStateAction<Dewar | null>>;
setSelectedShipment: React.Dispatch<React.SetStateAction<Shipment_Input>>;
sx?: SxProps;
setSelectedShipment: React.Dispatch<React.SetStateAction<ShipmentsService | null>>;
refreshShipments: () => void;
defaultContactPerson?: ContactPerson;
}
const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
isCreatingShipment,
sx,
selectedShipment,
selectedDewar,
setSelectedDewar,
setSelectedShipment,
sx = {},
refreshShipments,
defaultContactPerson
}) => {
const [localSelectedDewar, setLocalSelectedDewar] = React.useState<Dewar | null>(null);
const [isAddingDewar, setIsAddingDewar] = React.useState<boolean>(false);
const [newDewar, setNewDewar] = React.useState<Partial<Dewar>>({
dewar_name: '',
});
// To reset localSelectedDewar when selectedShipment changes
const initialNewDewarState: Partial<Dewar> = {
dewar_name: '',
tracking_number: '',
number_of_pucks: 0,
number_of_samples: 0,
status: 'In preparation',
ready_date: null,
shipping_date: null,
arrival_date: null,
returning_date: null,
qrcode: 'N/A'
};
const [newDewar, setNewDewar] = React.useState<Partial<Dewar>>(initialNewDewarState);
React.useEffect(() => {
setLocalSelectedDewar(null);
}, [selectedShipment]);
@ -39,8 +54,8 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
console.log('ShipmentDetails - selectedShipment updated:', selectedShipment);
}, [selectedShipment]);
const totalPucks = selectedShipment.dewars.reduce((acc, dewar) => acc + (dewar.number_of_pucks || 0), 0);
const totalSamples = selectedShipment.dewars.reduce((acc, dewar) => acc + (dewar.number_of_samples || 0), 0);
const totalPucks = selectedShipment?.dewars?.reduce((acc, dewar) => acc + (dewar.number_of_pucks || 0), 0) || 0;
const totalSamples = selectedShipment?.dewars?.reduce((acc, dewar) => acc + (dewar.number_of_samples || 0), 0) || 0;
const handleDewarSelection = (dewar: Dewar) => {
const newSelection = localSelectedDewar?.id === dewar.id ? null : dewar;
@ -50,18 +65,12 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
const handleDeleteDewar = async (dewarId: string) => {
const confirmed = window.confirm('Are you sure you want to delete this dewar?');
if (confirmed) {
if (confirmed && selectedShipment) {
try {
console.log('Selected Shipment ID:', selectedShipment.shipment_id);
console.log('Dewar ID to be deleted:', dewarId);
const updatedShipment = await DefaultService.removeDewarFromShipmentShipmentsShipmentIdRemoveDewarDewarIdDelete(
selectedShipment.shipment_id, dewarId
);
// Ensure state is updated with server response
const updatedShipment = await ShipmentsService.removeDewarFromShipmentShipmentsShipmentIdRemoveDewarDewarIdDelete(selectedShipment.shipment_id, dewarId);
setSelectedShipment(updatedShipment);
setLocalSelectedDewar(null);
refreshShipments();
} catch (error) {
console.error('Failed to delete dewar:', error);
alert('Failed to delete dewar. Please try again.');
@ -78,55 +87,42 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
};
const handleAddDewar = async () => {
if (selectedShipment && newDewar.dewar_name) {
if (newDewar.dewar_name?.trim()) {
try {
const newDewarToPost: Dewar = {
...newDewar as Dewar,
dewar_name: newDewar.dewar_name.trim() || 'Unnamed Dewar',
number_of_pucks: newDewar.number_of_pucks ?? 0,
number_of_samples: newDewar.number_of_samples ?? 0,
return_address: selectedShipment.return_address,
contact_person: selectedShipment.contact_person,
status: 'In preparation',
qrcode: newDewar.qrcode || 'N/A',
};
...initialNewDewarState,
...newDewar,
dewar_name: newDewar.dewar_name.trim(),
contact_person: selectedShipment?.contact_person,
contact_person_id: selectedShipment?.contact_person?.id, // Adding contact_person_id
return_address: selectedShipment?.return_address,
return_address_id: selectedShipment?.return_address?.id, // Adding return_address_id
} as Dewar;
// Create a new dewar
const createdDewar = await DefaultService.createDewarDewarsPost(newDewarToPost);
console.log('Created Dewar:', createdDewar);
// Check IDs before calling backend
console.log('Adding dewar to shipment:', {
shipment_id: selectedShipment.shipment_id,
dewar_id: createdDewar.id,
});
// Make an API call to associate the dewar with the shipment
const updatedShipment = await DefaultService.addDewarToShipmentShipmentsShipmentIdAddDewarPost(
selectedShipment.shipment_id,
createdDewar.id
);
if (updatedShipment) {
const createdDewar = await DewarsService.createDewarDewarsPost(newDewarToPost);
if (createdDewar && selectedShipment) {
const updatedShipment = await ShipmentsService.addDewarToShipmentShipmentsShipmentIdAddDewarPost(selectedShipment.shipment_id, createdDewar.id);
setSelectedShipment(updatedShipment);
} else {
throw new Error('Failed to update shipment with new dewar');
setIsAddingDewar(false);
setNewDewar(initialNewDewarState);
refreshShipments();
}
setIsAddingDewar(false);
setNewDewar({ dewar_name: '', tracking_number: '' });
refreshShipments()
} catch (error) {
alert('Failed to add dewar or update shipment. Please try again.');
console.error('Error adding dewar or updating shipment:', error);
if (error instanceof ApiError && error.body) {
console.error('Validation errors:', error.body.detail); // Log specific validation errors
} else {
console.error('Unexpected error:', error);
}
alert('Failed to add dewar or update shipment. Please check the data and try again.');
}
} else {
alert('Please fill in the Dewar Name');
}
};
const contactPerson = selectedShipment?.contact_person;
return (
<Box sx={{ ...sx, padding: 2, textAlign: 'left' }}>
{!localSelectedDewar && !isAddingDewar && (
@ -159,11 +155,19 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
</Box>
)}
<Typography variant="h5">{selectedShipment.shipment_name}</Typography>
<Typography variant="body1" color="textSecondary">Main contact person: {`${selectedShipment.contact_person[0].firstname} ${selectedShipment.contact_person[0].lastname}`}</Typography>
<Typography variant="body1">Number of Pucks: {totalPucks}</Typography>
<Typography variant="body1">Number of Samples: {totalSamples}</Typography>
<Typography variant="body1">Shipment Date: {selectedShipment.shipment_date}</Typography>
{selectedShipment ? (
<>
<Typography variant="h5">{selectedShipment.shipment_name}</Typography>
<Typography variant="body1" color="textSecondary">
Main contact person: {contactPerson ? `${contactPerson.firstname} ${contactPerson.lastname}` : 'N/A'}
</Typography>
<Typography variant="body1">Number of Pucks: {totalPucks}</Typography>
<Typography variant="body1">Number of Samples: {totalSamples}</Typography>
<Typography variant="body1">Shipment Date: {selectedShipment.shipment_date}</Typography>
</>
) : (
<Typography variant="h5" color="error">No shipment selected</Typography>
)}
{localSelectedDewar && !isAddingDewar && (
<DewarDetails
@ -172,17 +176,17 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
setTrackingNumber={(value) => {
setLocalSelectedDewar((prev) => (prev ? { ...prev, tracking_number: value as string } : prev));
}}
initialContactPersons={selectedShipment.contact_person}
initialReturnAddresses={selectedShipment.return_address}
defaultContactPerson={selectedShipment.contact_person[0]}
defaultReturnAddress={selectedShipment.return_address[0]}
shipmentId={selectedShipment.shipment_id}
initialContactPersons={selectedShipment?.contact_person ? [selectedShipment.contact_person] : []}
initialReturnAddresses={selectedShipment?.return_address ? [selectedShipment.return_address] : []}
defaultContactPerson={contactPerson}
defaultReturnAddress={selectedShipment?.return_address}
shipmentId={selectedShipment?.shipment_id || ''}
refreshShipments={refreshShipments}
/>
)}
<Stack spacing={1}>
{selectedShipment.dewars.map((dewar) => (
{selectedShipment?.dewars?.map((dewar) => (
<Button
key={dewar.id}
onClick={() => handleDewarSelection(dewar)}
@ -197,14 +201,7 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
backgroundColor: localSelectedDewar?.id === dewar.id ? '#f0f0f0' : '#fff',
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginRight: 2,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', marginRight: 2 }}>
{dewar.qrcode ? (
<QRCode value={dewar.qrcode} size={70} />
) : (
@ -230,7 +227,9 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
<Typography variant="body2">Number of Pucks: {dewar.number_of_pucks || 0}</Typography>
<Typography variant="body2">Number of Samples: {dewar.number_of_samples || 0}</Typography>
<Typography variant="body2">Tracking Number: {dewar.tracking_number}</Typography>
<Typography variant="body2">Contact Person: {`${dewar.contact_person[0].firstname} ${dewar.contact_person[0].lastname}`}</Typography>
<Typography variant="body2">
Contact Person: {dewar.contact_person?.firstname ? `${dewar.contact_person.firstname} ${dewar.contact_person.lastname}` : 'N/A'}
</Typography>
</Box>
<Box sx={{
@ -243,7 +242,7 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
<CustomStepper dewar={dewar} />
{localSelectedDewar?.id === dewar.id && (
<Button
onClick={() => handleDeleteDewar(dewar.id)} // <--- Pass the dewar ID here
onClick={() => handleDeleteDewar(dewar.id)}
color="error"
sx={{
minWidth: '40px',

View File

@ -1,14 +1,19 @@
import * as React from 'react';
import { Box, Button, TextField, Typography, Select, MenuItem, Stack, FormControl, InputLabel } from '@mui/material';
import {
Box, Button, TextField, Typography, Select, MenuItem, Stack, FormControl, InputLabel
} from '@mui/material';
import { SelectChangeEvent } from '@mui/material';
import { SxProps } from '@mui/system';
import { ContactPerson, Address, Proposal, DefaultService, OpenAPI, Shipment_Input } from "../../openapi";
import { useEffect } from "react";
import {
ContactPersonCreate, ContactPerson, Address, Proposal, ContactsService, AddressesService, ProposalsService,
OpenAPI, ShipmentCreate, ShipmentsService
} from '../../openapi';
import { useEffect } from 'react';
interface ShipmentFormProps {
sx?: SxProps;
onCancel: () => void;
refreshShipments: () => void; // Add the refresh function as a prop
refreshShipments: () => void;
}
const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshShipments }) => {
@ -17,37 +22,26 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
const [proposals, setProposals] = React.useState<Proposal[]>([]);
const [isCreatingContactPerson, setIsCreatingContactPerson] = React.useState(false);
const [isCreatingReturnAddress, setIsCreatingReturnAddress] = React.useState(false);
const [newContactPerson, setNewContactPerson] = React.useState({
firstName: '',
lastName: '',
phone_number: '',
email: '',
const [newContactPerson, setNewContactPerson] = React.useState<ContactPersonCreate>({
firstname: '', lastname: '', phone_number: '', email: ''
});
const [newReturnAddress, setNewReturnAddress] = React.useState<Address>({
street: '',
city: '',
zipcode: '',
country: ''
const [newReturnAddress, setNewReturnAddress] = React.useState<Omit<Address, 'id'>>({
street: '', city: '', zipcode: '', country: ''
});
const [newShipment, setNewShipment] = React.useState<Shipment_Input>({
comments: undefined,
contact_person: [],
dewars: [],
proposal_number: [],
return_address: [],
shipment_date: "",
shipment_id: undefined,
shipment_name: "",
shipment_status: ""
const [newShipment, setNewShipment] = React.useState<Partial<ShipmentCreate>>({
shipment_name: '', shipment_status: 'In preparation', comments: ''
});
const [selectedContactPersonId, setSelectedContactPersonId] = React.useState<number | null>(null);
const [selectedReturnAddressId, setSelectedReturnAddressId] = React.useState<number | null>(null);
const [selectedProposalId, setSelectedProposalId] = React.useState<number | null>(null);
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
useEffect(() => {
OpenAPI.BASE = 'http://127.0.0.1:8000'; // Define Base URL
OpenAPI.BASE = 'http://127.0.0.1:8000';
const getContacts = async () => {
try {
const c: ContactPerson[] = await DefaultService.getContactsContactsGet();
const c: ContactPerson[] = await ContactsService.getContactsContactsGet();
setContactPersons(c);
} catch {
setErrorMessage('Failed to load contact persons. Please try again later.');
@ -56,7 +50,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
const getAddresses = async () => {
try {
const a: Address[] = await DefaultService.getReturnAddressesReturnAddressesGet();
const a: Address[] = await AddressesService.getReturnAddressesAddressesGet();
setReturnAddresses(a);
} catch {
setErrorMessage('Failed to load return addresses. Please try again later.');
@ -65,7 +59,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
const getProposals = async () => {
try {
const p: Proposal[] = await DefaultService.getProposalsProposalsGet();
const p: Proposal[] = await ProposalsService.getProposalsProposalsGet();
setProposals(p);
} catch {
setErrorMessage('Failed to load proposals. Please try again later.');
@ -82,10 +76,10 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
const validateZipCode = (zipcode: string) => /^\d{5}(?:[-\s]\d{4})?$/.test(zipcode);
const isContactFormValid = () => {
const { firstName, lastName, phone_number, email } = newContactPerson;
const { firstname, lastname, phone_number, email } = newContactPerson;
if (isCreatingContactPerson) {
if (!firstName || !lastName || !validateEmail(email) || !validatePhoneNumber(phone_number)) return false;
if (!firstname || !lastname || !validateEmail(email) || !validatePhoneNumber(phone_number)) return false;
}
return true;
@ -102,10 +96,10 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
};
const isFormValid = () => {
const { shipment_name, proposal_number, contact_person, return_address } = newShipment;
const { shipment_name, shipment_status } = newShipment;
if (!shipment_name || !proposal_number.length) return false;
if (!contact_person.length || !return_address.length) return false;
if (!shipment_name) return false;
if (!selectedContactPersonId || !selectedReturnAddressId || !selectedProposalId) return false;
if (isCreatingContactPerson && !isContactFormValid()) return false;
if (isCreatingReturnAddress && !isAddressFormValid()) return false;
@ -116,15 +110,6 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setNewShipment((prev) => ({ ...prev, [name]: value }));
if (name === 'email') {
setNewContactPerson((prev) => ({ ...prev, email: value }));
}
if (name === 'phone_number') {
setNewContactPerson((prev) => ({ ...prev, phone_number: value }));
}
if (name === 'zipcode') {
setNewReturnAddress((prev) => ({ ...prev, zipcode: value }));
}
};
const handleSaveShipment = async () => {
@ -133,39 +118,26 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
return;
}
OpenAPI.BASE = 'http://127.0.0.1:8000';
const payload: Shipment_Input = {
const payload: ShipmentCreate = {
shipment_name: newShipment.shipment_name || '',
shipment_date: new Date().toISOString().split('T')[0],
shipment_status: 'In preparation',
contact_person: newShipment.contact_person ? newShipment.contact_person.map(person => ({
firstname: person.firstname,
lastname: person.lastname,
phone_number: person.phone_number,
email: person.email
})) : [],
proposal_number: newShipment.proposal_number ? [{
id: 1,
number: newShipment.proposal_number
}] : [],
return_address: newShipment.return_address ? newShipment.return_address.map(address => ({
id: address.id,
street: address.street,
city: address.city,
zipcode: address.zipcode,
country: address.country
})) : [],
shipment_date: new Date().toISOString().split('T')[0], // Remove if date is not required at all
shipment_status: newShipment.shipment_status || 'In preparation',
comments: newShipment.comments || '',
dewars: []
contact_person_id: selectedContactPersonId!,
return_address_id: selectedReturnAddressId!,
proposal_id: selectedProposalId!,
dewars: newShipment.dewars || []
};
console.log('Shipment Payload being sent:', payload);
try {
await DefaultService.createShipmentShipmentsPost(payload);
await ShipmentsService.createShipmentShipmentsPost(payload);
setErrorMessage(null);
refreshShipments(); // Ensure shipments are refreshed after creation
onCancel(); // close the form after saving
} catch {
refreshShipments();
onCancel();
} catch (error) {
console.error('Failed to save shipment:', error);
setErrorMessage('Failed to save shipment. Please try again.');
}
};
@ -174,13 +146,10 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
const value = event.target.value;
if (value === 'new') {
setIsCreatingContactPerson(true);
setNewShipment({ ...newShipment, contact_person: [] });
setSelectedContactPersonId(null);
} else {
setIsCreatingContactPerson(false);
const selectedPerson = contactPersons.find((person) => person.lastname === value);
if (selectedPerson) {
setNewShipment({ ...newShipment, contact_person: [{ ...selectedPerson }] });
}
setSelectedContactPersonId(parseInt(value));
}
};
@ -188,44 +157,44 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
const value = event.target.value;
if (value === 'new') {
setIsCreatingReturnAddress(true);
setNewShipment({ ...newShipment, return_address: [] });
setSelectedReturnAddressId(null);
} else {
setIsCreatingReturnAddress(false);
const selectedAddress = returnAddresses.find((address) => address.city === value);
if (selectedAddress) {
setNewShipment({ ...newShipment, return_address: [{ ...selectedAddress }] });
}
setSelectedReturnAddressId(parseInt(value));
}
};
const handleProposalChange = (event: SelectChangeEvent) => {
const selectedProposal = event.target.value;
setNewShipment({ ...newShipment, proposal_number: selectedProposal });
const value = event.target.value;
setSelectedProposalId(parseInt(value));
};
const handleSaveNewContactPerson = async () => {
if (!validateEmail(newContactPerson.email) || !validatePhoneNumber(newContactPerson.phone_number) ||
!newContactPerson.firstName || !newContactPerson.lastName) {
if (!isContactFormValid()) {
setErrorMessage('Please fill in all new contact person fields correctly.');
return;
}
const payload = {
firstname: newContactPerson.firstName,
lastname: newContactPerson.lastName,
const payload: ContactPersonCreate = {
firstname: newContactPerson.firstname,
lastname: newContactPerson.lastname,
phone_number: newContactPerson.phone_number,
email: newContactPerson.email,
};
console.log('Contact Person Payload being sent:', payload);
try {
const c: ContactPerson = await DefaultService.createContactContactsPost(payload);
setContactPersons([...contactPersons, c]);
const newPerson: ContactPerson = await ContactsService.createContactContactsPost(payload);
setContactPersons([...contactPersons, newPerson]);
setErrorMessage(null);
} catch {
setSelectedContactPersonId(newPerson.id);
} catch (error) {
console.error('Failed to create a new contact person:', error);
setErrorMessage('Failed to create a new contact person. Please try again later.');
}
setNewContactPerson({ firstName: '', lastName: '', phone_number: '', email: '' });
setNewContactPerson({ firstname: '', lastname: '', phone_number: '', email: '' });
setIsCreatingContactPerson(false);
};
@ -236,18 +205,22 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
return;
}
const payload = {
street: newReturnAddress.street.trim(),
city: newReturnAddress.city.trim(),
zipcode: newReturnAddress.zipcode.trim(),
country: newReturnAddress.country.trim(),
const payload: AddressCreate = {
street: newReturnAddress.street,
city: newReturnAddress.city,
zipcode: newReturnAddress.zipcode,
country: newReturnAddress.country,
};
console.log('Return Address Payload being sent:', payload);
try {
const a: Address = await DefaultService.createReturnAddressReturnAddressesPost(payload);
setReturnAddresses([...returnAddresses, a]);
const response: Address = await AddressesService.createReturnAddressAddressesPost(payload);
setReturnAddresses([...returnAddresses, response]);
setErrorMessage(null);
} catch {
setSelectedReturnAddressId(response.id);
} catch (error) {
console.error('Failed to create a new return address:', error);
setErrorMessage('Failed to create a new return address. Please try again later.');
}
@ -282,18 +255,12 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
<FormControl fullWidth required>
<InputLabel>Contact Person</InputLabel>
<Select
value={
isCreatingContactPerson
? 'new'
: newShipment.contact_person.length > 0
? newShipment.contact_person[0].lastname
: ''
}
value={selectedContactPersonId ? selectedContactPersonId.toString() : ''}
onChange={handleContactPersonChange}
displayEmpty
>
{contactPersons.map((person) => (
<MenuItem key={person.lastname} value={person.lastname}>
<MenuItem key={person.id} value={person.id.toString()}>
{`${person.lastname}, ${person.firstname}`}
</MenuItem>
))}
@ -306,17 +273,17 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
<>
<TextField
label="First Name"
name="firstName"
value={newContactPerson.firstName}
onChange={(e) => setNewContactPerson({ ...newContactPerson, firstName: e.target.value })}
name="firstname"
value={newContactPerson.firstname}
onChange={(e) => setNewContactPerson({ ...newContactPerson, firstname: e.target.value })}
fullWidth
required
/>
<TextField
label="Last Name"
name="lastName"
value={newContactPerson.lastName}
onChange={(e) => setNewContactPerson({ ...newContactPerson, lastName: e.target.value })}
name="lastname"
value={newContactPerson.lastname}
onChange={(e) => setNewContactPerson({ ...newContactPerson, lastname: e.target.value })}
fullWidth
required
/>
@ -325,28 +292,22 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
name="phone_number"
type="tel"
value={newContactPerson.phone_number}
onChange={(e) => {
setNewContactPerson({ ...newContactPerson, phone_number: e.target.value });
handleChange(e);
}}
onChange={(e) => setNewContactPerson({ ...newContactPerson, phone_number: e.target.value })}
fullWidth
required
error={!validatePhoneNumber(newContactPerson.phone_number)}
helperText={!validatePhoneNumber(newContactPerson.phone_number) ? "Invalid phone number" : ""}
helperText={!validatePhoneNumber(newContactPerson.phone_number) ? 'Invalid phone number' : ''}
/>
<TextField
label="Email"
name="email"
type="email"
value={newContactPerson.email}
onChange={(e) => {
setNewContactPerson({ ...newContactPerson, email: e.target.value });
handleChange(e);
}}
onChange={(e) => setNewContactPerson({ ...newContactPerson, email: e.target.value })}
fullWidth
required
error={!validateEmail(newContactPerson.email)}
helperText={!validateEmail(newContactPerson.email) ? "Invalid email" : ""}
helperText={!validateEmail(newContactPerson.email) ? 'Invalid email' : ''}
/>
<Button
variant="contained"
@ -361,12 +322,12 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
<FormControl fullWidth required>
<InputLabel>Proposal Number</InputLabel>
<Select
value={newShipment.proposal_number || ''}
value={selectedProposalId ? selectedProposalId.toString() : ''}
onChange={handleProposalChange}
displayEmpty
>
{proposals.map((proposal) => (
<MenuItem key={proposal.id} value={proposal.number}>
<MenuItem key={proposal.id} value={proposal.id.toString()}>
{proposal.number}
</MenuItem>
))}
@ -375,18 +336,12 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
<FormControl fullWidth required>
<InputLabel>Return Address</InputLabel>
<Select
value={
isCreatingReturnAddress
? 'new'
: newShipment.return_address.length > 0
? newShipment.return_address[0].city
: ''
}
value={selectedReturnAddressId ? selectedReturnAddressId.toString() : ''}
onChange={handleReturnAddressChange}
displayEmpty
>
{returnAddresses.map((address) => (
<MenuItem key={address.city} value={address.city}>
<MenuItem key={address.id} value={address.id.toString()}>
{`${address.street}, ${address.city}, ${address.zipcode}, ${address.country}`}
</MenuItem>
))}
@ -417,14 +372,11 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
label="Zip Code"
name="zipcode"
value={newReturnAddress.zipcode}
onChange={(e) => {
setNewReturnAddress({ ...newReturnAddress, zipcode: e.target.value });
handleChange(e);
}}
onChange={(e) => setNewReturnAddress({ ...newReturnAddress, zipcode: e.target.value })}
fullWidth
required
error={!validateZipCode(newReturnAddress.zipcode)}
helperText={!validateZipCode(newReturnAddress.zipcode) ? "Invalid zip code" : ""}
helperText={!validateZipCode(newReturnAddress.zipcode) ? 'Invalid zip code' : ''}
/>
<TextField
label="Country"
@ -459,7 +411,6 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
variant="contained"
color="primary"
onClick={handleSaveShipment}
disabled={!isFormValid()}
>
Save Shipment
</Button>

View File

@ -1,20 +1,20 @@
import * as React from 'react';
import React, { useState } from 'react';
import { Button, Box, Typography, IconButton } from '@mui/material';
import { Add as AddIcon, Delete as DeleteIcon, UploadFile as UploadFileIcon, Refresh as RefreshIcon } from '@mui/icons-material';
import UploadDialog from './UploadDialog';
import { Shipment_Input, Dewar, DefaultService } from '../../openapi';
import { Dewar, ShipmentsService } from '../../openapi';
import { SxProps } from '@mui/material';
import bottleGrey from '/src/assets/icons/bottle-svgrepo-com-grey.svg'
import bottleYellow from '/src/assets/icons/bottle-svgrepo-com-yellow.svg'
import bottleGreen from '/src/assets/icons/bottle-svgrepo-com-green.svg'
import bottleRed from '/src/assets/icons/bottle-svgrepo-com-red.svg'
import bottleGrey from '/src/assets/icons/bottle-svgrepo-com-grey.svg';
import bottleYellow from '/src/assets/icons/bottle-svgrepo-com-yellow.svg';
import bottleGreen from '/src/assets/icons/bottle-svgrepo-com-green.svg';
import bottleRed from '/src/assets/icons/bottle-svgrepo-com-red.svg';
interface ShipmentPanelProps {
setCreatingShipment: (value: boolean) => void;
selectedPage?: string;
selectShipment: (shipment: Shipment_Input | null) => void;
selectShipment: (shipment: ShipmentsService | null) => void;
selectedShipment: ShipmentsService | null;
sx?: SxProps;
shipments: Shipment_Input[];
shipments: ShipmentsService[];
refreshShipments: () => void;
error: string | null;
}
@ -29,13 +29,13 @@ const statusIconMap: Record<string, string> = {
const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
setCreatingShipment,
selectShipment,
selectedShipment,
sx,
shipments,
refreshShipments,
error
}) => {
const [selectedShipment, setSelectedShipment] = React.useState<Shipment_Input | null>(null);
const [uploadDialogOpen, setUploadDialogOpen] = React.useState(false);
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
const handleDeleteShipment = async () => {
if (selectedShipment) {
@ -49,28 +49,31 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
const deleteShipment = async (shipmentId: string | undefined) => {
if (!shipmentId) return;
try {
// Assumes DefaultService.deleteShipmentShipmentsShipmentIdDelete is already defined
await DefaultService.deleteShipmentShipmentsShipmentIdDelete(shipmentId);
refreshShipments(); // Call the refresh function after deletion
setSelectedShipment(null);
await ShipmentsService.deleteShipmentShipmentsShipmentIdDelete(shipmentId);
refreshShipments();
selectShipment(null);
} catch (error) {
// Handle error
console.error('Failed to delete shipment:', error);
}
};
const handleShipmentSelection = (shipment: Shipment_Input) => {
const handleShipmentSelection = (shipment: ShipmentsService) => {
const isSelected = selectedShipment?.shipment_id === shipment.shipment_id;
const updatedShipment = isSelected ? null : shipment;
setSelectedShipment(updatedShipment);
console.log("Shipment selected:", updatedShipment); // debug log
selectShipment(updatedShipment);
};
const openUploadDialog = () => setUploadDialogOpen(true);
const openUploadDialog = (event: React.MouseEvent) => {
event.stopPropagation(); // Prevent event bubbling
setUploadDialogOpen(true);
};
const closeUploadDialog = () => setUploadDialogOpen(false);
const getNumberOfDewars = (shipment: Shipment_Input): number => {
return shipment.dewars ? shipment.dewars.length : 0;
};
const getNumberOfDewars = (shipment: ShipmentsService): number => shipment.dewars?.length || 0;
console.log("Current selected shipment:", selectedShipment); // debug log
return (
<Box sx={{ width: '90%', borderRight: '1px solid #ccc', padding: 2, ...sx }}>
@ -123,37 +126,40 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
},
}}
>
<div style={{ display: 'flex', alignItems: 'center' }}>
<div style={{ position: 'relative', marginRight: '8px' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ position: 'relative', marginRight: '8px' }}>
<img
src={statusIconMap[shipment.shipment_status] || bottleGrey}
alt={`Status: ${shipment.shipment_status}`}
width="24"
/>
<span style={{
position: 'absolute',
top: '0%',
right: '0%',
transform: 'translate(50%, -50%)',
color: 'white',
fontWeight: 'bold',
fontSize: '0.6rem',
backgroundColor: 'transparent',
borderRadius: '50%',
padding: '0 2px',
}}>
{getNumberOfDewars(shipment)} {/* Calculate number of dewars */}
</span>
</div>
<div>
<div>{shipment.shipment_name}</div>
<div style={{ fontSize: '0.6rem', color: '#ccc' }}>{shipment.shipment_date}</div>
<div style={{ fontSize: '0.6rem', color: '#ccc' }}>
<Typography
component="span"
sx={{
position: 'absolute',
top: '0%',
right: '0%',
transform: 'translate(50%, -50%)',
color: 'white',
fontWeight: 'bold',
fontSize: '0.6rem',
backgroundColor: 'transparent',
borderRadius: '50%',
padding: '0 2px',
}}
>
{getNumberOfDewars(shipment)}
</Typography>
</Box>
<Box>
<Typography>{shipment.shipment_name}</Typography>
<Typography sx={{ fontSize: '0.6rem', color: '#ccc' }}>{shipment.shipment_date}</Typography>
<Typography sx={{ fontSize: '0.6rem', color: '#ccc' }}>
Total Pucks: {shipment.dewars.reduce((total, dewar: Dewar) => total + dewar.number_of_pucks, 0)}
</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center' }}>
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<IconButton
onClick={openUploadDialog}
color="primary"
@ -164,7 +170,11 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
</IconButton>
{selectedShipment?.shipment_id === shipment.shipment_id && (
<IconButton
onClick={handleDeleteShipment}
onClick={(event) => {
event.stopPropagation();
console.log('Delete button clicked'); // debug log
handleDeleteShipment();
}}
color="error"
title="Delete Shipment"
sx={{ marginLeft: 1 }}
@ -172,7 +182,7 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
<DeleteIcon />
</IconButton>
)}
</div>
</Box>
</Button>
))}
<UploadDialog

View File

@ -3,24 +3,25 @@ import Grid from '@mui/material/Grid';
import ShipmentPanel from '../components/ShipmentPanel';
import ShipmentDetails from '../components/ShipmentDetails';
import ShipmentForm from '../components/ShipmentForm';
import { Dewar, Shipment_Input, DefaultService, OpenAPI, ContactPerson } from '../../openapi';
import { Dewar, OpenAPI, ContactPerson, ShipmentsService } from '../../openapi';
type ShipmentViewProps = React.PropsWithChildren<Record<string, never>>;
const API_BASE_URL = 'http://127.0.0.1:8000';
OpenAPI.BASE = API_BASE_URL;
OpenAPI.BASE = API_BASE_URL; // Setting API base URL
const ShipmentView: React.FC<ShipmentViewProps> = () => {
const [isCreatingShipment, setIsCreatingShipment] = useState(false);
const [selectedShipment, setSelectedShipment] = useState<Shipment_Input | null>(null);
const [selectedShipment, setSelectedShipment] = useState<ShipmentsService | null>(null);
const [selectedDewar, setSelectedDewar] = useState<Dewar | null>(null);
const [shipments, setShipments] = useState<Shipment_Input[]>([]);
const [shipments, setShipments] = useState<ShipmentsService[]>([]);
const [error, setError] = useState<string | null>(null);
const [defaultContactPerson, setDefaultContactPerson] = useState<ContactPerson | undefined>();
// Function to fetch and set shipments
const fetchAndSetShipments = async () => {
try {
const shipmentsData: Shipment_Input[] = await DefaultService.getShipmentsShipmentsGet();
const shipmentsData: ShipmentsService[] = await ShipmentsService.fetchShipmentsShipmentsGet();
shipmentsData.sort((a, b) => new Date(b.shipment_date).getTime() - new Date(a.shipment_date).getTime());
setShipments(shipmentsData);
console.log('Fetched and set shipments:', shipmentsData);
@ -30,15 +31,18 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
}
};
// Function to fetch the default contact person
const fetchDefaultContactPerson = async () => {
try {
const c: ContactPerson[] = await DefaultService.getContactsContactsGet();
setDefaultContactPerson(c[0]);
} catch {
const contacts: ContactPerson[] = await ShipmentsService.getShipmentContactPersonsShipmentsContactPersonsGet();
setDefaultContactPerson(contacts[0]);
} catch (error) {
console.error('Failed to fetch contact persons:', error);
setError('Failed to load contact persons. Please try again later.');
}
};
// Use effects to fetch data on component mount
useEffect(() => {
fetchAndSetShipments();
fetchDefaultContactPerson();
@ -48,7 +52,8 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
console.log('Updated shipments:', shipments);
}, [shipments]);
const handleSelectShipment = (shipment: Shipment_Input | null) => {
// Handlers for selecting shipment and canceling form
const handleSelectShipment = (shipment: ShipmentsService | null) => {
setSelectedShipment(shipment);
setIsCreatingShipment(false);
};
@ -57,6 +62,7 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
setIsCreatingShipment(false);
};
// Render the shipment content based on state
const renderShipmentContent = () => {
if (isCreatingShipment) {
return (
@ -76,14 +82,15 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
selectedDewar={selectedDewar}
setSelectedDewar={setSelectedDewar}
setSelectedShipment={setSelectedShipment}
refreshShipments={fetchAndSetShipments}
defaultContactPerson={defaultContactPerson}
refreshShipments={fetchAndSetShipments} // Ensure refreshShipments is passed here
/>
);
}
return <div>No shipment details available.</div>;
};
// Render the main layout
return (
<Grid container spacing={2} sx={{ height: '100vh' }}>
<Grid
@ -97,10 +104,10 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
}}
>
<ShipmentPanel
selectedPage="Shipments"
setCreatingShipment={setIsCreatingShipment}
selectShipment={handleSelectShipment}
shipments={shipments}
selectedShipment={selectedShipment}
refreshShipments={fetchAndSetShipments}
error={error}
/>