now creating dewars from spreadsheet

This commit is contained in:
GotthardG 2024-11-12 14:00:32 +01:00
parent 5e6eb40033
commit 86883133a7
14 changed files with 1284 additions and 1027 deletions

View File

@ -25,7 +25,7 @@ class Shipment(Base):
class ContactPerson(Base): class ContactPerson(Base):
__tablename__ = "contact_persons" __tablename__ = "contact_persons"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True, autoincrement=True)
firstname = Column(String) firstname = Column(String)
lastname = Column(String) lastname = Column(String)
phone_number = Column(String) phone_number = Column(String)
@ -37,7 +37,7 @@ class ContactPerson(Base):
class Address(Base): class Address(Base):
__tablename__ = "addresses" __tablename__ = "addresses"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True, autoincrement=True)
street = Column(String) street = Column(String)
city = Column(String) city = Column(String)
zipcode = Column(String) zipcode = Column(String)
@ -81,7 +81,7 @@ class Dewar(Base):
class Proposal(Base): class Proposal(Base):
__tablename__ = "proposals" __tablename__ = "proposals"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True, autoincrement=True)
number = Column(String) number = Column(String)
shipments = relationship("Shipment", back_populates="proposal") shipments = relationship("Shipment", back_populates="proposal")
@ -90,21 +90,21 @@ class Proposal(Base):
class Puck(Base): class Puck(Base):
__tablename__ = 'pucks' __tablename__ = 'pucks'
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True, autoincrement=True)
puck_name = Column(String, index=True) puck_name = Column(String, index=True)
puck_type = Column(String) puck_type = Column(String)
puck_location_in_dewar = Column(Integer) puck_location_in_dewar = Column(Integer)
# Foreign keys and relationships # Foreign keys and relationships
dewar_id = Column(Integer, ForeignKey('dewars.id')) dewar_id = Column(Integer, ForeignKey('dewars.id'))
dewar = relationship("Dewar", back_populates="pucks") # Properly define the other side of the relationship dewar = relationship("Dewar", back_populates="pucks")
samples = relationship("Sample", back_populates="puck") samples = relationship("Sample", back_populates="puck")
class Sample(Base): class Sample(Base):
__tablename__ = 'samples' __tablename__ = 'samples'
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True, autoincrement=True)
sample_name = Column(String, index=True) # Matches `sample_name` in data creation sample_name = Column(String, index=True) # Matches `sample_name` in data creation
position = Column(Integer) # Matches `position` in data creation script position = Column(Integer) # Matches `position` in data creation script

View File

