diff --git a/backend/app/crud.py b/backend/app/crud.py index 44af03a..2002b76 100644 --- a/backend/app/crud.py +++ b/backend/app/crud.py @@ -8,7 +8,7 @@ def get_shipments(db: Session): shipments = ( db.query(Shipment) .options( - joinedload(Shipment.contact_person), + joinedload(Shipment.contact), joinedload(Shipment.return_address), joinedload(Shipment.proposal), joinedload(Shipment.dewars), @@ -30,7 +30,7 @@ def get_shipment_by_id(db: Session, id: int): shipment = ( db.query(Shipment) .options( - joinedload(Shipment.contact_person), + joinedload(Shipment.contact), joinedload(Shipment.return_address), joinedload(Shipment.proposal), joinedload(Shipment.dewars), diff --git a/backend/app/data/data.py b/backend/app/data/data.py index 55887e8..d5d494a 100644 --- a/backend/app/data/data.py +++ b/backend/app/data/data.py @@ -1,5 +1,5 @@ from app.models import ( - ContactPerson, + Contact, Address, Dewar, Proposal, @@ -34,71 +34,81 @@ serial_numbers = [ # Define contact persons contacts = [ - ContactPerson( + Contact( id=1, + pgroups="p20000, p20001", firstname="Frodo", lastname="Baggins", phone_number="123-456-7890", email="frodo.baggins@lotr.com", ), - ContactPerson( + Contact( id=2, + pgroups="p20000, p20002", firstname="Samwise", lastname="Gamgee", phone_number="987-654-3210", email="samwise.gamgee@lotr.com", ), - ContactPerson( + Contact( id=3, + pgroups="p20001, p20002", firstname="Aragorn", lastname="Elessar", phone_number="123-333-4444", email="aragorn.elessar@lotr.com", ), - ContactPerson( + Contact( id=4, + pgroups="p20003, p20004", firstname="Legolas", lastname="Greenleaf", phone_number="555-666-7777", email="legolas.greenleaf@lotr.com", ), - ContactPerson( + Contact( id=5, + pgroups="p20002, p20003", firstname="Gimli", lastname="Son of Gloin", phone_number="888-999-0000", email="gimli.sonofgloin@lotr.com", ), - ContactPerson( + Contact( id=6, + pgroups="p20001, p20002", firstname="Gandalf", lastname="The Grey", phone_number="222-333-4444", email="gandalf.thegrey@lotr.com", ), - ContactPerson( + Contact( id=7, + pgroups="p20000, p20004", firstname="Boromir", lastname="Son of Denethor", phone_number="111-222-3333", email="boromir.sonofdenethor@lotr.com", ), - ContactPerson( + Contact( id=8, + pgroups="p20001, p20002", firstname="Galadriel", lastname="Lady of Lothlórien", phone_number="444-555-6666", email="galadriel.lothlorien@lotr.com", ), - ContactPerson( + Contact( id=9, + pgroups="p20001, p20004", firstname="Elrond", lastname="Half-elven", phone_number="777-888-9999", email="elrond.halfelven@lotr.com", ), - ContactPerson( + Contact( id=10, + pgroups="p20004, p20006", firstname="Eowyn", lastname="Shieldmaiden of Rohan", phone_number="000-111-2222", @@ -184,7 +194,7 @@ dewars = [ dewar_serial_number_id=2, tracking_number="TRACK123", return_address_id=1, - contact_person_id=1, + contact_id=1, status="Ready for Shipping", ready_date=datetime.strptime("2023-09-30", "%Y-%m-%d"), shipping_date=None, @@ -199,7 +209,7 @@ dewars = [ dewar_serial_number_id=1, tracking_number="TRACK124", return_address_id=2, - contact_person_id=2, + contact_id=2, status="In Preparation", ready_date=None, shipping_date=None, @@ -214,7 +224,7 @@ dewars = [ dewar_serial_number_id=3, tracking_number="TRACK125", return_address_id=1, - contact_person_id=3, + contact_id=3, status="Not Shipped", ready_date=datetime.strptime("2024-01-01", "%Y-%m-%d"), shipping_date=None, @@ -229,7 +239,7 @@ dewars = [ dewar_serial_number_id=4, tracking_number="", return_address_id=1, - contact_person_id=3, + contact_id=3, status="Delayed", ready_date=datetime.strptime("2024-01-01", "%Y-%m-%d"), shipping_date=datetime.strptime("2024-01-02", "%Y-%m-%d"), @@ -244,7 +254,7 @@ dewars = [ dewar_serial_number_id=1, tracking_number="", return_address_id=1, - contact_person_id=3, + contact_id=3, status="Returned", arrival_date=datetime.strptime("2024-01-03", "%Y-%m-%d"), returning_date=datetime.strptime("2024-01-07", "%Y-%m-%d"), @@ -277,7 +287,7 @@ shipments = [ shipment_date=datetime.strptime("2024-10-10", "%Y-%m-%d"), shipment_name="Shipment from Mordor", shipment_status="Delivered", - contact_person_id=2, + contact_id=2, proposal_id=3, return_address_id=1, comments="Handle with care", @@ -288,7 +298,7 @@ shipments = [ shipment_date=datetime.strptime("2024-10-24", "%Y-%m-%d"), shipment_name="Shipment from Mordor", shipment_status="In Transit", - contact_person_id=4, + contact_id=4, proposal_id=4, return_address_id=2, comments="Contains the one ring", @@ -299,7 +309,7 @@ shipments = [ shipment_date=datetime.strptime("2024-10-28", "%Y-%m-%d"), shipment_name="Shipment from Mordor", shipment_status="In Transit", - contact_person_id=5, + contact_id=5, proposal_id=5, return_address_id=1, comments="Contains the one ring", diff --git a/backend/app/database.py b/backend/app/database.py index bdd244a..f3dd8ed 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -80,7 +80,7 @@ def load_sample_data(session: Session): ) # If any data exists, don't reseed - if session.query(models.ContactPerson).first(): + if session.query(models.Contact).first(): return session.add_all( diff --git a/backend/app/models.py b/backend/app/models.py index 015a3ed..925be64 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -21,25 +21,27 @@ class Shipment(Base): shipment_date = Column(Date) shipment_status = Column(String(255)) comments = Column(String(200), nullable=True) - contact_person_id = Column(Integer, ForeignKey("contact_persons.id")) + contact_id = Column(Integer, ForeignKey("contacts.id")) return_address_id = Column(Integer, ForeignKey("addresses.id")) proposal_id = Column(Integer, ForeignKey("proposals.id"), nullable=True) - contact_person = relationship("ContactPerson", back_populates="shipments") + contact = relationship("Contact", back_populates="shipments") return_address = relationship("Address", back_populates="shipments") proposal = relationship("Proposal", back_populates="shipments") dewars = relationship("Dewar", back_populates="shipment") -class ContactPerson(Base): - __tablename__ = "contact_persons" +class Contact(Base): + __tablename__ = "contacts" id = Column(Integer, primary_key=True, index=True, autoincrement=True) - firstname = Column(String(255)) + status = Column(String(255), default="active") + pgroups = Column(String(255), nullable=False) + firstname = Column(String(255), nullable=False) lastname = Column(String(255)) phone_number = Column(String(255)) email = Column(String(255)) - shipments = relationship("Shipment", back_populates="contact_person") + shipments = relationship("Shipment", back_populates="contact") class Address(Base): @@ -91,11 +93,11 @@ class Dewar(Base): unique_id = Column(String(255), unique=True, index=True, 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")) + contact_id = Column(Integer, ForeignKey("contacts.id")) shipment = relationship("Shipment", back_populates="dewars") return_address = relationship("Address") - contact_person = relationship("ContactPerson") + contact = relationship("Contact") pucks = relationship("Puck", back_populates="dewar") dewar_type = relationship("DewarType") diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py index 06abfac..8db84e8 100644 --- a/backend/app/routers/__init__.py +++ b/backend/app/routers/__init__.py @@ -1,5 +1,5 @@ -from .address import protected_router as address_router -from .contact import router as contact_router +from .address import address_router +from .contact import contact_router from .proposal import router as proposal_router from .dewar import router as dewar_router from .shipment import router as shipment_router diff --git a/backend/app/routers/address.py b/backend/app/routers/address.py index 303902f..f79ff25 100644 --- a/backend/app/routers/address.py +++ b/backend/app/routers/address.py @@ -1,4 +1,4 @@ -from fastapi import Depends, HTTPException, status, Query +from fastapi import Depends, HTTPException, status, Query, APIRouter from sqlalchemy.orm import Session from sqlalchemy import or_ from typing import List @@ -12,10 +12,11 @@ from app.schemas import ( ) from app.models import Address as AddressModel from app.dependencies import get_db -from app.routers.protected_router import protected_router + +address_router = APIRouter() -@protected_router.get("/", response_model=List[AddressSchema]) +@address_router.get("/", response_model=List[AddressSchema]) async def get_return_addresses( active_pgroup: str = Query(...), db: Session = Depends(get_db), @@ -36,7 +37,7 @@ async def get_return_addresses( return user_addresses -@protected_router.get("/all", response_model=List[AddressSchema]) +@address_router.get("/all", response_model=List[AddressSchema]) async def get_all_addresses( db: Session = Depends(get_db), current_user: loginData = Depends(get_current_user), @@ -52,7 +53,7 @@ async def get_all_addresses( return user_addresses -@protected_router.post( +@address_router.post( "/", response_model=AddressSchema, status_code=status.HTTP_201_CREATED ) async def create_return_address(address: AddressCreate, db: Session = Depends(get_db)): @@ -81,7 +82,7 @@ async def create_return_address(address: AddressCreate, db: Session = Depends(ge return db_address -@protected_router.put("/{address_id}", response_model=AddressSchema) +@address_router.put("/{address_id}", response_model=AddressSchema) async def update_return_address( address_id: int, address: AddressUpdate, db: Session = Depends(get_db) ): @@ -140,7 +141,7 @@ async def update_return_address( return new_address -@protected_router.delete("/{address_id}", status_code=status.HTTP_204_NO_CONTENT) +@address_router.delete("/{address_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_return_address(address_id: int, db: Session = Depends(get_db)): db_address = db.query(AddressModel).filter(AddressModel.id == address_id).first() if not db_address: diff --git a/backend/app/routers/contact.py b/backend/app/routers/contact.py index d64d117..fe0b059 100644 --- a/backend/app/routers/contact.py +++ b/backend/app/routers/contact.py @@ -1,36 +1,83 @@ -from fastapi import APIRouter, HTTPException, status, Depends +from fastapi import APIRouter, HTTPException, status, Depends, Query from sqlalchemy.orm import Session +from sqlalchemy import or_ from typing import List -from app.schemas import ContactPerson, ContactPersonCreate, ContactPersonUpdate -from app.models import ContactPerson as ContactPersonModel + +from app.schemas import Contact, ContactCreate, ContactUpdate, loginData +from app.models import Contact as ContactModel from app.dependencies import get_db +from app.routers.auth import get_current_user -router = APIRouter() +contact_router = APIRouter() -# Existing routes -@router.get("/", response_model=List[ContactPerson]) -async def get_contacts(db: Session = Depends(get_db)): - return db.query(ContactPersonModel).all() +# GET /contacts: Retrieve active contacts from the active_pgroup +@contact_router.get("/", response_model=List[Contact]) +async def get_contacts( + active_pgroup: str = Query(...), + db: Session = Depends(get_db), + current_user: loginData = Depends(get_current_user), +): + # Validate that the active_pgroup belongs to the user + if active_pgroup not in current_user.pgroups: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid pgroup provided.", + ) + + # Query for active contacts in the active_pgroup + contacts = ( + db.query(ContactModel) + .filter( + ContactModel.pgroups.like(f"%{active_pgroup}%"), + ContactModel.status == "active", + ) + .all() + ) + return contacts -@router.post("/", response_model=ContactPerson, status_code=status.HTTP_201_CREATED) -async def create_contact(contact: ContactPersonCreate, db: Session = Depends(get_db)): +# GET /contacts/all: Retrieve all contacts from the user's pgroups +@contact_router.get("/all", response_model=List[Contact]) +async def get_all_contacts( + db: Session = Depends(get_db), + current_user: loginData = Depends(get_current_user), +): + # Query for all contacts belonging to any of the user's pgroups + user_pgroups = current_user.pgroups + filters = [ContactModel.pgroups.like(f"%{pgroup}%") for pgroup in user_pgroups] + contacts = db.query(ContactModel).filter(or_(*filters)).all() + return contacts + + +@contact_router.post("/", response_model=Contact, status_code=status.HTTP_201_CREATED) +async def create_contact( + contact: ContactCreate, # Body parameter ONLY + db: Session = Depends(get_db), # Secondary dependency for database access +): + # Check if a contact with the same email already exists in this pgroup if ( - db.query(ContactPersonModel) - .filter(ContactPersonModel.email == contact.email) + db.query(ContactModel) + .filter( + ContactModel.email == contact.email, + ContactModel.pgroups.like(f"%{contact.pgroups}%"), + ContactModel.status == "active", + ) .first() ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="This contact already exists.", + detail="This contact already exists in the provided pgroup.", ) - db_contact = ContactPersonModel( + # Create a new contact + db_contact = ContactModel( firstname=contact.firstname, lastname=contact.lastname, phone_number=contact.phone_number, email=contact.email, + pgroups=contact.pgroups, # Use the pgroups from the body + status="active", # Newly created contacts will be active ) db.add(db_contact) db.commit() @@ -38,34 +85,78 @@ async def create_contact(contact: ContactPersonCreate, db: Session = Depends(get return db_contact -# New routes -@router.put("/{contact_id}", response_model=ContactPerson) +# PUT /contacts/{contact_id}: Update a contact +@contact_router.put("/{contact_id}", response_model=Contact) async def update_contact( - contact_id: int, contact: ContactPersonUpdate, db: Session = Depends(get_db) + contact_id: int, + contact: ContactUpdate, + db: Session = Depends(get_db), ): - db_contact = ( - db.query(ContactPersonModel).filter(ContactPersonModel.id == contact_id).first() - ) + # Retrieve the existing contact + db_contact = db.query(ContactModel).filter(ContactModel.id == contact_id).first() if not db_contact: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Contact not found." + status_code=status.HTTP_404_NOT_FOUND, + detail="Contact not found.", ) - for key, value in contact.dict(exclude_unset=True).items(): - setattr(db_contact, key, value) + # Normalize existing and new pgroups (remove whitespace, handle case + # sensitivity if needed) + existing_pgroups = ( + set(p.strip() for p in db_contact.pgroups.split(",") if p.strip()) + if db_contact.pgroups + else set() + ) + new_pgroups = ( + set(p.strip() for p in contact.pgroups.split(",") if p.strip()) + if contact.pgroups + else set() + ) + + # Check if any old pgroups are being removed (strict validation against removal) + if not new_pgroups.issuperset(existing_pgroups): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Modifying pgroups to remove existing ones is not allowed.", + ) + + combined_pgroups = existing_pgroups.union(new_pgroups) + + # Mark the old contact as inactive + db_contact.status = "inactive" db.commit() db.refresh(db_contact) - return db_contact - -@router.delete("/{contact_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_contact(contact_id: int, db: Session = Depends(get_db)): - db_contact = ( - db.query(ContactPersonModel).filter(ContactPersonModel.id == contact_id).first() + # Create a new contact with the updated data + new_contact = ContactModel( + firstname=contact.firstname or db_contact.firstname, + lastname=contact.lastname or db_contact.lastname, + phone_number=contact.phone_number or db_contact.phone_number, + email=contact.email or db_contact.email, + pgroups=",".join(combined_pgroups), # Use the active_pgroup + status="active", # Newly created contacts will be active ) + db.add(new_contact) + db.commit() + db.refresh(new_contact) + + return new_contact + + +# DELETE /contacts/{contact_id}: Mark a contact as inactive +@contact_router.delete("/{contact_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_contact( + contact_id: int, + db: Session = Depends(get_db), +): + # Retrieve the existing contact + db_contact = db.query(ContactModel).filter(ContactModel.id == contact_id).first() if not db_contact: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Contact not found." + status_code=status.HTTP_404_NOT_FOUND, + detail="Contact not found.", ) - db.delete(db_contact) + + # Mark the contact as inactive + db_contact.status = "inactive" db.commit() return diff --git a/backend/app/routers/protected_router.py b/backend/app/routers/protected_router.py index 62b0313..bb6a387 100644 --- a/backend/app/routers/protected_router.py +++ b/backend/app/routers/protected_router.py @@ -1,7 +1,12 @@ from fastapi import APIRouter, Depends from app.routers.auth import get_current_user +from app.routers.address import address_router +from app.routers.contact import contact_router protected_router = APIRouter( dependencies=[Depends(get_current_user)] # Applies to all routes ) + +protected_router.include_router(address_router, prefix="/addresses", tags=["addresses"]) +protected_router.include_router(contact_router, prefix="/contacts", tags=["contacts"]) diff --git a/backend/app/routers/shipment.py b/backend/app/routers/shipment.py index 1cb3cd0..f0e07e8 100644 --- a/backend/app/routers/shipment.py +++ b/backend/app/routers/shipment.py @@ -7,7 +7,7 @@ import json from app.models import ( Shipment as ShipmentModel, - ContactPerson as ContactPersonModel, + Contact as ContactModel, Address as AddressModel, Proposal as ProposalModel, Dewar as DewarModel, @@ -19,7 +19,7 @@ from app.schemas import ( ShipmentCreate, UpdateShipmentComments, Shipment as ShipmentSchema, - ContactPerson as ContactPersonSchema, + Contact as ContactSchema, Sample as SampleSchema, DewarSchema, ) @@ -71,10 +71,8 @@ async def get_dewars_by_shipment_id(shipment_id: int, db: Session = Depends(get_ @router.post("", response_model=ShipmentSchema, status_code=status.HTTP_201_CREATED) async def create_shipment(shipment: ShipmentCreate, db: Session = Depends(get_db)): - contact_person = ( - db.query(ContactPersonModel) - .filter(ContactPersonModel.id == shipment.contact_person_id) - .first() + contact = ( + db.query(ContactModel).filter(ContactModel.id == shipment.contact_id).first() ) return_address = ( db.query(AddressModel) @@ -85,7 +83,7 @@ async def create_shipment(shipment: ShipmentCreate, db: Session = Depends(get_db db.query(ProposalModel).filter(ProposalModel.id == shipment.proposal_id).first() ) - if not (contact_person or return_address or proposal): + if not (contact or return_address or proposal): raise HTTPException(status_code=404, detail="Associated entity not found") db_shipment = ShipmentModel( @@ -93,7 +91,7 @@ async def create_shipment(shipment: ShipmentCreate, db: Session = Depends(get_db shipment_date=shipment.shipment_date, shipment_status=shipment.shipment_status, comments=shipment.comments, - contact_person_id=contact_person.id, + contact_id=contact.id, return_address_id=return_address.id, proposal_id=proposal.id, ) @@ -189,8 +187,8 @@ async def update_shipment( # Validate relationships by IDs contact_person = ( - db.query(ContactPersonModel) - .filter(ContactPersonModel.id == updated_shipment.contact_person_id) + db.query(ContactModel) + .filter(ContactModel.id == updated_shipment.contact_person_id) .first() ) return_address = ( @@ -225,9 +223,7 @@ async def update_shipment( for key, value in update_fields.items(): if key == "contact_person_id": contact_person = ( - db.query(ContactPersonModel) - .filter(ContactPersonModel.id == value) - .first() + db.query(ContactModel).filter(ContactModel.id == value).first() ) if not contact_person: raise HTTPException( @@ -342,9 +338,9 @@ async def remove_dewar_from_shipment( return shipment -@router.get("/contact_persons", response_model=List[ContactPersonSchema]) +@router.get("/contact_persons", response_model=List[ContactSchema]) async def get_shipment_contact_persons(db: Session = Depends(get_db)): - contact_persons = db.query(ContactPersonModel).all() + contact_persons = db.query(ContactModel).all() return contact_persons diff --git a/backend/app/schemas.py b/backend/app/schemas.py index b8e4a53..411fa3d 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -366,25 +366,24 @@ class Results(BaseModel): pass -class ContactPersonBase(BaseModel): +class ContactCreate(BaseModel): + pgroups: str firstname: str lastname: str phone_number: str email: EmailStr -class ContactPersonCreate(ContactPersonBase): - pass - - -class ContactPerson(ContactPersonBase): +class Contact(ContactCreate): id: int + status: str = "active" class Config: from_attributes = True -class ContactPersonUpdate(BaseModel): +class ContactUpdate(BaseModel): + pgroups: str firstname: Optional[str] = None lastname: Optional[str] = None phone_number: Optional[str] = None @@ -510,7 +509,7 @@ class DewarBase(BaseModel): shipping_date: Optional[date] arrival_date: Optional[date] returning_date: Optional[date] - contact_person_id: Optional[int] + contact_id: Optional[int] return_address_id: Optional[int] pucks: List[PuckCreate] = [] @@ -525,7 +524,7 @@ class DewarCreate(DewarBase): class Dewar(DewarBase): id: int shipment_id: Optional[int] - contact_person: Optional[ContactPerson] + contact: Optional[Contact] return_address: Optional[Address] pucks: List[Puck] = [] # List of pucks within this dewar @@ -544,7 +543,7 @@ class DewarUpdate(BaseModel): shipping_date: Optional[date] = None arrival_date: Optional[date] = None returning_date: Optional[date] = None - contact_person_id: Optional[int] = None + contact_id: Optional[int] = None address_id: Optional[int] = None @@ -553,7 +552,7 @@ class DewarSchema(BaseModel): dewar_name: str tracking_number: str status: str - contact_person_id: int + contact_id: int return_address_id: int class Config: @@ -574,7 +573,7 @@ class Shipment(BaseModel): shipment_date: date shipment_status: str comments: Optional[str] - contact_person: Optional[ContactPerson] + contact: Optional[Contact] return_address: Optional[Address] proposal: Optional[Proposal] dewars: List[Dewar] = [] @@ -588,7 +587,7 @@ class ShipmentCreate(BaseModel): shipment_date: date shipment_status: str comments: Optional[constr(max_length=200)] - contact_person_id: int + contact_id: int return_address_id: int proposal_id: int dewars: List[DewarCreate] = [] @@ -621,7 +620,7 @@ class SlotSchema(BaseModel): retrievedTimestamp: Optional[str] beamlineLocation: Optional[str] shipment_name: Optional[str] - contact_person: Optional[str] + contact: Optional[str] local_contact: Optional[str] class Config: diff --git a/backend/main.py b/backend/main.py index b6c62a3..3d3b3c2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -6,8 +6,6 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app import ssl_heidi from app.routers import ( - address, - contact, proposal, dewar, shipment, @@ -157,8 +155,6 @@ def on_startup(): # Include routers with correct configuration app.include_router(protected_router, prefix="/protected", tags=["protected"]) app.include_router(auth.router, prefix="/auth", tags=["auth"]) -app.include_router(contact.router, prefix="/contacts", tags=["contacts"]) -app.include_router(address.protected_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"]) diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index e0b0d5a..e939409 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -33,4 +33,7 @@ def test_protected_route(): headers = {"Authorization": f"Bearer {token}"} response = client.get("/auth/protected-route", headers=headers) assert response.status_code == 200 - assert response.json() == {"username": "testuser", "pgroups": [20000, 20001, 20003]} + assert response.json() == { + "username": "testuser", + "pgroups": [20000, 20001, 20002, 20003], + } diff --git a/frontend/public/shipmentsdb.json b/frontend/public/shipmentsdb.json index 1c0914c..fd8a6f8 100644 --- a/frontend/public/shipmentsdb.json +++ b/frontend/public/shipmentsdb.json @@ -6,7 +6,7 @@ "number_of_dewars": 2, "shipment_status": "In Transit", "shipment_date": "2024-01-15", - "contact_person": [ + "contact": [ { "name": "Alice Johnson", "id": "alice" } ], "dewars": [ @@ -19,7 +19,7 @@ "return_address": [ { "address": "123 Main St, Anytown, USA", "id": "address1" } ], - "contact_person": [ + "contact": [ { "name": "Alice Johnson", "id": "alice" } ], "status": "in preparation", @@ -40,7 +40,7 @@ "return_address": [ { "address": "123 Main St, Anytown, USA", "id": "address1" } ], - "contact_person": [ + "contact": [ { "name": "Alice Johnson", "id": "alice" } ], "status": "in preparation", @@ -60,7 +60,7 @@ "number_of_dewars": 3, "shipment_status": "In Transit", "shipment_date": "2024-02-20", - "contact_person": [ + "contact": [ { "name": "Bob Smith", "id": "bob" } ], "dewars": [ @@ -70,7 +70,7 @@ "tracking_number": "TRACK987654", "number_of_pucks": 5, "number_of_samples": 30, - "contact_person": [ + "contact": [ { "name": "Bob Smith", "id": "bob" } ], "return_address": [ @@ -91,7 +91,7 @@ "tracking_number": "TRACK876543", "number_of_pucks": 6, "number_of_samples": 36, - "contact_person": [ + "contact": [ { "name": "Bob Smith", "id": "bob" } ], "return_address": [ @@ -112,7 +112,7 @@ "tracking_number": "TRACK765432", "number_of_pucks": 4, "number_of_samples": 24, - "contact_person": [ + "contact": [ { "name": "Bob Smith", "id": "bob" } ], "return_address": [ @@ -135,7 +135,7 @@ "number_of_dewars": 5, "shipment_status": "Pending", "shipment_date": "2024-03-10", - "contact_person": [ + "contact": [ { "name": "Charlie Brown", "id": "charlie" } ], "dewars": [ @@ -145,7 +145,7 @@ "tracking_number": "TRACK112233", "number_of_pucks": 7, "number_of_samples": 42, - "contact_person": [ + "contact": [ { "name": "Charlie Brown", "id": "charlie" } ], "return_address": [ @@ -166,7 +166,7 @@ "tracking_number": "TRACK223344", "number_of_pucks": 5, "number_of_samples": 30, - "contact_person": [ + "contact": [ { "name": "Charlie Brown", "id": "charlie" } ], "return_address": [ @@ -187,7 +187,7 @@ "tracking_number": "TRACK334455", "number_of_pucks": 8, "number_of_samples": 48, - "contact_person": [ + "contact": [ { "name": "Charlie Brown", "id": "charlie" } ], "return_address": [ @@ -208,7 +208,7 @@ "tracking_number": "TRACK445566", "number_of_pucks": 6, "number_of_samples": 36, - "contact_person": [ + "contact": [ { "name": "Charlie Brown", "id": "charlie" } ], "return_address": [ @@ -229,7 +229,7 @@ "tracking_number": "TRACK556677", "number_of_pucks": 4, "number_of_samples": 24, - "contact_person": [ + "contact": [ { "name": "Charlie Brown", "id": "charlie" } ], "return_address": [ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 04170ca..23faa19 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -90,7 +90,7 @@ const App: React.FC = () => { - + ); diff --git a/frontend/src/components/DewarDetails.tsx b/frontend/src/components/DewarDetails.tsx index 2ed3359..903ef89 100644 --- a/frontend/src/components/DewarDetails.tsx +++ b/frontend/src/components/DewarDetails.tsx @@ -22,7 +22,7 @@ import { Dewar, DewarType, DewarSerialNumber, - ContactPerson, + Contact, Address, ContactsService, AddressesService, @@ -37,14 +37,14 @@ interface DewarDetailsProps { dewar: Dewar; trackingNumber: string; setTrackingNumber: (trackingNumber: string) => void; - initialContactPersons?: ContactPerson[]; + initialContacts?: Contact[]; initialReturnAddresses?: Address[]; - defaultContactPerson?: ContactPerson; + defaultContact?: Contact; defaultReturnAddress?: Address; shipmentId: number; } -interface NewContactPerson { +interface NewContact { id: number; firstName: string; lastName: string; @@ -64,21 +64,21 @@ const DewarDetails: React.FC = ({ dewar, trackingNumber, setTrackingNumber, - initialContactPersons = [], + initialContacts = [], initialReturnAddresses = [], - defaultContactPerson, + defaultContact, defaultReturnAddress, shipmentId, }) => { const [localTrackingNumber, setLocalTrackingNumber] = useState(trackingNumber); - const [contactPersons, setContactPersons] = useState(initialContactPersons); + const [contacts, setContacts] = useState(initialContacts); const [returnAddresses, setReturnAddresses] = useState(initialReturnAddresses); - const [selectedContactPerson, setSelectedContactPerson] = useState(''); + const [selectedContact, setSelectedContact] = useState(''); const [selectedReturnAddress, setSelectedReturnAddress] = useState(''); - const [isCreatingContactPerson, setIsCreatingContactPerson] = useState(false); + const [isCreatingContact, setIsCreatingContact] = useState(false); const [isCreatingReturnAddress, setIsCreatingReturnAddress] = useState(false); const [puckStatuses, setPuckStatuses] = useState([]); - const [newContactPerson, setNewContactPerson] = useState({ + const [newContact, setNewContact] = useState({ id: 0, firstName: '', lastName: '', @@ -140,9 +140,9 @@ const DewarDetails: React.FC = ({ useEffect(() => { setLocalTrackingNumber(dewar.tracking_number || ''); - const setInitialContactPerson = () => { - setSelectedContactPerson( - dewar.contact_person?.id?.toString() || defaultContactPerson?.id?.toString() || '' + const setInitialContact = () => { + setSelectedContact( + dewar.contact?.id?.toString() || defaultContact?.id?.toString() || '' ); }; @@ -152,7 +152,7 @@ const DewarDetails: React.FC = ({ ); }; - setInitialContactPerson(); + setInitialContact(); setInitialReturnAddress(); if (dewar.dewar_type_id) { @@ -161,7 +161,7 @@ const DewarDetails: React.FC = ({ if (dewar.dewar_serial_number_id) { setSelectedSerialNumber(dewar.dewar_serial_number_id.toString()); } - }, [dewar, defaultContactPerson, defaultReturnAddress]); + }, [dewar, defaultContact, defaultReturnAddress]); useEffect(() => { const getContacts = async () => { @@ -375,7 +375,7 @@ const DewarDetails: React.FC = ({ arrival_date: dewar.arrival_date, returning_date: dewar.returning_date, return_address_id: parseInt(selectedReturnAddress ?? '', 10), - contact_person_id: parseInt(selectedContactPerson ?? '', 10), + contact_id: parseInt(selectedContactPerson ?? '', 10), }; await DewarsService.updateDewarDewarsDewarIdPut(dewarId, payload); diff --git a/frontend/src/components/DewarStepper.tsx b/frontend/src/components/DewarStepper.tsx index 3932cb4..e717257 100644 --- a/frontend/src/components/DewarStepper.tsx +++ b/frontend/src/components/DewarStepper.tsx @@ -85,7 +85,7 @@ const StepIconComponent: React.FC = ({ icon, dewar, isSe returning_date: dewar.returning_date, qrcode: dewar.qrcode, return_address_id: dewar.return_address_id, - contact_person_id: dewar.contact_person_id, + contact_id: dewar.contact_id, }; await DewarsService.updateDewarDewarsDewarIdPut(dewar.id, payload); diff --git a/frontend/src/components/ShipmentDetails.tsx b/frontend/src/components/ShipmentDetails.tsx index 0e232ba..40de626 100644 --- a/frontend/src/components/ShipmentDetails.tsx +++ b/frontend/src/components/ShipmentDetails.tsx @@ -4,7 +4,7 @@ import QRCode from 'react-qr-code'; import DeleteIcon from "@mui/icons-material/Delete"; import CheckIcon from '@mui/icons-material/Check'; import CloseIcon from '@mui/icons-material/Close'; -import { Dewar, DewarsService, Shipment, ContactPerson, ApiError, ShipmentsService } from "../../openapi"; +import { Dewar, DewarsService, Shipment, Contact, ApiError, ShipmentsService } from "../../openapi"; import { SxProps } from "@mui/system"; import CustomStepper from "./DewarStepper"; import DewarDetails from './DewarDetails'; @@ -20,7 +20,7 @@ interface ShipmentDetailsProps { setSelectedDewar: React.Dispatch>; setSelectedShipment: React.Dispatch>; refreshShipments: () => void; - defaultContactPerson?: ContactPerson; + defaultContact?: Contact; } const ShipmentDetails: React.FC = ({ @@ -48,7 +48,7 @@ const ShipmentDetails: React.FC = ({ shipping_date: null, arrival_date: null, returning_date: null, - contact_person_id: selectedShipment?.contact_person?.id, + contact_id: selectedShipment?.contact?.id, return_address_id: selectedShipment?.return_address?.id, }; @@ -59,7 +59,7 @@ const ShipmentDetails: React.FC = ({ // Ensure to update the default contact person and return address when the shipment changes setNewDewar((prev) => ({ ...prev, - contact_person_id: selectedShipment?.contact_person?.id, + contact_id: selectedShipment?.contact?.id, return_address_id: selectedShipment?.return_address?.id })); }, [selectedShipment]); @@ -122,7 +122,7 @@ const ShipmentDetails: React.FC = ({ ...initialNewDewarState, ...newDewar, dewar_name: newDewar.dewar_name.trim(), - contact_person_id: selectedShipment?.contact_person?.id, + contact_id: selectedShipment?.contact?.id, return_address_id: selectedShipment?.return_address?.id } as Dewar; @@ -179,7 +179,7 @@ const ShipmentDetails: React.FC = ({ }; const isCommentsEdited = comments !== initialComments; - const contactPerson = selectedShipment?.contact_person; + const contact = selectedShipment?.contact; return ( @@ -228,7 +228,7 @@ const ShipmentDetails: React.FC = ({ {selectedShipment.shipment_name} - Main contact person: {contactPerson ? `${contactPerson.firstname} ${contactPerson.lastname}` : 'N/A'} + Main contact person: {contact ? `${contact.firstname} ${contact.lastname}` : 'N/A'} Number of Pucks: {totalPucks} Number of Samples: {totalSamples} @@ -318,7 +318,7 @@ const ShipmentDetails: React.FC = ({ Number of Samples: {dewar.number_of_samples || 0} Tracking Number: {dewar.tracking_number} - Contact Person: {dewar.contact_person?.firstname ? `${dewar.contact_person.firstname} ${dewar.contact_person.lastname}` : 'N/A'} + Contact Person: {dewar.contact?.firstname ? `${dewar.contact.firstname} ${dewar.contact.lastname}` : 'N/A'} = ({ setTrackingNumber={(value) => { setLocalSelectedDewar((prev) => (prev ? { ...prev, tracking_number: value as string } : prev)); }} - initialContactPersons={localSelectedDewar?.contact_person ? [localSelectedDewar.contact_person] : []} + initialContacts={localSelectedDewar?.contact ? [localSelectedDewar.contact] : []} initialReturnAddresses={localSelectedDewar?.return_address ? [localSelectedDewar.return_address] : []} - defaultContactPerson={localSelectedDewar?.contact_person ?? undefined} + defaultContact={localSelectedDewar?.contact ?? undefined} defaultReturnAddress={localSelectedDewar?.return_address ?? undefined} shipmentId={selectedShipment?.id ?? null} refreshShipments={refreshShipments} diff --git a/frontend/src/components/ShipmentForm.tsx b/frontend/src/components/ShipmentForm.tsx index 4379237..f973f03 100644 --- a/frontend/src/components/ShipmentForm.tsx +++ b/frontend/src/components/ShipmentForm.tsx @@ -6,7 +6,7 @@ import { import { SelectChangeEvent } from '@mui/material'; import { SxProps } from '@mui/system'; import { - ContactPersonCreate, ContactPerson, Address, AddressCreate, Proposal, ContactsService, AddressesService, ProposalsService, + ContactCreate, Contact, Address, AddressCreate, Proposal, ContactsService, AddressesService, ProposalsService, OpenAPI, ShipmentCreate, ShipmentsService } from '../../openapi'; import { useEffect } from 'react'; @@ -41,21 +41,21 @@ const ShipmentForm: React.FC = ({ onCancel, refreshShipments }) => { const [countrySuggestions, setCountrySuggestions] = React.useState([]); - const [contactPersons, setContactPersons] = React.useState([]); + const [contacts, setContacts] = React.useState([]); const [returnAddresses, setReturnAddresses] = React.useState([]); const [proposals, setProposals] = React.useState([]); - const [isCreatingContactPerson, setIsCreatingContactPerson] = React.useState(false); + const [isCreatingContact, setIsCreatingContact] = React.useState(false); const [isCreatingReturnAddress, setIsCreatingReturnAddress] = React.useState(false); - const [newContactPerson, setNewContactPerson] = React.useState({ - firstname: '', lastname: '', phone_number: '', email: '' + const [newContact, setNewContact] = React.useState({ + pgroups:'', firstname: '', lastname: '', phone_number: '', email: '' }); const [newReturnAddress, setNewReturnAddress] = React.useState>({ - pgroup:'', house_number: '', street: '', city: '', state: '', zipcode: '', country: '' + pgroups:'', house_number: '', street: '', city: '', state: '', zipcode: '', country: '' }); const [newShipment, setNewShipment] = React.useState>({ shipment_name: '', shipment_status: 'In preparation', comments: '' }); - const [selectedContactPersonId, setSelectedContactPersonId] = React.useState(null); + const [selectedContactId, setSelectedContactId] = React.useState(null); const [selectedReturnAddressId, setSelectedReturnAddressId] = React.useState(null); const [selectedProposalId, setSelectedProposalId] = React.useState(null); const [errorMessage, setErrorMessage] = React.useState(null); @@ -83,12 +83,17 @@ const ShipmentForm: React.FC = ({ // Fetch necessary data const getContacts = async () => { + if (!activePgroup) { + console.error("Active pgroup is missing."); + setErrorMessage("Active pgroup is missing. Unable to load contacts."); + return; + } try { - const fetchedContacts: ContactPerson[] = - await ContactsService.getContactsContactsGet(); - setContactPersons(fetchedContacts); + const fetchedContacts: Contact[] = + await ContactsService.getContactsProtectedContactsGet(activePgroup); + setContacts(fetchedContacts); } catch { - setErrorMessage('Failed to load contact persons.'); + setErrorMessage('Failed to load contact s.'); } }; @@ -102,7 +107,7 @@ const ShipmentForm: React.FC = ({ try { // Pass activePgroup directly as a string (not as an object) const fetchedAddresses: Address[] = - await AddressesService.getReturnAddressesAddressesGet(activePgroup); + await AddressesService.getReturnAddressesProtectedAddressesGet(activePgroup); setReturnAddresses(fetchedAddresses); } catch (error) { @@ -150,9 +155,9 @@ const ShipmentForm: React.FC = ({ }; const isContactFormValid = () => { - const { firstname, lastname, phone_number, email } = newContactPerson; + const { firstname, lastname, phone_number, email } = newContact; - if (isCreatingContactPerson) { + if (isCreatingContact) { if (!firstname || !lastname || !validateEmail(email) || !validatePhoneNumber(phone_number)) return false; } @@ -173,9 +178,9 @@ const ShipmentForm: React.FC = ({ const { shipment_name } = newShipment; if (!shipment_name) return false; - if (!selectedContactPersonId || !selectedReturnAddressId || !selectedProposalId) return false; + if (!selectedContactId || !selectedReturnAddressId || !selectedProposalId) return false; - if (isCreatingContactPerson && !isContactFormValid()) return false; + if (isCreatingContact && !isContactFormValid()) return false; if (isCreatingReturnAddress && !isAddressFormValid()) return false; return true; @@ -197,7 +202,7 @@ const ShipmentForm: React.FC = ({ shipment_date: new Date().toISOString().split('T')[0], // Remove if date is not required shipment_status: newShipment.shipment_status || 'In preparation', comments: newShipment.comments || '', - contact_person_id: selectedContactPersonId!, + contact_id: selectedContactId!, return_address_id: selectedReturnAddressId!, proposal_id: selectedProposalId!, dewars: newShipment.dewars || [], @@ -217,14 +222,14 @@ const ShipmentForm: React.FC = ({ } }; - const handleContactPersonChange = (event: SelectChangeEvent) => { + const handleContactChange = (event: SelectChangeEvent) => { const value = event.target.value; if (value === 'new') { - setIsCreatingContactPerson(true); - setSelectedContactPersonId(null); + setIsCreatingContact(true); + setSelectedContactId(null); } else { - setIsCreatingContactPerson(false); - setSelectedContactPersonId(parseInt(value)); + setIsCreatingContact(false); + setSelectedContactId(parseInt(value)); } }; @@ -244,33 +249,52 @@ const ShipmentForm: React.FC = ({ setSelectedProposalId(parseInt(value)); }; - const handleSaveNewContactPerson = async () => { - if (!isContactFormValid()) { + const handleSaveNewContact = async () => { + // Validate contact form fields + if (!isContactFormValid(newContact)) { setErrorMessage('Please fill in all new contact person fields correctly.'); return; } - 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 newPerson: ContactPerson = await ContactsService.createContactContactsPost(payload); - setContactPersons([...contactPersons, newPerson]); - setErrorMessage(null); - 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.'); + // Ensure activePgroup is available + if (!activePgroup) { + setErrorMessage('Active pgroup is missing. Please try again.'); + return; } - setNewContactPerson({ firstname: '', lastname: '', phone_number: '', email: '' }); - setIsCreatingContactPerson(false); + // Construct the payload + const payload: ContactCreate = { + pgroups: activePgroup, // Ensure this value is available + firstname: newContact.firstname.trim(), + lastname: newContact.lastname.trim(), + phone_number: newContact.phone_number.trim(), + email: newContact.email.trim(), + }; + + console.log('Payload being sent:', JSON.stringify(payload, null, 2)); + + try { + // Call the API with the correctly constructed payload + const newPerson: Contact = await ContactsService.createContactProtectedContactsPost(payload); + + // Update state on success + setContacts([...contacts, newPerson]); // Add new contact to the list + setErrorMessage(null); // Clear error messages + setSelectedContactId(newPerson.id); // Optionally select the contact + + // Reset form inputs + setNewContact({ pgroups: '', firstname: '', lastname: '', phone_number: '', email: '' }); + setIsCreatingContact(false); + } catch (error) { + console.error('Failed to create a new contact person:', error); + + // Handle detailed backend error messages if available + if (error.response?.data?.detail) { + setErrorMessage(`Error: ${error.response.data.detail}`); + } else { + setErrorMessage('Failed to create a new contact person. Please try again later.'); + } + } }; const handleSaveNewReturnAddress = async () => { @@ -301,7 +325,7 @@ const ShipmentForm: React.FC = ({ // Call the API with the completed payload try { - const response: Address = await AddressesService.createReturnAddressAddressesPost(payload); + const response: Address = await AddressesService.createReturnAddressProtectedAddressesPost(payload); setReturnAddresses([...returnAddresses, response]); // Update the address state setErrorMessage(null); setSelectedReturnAddressId(response.id); // Set the newly created address ID to the form @@ -311,7 +335,7 @@ const ShipmentForm: React.FC = ({ } // Reset form inputs and close the "Create New Address" form - setNewReturnAddress({ pgroup: '', house_number: '', street: '', city: '', state: '', zipcode: '', country: '' }); + setNewReturnAddress({ pgroups: '', house_number: '', street: '', city: '', state: '', zipcode: '', country: '' }); setIsCreatingReturnAddress(false); }; @@ -342,11 +366,11 @@ const ShipmentForm: React.FC = ({ Contact Person - {isCreatingContactPerson && ( + {isCreatingContact && ( <> setNewContactPerson({ ...newContactPerson, firstname: e.target.value })} + value={newContact.firstname} + onChange={(e) => setNewContact({ ...newContact, firstname: e.target.value })} fullWidth required /> setNewContactPerson({ ...newContactPerson, lastname: e.target.value })} + value={newContact.lastname} + onChange={(e) => setNewContact({ ...newContact, lastname: e.target.value })} fullWidth required /> @@ -378,28 +402,28 @@ const ShipmentForm: React.FC = ({ label="Phone" name="phone_number" type="tel" - value={newContactPerson.phone_number} - onChange={(e) => setNewContactPerson({ ...newContactPerson, phone_number: e.target.value })} + value={newContact.phone_number} + onChange={(e) => setNewContact({ ...newContact, phone_number: e.target.value })} fullWidth required - error={!validatePhoneNumber(newContactPerson.phone_number)} - helperText={!validatePhoneNumber(newContactPerson.phone_number) ? 'Invalid phone number' : ''} + error={!validatePhoneNumber(newContact.phone_number)} + helperText={!validatePhoneNumber(newContact.phone_number) ? 'Invalid phone number' : ''} /> setNewContactPerson({ ...newContactPerson, email: e.target.value })} + value={newContact.email} + onChange={(e) => setNewContact({ ...newContact, email: e.target.value })} fullWidth required - error={!validateEmail(newContactPerson.email)} - helperText={!validateEmail(newContactPerson.email) ? 'Invalid email' : ''} + error={!validateEmail(newContact.email)} + helperText={!validateEmail(newContact.email) ? 'Invalid email' : ''} /> - - - - - ); + openDialog(contact)}> + + + + + )) + ) : ( + No contacts found + )} + + + {"Confirm Delete"} + + + Are you sure you want to delete this contact? + + + + + + + + + ); }; export default ContactsManager; \ No newline at end of file diff --git a/frontend/src/pages/ShipmentView.tsx b/frontend/src/pages/ShipmentView.tsx index 02127b3..1cc2c9a 100644 --- a/frontend/src/pages/ShipmentView.tsx +++ b/frontend/src/pages/ShipmentView.tsx @@ -11,7 +11,7 @@ type ShipmentViewProps = { }; const ShipmentView: React.FC = ( { activePgroup }) => { - const { shipments, error, defaultContactPerson, fetchAndSetShipments } = useShipments(); + const { shipments, error, defaultContact, fetchAndSetShipments } = useShipments(); const [selectedShipment, setSelectedShipment] = useState(null); const [selectedDewar, setSelectedDewar] = useState(null); const [isCreatingShipment, setIsCreatingShipment] = useState(false); @@ -76,7 +76,7 @@ const ShipmentView: React.FC = ( { activePgroup }) => { setSelectedDewar={setSelectedDewar} setSelectedShipment={setSelectedShipment} refreshShipments={fetchAndSetShipments} - defaultContactPerson={defaultContactPerson} + defaultContact={defaultContact} /> ); } diff --git a/frontend/src/types.ts b/frontend/src/types.ts index e25b310..dc67538 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1,4 +1,4 @@ -export interface ContactPerson { +export interface Contact { id: string; lastname: string; firstname: string; @@ -29,7 +29,7 @@ export interface Dewar { number_of_pucks: number; number_of_samples: number; return_address: ReturnAddress[]; - contact_person: ContactPerson[]; + contact_: Contact[]; status: string; ready_date?: string; // Make sure this is included shipping_date?: string; // Make sure this is included @@ -45,7 +45,7 @@ export interface Shipment { shipment_date: string; number_of_dewars: number; shipment_status: string; - contact_person: ContactPerson[] | null; // Change to an array to accommodate multiple contacts + contact_: Contact[] | null; // Change to an array to accommodate multiple contacts proposal_number?: string; return_address: Address[]; // Change to an array of Address comments?: string;