From 5e6eb40033e2b70db6d1304651079755cc5a3f75 Mon Sep 17 00:00:00 2001 From: GotthardG <51994228+GotthardG@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:37:59 +0100 Subject: [PATCH] now creating dewars from spreadsheet --- backend/app/routers/shipment.py | 45 ++++ backend/app/schemas.py | 9 +- backend/app/services/spreadsheet_service.py | 16 +- frontend/src/components/ShipmentPanel.tsx | 4 + frontend/src/components/SpreadsheetTable.tsx | 210 +++++++++++++------ frontend/src/components/UploadDialog.tsx | 2 +- 6 files changed, 207 insertions(+), 79 deletions(-) diff --git a/backend/app/routers/shipment.py b/backend/app/routers/shipment.py index 491c7fa..a1d26ae 100644 --- a/backend/app/routers/shipment.py +++ b/backend/app/routers/shipment.py @@ -263,3 +263,48 @@ async def add_dewar_puck_sample_to_shipment(shipment_id: int, payload: DewarCrea 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( + shipment_id=shipment_id, + dewar_name=dewar_data.dewar_name, # Ensure this field aligns with the frontend + 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 = 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.crystalname, + position=sample_data.positioninpuck, + data_collection_parameters=sample_data.data_collection_parameters + ) + db.add(sample) + db.commit() + db.refresh(sample) + + db.refresh(shipment) + return shipment \ No newline at end of file diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 3fa4c25..0eaf61c 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -154,13 +154,8 @@ class DewarBase(BaseModel): return_address_id: Optional[int] -class DewarCreate(BaseModel): - dewar_name: str = Field(..., alias="dewarname") - tracking_number: Optional[str] - status: Optional[str] = None - contact_person_id: Optional[int] = None - return_address_id: Optional[int] = None - pucks: List[PuckCreate] = [] +class DewarCreate(DewarBase): + pass class Dewar(DewarBase): diff --git a/backend/app/services/spreadsheet_service.py b/backend/app/services/spreadsheet_service.py index 354a011..3150957 100644 --- a/backend/app/services/spreadsheet_service.py +++ b/backend/app/services/spreadsheet_service.py @@ -8,33 +8,41 @@ from app.sample_models import SpreadsheetModel logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) + class SpreadsheetImportError(Exception): pass + class SampleSpreadsheetImporter: def __init__(self): self.filename = None self.model = None def _clean_value(self, value, expected_type=None): - """Clean value by converting it to the expected type and stripping whitespace for strings.""" + """Clean value by converting it to the expected type and handle edge cases.""" if value is None: return None if expected_type == str: + # Ensure value is converted to string and stripped of whitespace return str(value).strip() if expected_type in [float, int]: try: return expected_type(value) - except ValueError: + except (ValueError, TypeError): + # If conversion fails, return None return None if isinstance(value, str): try: + # Handle numeric strings if '.' in value: return float(value) else: return int(value) except ValueError: - return value.strip() + pass + # In case of failure, return the stripped string + return value.strip() + # If no expected type or value type match, return the original value return value def import_spreadsheet(self, file): @@ -209,4 +217,4 @@ class SampleSpreadsheetImporter: self.model = model logger.info(f"Finished processing {len(model)} records with {len(errors)} errors") - return self.model, errors, raw_data, headers # Include headers in the response \ No newline at end of file + return self.model, errors, raw_data, headers # Include headers in the response diff --git a/frontend/src/components/ShipmentPanel.tsx b/frontend/src/components/ShipmentPanel.tsx index 8fd504f..067f266 100644 --- a/frontend/src/components/ShipmentPanel.tsx +++ b/frontend/src/components/ShipmentPanel.tsx @@ -61,6 +61,10 @@ const ShipmentPanel: React.FC = ({ const isSelected = selectedShipment?.id === shipment.id; const updatedShipment = 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); }; diff --git a/frontend/src/components/SpreadsheetTable.tsx b/frontend/src/components/SpreadsheetTable.tsx index eda44ee..82d8158 100644 --- a/frontend/src/components/SpreadsheetTable.tsx +++ b/frontend/src/components/SpreadsheetTable.tsx @@ -13,10 +13,16 @@ import { Button, Box } from '@mui/material'; -import { SpreadsheetService, ShipmentsService } from '../../openapi'; +import { SpreadsheetService, ShipmentsService, DewarsService, ApiError } from '../../openapi'; import * as ExcelJS from 'exceljs'; import { saveAs } from 'file-saver'; +const HeaderMapping = { + 'dewarname': 'dewar_name', + 'trackingnumber': 'tracking_number', + 'status': 'status' +}; + const SpreadsheetTable = ({ raw_data, errors, @@ -24,11 +30,37 @@ const SpreadsheetTable = ({ setRawData, onCancel, fileBlob, - shipmentId // Accept the shipmentId + selectedShipment }) => { const [localErrors, setLocalErrors] = useState(errors || []); const [editingCell, setEditingCell] = useState({}); const [nonEditableCells, setNonEditableCells] = useState(new Set()); + const [isSubmitting, setIsSubmitting] = useState(false); + + const initialNewDewarState = { + number_of_pucks: 0, + number_of_samples: 0, + ready_date: null, + shipping_date: null, + arrival_date: null, + returning_date: null, + qrcode: 'N/A', + contact_person_id: selectedShipment?.contact_person?.id, + return_address_id: selectedShipment?.return_address?.id, + dewar_name: '', + tracking_number: 'UNKNOWN', + status: 'In preparation', + }; + + const [newDewar, setNewDewar] = useState(initialNewDewarState); + + useEffect(() => { + setNewDewar((prev) => ({ + ...prev, + contact_person_id: selectedShipment?.contact_person?.id, + return_address_id: selectedShipment?.return_address?.id + })); + }, [selectedShipment]); const generateErrorMap = (errorsList) => { const errorMap = new Map(); @@ -112,78 +144,122 @@ const SpreadsheetTable = ({ const allCellsValid = () => nonEditableCells.size === raw_data.length * headers.length; - const handleSubmit = async () => { - if (allCellsValid()) { - console.log('All data is valid. Proceeding with submission...'); - const processedData = createPayload(raw_data); + const fieldToCol = { + 'dewarname': 0, + 'puckname': 1, + 'pucktype': 2, + '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, + }; - try { - const response = await ShipmentsService.addDewarPuckSampleToShipmentShipmentsShipmentIdAddDewarPuckSamplePost(shipmentId, processedData); - console.log('Shipment processed successfully:', response); + const createDewarsFromSheet = async (data, contactPerson, returnAddress) => { + if (!contactPerson?.id || !returnAddress?.id) { + console.error('contact_person_id or return_address_id is missing'); + return null; + } - // Handle success actions, e.g., display notification, reset state, etc. - } catch (error) { - console.error('Error processing shipment:', error); - // Handle error actions, e.g., display notification, etc. + const dewars = new Map(); // Use a Map to prevent duplicates + + for (const row of data) { + if (!row.data) { + console.error(`Row data is missing`); + continue; } + + const dewarNameIdx = fieldToCol['dewarname']; + const dewarName = row.data[dewarNameIdx]?.trim(); + + if (dewarName && !dewars.has(dewarName)) { + 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 { + const createdDewar = await DewarsService.createDewarDewarsPost(newDewarToPost); + if (createdDewar && selectedShipment) { + const updatedShipment = await ShipmentsService.addDewarToShipmentShipmentsShipmentIdAddDewarPost( + selectedShipment.id, + createdDewar.id + ); + dewars.set(dewarName, updatedShipment); // Track the added dewar + } + } catch (error) { + console.error(`Error adding dewar for row: ${row.row_num}`, error); + if (error instanceof ApiError && error.body) { + console.error('Validation errors:', error.body.detail); + } else { + console.error('Unexpected error:', error); + } + } + } else if (!dewarName) { + console.error('Dewar name is missing in the row'); + } + } + + return Array.from(dewars.values()); + }; + + const handleSubmit = async () => { + if (isSubmitting) return; // Prevent multiple submissions + if (!headers || headers.length === 0) { + console.error('Cannot submit, headers are not defined or empty'); + return; + } + + if (allCellsValid()) { + setIsSubmitting(true); + console.log('All data is valid. Proceeding with submission...'); + + const processedDewars = await createDewarsFromSheet( + 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.'); } }; - const createPayload = (data) => { - const allowedFields = [ - 'priority', 'comments', 'directory', 'proteinname', 'oscillation', 'aperture', - 'exposure', 'totalrange', 'transmission', 'dose', 'targetresolution', 'datacollectiontype', - 'processingpipeline', 'spacegroupnumber', 'cellparameters', 'rescutkey', 'rescutvalue', - 'userresolution', 'pdbid', 'autoprocfull', 'procfull', 'adpenabled', 'noano', - 'ffcscampaign', 'trustedhigh', 'autoprocextraparams', 'chiphiangles' - ]; - - let dewars = {}; - - data.forEach((row) => { - const dewarname = row.data[headers.indexOf('dewarname')]; - const puckname = row.data[headers.indexOf('puckname')]; - const crystalname = row.data[headers.indexOf('crystalname')]; - const positioninpuck = row.data[headers.indexOf('positioninpuck')]; - - if (!dewars[dewarname]) { - dewars[dewarname] = { - dewarname: dewarname, - pucks: {} - }; - } - - if (!dewars[dewarname].pucks[puckname]) { - dewars[dewarname].pucks[puckname] = { - puckname: puckname, - samples: [] - }; - } - - const dataCollectionParams = {}; - headers.forEach((header, index) => { - if (allowedFields.includes(header)) { - dataCollectionParams[header] = row.data[index]; - } - }); - - dewars[dewarname].pucks[puckname].samples.push({ - crystalname: crystalname, - positioninpuck: positioninpuck, - data_collection_parameters: dataCollectionParams - }); - }); - - const dewarsList = Object.values(dewars).map(dewar => ({ - dewarname: dewar.dewarname, - pucks: Object.values(dewar.pucks) - })); - - return { dewars: dewarsList }; - }; - const downloadCorrectedSpreadsheet = async () => { const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(fileBlob); @@ -227,7 +303,7 @@ const SpreadsheetTable = ({ -