@ -2,8 +2,8 @@ from fastapi import APIRouter, HTTPException, status, Depends
from sqlalchemy.orm import Session, joinedload from sqlalchemy.orm import Session, joinedload
from typing import List from typing import List
import uuid import uuid
from app.schemas import Dewar as DewarSchema, DewarCreate, DewarUpdate, Sample as SampleSchema, Puck as PuckSchema from app.schemas import Dewar as DewarSchema, DewarCreate, DewarUpdate
from app.models import Dewar as DewarModel, Puck as PuckModel, Sample as SampleModel from app.models import Dewar as DewarModel, Puck as PuckModel
from app.dependencies import get_db from app.dependencies import get_db
router = APIRouter() router = APIRouter()
@ -38,7 +38,7 @@ async def create_dewar(dewar: DewarCreate, db: Session = Depends(get_db)) -> Dew
@router.get("/{dewar_id}", response_model=DewarSchema) @router.get("/{dewar_id}", response_model=DewarSchema)
async def get_dewar(dewar_id: str, db: Session = Depends(get_db)): async def get_dewar(dewar_id: int, db: Session = Depends(get_db)):
dewar = db.query(DewarModel).options( dewar = db.query(DewarModel).options(
joinedload(DewarModel.pucks).joinedload(PuckModel.positions) joinedload(DewarModel.pucks).joinedload(PuckModel.positions)
).filter(DewarModel.id == dewar_id).first() ).filter(DewarModel.id == dewar_id).first()
@ -55,7 +55,7 @@ async def get_dewar(dewar_id: str, db: Session = Depends(get_db)):
@router.put("/{dewar_id}", response_model=DewarSchema) @router.put("/{dewar_id}", response_model=DewarSchema)
async def update_dewar(dewar_id: str, dewar_update: DewarUpdate, db: Session = Depends(get_db)) -> DewarSchema: async def update_dewar(dewar_id: int, dewar_update: DewarUpdate, db: Session = Depends(get_db)) -> DewarSchema:
dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first() dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first()
if not dewar: if not dewar:
@ -70,3 +70,14 @@ async def update_dewar(dewar_id: str, dewar_update: DewarUpdate, db: Session = D
db.refresh(dewar) db.refresh(dewar)
return dewar return dewar
@router.delete("/{dewar_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_dewar(dewar_id: int, db: Session = Depends(get_db)):
dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first()
if not dewar:
raise HTTPException(status_code=404, detail="Dewar not found")
db.delete(dewar)
db.commit()
return

View File

@ -1,13 +1,13 @@
# app/routers/shipment.py
from fastapi import APIRouter, HTTPException, status, Query, Depends from fastapi import APIRouter, HTTPException, status, Query, Depends
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import List, Optional from typing import List, Optional
import logging import logging
from pydantic import BaseModel from pydantic import BaseModel, ValidationError
from datetime import date
from sqlalchemy.exc import SQLAlchemyError
from app.models import Shipment as ShipmentModel, ContactPerson as ContactPersonModel, Address as AddressModel, \ from app.models import Shipment as ShipmentModel, ContactPerson as ContactPersonModel, Address as AddressModel, \
Proposal as ProposalModel, Dewar as DewarModel Proposal as ProposalModel, Dewar as DewarModel, Puck as PuckModel, Sample as SampleModel
from app.schemas import ShipmentCreate, UpdateShipmentComments, Shipment as ShipmentSchema, DewarUpdate, \ from app.schemas import ShipmentCreate, UpdateShipmentComments, Shipment as ShipmentSchema, DewarUpdate, \
ContactPerson as ContactPersonSchema, Sample as SampleSchema, DewarCreate, PuckCreate, SampleCreate ContactPerson as ContactPersonSchema, Sample as SampleSchema, DewarCreate, PuckCreate, SampleCreate
from app.database import get_db from app.database import get_db
@ -74,8 +74,8 @@ async def create_shipment(shipment: ShipmentCreate, db: Session = Depends(get_db
@router.delete("/{shipment_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{shipment_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_shipment(id: int, db: Session = Depends(get_db)): async def delete_shipment(shipment_id: int, db: Session = Depends(get_db)):
shipment = db.query(ShipmentModel).filter(ShipmentModel.id == id).first() shipment = db.query(ShipmentModel).filter(ShipmentModel.id == shipment_id).first()
if not shipment: if not shipment:
raise HTTPException(status_code=404, detail="Shipment not found") raise HTTPException(status_code=404, detail="Shipment not found")
db.delete(shipment) db.delete(shipment)
@ -84,10 +84,10 @@ async def delete_shipment(id: int, db: Session = Depends(get_db)):
@router.put("/{shipment_id}", response_model=ShipmentSchema) @router.put("/{shipment_id}", response_model=ShipmentSchema)
async def update_shipment(id: int, updated_shipment: ShipmentCreate, db: Session = Depends(get_db)): async def update_shipment(shipment_id: int, updated_shipment: ShipmentCreate, db: Session = Depends(get_db)):
print("Received payload:", json.dumps(updated_shipment.dict(), indent=2, default=default_serializer)) print("Received payload:", json.dumps(updated_shipment.dict(), indent=2, default=default_serializer))
shipment = db.query(ShipmentModel).filter(ShipmentModel.id == id).first() shipment = db.query(ShipmentModel).filter(ShipmentModel.id == shipment_id).first()
if not shipment: if not shipment:
raise HTTPException(status_code=404, detail="Shipment not found") raise HTTPException(status_code=404, detail="Shipment not found")
@ -137,8 +137,8 @@ async def update_shipment(id: int, updated_shipment: ShipmentCreate, db: Session
@router.post("/{shipment_id}/add_dewar", response_model=ShipmentSchema) @router.post("/{shipment_id}/add_dewar", response_model=ShipmentSchema)
async def add_dewar_to_shipment(id: int, dewar_id: int, db: Session = Depends(get_db)): async def add_dewar_to_shipment(shipment_id: int, dewar_id: int, db: Session = Depends(get_db)):
shipment = db.query(ShipmentModel).filter(ShipmentModel.id == id).first() shipment = db.query(ShipmentModel).filter(ShipmentModel.id == shipment_id).first()
if not shipment: if not shipment:
raise HTTPException(status_code=404, detail="Shipment not found") raise HTTPException(status_code=404, detail="Shipment not found")
dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first() dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first()
@ -175,8 +175,8 @@ async def get_shipment_contact_persons(db: Session = Depends(get_db)):
@router.get("/{shipment_id}/samples", response_model=List[SampleSchema]) @router.get("/{shipment_id}/samples", response_model=List[SampleSchema])
def get_samples_in_shipment(id: int, db: Session = Depends(get_db)): async def get_samples_in_shipment(shipment_id: int, db: Session = Depends(get_db)):
shipment = db.query(ShipmentModel).filter(ShipmentModel.id == id).first() shipment = db.query(ShipmentModel).filter(ShipmentModel.id == shipment_id).first()
if shipment is None: if shipment is None:
raise HTTPException(status_code=404, detail="Shipment not found") raise HTTPException(status_code=404, detail="Shipment not found")
@ -189,7 +189,7 @@ def get_samples_in_shipment(id: int, db: Session = Depends(get_db)):
@router.get("/shipments/{shipment_id}/dewars/{dewar_id}/samples", response_model=List[SampleSchema]) @router.get("/shipments/{shipment_id}/dewars/{dewar_id}/samples", response_model=List[SampleSchema])
def get_samples_in_dewar(shipment_id: int, dewar_id: int, db: Session = Depends(get_db)): async def get_samples_in_dewar(shipment_id: int, dewar_id: int, db: Session = Depends(get_db)):
shipment = get_shipment_by_id(db, shipment_id) shipment = get_shipment_by_id(db, shipment_id)
if not shipment: if not shipment:
raise HTTPException(status_code=404, detail="Shipment not found") raise HTTPException(status_code=404, detail="Shipment not found")
@ -207,8 +207,9 @@ def get_samples_in_dewar(shipment_id: int, dewar_id: int, db: Session = Depends(
@router.put("/{shipment_id}/comments", response_model=ShipmentSchema) @router.put("/{shipment_id}/comments", response_model=ShipmentSchema)
async def update_shipment_comments(id: int, comments_data: UpdateShipmentComments, db: Session = Depends(get_db)): async def update_shipment_comments(shipment_id: int, comments_data: UpdateShipmentComments,
shipment = db.query(ShipmentModel).filter(ShipmentModel.id == id).first() db: Session = Depends(get_db)):
shipment = db.query(ShipmentModel).filter(ShipmentModel.id == shipment_id).first()
if not shipment: if not shipment:
raise HTTPException(status_code=404, detail="Shipment not found") raise HTTPException(status_code=404, detail="Shipment not found")
@ -219,72 +220,30 @@ async def update_shipment_comments(id: int, comments_data: UpdateShipmentComment
@router.post("/{shipment_id}/add_dewar_puck_sample", response_model=ShipmentSchema, status_code=status.HTTP_201_CREATED) @router.post("/{shipment_id}/add_dewar_puck_sample", response_model=ShipmentSchema, status_code=status.HTTP_201_CREATED)
async def add_dewar_puck_sample_to_shipment(shipment_id: int, payload: DewarCreate, db: Session = Depends(get_db)): async def add_dewar_puck_sample_to_shipment(
shipment_id: int,
payload: DewarCreate,
db: Session = Depends(get_db)
):
shipment = db.query(ShipmentModel).filter(ShipmentModel.id == shipment_id).first() shipment = db.query(ShipmentModel).filter(ShipmentModel.id == shipment_id).first()
if not shipment: if not shipment:
logging.error(f"Shipment not found with ID: {shipment_id}")
raise HTTPException(status_code=404, detail="Shipment not found") raise HTTPException(status_code=404, detail="Shipment not found")
for dewar_data in payload.dewars: try:
dewar = Dewar(
shipment_id=shipment_id,
dewar_name=dewar_data.dewar_name,
tracking_number=dewar_data.tracking_number,
status=dewar_data.status,
contact_person_id=dewar_data.contact_person_id,
return_address_id=dewar_data.return_address_id,
)
db.add(dewar)
db.commit()
db.refresh(dewar)
for puck_data in dewar_data.pucks:
puck = Puck(
dewar_id=dewar.id,
puck_name=puck_data.puck_name,
puck_type=puck_data.puck_type,
puck_location_in_dewar=puck_data.puck_location_in_dewar,
)
db.add(puck)
db.commit()
db.refresh(puck)
for sample_data in puck_data.samples:
sample = Sample(
puck_id=puck.id,
sample_name=sample_data.sample_name,
position=sample_data.position,
data_collection_parameters=DataCollectionParameters(
**sample_data.data_collection_parameters
),
)
db.add(sample)
db.commit()
db.refresh(sample)
db.refresh(shipment)
return shipment
@router.post("/{shipment_id}/add_dewar_puck_sample", response_model=ShipmentSchema, status_code=status.HTTP_201_CREATED)
async def add_dewar_puck_sample_to_shipment(shipment_id: int, payload: ShipmentCreate, db: Session = Depends(get_db)):
shipment = db.query(ShipmentModel).filter(ShipmentModel.id == shipment_id).first()
if not shipment:
raise HTTPException(status_code=404, detail="Shipment not found")
for dewar_data in payload.dewars:
dewar = DewarModel( dewar = DewarModel(
shipment_id=shipment_id, shipment_id=shipment_id,
dewar_name=dewar_data.dewar_name, # Ensure this field aligns with the frontend dewar_name=payload.dewar_name,
tracking_number=dewar_data.tracking_number, tracking_number=payload.tracking_number,
status=dewar_data.status, status=payload.status,
contact_person_id=dewar_data.contact_person_id, contact_person_id=payload.contact_person_id,
return_address_id=dewar_data.return_address_id, return_address_id=payload.return_address_id,
) )
db.add(dewar) db.add(dewar)
db.commit() db.commit()
db.refresh(dewar) db.refresh(dewar)
for puck_data in dewar_data.pucks: for puck_data in payload.pucks:
puck = PuckModel( puck = PuckModel(
dewar_id=dewar.id, dewar_id=dewar.id,
puck_name=puck_data.puck_name, puck_name=puck_data.puck_name,
@ -298,13 +257,21 @@ async def add_dewar_puck_sample_to_shipment(shipment_id: int, payload: ShipmentC
for sample_data in puck_data.samples: for sample_data in puck_data.samples:
sample = SampleModel( sample = SampleModel(
puck_id=puck.id, puck_id=puck.id,
sample_name=sample_data.crystalname, sample_name=sample_data.sample_name,
position=sample_data.positioninpuck, position=sample_data.position,
data_collection_parameters=sample_data.data_collection_parameters data_collection_parameters=sample_data.data_collection_parameters,
) )
db.add(sample) db.add(sample)
db.commit() db.commit()
db.refresh(sample) db.refresh(sample)
db.refresh(shipment) db.refresh(shipment)
except SQLAlchemyError as e:
logging.error(f"Database error occurred: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
except ValidationError as e:
logging.error(f"Validation error occurred: {e}")
raise HTTPException(status_code=400, detail="Validation error")
logging.info(f"Successfully added dewar, puck, and sample for shipment ID: {shipment_id}")
return shipment return shipment

View File

@ -91,7 +91,7 @@ async function fetchAndGenerate() {
} }
} }
const backendDirectory = '/Users/gotthardg/PycharmProjects/heidi-v2/backend/'; const backendDirectory = '/Users/gotthardg/PycharmProjects/heidi-v2/backend/app';
console.log(`👀 Watching for changes in ${backendDirectory}`); console.log(`👀 Watching for changes in ${backendDirectory}`);
const watcher = chokidar.watch(backendDirectory, { persistent: true, ignored: [SCHEMA_PATH, OUTPUT_DIRECTORY] }); const watcher = chokidar.watch(backendDirectory, { persistent: true, ignored: [SCHEMA_PATH, OUTPUT_DIRECTORY] });

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,8 @@
"@fullcalendar/react": "^6.1.15", "@fullcalendar/react": "^6.1.15",
"@fullcalendar/timegrid": "^6.1.15", "@fullcalendar/timegrid": "^6.1.15",
"@mui/icons-material": "^6.1.5", "@mui/icons-material": "^6.1.5",
"@mui/material": "^6.1.5", "@mui/material": "^6.1.6",
"@mui/system": "^6.1.6",
"axios": "^1.7.7", "axios": "^1.7.7",
"chokidar": "^4.0.1", "chokidar": "^4.0.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
@ -33,7 +34,7 @@
"react-big-calendar": "^1.15.0", "react-big-calendar": "^1.15.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-qr-code": "^2.0.15", "react-qr-code": "^2.0.15",
"react-router-dom": "^6.27.0", "react-router-dom": "^6.28.0",
"react-scheduler": "^0.1.0", "react-scheduler": "^0.1.0",
"rimraf": "^5.0.10" "rimraf": "^5.0.10"
}, },

View File

@ -1,8 +1,9 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Box, Typography, TextField, Button, Select, MenuItem, Snackbar } from '@mui/material'; import { Box, Typography, TextField, Button, Select, MenuItem, Snackbar } from '@mui/material';
import QRCode from 'react-qr-code'; import QRCode from 'react-qr-code';
import { ContactPerson, Address, Dewar, ContactsService, AddressesService, DewarsService, ShipmentsService } from '../../openapi'; import { ContactPerson, Address, Dewar, ContactsService, AddressesService, DewarsService, ShipmentsService } from '../../openapi'; // Adjust path if necessary
import Unipuck from '../components/Unipuck'; import Unipuck from '../components/Unipuck'; // This path should be checked and corrected if necessary
import { Shipment } from "../types.ts"; // Correct or adjust as needed
interface DewarDetailsProps { interface DewarDetailsProps {
dewar: Dewar; dewar: Dewar;
@ -12,9 +13,24 @@ interface DewarDetailsProps {
initialReturnAddresses?: Address[]; initialReturnAddresses?: Address[];
defaultContactPerson?: ContactPerson; defaultContactPerson?: ContactPerson;
defaultReturnAddress?: Address; defaultReturnAddress?: Address;
shipmentId: string; shipmentId: number;
refreshShipments: () => void; selectedShipment?: Shipment;
selectedShipment?: any; }
interface NewContactPerson {
id: number;
firstName: string;
lastName: string;
phone_number: string;
email: string;
}
interface NewReturnAddress {
id: number;
street: string;
city: string;
zipcode: string;
country: string;
} }
const DewarDetails: React.FC<DewarDetailsProps> = ({ const DewarDetails: React.FC<DewarDetailsProps> = ({
@ -26,24 +42,20 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
defaultContactPerson, defaultContactPerson,
defaultReturnAddress, defaultReturnAddress,
shipmentId, shipmentId,
refreshShipments,
selectedShipment,
}) => { }) => {
const [localTrackingNumber, setLocalTrackingNumber] = useState(trackingNumber); const [localTrackingNumber, setLocalTrackingNumber] = useState(trackingNumber);
const [contactPersons, setContactPersons] = useState(initialContactPersons); const [contactPersons, setContactPersons] = useState(initialContactPersons);
const [returnAddresses, setReturnAddresses] = useState(initialReturnAddresses); const [returnAddresses, setReturnAddresses] = useState(initialReturnAddresses);
const [selectedContactPerson, setSelectedContactPerson] = useState(''); const [selectedContactPerson, setSelectedContactPerson] = useState<string>('');
const [selectedReturnAddress, setSelectedReturnAddress] = useState(''); const [selectedReturnAddress, setSelectedReturnAddress] = useState<string>('');
const [isCreatingContactPerson, setIsCreatingContactPerson] = useState(false); const [isCreatingContactPerson, setIsCreatingContactPerson] = useState(false);
const [isCreatingReturnAddress, setIsCreatingReturnAddress] = useState(false); const [isCreatingReturnAddress, setIsCreatingReturnAddress] = useState(false);
const [puckStatuses, setPuckStatuses] = useState<string[][]>([]); const [puckStatuses, setPuckStatuses] = useState<string[][]>([]);
const [newContactPerson, setNewContactPerson] = useState({ id: 0, firstName: '', lastName: '', phone_number: '', email: '' }); const [newContactPerson, setNewContactPerson] = useState<NewContactPerson>({ id: 0, firstName: '', lastName: '', phone_number: '', email: '' });
const [newReturnAddress, setNewReturnAddress] = useState({ id: 0, street: '', city: '', zipcode: '', country: '' }); const [newReturnAddress, setNewReturnAddress] = useState<NewReturnAddress>({ id: 0, street: '', city: '', zipcode: '', country: '' });
const [changesMade, setChangesMade] = useState(false); const [changesMade, setChangesMade] = useState(false);
const [feedbackMessage, setFeedbackMessage] = useState(''); const [feedbackMessage, setFeedbackMessage] = useState('');
const [openSnackbar, setOpenSnackbar] = useState(false); const [openSnackbar, setOpenSnackbar] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => { useEffect(() => {
const setInitialContactPerson = () => { const setInitialContactPerson = () => {
@ -99,17 +111,14 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
const fetchedSamples = await ShipmentsService.getSamplesInDewarShipmentsShipmentsShipmentIdDewarsDewarIdSamplesGet(shipmentId, dewar.id); const fetchedSamples = await ShipmentsService.getSamplesInDewarShipmentsShipmentsShipmentIdDewarsDewarIdSamplesGet(shipmentId, dewar.id);
console.log("Fetched Samples: ", fetchedSamples); console.log("Fetched Samples: ", fetchedSamples);
const updatedPuckStatuses = dewar.pucks.map(puck => { const updatedPuckStatuses = (dewar.pucks ?? []).map(puck => {
// Filter samples for the current puck
const puckSamples = fetchedSamples.filter(sample => sample.puck_id === puck.id); const puckSamples = fetchedSamples.filter(sample => sample.puck_id === puck.id);
// Initialize positions as 'empty'
const statusArray = Array(16).fill('empty'); const statusArray = Array(16).fill('empty');
// Update positions based on puckSamples' positions
puckSamples.forEach(sample => { puckSamples.forEach(sample => {
if (sample.position >= 1 && sample.position <= 16) { if (sample.position >= 1 && sample.position <= 16) {
statusArray[sample.position - 1] = 'filled'; // Adjust for 0-based index statusArray[sample.position - 1] = 'filled'; // Corrected line
} }
}); });
@ -118,9 +127,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
setPuckStatuses(updatedPuckStatuses); setPuckStatuses(updatedPuckStatuses);
} catch (error) { } catch (error) {
setError('Failed to load samples. Please try again later.'); console.error("Error fetching samples:", error);
} finally {
setLoading(false);
} }
} }
}; };
@ -194,7 +201,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
}; };
const handleSaveChanges = async () => { const handleSaveChanges = async () => {
const formatDate = (dateString: string) => { const formatDate = (dateString: string | null) => {
if (!dateString) return null; if (!dateString) return null;
const date = new Date(dateString); const date = new Date(dateString);
if (isNaN(date.getTime())) return null; if (isNaN(date.getTime())) return null;
@ -228,14 +235,13 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
arrival_date: dewar.arrival_date, arrival_date: dewar.arrival_date,
returning_date: dewar.returning_date, returning_date: dewar.returning_date,
qrcode: dewar.qrcode, qrcode: dewar.qrcode,
return_address_id: selectedReturnAddress, return_address_id: parseInt(selectedReturnAddress ?? '', 10),
contact_person_id: selectedContactPerson, contact_person_id: parseInt(selectedContactPerson ?? '', 10),
}; };
await DewarsService.updateDewarDewarsDewarIdPut(dewarId, payload); await DewarsService.updateDewarDewarsDewarIdPut(dewarId, payload);
setFeedbackMessage("Changes saved successfully."); setFeedbackMessage("Changes saved successfully.");
setChangesMade(false); setChangesMade(false);
refreshShipments();
} catch (error) { } catch (error) {
setFeedbackMessage("Failed to save changes. Please try again later."); setFeedbackMessage("Failed to save changes. Please try again later.");
setOpenSnackbar(true); setOpenSnackbar(true);
@ -270,7 +276,9 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
</Box> </Box>
<Box sx={{ marginTop: 2 }}> <Box sx={{ marginTop: 2 }}>
<Typography variant="body1">Number of Pucks: {dewar.number_of_pucks}</Typography> <Typography variant="body1">Number of Pucks: {dewar.number_of_pucks}</Typography>
{dewar.number_of_pucks > 0 ? <Unipuck pucks={dewar.number_of_pucks} samples={puckStatuses} /> : <Typography>No pucks attached to the dewar.</Typography>} {(dewar.pucks ?? []).length > 0
? <Unipuck pucks={(dewar.pucks ?? []).length} samples={puckStatuses} />
: <Typography>No pucks attached to the dewar.</Typography>}
<Typography variant="body1">Number of Samples: {dewar.number_of_samples}</Typography> <Typography variant="body1">Number of Samples: {dewar.number_of_samples}</Typography>
</Box> </Box>
<Typography variant="body1">Current Contact Person:</Typography> <Typography variant="body1">Current Contact Person:</Typography>

View File

@ -1,21 +1,21 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Stepper, Step, StepLabel, StepIconProps, Typography, Menu, MenuItem } from '@mui/material'; import { Stepper, Step, StepLabel, Typography, Menu, MenuItem } from '@mui/material';
import AirplanemodeActiveIcon from '@mui/icons-material/AirplanemodeActive'; import AirplanemodeActiveIcon from '@mui/icons-material/AirplanemodeActive';
import StoreIcon from '@mui/icons-material/Store'; import StoreIcon from '@mui/icons-material/Store';
import RecycleIcon from '@mui/icons-material/Restore'; import RecycleIcon from '@mui/icons-material/Restore';
import { Dewar, DewarsService } from "../../openapi"; import { Dewar, DewarsService } from "../../openapi";
import { DewarStatus, getStatusStepIndex, determineIconColor } from './statusUtils'; // Utilities moved to a new file import { DewarStatus, getStatusStepIndex, determineIconColor } from './statusUtils';
const ICON_STYLE = { width: 24, height: 24 }; const ICON_STYLE = { width: 24, height: 24 };
// Inline SVG Component // Custom SVG Icon Component
const BottleIcon: React.FC<{ fill: string }> = ({ fill }) => ( const BottleIcon: React.FC<{ fill: string }> = ({ fill }) => (
<svg fill={fill} height="24px" width="24px" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 276.777 276.777"> <svg fill={fill} height="24px" width="24px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 276.777 276.777">
<path d="M190.886,82.273c-3.23-2.586-7.525-7.643-7.525-21.639V43h8.027V0h-106v43h8.027v17.635c0,11.66-1.891,17.93-6.524,21.639 c-21.813,17.459-31.121,36.748-31.121,64.5v100.088c0,16.496,13.42,29.916,29.916,29.916h105.405 c16.496,0,29.916-13.42,29.916-29.916V146.773C221.007,121.103,210.029,97.594,190.886,82.273z M191.007,246.777H85.77V146.773 c0-18.589,5.199-29.339,19.867-41.078c15.758-12.612,17.778-30.706,17.778-45.061V43h29.945v17.635 c0,19.927,6.318,35.087,18.779,45.061c11.99,9.597,18.867,24.568,18.867,41.078V246.777z"/> <path d="M190.886,82.273c-3.23-2.586-7.525-7.643-7.525-21.639V43h8.027V0h-106v43h8.027v17.635c0,11.66-1.891,17.93-6.524,21.639 c-21.813,17.459-31.121,36.748-31.121,64.5v100.088c0,16.496,13.42,29.916,29.916,29.916h105.405 c16.496,0,29.916-13.42,29.916-29.916V146.773C221.007,121.103,210.029,97.594,190.886,82.273z M191.007,246.777H85.77V146.773 c0-18.589,5.199-29.339,19.867-41.078c15.758-12.612,17.778-30.706,17.778-45.061V43h29.945v17.635 c0,19.927,6.318,35.087,18.779,45.061c11.99,9.597,18.867,24.568,18.867,41.078V246.777z"/>
</svg> </svg>
); );
// Define types for icons mapping. // Icons Mapping
const ICONS: { [key: number]: React.ReactElement } = { const ICONS: { [key: number]: React.ReactElement } = {
0: <BottleIcon fill="grey" />, 0: <BottleIcon fill="grey" />,
1: <AirplanemodeActiveIcon style={{ ...ICON_STYLE, color: 'blue' }} />, 1: <AirplanemodeActiveIcon style={{ ...ICON_STYLE, color: 'blue' }} />,
@ -25,24 +25,38 @@ const ICONS: { [key: number]: React.ReactElement } = {
5: <AirplanemodeActiveIcon style={{ ...ICON_STYLE, color: 'orange' }} />, 5: <AirplanemodeActiveIcon style={{ ...ICON_STYLE, color: 'orange' }} />,
}; };
interface StepIconContainerProps extends React.HTMLAttributes<HTMLDivElement> { // StepIconContainer Component
color: string; interface StepIconContainerProps {
completed?: boolean;
active?: boolean;
error?: boolean;
children?: React.ReactNode;
} }
const StepIconContainer: React.FC<StepIconContainerProps> = ({ color, children, ...rest }) => ( const StepIconContainer: React.FC<StepIconContainerProps> = ({ completed, active, error, children }) => {
<div style={{ color }} {...rest}> const className = [
{children} completed ? 'completed' : '',
</div> active ? 'active' : '',
); error ? 'error' : '',
].join(' ').trim();
return (
<div className={className}>
{children}
</div>
);
};
// StepIconComponent Props
type StepIconComponentProps = { type StepIconComponentProps = {
icon: number; icon: number;
dewar: Dewar; dewar: Dewar;
isSelected: boolean; isSelected: boolean;
refreshShipments: () => void; refreshShipments: () => void;
} & StepIconProps; } & Omit<React.HTMLAttributes<HTMLDivElement>, 'icon'>;
const StepIconComponent = ({ icon, dewar, isSelected, refreshShipments, ...props }: StepIconComponentProps) => { // StepIconComponent
const StepIconComponent: React.FC<StepIconComponentProps> = ({ icon, dewar, isSelected, refreshShipments, ...rest }) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const handleIconEnter = (event: React.MouseEvent<HTMLDivElement>) => { const handleIconEnter = (event: React.MouseEvent<HTMLDivElement>) => {
@ -74,9 +88,9 @@ const StepIconComponent = ({ icon, dewar, isSelected, refreshShipments, ...props
contact_person_id: dewar.contact_person_id, contact_person_id: dewar.contact_person_id,
}; };
await DewarsService.updateDewarDewarsDewarIdPut(dewar.id || '', payload); await DewarsService.updateDewarDewarsDewarIdPut(dewar.id, payload);
setAnchorEl(null); setAnchorEl(null);
refreshShipments(); // Refresh shipments after status update refreshShipments();
} catch (error) { } catch (error) {
console.error('Failed to update dewar status:', error); console.error('Failed to update dewar status:', error);
alert('Failed to update dewar status. Please try again.'); alert('Failed to update dewar status. Please try again.');
@ -85,17 +99,21 @@ const StepIconComponent = ({ icon, dewar, isSelected, refreshShipments, ...props
const { iconIndex, color } = getIconProperties(icon, dewar); const { iconIndex, color } = getIconProperties(icon, dewar);
const IconComponent = ICONS[iconIndex]; const IconComponent = ICONS[iconIndex];
const iconProps = iconIndex === 0 ? { fill: color } : {};
return ( return (
<div <div
onMouseEnter={handleIconEnter} onMouseEnter={handleIconEnter}
onMouseLeave={handleIconLeave} onMouseLeave={handleIconLeave}
style={{ position: 'relative' }} style={{ position: 'relative' }}
{...rest}
> >
<StepIconContainer color={color} {...props}> <StepIconContainer
completed={rest['aria-activedescendant'] ? true : undefined}
active={Boolean(rest['aria-activedescendant'])}
error={rest.role === 'error'}
>
{IconComponent {IconComponent
? React.cloneElement(IconComponent, iconProps) ? React.cloneElement(IconComponent, iconIndex === 0 ? { fill: color } : {})
: <Typography variant="body2" color="error">Invalid icon</Typography> : <Typography variant="body2" color="error">Invalid icon</Typography>
} }
</StepIconContainer> </StepIconContainer>
@ -119,23 +137,26 @@ const StepIconComponent = ({ icon, dewar, isSelected, refreshShipments, ...props
); );
}; };
// Icon properties retrieval based on the status and icon number
const getIconProperties = (icon: number, dewar: Dewar) => { const getIconProperties = (icon: number, dewar: Dewar) => {
const status = dewar.status as DewarStatus; const status = dewar.status as DewarStatus;
const iconIndex = status === 'Delayed' && icon === 1 ? 5 : icon; const iconIndex = status === 'Delayed' && icon === 1 ? 5 : icon;
const color = determineIconColor(icon, status); const color = determineIconColor(icon, status);
const fill = status === 'In Preparation' ? color : undefined; return { iconIndex, color };
return { iconIndex, color, fill };
}; };
// Steps of the stepper
const steps = ['In-House', 'Transit', 'At SLS', 'Returned']; const steps = ['In-House', 'Transit', 'At SLS', 'Returned'];
// Props for the CustomStepper
type CustomStepperProps = { type CustomStepperProps = {
dewar: Dewar; dewar: Dewar;
selectedDewarId: string | null; selectedDewarId: number | null;
refreshShipments: () => void; // Add refreshShipments prop refreshShipments: () => void;
} };
const CustomStepper = ({ dewar, selectedDewarId, refreshShipments }: CustomStepperProps) => { // CustomStepper Component
const CustomStepper: React.FC<CustomStepperProps> = ({ dewar, selectedDewarId, refreshShipments }) => {
const activeStep = getStatusStepIndex(dewar.status as DewarStatus); const activeStep = getStatusStepIndex(dewar.status as DewarStatus);
const isSelected = dewar.id === selectedDewarId; const isSelected = dewar.id === selectedDewarId;

View File

@ -163,11 +163,13 @@ const ResponsiveAppBar: React.FC<ResponsiveAppBarProps> = ({ onOpenAddressManage
{item.name} {item.name}
</MenuItem> </MenuItem>
) : ( ) : (
<MenuItem key={item.name} onClick={handleCloseUserMenu}> item.path && (
<Link to={item.path} style={{ textDecoration: 'none', color: 'inherit' }}> <MenuItem key={item.name} onClick={handleCloseUserMenu}>
{item.name} <Link to={item.path} style={{ textDecoration: 'none', color: 'inherit' }}>
</Link> {item.name}
</MenuItem> </Link>
</MenuItem>
)
) )
)} )}
</Menu> </Menu>

View File

@ -4,7 +4,7 @@ import QRCode from 'react-qr-code';
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import CheckIcon from '@mui/icons-material/Check'; import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import { Dewar, DewarsService, ShipmentsService, ContactPerson, ApiError } from "../../openapi"; import { Dewar, DewarsService, Shipment, ContactPerson, ApiError, ShipmentsService } from "../../openapi";
import { SxProps } from "@mui/system"; import { SxProps } from "@mui/system";
import CustomStepper from "./DewarStepper"; import CustomStepper from "./DewarStepper";
import DewarDetails from './DewarDetails'; import DewarDetails from './DewarDetails';
@ -14,10 +14,10 @@ const MAX_COMMENTS_LENGTH = 200;
interface ShipmentDetailsProps { interface ShipmentDetailsProps {
isCreatingShipment: boolean; isCreatingShipment: boolean;
sx?: SxProps; sx?: SxProps;
selectedShipment: ShipmentsService | null; selectedShipment: Shipment | null;
selectedDewar: Dewar | null; selectedDewar: Dewar | null;
setSelectedDewar: React.Dispatch<React.SetStateAction<Dewar | null>>; setSelectedDewar: React.Dispatch<React.SetStateAction<Dewar | null>>;
setSelectedShipment: React.Dispatch<React.SetStateAction<ShipmentsService | null>>; setSelectedShipment: React.Dispatch<React.SetStateAction<Shipment | null>>;
refreshShipments: () => void; refreshShipments: () => void;
defaultContactPerson?: ContactPerson; defaultContactPerson?: ContactPerson;
} }
@ -75,7 +75,7 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
setSelectedDewar(newSelection); setSelectedDewar(newSelection);
}; };
const handleDeleteDewar = async (dewarId: string) => { const handleDeleteDewar = async (dewarId: number) => {
const confirmed = window.confirm('Are you sure you want to delete this dewar?'); const confirmed = window.confirm('Are you sure you want to delete this dewar?');
if (confirmed && selectedShipment) { if (confirmed && selectedShipment) {
try { try {
@ -311,10 +311,9 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between' justifyContent: 'space-between'
}}> }}>
<CustomStepper dewar={dewar} selectedDewarId={localSelectedDewar?.id} refreshShipments={refreshShipments} /> <CustomStepper dewar={dewar} selectedDewarId={localSelectedDewar?.id ?? null} refreshShipments={refreshShipments} />
</Box> </Box>
{localSelectedDewar?.id === dewar.id && ( {localSelectedDewar?.id === dewar.id && (
<IconButton <IconButton
onClick={(e) => { onClick={(e) => {
@ -333,21 +332,19 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
</Button> </Button>
{localSelectedDewar?.id === dewar.id && ( {localSelectedDewar?.id === dewar.id && (
<Box sx={{ padding: 2, border: '1px solid #ccc', borderRadius: '4px' }}> <DewarDetails
<DewarDetails dewar={localSelectedDewar}
dewar={localSelectedDewar} trackingNumber={localSelectedDewar?.tracking_number || ''}
trackingNumber={localSelectedDewar.tracking_number || ''} setTrackingNumber={(value) => {
setTrackingNumber={(value) => { setLocalSelectedDewar((prev) => (prev ? { ...prev, tracking_number: value as string } : prev));
setLocalSelectedDewar((prev) => (prev ? { ...prev, tracking_number: value as string } : prev)); }}
}} initialContactPersons={localSelectedDewar?.contact_person ? [localSelectedDewar.contact_person] : []} // Focus on dewar contact person
initialContactPersons={localSelectedDewar?.contact_person ? [localSelectedDewar.contact_person] : []} // Focus on dewar contact person initialReturnAddresses={localSelectedDewar?.return_address ? [localSelectedDewar.return_address] : []} // Focus on dewar return address
initialReturnAddresses={localSelectedDewar?.return_address ? [localSelectedDewar.return_address] : []} // Focus on dewar return address defaultContactPerson={localSelectedDewar?.contact_person ?? undefined} // Use `?? undefined`
defaultContactPerson={localSelectedDewar?.contact_person} defaultReturnAddress={localSelectedDewar?.return_address ?? undefined} // Use `?? undefined`
defaultReturnAddress={localSelectedDewar?.return_address} shipmentId={selectedShipment?.id ?? null}
shipmentId={selectedShipment?.id || ''} refreshShipments={refreshShipments}
refreshShipments={refreshShipments} />
/>
</Box>
)} )}
</React.Fragment> </React.Fragment>
))} ))}

View File

@ -5,7 +5,7 @@ import {
import { SelectChangeEvent } from '@mui/material'; import { SelectChangeEvent } from '@mui/material';
import { SxProps } from '@mui/system'; import { SxProps } from '@mui/system';
import { import {
ContactPersonCreate, ContactPerson, Address, Proposal, ContactsService, AddressesService, ProposalsService, ContactPersonCreate, ContactPerson, Address, Proposal, ContactsService, AddressesService, AddressCreate, ProposalsService,
OpenAPI, ShipmentCreate, ShipmentsService OpenAPI, ShipmentCreate, ShipmentsService
} from '../../openapi'; } from '../../openapi';
import { useEffect } from 'react'; import { useEffect } from 'react';
@ -98,7 +98,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
}; };
const isFormValid = () => { const isFormValid = () => {
const { shipment_name, shipment_status } = newShipment; const { shipment_name } = newShipment;
if (!shipment_name) return false; if (!shipment_name) return false;
if (!selectedContactPersonId || !selectedReturnAddressId || !selectedProposalId) return false; if (!selectedContactPersonId || !selectedReturnAddressId || !selectedProposalId) return false;
@ -431,7 +431,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
variant="contained" variant="contained"
color="primary" color="primary"
onClick={handleSaveShipment} onClick={handleSaveShipment}
disabled={newShipment.comments?.length > MAX_COMMENTS_LENGTH} disabled={(newShipment.comments?.length ?? 0) > MAX_COMMENTS_LENGTH}
> >
Save Shipment Save Shipment
</Button> </Button>

View File

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { Button, Box, Typography, IconButton } from '@mui/material'; 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 { Add as AddIcon, Delete as DeleteIcon, UploadFile as UploadFileIcon, Refresh as RefreshIcon } from '@mui/icons-material';
import UploadDialog from './UploadDialog'; import UploadDialog from './UploadDialog';
import { Dewar, ShipmentsService } from '../../openapi'; import { Dewar, Shipment, ShipmentsService } from '../../openapi';
import { SxProps } from '@mui/material'; import { SxProps } from '@mui/material';
import bottleGrey from '/src/assets/icons/bottle-svgrepo-com-grey.svg'; import bottleGrey from '/src/assets/icons/bottle-svgrepo-com-grey.svg';
import bottleYellow from '/src/assets/icons/bottle-svgrepo-com-yellow.svg'; import bottleYellow from '/src/assets/icons/bottle-svgrepo-com-yellow.svg';
@ -11,10 +11,10 @@ import bottleRed from '/src/assets/icons/bottle-svgrepo-com-red.svg';
interface ShipmentPanelProps { interface ShipmentPanelProps {
setCreatingShipment: (value: boolean) => void; setCreatingShipment: (value: boolean) => void;
selectShipment: (shipment: ShipmentsService | null) => void; selectShipment: (shipment: Shipment | null) => void;
selectedShipment: ShipmentsService | null; selectedShipment: Shipment | null;
sx?: SxProps; sx?: SxProps;
shipments: ShipmentsService[]; shipments: Shipment[];
refreshShipments: () => void; refreshShipments: () => void;
error: string | null; error: string | null;
} }
@ -46,7 +46,7 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
} }
}; };
const deleteShipment = async (shipmentId: string | undefined) => { const deleteShipment = async (shipmentId: number) => {
if (!shipmentId) return; if (!shipmentId) return;
try { try {
await ShipmentsService.deleteShipmentShipmentsShipmentIdDelete(shipmentId); await ShipmentsService.deleteShipmentShipmentsShipmentIdDelete(shipmentId);
@ -57,27 +57,19 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
} }
}; };
const handleShipmentSelection = (shipment: ShipmentsService) => { const handleShipmentSelection = (shipment: Shipment) => {
const isSelected = selectedShipment?.id === shipment.id; const isSelected = selectedShipment?.id === shipment.id;
const updatedShipment = isSelected ? null : shipment; selectShipment(isSelected ? null : shipment);
console.log("Shipment selected:", updatedShipment); // debug log
if (updatedShipment) {
console.log("Contact Person ID:", updatedShipment.contact_person_id);
console.log("Return Address ID:", updatedShipment.return_address_id);
}
selectShipment(updatedShipment);
}; };
const openUploadDialog = (event: React.MouseEvent) => { const openUploadDialog = (event: React.MouseEvent) => {
event.stopPropagation(); // Prevent event bubbling event.stopPropagation();
setUploadDialogOpen(true); setUploadDialogOpen(true);
}; };
const closeUploadDialog = () => setUploadDialogOpen(false); const closeUploadDialog = () => setUploadDialogOpen(false);
const getNumberOfDewars = (shipment: ShipmentsService): number => shipment.dewars?.length || 0; const getNumberOfDewars = (shipment: Shipment): number => shipment.dewars?.length || 0;
console.log("Current selected shipment:", selectedShipment); // debug log
return ( return (
<Box sx={{ width: '90%', borderRight: '1px solid #ccc', padding: 2, ...sx }}> <Box sx={{ width: '90%', borderRight: '1px solid #ccc', padding: 2, ...sx }}>
@ -104,70 +96,67 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
<RefreshIcon /> <RefreshIcon />
</IconButton> </IconButton>
</Box> </Box>
{shipments.map((shipment) => ( {shipments.map((shipment) => {
<Button const isSelected = selectedShipment?.id === shipment.id;
key={shipment.id} return (
onClick={() => handleShipmentSelection(shipment)} <Box
sx={{ key={shipment.id}
width: '100%', sx={{
textAlign: 'left', marginBottom: 1,
marginBottom: 1, padding: '10px 16px',
color: 'white', backgroundColor: isSelected ? '#52893e' : '#424242',
whiteSpace: 'nowrap', display: 'flex',
overflow: 'hidden', justifyContent: 'space-between',
textOverflow: 'ellipsis', alignItems: 'center',
padding: '10px 16px', color: 'white',
fontSize: '0.7rem', borderRadius: 1,
display: 'flex', '&:hover': {
alignItems: 'center', backgroundColor: isSelected ? '#9aca8c' : '#616161'
justifyContent: 'space-between', },
backgroundColor: selectedShipment?.id === shipment.id ? '#52893e' : '#424242', cursor: 'pointer'
'&:hover': { }}
backgroundColor: selectedShipment?.id === shipment.id ? '#9aca8c' : '#616161', onClick={() => handleShipmentSelection(shipment)}
}, >
'&:active': { <Box sx={{ display: 'flex', alignItems: 'center' }}>
backgroundColor: selectedShipment?.id === shipment.id ? '#915151' : '#212121', <Box sx={{ position: 'relative', marginRight: 2 }}>
}, <img
}} src={statusIconMap[shipment.shipment_status] || bottleGrey}
> alt={`Status: ${shipment.shipment_status}`}
<Box sx={{ display: 'flex', alignItems: 'center' }}> width="24"
<Box sx={{ position: 'relative', marginRight: '8px' }}> />
<img <Typography
src={statusIconMap[shipment.shipment_status] || bottleGrey} component="span"
alt={`Status: ${shipment.shipment_status}`} sx={{
width="24" position: 'absolute',
/> top: '0%',
<Typography right: '0%',
component="span" transform: 'translate(50%, -50%)',
sx={{ color: 'white',
position: 'absolute', fontWeight: 'bold',
top: '0%', fontSize: '0.6rem',
right: '0%', backgroundColor: 'transparent',
transform: 'translate(50%, -50%)', borderRadius: '50%',
color: 'white', padding: '0 2px',
fontWeight: 'bold', }}
fontSize: '0.6rem', >
backgroundColor: 'transparent', {getNumberOfDewars(shipment)}
borderRadius: '50%', </Typography>
padding: '0 2px', </Box>
}} <Box>
> <Typography>{shipment.shipment_name}</Typography>
{getNumberOfDewars(shipment)} <Typography sx={{ fontSize: '0.6rem', color: '#ccc' }}>{shipment.shipment_date}</Typography>
</Typography> <Typography sx={{ fontSize: '0.6rem', color: '#ccc' }}>
Total Pucks: {shipment.dewars?.reduce((total, dewar: Dewar) => total + (dewar.number_of_pucks || 0), 0) ?? 0}
</Typography>
</Box>
</Box> </Box>
<Box> {isSelected && (
<Typography>{shipment.shipment_name}</Typography> <Box sx={{ display: 'flex', alignItems: 'center' }}>
<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)}
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{selectedShipment?.id === shipment.id && (
<>
<IconButton <IconButton
onClick={openUploadDialog} onClick={(event) => {
event.stopPropagation();
openUploadDialog(event);
}}
color="primary" color="primary"
title="Upload Sample Data Sheet" title="Upload Sample Data Sheet"
sx={{ marginLeft: 1 }} sx={{ marginLeft: 1 }}
@ -177,7 +166,6 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
<IconButton <IconButton
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
console.log('Delete button clicked'); // debug log
handleDeleteShipment(); handleDeleteShipment();
}} }}
color="error" color="error"
@ -186,15 +174,15 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
> >
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
</> </Box>
)} )}
</Box> </Box>
</Button> );
))} })}
<UploadDialog <UploadDialog
open={uploadDialogOpen} open={uploadDialogOpen}
onClose={closeUploadDialog} onClose={closeUploadDialog}
selectedShipment={selectedShipment} // <-- Pass selectedShipment here selectedShipment={selectedShipment}
/> />
</Box> </Box>
); );

View File

@ -17,12 +17,6 @@ import { SpreadsheetService, ShipmentsService, DewarsService, ApiError } from '.
import * as ExcelJS from 'exceljs'; import * as ExcelJS from 'exceljs';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
const HeaderMapping = {
'dewarname': 'dewar_name',
'trackingnumber': 'tracking_number',
'status': 'status'
};
const SpreadsheetTable = ({ const SpreadsheetTable = ({
raw_data, raw_data,
errors, errors,
@ -50,6 +44,7 @@ const SpreadsheetTable = ({
dewar_name: '', dewar_name: '',
tracking_number: 'UNKNOWN', tracking_number: 'UNKNOWN',
status: 'In preparation', status: 'In preparation',
pucks: []
}; };
const [newDewar, setNewDewar] = useState(initialNewDewarState); const [newDewar, setNewDewar] = useState(initialNewDewarState);
@ -77,7 +72,6 @@ const SpreadsheetTable = ({
useEffect(() => { useEffect(() => {
const initialNonEditableCells = new Set(); const initialNonEditableCells = new Set();
raw_data.forEach((row, rowIndex) => { raw_data.forEach((row, rowIndex) => {
headers.forEach((_, colIndex) => { headers.forEach((_, colIndex) => {
const key = `${row.row_num}-${headers[colIndex]}`; const key = `${row.row_num}-${headers[colIndex]}`;
@ -86,7 +80,6 @@ const SpreadsheetTable = ({
} }
}); });
}); });
setNonEditableCells(initialNonEditableCells); setNonEditableCells(initialNonEditableCells);
}, [raw_data, headers, errorMap]); }, [raw_data, headers, errorMap]);
@ -148,35 +141,7 @@ const SpreadsheetTable = ({
'dewarname': 0, 'dewarname': 0,
'puckname': 1, 'puckname': 1,
'pucktype': 2, 'pucktype': 2,
'crystalname': 3, // Add other fields as needed
'positioninpuck': 4,
'priority': 5,
'comments': 6,
'directory': 7,
'proteinname': 8,
'oscillation': 9,
'aperture': 10,
'exposure': 11,
'totalrange': 12,
'transmission': 13,
'dose': 14,
'targetresolution': 15,
'datacollectiontype': 16,
'processingpipeline': 17,
'spacegroupnumber': 18,
'cellparameters': 19,
'rescutkey': 20,
'rescutvalue': 21,
'userresolution': 22,
'pdbid': 23,
'autoprocfull': 24,
'procfull': 25,
'adpenabled': 26,
'noano': 27,
'ffcscampaign': 28,
'trustedhigh': 29,
'autoprocextraparams': 30,
'chiphiangles': 31,
}; };
const createDewarsFromSheet = async (data, contactPerson, returnAddress) => { const createDewarsFromSheet = async (data, contactPerson, returnAddress) => {
@ -185,7 +150,13 @@ const SpreadsheetTable = ({
return null; return null;
} }
const dewars = new Map(); // Use a Map to prevent duplicates const dewars = new Map();
const dewarNameIdx = fieldToCol['dewarname'];
const puckNameIdx = fieldToCol['puckname'];
const puckTypeIdx = fieldToCol['pucktype'];
let puckPositionInDewar = 1;
for (const row of data) { for (const row of data) {
if (!row.data) { if (!row.data) {
@ -193,46 +164,74 @@ const SpreadsheetTable = ({
continue; continue;
} }
const dewarNameIdx = fieldToCol['dewarname']; const dewarName = typeof row.data[dewarNameIdx] === 'string' ? row.data[dewarNameIdx].trim() : null;
const dewarName = row.data[dewarNameIdx]?.trim(); const puckName = typeof row.data[puckNameIdx] === 'string' ? row.data[puckNameIdx].trim() : null;
const puckType = typeof row.data[puckTypeIdx] === 'string' ? row.data[puckTypeIdx] : 'Unipuck';
if (dewarName && !dewars.has(dewarName)) { console.log(`Processing Dewar: ${dewarName}, Puck: ${puckName}, Type: ${puckType}`);
const newDewarToPost = {
...initialNewDewarState,
dewar_name: dewarName,
tracking_number: row.data[fieldToCol['trackingnumber']] || "UNKNOWN",
status: row.data[fieldToCol['status']] || "In preparation",
contact_person_id: contactPerson.id,
return_address_id: returnAddress.id,
};
try { if (dewarName) {
const createdDewar = await DewarsService.createDewarDewarsPost(newDewarToPost); let dewar;
if (createdDewar && selectedShipment) { if (!dewars.has(dewarName)) {
const updatedShipment = await ShipmentsService.addDewarToShipmentShipmentsShipmentIdAddDewarPost( dewar = {
selectedShipment.id, ...initialNewDewarState,
createdDewar.id dewar_name: dewarName,
); contact_person_id: contactPerson.id,
dewars.set(dewarName, updatedShipment); // Track the added dewar return_address_id: returnAddress.id,
} pucks: []
} catch (error) { };
console.error(`Error adding dewar for row: ${row.row_num}`, error); dewars.set(dewarName, dewar);
if (error instanceof ApiError && error.body) { puckPositionInDewar = 1;
console.error('Validation errors:', error.body.detail); console.log(`Created new dewar: ${dewarName}`);
} else { } else {
console.error('Unexpected error:', error); dewar = dewars.get(dewarName);
} puckPositionInDewar++;
console.log(`Found existing dewar: ${dewarName}`);
} }
} else if (!dewarName) {
const puck = {
puck_name: puckName || 'test', // Fixed puck name
puck_type: puckType || 'Unipuck', // Fixed puck type
puck_position_in_dewar: puckPositionInDewar
};
dewar.pucks.push(puck);
console.log(`Added puck: ${JSON.stringify(puck)}`);
} else {
console.error('Dewar name is missing in the row'); console.error('Dewar name is missing in the row');
} }
} }
return Array.from(dewars.values()); const dewarsArray = Array.from(dewars.values());
for (const dewar of dewarsArray) {
try {
// Call to create the dewar
const createdDewar = await DewarsService.createDewarDewarsPost(dewar);
console.log(`Created dewar: ${createdDewar.id}`);
// Add dewar to the shipment if created successfully
if (createdDewar && selectedShipment) {
await ShipmentsService.addDewarToShipmentShipmentsShipmentIdAddDewarPost(
selectedShipment.id,
createdDewar.id
);
console.log(`Added dewar to shipment: ${createdDewar.id}`);
}
} catch (error) {
console.error(`Error adding dewar`, error);
if (error instanceof ApiError && error.body) {
console.error('Validation errors:', error.body.detail);
} else {
console.error('Unexpected error:', error);
}
}
}
return dewarsArray;
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (isSubmitting) return; // Prevent multiple submissions if (isSubmitting) return;
if (!headers || headers.length === 0) { if (!headers || headers.length === 0) {
console.error('Cannot submit, headers are not defined or empty'); console.error('Cannot submit, headers are not defined or empty');
return; return;

View File

@ -1,28 +1,29 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import Grid from '@mui/material/Grid';
import ShipmentPanel from '../components/ShipmentPanel'; import ShipmentPanel from '../components/ShipmentPanel';
import ShipmentDetails from '../components/ShipmentDetails'; import ShipmentDetails from '../components/ShipmentDetails';
import ShipmentForm from '../components/ShipmentForm'; import ShipmentForm from '../components/ShipmentForm';
import { Dewar, OpenAPI, ContactPerson, ShipmentsService } from '../../openapi'; import { Dewar, OpenAPI, Shipment } from '../../openapi';
import useShipments from '../hooks/useShipments'; import useShipments from '../hooks/useShipments';
import { Grid, Container } from '@mui/material';
// Define props for Shipments View
type ShipmentViewProps = React.PropsWithChildren<Record<string, never>>; type ShipmentViewProps = React.PropsWithChildren<Record<string, never>>;
const API_BASE_URL = 'http://127.0.0.1:8000'; const API_BASE_URL = 'http://127.0.0.1:8000';
OpenAPI.BASE = API_BASE_URL; // Setting API base URL OpenAPI.BASE = API_BASE_URL;
const ShipmentView: React.FC<ShipmentViewProps> = () => { const ShipmentView: React.FC<ShipmentViewProps> = () => {
const { shipments, error, defaultContactPerson, fetchAndSetShipments } = useShipments(); const { shipments, error, defaultContactPerson, fetchAndSetShipments } = useShipments();
const [selectedShipment, setSelectedShipment] = useState<ShipmentsService | null>(null); const [selectedShipment, setSelectedShipment] = useState<Shipment | null>(null);
const [selectedDewar, setSelectedDewar] = useState<Dewar | null>(null);const [isCreatingShipment, setIsCreatingShipment] = useState(false); const [selectedDewar, setSelectedDewar] = useState<Dewar | null>(null);
const [isCreatingShipment, setIsCreatingShipment] = useState(false);
useEffect(() => { useEffect(() => {
console.log('Updated shipments:', shipments); console.log('Updated shipments:', shipments);
}, [shipments]); }, [shipments]);
// Handlers for selecting shipment and canceling form // Handlers for selecting shipment and canceling form
const handleSelectShipment = (shipment: ShipmentsService | null) => { const handleSelectShipment = (shipment: Shipment | null) => {
setSelectedShipment(shipment); setSelectedShipment(shipment);
setIsCreatingShipment(false); setIsCreatingShipment(false);
}; };
@ -59,41 +60,43 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
return <div>No shipment details available.</div>; return <div>No shipment details available.</div>;
}; };
// Render the main layout // Render the main layout using Grid for layout
return ( return (
<Grid container spacing={2} sx={{ height: '100vh' }}> <Container maxWidth={false} disableGutters sx={{ display: 'flex', height: '100vh' }}>
<Grid <Grid container spacing={2} sx={{ height: '100vh' }}>
item <Grid
xs={12} item
md={3} xs={12}
sx={{ md={3}
display: 'flex', sx={{
flexDirection: 'column', display: 'flex',
flexGrow: 0, flexDirection: 'column',
}} flexGrow: 0,
> }}
<ShipmentPanel >
setCreatingShipment={setIsCreatingShipment} <ShipmentPanel
selectShipment={handleSelectShipment} setCreatingShipment={setIsCreatingShipment}
shipments={shipments} selectShipment={handleSelectShipment}
selectedShipment={selectedShipment} shipments={shipments}
refreshShipments={fetchAndSetShipments} selectedShipment={selectedShipment}
error={error} refreshShipments={fetchAndSetShipments}
/> error={error}
/>
</Grid>
<Grid
item
xs={12}
md={9}
sx={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
}}
>
{renderShipmentContent()}
</Grid>
</Grid> </Grid>
<Grid </Container>
item
xs={12}
md={8}
sx={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
}}
>
{renderShipmentContent()}
</Grid>
</Grid>
); );
}; };