diff --git a/backend/app/models.py b/backend/app/models.py index dba5758..ad1ebab 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, String, Date, ForeignKey +from sqlalchemy import Column, Integer, String, Date, ForeignKey, JSON from sqlalchemy.orm import relationship from app.database import Base from app.calculations import calculate_number_of_pucks, calculate_number_of_samples @@ -107,6 +107,7 @@ class Sample(Base): id = Column(Integer, primary_key=True, index=True, autoincrement=True) sample_name = Column(String, index=True) # Matches `sample_name` in data creation position = Column(Integer) # Matches `position` in data creation script + data_collection_parameters = Column(JSON, nullable=True) # Foreign keys and relationships puck_id = Column(Integer, ForeignKey('pucks.id')) diff --git a/backend/app/routers/dewar.py b/backend/app/routers/dewar.py index 01e4020..ce20bbd 100644 --- a/backend/app/routers/dewar.py +++ b/backend/app/routers/dewar.py @@ -1,9 +1,12 @@ from fastapi import APIRouter, HTTPException, status, Depends from sqlalchemy.orm import Session, joinedload from typing import List -import uuid +import logging +from sqlalchemy.exc import SQLAlchemyError +from pydantic import ValidationError from app.schemas import Dewar as DewarSchema, DewarCreate, DewarUpdate -from app.models import Dewar as DewarModel, Puck as PuckModel +from app.models import Dewar as DewarModel, Puck as PuckModel, \ + Sample as SampleModel # Assuming SampleModel is defined in models from app.dependencies import get_db router = APIRouter() @@ -17,24 +20,55 @@ async def get_dewars(db: Session = Depends(get_db)): @router.post("/", response_model=DewarSchema, status_code=status.HTTP_201_CREATED) async def create_dewar(dewar: DewarCreate, db: Session = Depends(get_db)) -> DewarSchema: - db_dewar = DewarModel( - dewar_name=dewar.dewar_name, - tracking_number=dewar.tracking_number, - status=dewar.status, - ready_date=dewar.ready_date, - shipping_date=dewar.shipping_date, - arrival_date=dewar.arrival_date, - returning_date=dewar.returning_date, - qrcode=dewar.qrcode, - contact_person_id=dewar.contact_person_id, - return_address_id=dewar.return_address_id - ) + try: + db_dewar = DewarModel( + dewar_name=dewar.dewar_name, + tracking_number=dewar.tracking_number, + status=dewar.status, + ready_date=dewar.ready_date, + shipping_date=dewar.shipping_date, + arrival_date=dewar.arrival_date, + returning_date=dewar.returning_date, + qrcode=dewar.qrcode, + contact_person_id=dewar.contact_person_id, + return_address_id=dewar.return_address_id + ) - db.add(db_dewar) - db.commit() - db.refresh(db_dewar) + db.add(db_dewar) + db.commit() + db.refresh(db_dewar) - return db_dewar + for puck_data in dewar.pucks: + puck = PuckModel( + dewar_id=db_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 = SampleModel( + puck_id=puck.id, + sample_name=sample_data.sample_name, + position=sample_data.position, + # Ensure only valid attributes are set + data_collection_parameters=sample_data.data_collection_parameters, + ) + db.add(sample) + db.commit() + db.refresh(sample) + + return db_dewar + + 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") @router.get("/{dewar_id}", response_model=DewarSchema) diff --git a/backend/app/routers/shipment.py b/backend/app/routers/shipment.py index 6627772..b9869f3 100644 --- a/backend/app/routers/shipment.py +++ b/backend/app/routers/shipment.py @@ -9,7 +9,7 @@ from sqlalchemy.exc import SQLAlchemyError from app.models import Shipment as ShipmentModel, ContactPerson as ContactPersonModel, Address as AddressModel, \ Proposal as ProposalModel, Dewar as DewarModel, Puck as PuckModel, Sample as SampleModel 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, DewarSchema from app.database import get_db from app.crud import get_shipments, get_shipment_by_id @@ -38,6 +38,19 @@ async def fetch_shipments(id: Optional[int] = Query(None), db: Session = Depends logging.info(f"Shipment ID: {shipment.id}, Shipment Name: {shipment.shipment_name}") return shipments +@router.get("/{shipment_id}/dewars", response_model=List[DewarSchema]) +async def get_dewars_by_shipment_id(shipment_id: int, 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") + + dewars = db.query(DewarModel).filter(DewarModel.shipment_id == shipment_id).all() + if not dewars: + raise HTTPException(status_code=404, detail="No dewars found for this shipment") + + return dewars + + @router.post("", response_model=ShipmentSchema, status_code=status.HTTP_201_CREATED) async def create_shipment(shipment: ShipmentCreate, db: Session = Depends(get_db)): @@ -220,58 +233,55 @@ async def update_shipment_comments(shipment_id: int, comments_data: UpdateShipme @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) -): +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() if not shipment: - logging.error(f"Shipment not found with ID: {shipment_id}") raise HTTPException(status_code=404, detail="Shipment not found") try: - dewar = DewarModel( - shipment_id=shipment_id, - dewar_name=payload.dewar_name, - tracking_number=payload.tracking_number, - status=payload.status, - contact_person_id=payload.contact_person_id, - return_address_id=payload.return_address_id, - ) - db.add(dewar) - db.commit() - db.refresh(dewar) - - for puck_data in payload.pucks: - puck = PuckModel( - 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 = SampleModel( - puck_id=puck.id, - sample_name=sample_data.sample_name, - position=sample_data.position, - data_collection_parameters=sample_data.data_collection_parameters, - ) - db.add(sample) + for dewar_data in payload.dewars: + dewar = db.query(DewarModel).filter(DewarModel.dewar_name == dewar_data.dewar_name).first() + if dewar: + # Update existing dewar + dewar.tracking_number = dewar_data.tracking_number + dewar.status = dewar_data.status db.commit() - db.refresh(sample) + else: + dewar = DewarModel( + shipment_id=shipment_id, + dewar_name=dewar_data.dewar_name, + tracking_number=dewar_data.tracking_number, + status=dewar_data.status, + ) + db.add(dewar) + db.commit() + db.refresh(dewar) + + for puck_data in dewar_data.pucks: + puck = PuckModel( + 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 = SampleModel( + puck_id=puck.id, + sample_name=sample_data.sample_name, + position=sample_data.position, + ) + db.add(sample) + db.commit() + db.refresh(sample) db.refresh(shipment) except SQLAlchemyError as e: - logging.error(f"Database error occurred: {e}") - raise HTTPException(status_code=500, detail="Internal server error") + raise HTTPException(status_code=500, detail=f"Database error: {e}") except ValidationError as e: - logging.error(f"Validation error occurred: {e}") - raise HTTPException(status_code=400, detail="Validation error") + raise HTTPException(status_code=400, detail=f"Validation error: {e}") - logging.info(f"Successfully added dewar, puck, and sample for shipment ID: {shipment_id}") - return shipment + return shipment \ No newline at end of file diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 0eaf61c..4b17553 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -101,7 +101,8 @@ class Sample(BaseModel): class SampleCreate(BaseModel): sample_name: str = Field(..., alias="crystalname") position: int = Field(..., alias="positioninpuck") - data_collection_parameters: DataCollectionParameters + data_collection_parameters: Optional[DataCollectionParameters] = None + results: Optional[Results] = None class Config: populate_by_name = True @@ -115,8 +116,11 @@ class PuckBase(BaseModel): puck_location_in_dewar: int -class PuckCreate(PuckBase): - pass +class PuckCreate(BaseModel): + puck_name: str + puck_type: str + puck_location_in_dewar: int + samples: List[SampleCreate] = [] class PuckUpdate(BaseModel): @@ -152,6 +156,7 @@ class DewarBase(BaseModel): qrcode: str contact_person_id: Optional[int] return_address_id: Optional[int] + pucks: List[PuckCreate] = [] class DewarCreate(DewarBase): @@ -181,6 +186,17 @@ class DewarUpdate(BaseModel): contact_person_id: Optional[int] = None address_id: Optional[int] = None +class DewarSchema(BaseModel): + id: int + dewar_name: str + tracking_number: str + status: str + contact_person_id: int + return_address_id: int + + class Config: + from_attributes = True + # Proposal schemas class Proposal(BaseModel): diff --git a/frontend/src/components/SpreadsheetTable.tsx b/frontend/src/components/SpreadsheetTable.tsx index f93992d..017dbc9 100644 --- a/frontend/src/components/SpreadsheetTable.tsx +++ b/frontend/src/components/SpreadsheetTable.tsx @@ -11,7 +11,12 @@ import { TextField, Typography, Button, - Box + Box, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle } from '@mui/material'; import { SpreadsheetService, ShipmentsService, DewarsService, ApiError } from '../../openapi'; import * as ExcelJS from 'exceljs'; @@ -30,6 +35,9 @@ const SpreadsheetTable = ({ const [editingCell, setEditingCell] = useState({}); const [nonEditableCells, setNonEditableCells] = useState(new Set()); const [isSubmitting, setIsSubmitting] = useState(false); + const [showUpdateDialog, setShowUpdateDialog] = useState(false); + const [dewarsToReplace, setDewarsToReplace] = useState([]); + const [dewarsToCreate, setDewarsToCreate] = useState(new Map()); const initialNewDewarState = { number_of_pucks: 0, @@ -44,7 +52,7 @@ const SpreadsheetTable = ({ dewar_name: '', tracking_number: 'UNKNOWN', status: 'In preparation', - pucks: [] + pucks: [] // Ensure 'pucks' array exists }; const [newDewar, setNewDewar] = useState(initialNewDewarState); @@ -141,36 +149,80 @@ const SpreadsheetTable = ({ 'dewarname': 0, 'puckname': 1, 'pucktype': 2, - // Add other fields as needed + 'crystalname': 3, + '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 checkIfDewarExists = async (dewarName) => { + if (!selectedShipment) return null; + + try { + const shipDewars = await ShipmentsService.getDewarsByShipmentIdShipmentsShipmentIdDewarsGet(selectedShipment.id); + return shipDewars.find((d) => d.dewar_name === dewarName); + } catch (error) { + console.error('Failed to fetch existing dewars:', error); + return null; + } + }; + + const createOrUpdateDewarsFromSheet = async (data, contactPerson, returnAddress) => { if (!contactPerson?.id || !returnAddress?.id) { console.error('contact_person_id or return_address_id is missing'); return null; } const dewars = new Map(); + const puckPositionMap = new Map(); + const dewarsToReplace = []; const dewarNameIdx = fieldToCol['dewarname']; const puckNameIdx = fieldToCol['puckname']; const puckTypeIdx = fieldToCol['pucktype']; + const sampleNameIdx = fieldToCol['crystalname']; + const samplePositionIdx = fieldToCol['positioninpuck']; - let puckPositionInDewar = 1; + for (let rowIndex = 0; rowIndex < data.length; rowIndex++) { + const row = data[rowIndex]; - for (const row of data) { if (!row.data) { - console.error(`Row data is missing`); + console.error('Row data is missing'); continue; } const dewarName = typeof row.data[dewarNameIdx] === 'string' ? row.data[dewarNameIdx].trim() : null; - const puckName = typeof row.data[puckNameIdx] === 'string' ? row.data[puckNameIdx].trim() : null; + const puckName = row.data[puckNameIdx] !== undefined && row.data[puckNameIdx] !== null ? String(row.data[puckNameIdx]).trim() : null; const puckType = typeof row.data[puckTypeIdx] === 'string' ? row.data[puckTypeIdx] : 'Unipuck'; + const sampleName = typeof row.data[sampleNameIdx] === 'string' ? row.data[sampleNameIdx].trim() : null; + const samplePosition = row.data[samplePositionIdx] !== undefined && row.data[samplePositionIdx] !== null ? Number(row.data[samplePositionIdx]) : null; - console.log(`Processing Dewar: ${dewarName}, Puck: ${puckName}, Type: ${puckType}`); - - if (dewarName) { + if (dewarName && puckName) { let dewar; if (!dewars.has(dewarName)) { dewar = { @@ -181,44 +233,116 @@ const SpreadsheetTable = ({ pucks: [] }; dewars.set(dewarName, dewar); - puckPositionInDewar = 1; - console.log(`Created new dewar: ${dewarName}`); + puckPositionMap.set(dewarName, new Map()); + + // Check if the dewar exists in the shipment + const existingDewar = await checkIfDewarExists(dewarName); + if (existingDewar) { + dewarsToReplace.push(existingDewar); + } } else { dewar = dewars.get(dewarName); - puckPositionInDewar++; - console.log(`Found existing dewar: ${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); + let puckPositions = puckPositionMap.get(dewarName); + if (!puckPositions.has(puckName)) { + puckPositions.set(puckName, puckPositions.size + 1); + } + const puckPosition = puckPositions.get(puckName); - console.log(`Added puck: ${JSON.stringify(puck)}`); + let puck = dewar.pucks.find(p => p.puck_name === puckName); + if (!puck) { + puck = { + puck_name: puckName, + puck_type: puckType, + puck_location_in_dewar: puckPosition, + samples: [] + }; + dewar.pucks.push(puck); + } + + const sample = { + sample_name: sampleName, + position: samplePosition, + results: null // Placeholder for results field + }; + + if (isNaN(sample.position)) { + console.error(`Invalid sample position for sample ${sample.sample_name} in puck ${puckName}`); + } else { + puck.samples.push(sample); + } } else { - console.error('Dewar name is missing in the row'); + if (!dewarName) { + console.error(`Dewar name is missing in row ${rowIndex}`); + } + if (!puckName) { + console.error(`Puck name is missing in row ${rowIndex}`); + } } } const dewarsArray = Array.from(dewars.values()); + + // Save dewars array for later use in handleConfirmUpdate + setDewarsToCreate(dewars); + + if (dewarsArray.length > 0 && dewarsToReplace.length > 0) { + setDewarsToReplace(dewarsToReplace); + setShowUpdateDialog(true); + } else { + await handleDewarCreation(dewarsArray); + } + }; + + const handleConfirmUpdate = async () => { + if (dewarsToReplace.length === 0) return; + + try { + for (const dewar of dewarsToReplace) { + await DewarsService.deleteDewarDewarsDewarIdDelete(dewar.id); + } + const dewarsArray = Array.from(dewarsToCreate.values()); + await handleDewarCreation(dewarsArray); + console.log('Dewars replaced successfully'); + } catch (error) { + console.error('Error replacing dewar', error); + if (error instanceof ApiError && error.body) { + console.error('Validation errors:', error.body.detail); + } else { + console.error('Unexpected error:', error); + } + } + setShowUpdateDialog(false); + setDewarsToReplace([]); + setDewarsToCreate(new Map()); + }; + + const handleCancelUpdate = () => { + setShowUpdateDialog(false); + setDewarsToReplace([]); + setDewarsToCreate(new Map()); + }; + + const handleDewarCreation = async (dewarsArray) => { for (const dewar of dewarsArray) { try { - // Call to create the dewar - const createdDewar = await DewarsService.createDewarDewarsPost(dewar); - console.log(`Created dewar: ${createdDewar.id}`); + if (!dewar.pucks || dewar.pucks.length === 0) { + console.error(`Dewar ${dewar.dewar_name} does not have any pucks.`); + continue; + } + + const createdDewar = await DewarsService.createDewarDewarsPost(dewar); - // 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}`); + console.log(`Dewar ${createdDewar.dewar_name} with ID ${createdDewar.id} created and added to the shipment.`); } } catch (error) { - console.error(`Error adding dewar`, error); + console.error('Error adding dewar', error); if (error instanceof ApiError && error.body) { console.error('Validation errors:', error.body.detail); } else { @@ -226,8 +350,6 @@ const SpreadsheetTable = ({ } } } - - return dewarsArray; }; const handleSubmit = async () => { @@ -241,18 +363,12 @@ const SpreadsheetTable = ({ setIsSubmitting(true); console.log('All data is valid. Proceeding with submission...'); - const processedDewars = await createDewarsFromSheet( + await createOrUpdateDewarsFromSheet( raw_data, selectedShipment?.contact_person, selectedShipment?.return_address ); - if (processedDewars && processedDewars.length > 0) { - console.log('Dewars processed successfully.'); - } else { - console.error('No valid dewars were created.'); - } - setIsSubmitting(false); } else { console.log('There are validation errors in the dataset. Please correct them before submission.'); @@ -270,22 +386,12 @@ const SpreadsheetTable = ({ }); }); - const buffer = await workbook.xlsx.writeBuffer(); - const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); - - saveAs(blob, 'corrected_data.xlsx'); + workbook.xlsx.writeBuffer().then((buffer) => { + const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); + saveAs(blob, 'corrected_spreadsheet.xlsx'); + }); }; - useEffect(() => { - console.log('Raw data:', raw_data); - console.log('Errors:', localErrors); - console.log('Headers:', headers); - }, [raw_data, localErrors, headers]); - - if (!raw_data || !headers) { - return
Loading...
; - } - return ( @@ -323,21 +429,25 @@ const SpreadsheetTable = ({ return ( - setEditingCell({ ...editingCell, [`${rowIndex}-${colIndex}`]: e.target.value })} - onKeyDown={(e) => { - if (e.key === "Enter") { - handleCellEdit(rowIndex, colIndex); - } - }} - onBlur={() => handleCellBlur(rowIndex, colIndex)} - error={isInvalid} - fullWidth - variant="outlined" - size="small" - disabled={isReadonly} - /> + {isInvalid ? ( + setEditingCell({ ...editingCell, [`${rowIndex}-${colIndex}`]: e.target.value })} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleCellEdit(rowIndex, colIndex); + } + }} + onBlur={() => handleCellBlur(rowIndex, colIndex)} + error={isInvalid} + fullWidth + variant="outlined" + size="small" + disabled={isReadonly} + /> + ) : ( + cellValue + )} ); @@ -346,6 +456,28 @@ const SpreadsheetTable = ({ ))}
+ + + Replace Dewars + + + The following dewars already exist: {dewarsToReplace.map(dewar => dewar.dewar_name).join(', ')}. Would you like to replace them? + + + + + + +
); };