import React, { useState, useEffect } from 'react'; import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Tooltip, TextField, Typography, Button, Box, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material'; import { SpreadsheetService, ShipmentsService, DewarsService, ApiError } from '../../openapi'; import * as ExcelJS from 'exceljs'; import { saveAs } from 'file-saver'; const SpreadsheetTable = ({ raw_data, errors, headers, setRawData, onCancel, fileBlob, selectedShipment, addinfo, activePgroup, }) => { const [localErrors, setLocalErrors] = useState(errors || []); 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 [correctionMetadata, setCorrectionMetadata] = useState(addinfo || []); // Store addinfo const [errorDialogOpen, setErrorDialogOpen] = useState(false); // Controls dialog visibility const [errorMessages, setErrorMessages] = useState([]); // Error messages from the server const enhancedRawData = raw_data.map((row) => { const metadata = correctionMetadata.find((info) => info.row_num === row.row_num) || {}; // Combine original row data with metadata return { ...row, corrected_columns: metadata.corrected_columns || [], // Columns corrected default_set_columns: metadata.default_set || [], // Specific columns default-assigned }; }); useEffect(() => { console.log("Correction Metadata:", correctionMetadata); console.log("Addinfo:", addinfo); }, [correctionMetadata, addinfo]); const initialNewDewarState = { pgroups: activePgroup, number_of_pucks: 0, number_of_samples: 0, contact_id: selectedShipment?.contact?.id, return_address_id: selectedShipment?.return_address?.id, dewar_name: '', tracking_number: 'UNKNOWN', status: 'active', pucks: [] // Ensure 'pucks' array exists }; const [newDewar, setNewDewar] = useState(initialNewDewarState); useEffect(() => { setNewDewar((prev) => ({ ...prev, contact_id: selectedShipment?.contact?.id, return_address_id: selectedShipment?.return_address?.id })); }, [selectedShipment]); const generateErrorMap = (errorsList) => { const errorMap = new Map(); if (Array.isArray(errorsList)) { errorsList.forEach((error) => { const colIndex = headers.findIndex(header => header === error.column); if (colIndex > -1) { const key = `${error.row}-${headers[colIndex]}`; errorMap.set(key, error.message); } }); } return errorMap; }; const errorMap = generateErrorMap(localErrors); useEffect(() => { const updatedNonEditableCells = new Set(); raw_data.forEach((row, rowIndex) => { headers.forEach((_, colIndex) => { const key = `${row.row_num}-${headers[colIndex]}`; if (!errorMap.has(key)) { updatedNonEditableCells.add(`${rowIndex}-${colIndex}`); } }); }); setNonEditableCells(updatedNonEditableCells); console.log("Recalculated nonEditableCells:", updatedNonEditableCells); }, [raw_data, headers, errorMap]); const handleCellEdit = async (rowIndex, colIndex) => { const updatedRawData = [...raw_data]; const columnName = headers[colIndex]; const currentRow = updatedRawData[rowIndex]; const newValue = editingCell[`${rowIndex}-${colIndex}`]; if (newValue === undefined) return; // Ensure value is provided // Prepare for validation request if (!currentRow.data) { currentRow.data = []; } currentRow.data[colIndex] = newValue; // Reset editing state setEditingCell((prev) => { const updated = { ...prev }; delete updated[`${rowIndex}-${colIndex}`]; return updated; }); try { const response = await SpreadsheetService.validateCellValidateCellPost({ row: currentRow.row_num, column: columnName, value: newValue, }); if (response && response.is_valid !== undefined) { if (response.is_valid) { const correctedValue = response.corrected_value ?? newValue; currentRow.data[colIndex] = correctedValue; updatedRawData[rowIndex] = currentRow; setRawData(updatedRawData); // Remove the error and mark as non-editable const updatedErrors = localErrors.filter( (error) => !( error.row === currentRow.row_num && error.cell === colIndex && error.message.toLowerCase().includes("duplicate position") ) ); setLocalErrors(updatedErrors); // Update error list setNonEditableCells((prev) => new Set([...prev, `${rowIndex}-${colIndex}`])); } else { // If not valid, don't add to nonEditableCells and update the error list const errorMessage = response.message || "Invalid value."; const newError = { row: currentRow.row_num, cell: colIndex, message: errorMessage, }; const updatedErrors = [ ...localErrors.filter( (error) => !(error.row === newError.row && error.cell === newError.cell) ), newError, ]; setLocalErrors(updatedErrors); setNonEditableCells((prev) => { const updatedSet = new Set(prev); updatedSet.delete(`${rowIndex}-${colIndex}`); // Ensure it stays editable return updatedSet; }); } } else { console.error("Unexpected response from backend:", response); } } catch (error) { console.error("Validation request failed:", error); } }; const handleCellBlur = (rowIndex, colIndex) => { handleCellEdit(rowIndex, colIndex); }; const allCellsValid = () => nonEditableCells.size === raw_data.length * headers.length && !localErrors.some((error) => error.message.toLowerCase().includes("duplicate position") ); 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 }; const checkIfDewarExists = async (dewarName: string) => { if (!selectedShipment) return null; try { // Fetch dewars related to the current shipment via API const shipDewars = await ShipmentsService.getDewarsByShipmentIdProtectedShipmentsShipmentIdDewarsGet(selectedShipment.id); // Search for dewar by name within the shipment const existingDewar = shipDewars.find((d) => d.dewar_name === dewarName); if (existingDewar) { console.log(`Dewar "${dewarName}" exists with ID: ${existingDewar.id}`); return existingDewar; } return null; } catch (error) { console.error("Failed to fetch existing dewars:", error); return null; } }; const createOrUpdateDewarsFromSheet = async (data, contact, address) => { if (!contact?.id || !address?.id) { console.error('contact_id or return_address_id is missing'); return null; } const dewars = new Map(); const puckPositionMap = new Map(); const dewarsToReplace = []; for (let rowIndex = 0; rowIndex < data.length; rowIndex++) { const row = data[rowIndex]; if (!row.data) { console.error(`Row ${rowIndex}: Missing or invalid data`); continue; } const dewarName = sanitizeAndValidateColumn(row, fieldToCol['dewarname'], 'string', 'Dewar Name'); const puckName = sanitizeAndValidateColumn(row, fieldToCol['puckname'], 'string', 'Puck Name'); const puckType = sanitizeAndValidateColumn(row, fieldToCol['pucktype'], 'string', 'Unipuck', true); // Default: `Unipuck` const sampleName = sanitizeAndValidateColumn(row, fieldToCol['crystalname'], 'string', 'Sample Name'); if (dewarName && puckName) { let dewar; if (!dewars.has(dewarName)) { // Initialize new dewar object dewar = { ...initialNewDewarState, pgroups: activePgroup, dewar_name: dewarName, contact_id: contact.id, return_address_id: address.id, pucks: [], }; dewars.set(dewarName, dewar); puckPositionMap.set(dewarName, new Map()); // Check if dewar exists using backend const existingDewar = await checkIfDewarExists(dewarName); if (existingDewar) { dewarsToReplace.push(existingDewar); } } else { dewar = dewars.get(dewarName); } // Handle puck positions let puckPositions = puckPositionMap.get(dewarName); if (!puckPositions.has(puckName)) { puckPositions.set(puckName, puckPositions.size + 1); } const puckPosition = puckPositions.get(puckName); // Create puck and attach it to the dewar 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); } // Add sample to puck const sample = { sample_name: sampleName, position: sanitizeIntColumn(row, fieldToCol['positioninpuck'], 'Sample Position'), proteinname: sanitizeAndValidateColumn(row, fieldToCol['proteinname'], 'string', 'Protein Name'), priority: sanitizeIntColumn(row, fieldToCol['priority'], 'Priority'), comments: sanitizeAndValidateColumn(row, fieldToCol['comments'], 'string', 'Comments', true), data_collection_parameters: collectDataParameters(row), // Consolidate data parameters }; puck.samples.push(sample); } else { console.error(`Row ${rowIndex} is missing required fields for dewar/puck creation.`); } } const dewarsArray = Array.from(dewars.values()); // Save for update dialog control setDewarsToCreate(dewars); if (dewarsArray.length > 0 && dewarsToReplace.length > 0) { setDewarsToReplace(dewarsToReplace); setShowUpdateDialog(true); } else if (dewarsArray.length > 0) { await handleDewarCreation(dewarsArray); } }; const sanitizeAndValidateColumn = (row, colIndex, type, columnName, isOptional = false) => { const value = row?.data?.[colIndex]; const sanitizedValue = type === 'string' ? value?.trim() : value; if (!sanitizedValue && !isOptional) { console.error(`${columnName} is missing or invalid.`); return null; } if (type === 'number' && isNaN(sanitizedValue)) { console.error(`${columnName} is not a valid number.`); } return sanitizedValue; }; // Utility to sanitize integer columns const sanitizeIntColumn = (row, colIndex, columnName) => { const rawValue = row?.data?.[colIndex]; const intValue = parseInt(rawValue, 10); if (isNaN(intValue)) { console.error(`${columnName} is not a valid integer.`); } return intValue; }; // Consolidate data collection parameters const collectDataParameters = (row) => ({ directory: sanitizeAndValidateColumn(row, fieldToCol['directory'], 'string', 'Directory', true), oscillation: parseFloat(sanitizeAndValidateColumn(row, fieldToCol['oscillation'], 'number', 'Oscillation', true)), aperture: sanitizeAndValidateColumn(row, fieldToCol['aperture'], 'string', 'Aperture', true), exposure: parseFloat(sanitizeAndValidateColumn(row, fieldToCol['exposure'], 'number', 'Exposure', true)), totalrange: sanitizeIntColumn(row, fieldToCol['totalrange'], 'Total Range'), transmission: sanitizeIntColumn(row, fieldToCol['transmission'], 'Transmission'), dose: parseFloat(sanitizeAndValidateColumn(row, fieldToCol['dose'], 'number', 'Dose', true)), targetresolution: parseFloat(sanitizeAndValidateColumn(row, fieldToCol['targetresolution'], 'number', 'Target Resolution', true)), datacollectiontype: sanitizeAndValidateColumn(row, fieldToCol['datacollectiontype'], 'string', 'Data Collection Type', true), processingpipeline: sanitizeAndValidateColumn(row, fieldToCol['processingpipeline'], 'string', 'Processing Pipeline', true), spacegroupnumber: sanitizeIntColumn(row, fieldToCol['spacegroupnumber'], 'Space Group Number'), cellparameters: sanitizeAndValidateColumn(row, fieldToCol['cellparameters'], 'string', 'Cell Parameters', true), rescutkey: sanitizeAndValidateColumn(row, fieldToCol['rescutkey'], 'string', 'Resolution Cut Key', true), rescutvalue: parseFloat(sanitizeAndValidateColumn(row, fieldToCol['rescutvalue'], 'number', 'Resolution Cut Value', true)), userresolution: parseFloat(sanitizeAndValidateColumn(row, fieldToCol['userresolution'], 'number', 'User Resolution', true)), pdbid: sanitizeAndValidateColumn(row, fieldToCol['pdbid'], 'string', 'PDB ID', true), autoprocfull: row.data[fieldToCol['autoprocfull']] === true, procfull: row.data[fieldToCol['procfull']] === true, adpenabled: row.data[fieldToCol['adpenabled']] === true, noano: row.data[fieldToCol['noano']] === true, ffcscampaign: row.data[fieldToCol['ffcscampaign']] === true, trustedhigh: parseFloat(sanitizeAndValidateColumn(row, fieldToCol['trustedhigh'], 'number', 'Trusted High', true)), autoprocextraparams: sanitizeAndValidateColumn(row, fieldToCol['autoprocextraparams'], 'string', 'Autoproc Extra Params', true), chiphiangles: parseFloat(sanitizeAndValidateColumn(row, fieldToCol['chiphiangles'], 'number', 'Chi/Phi Angles', true)), }); const handleConfirmUpdate = async () => { if (dewarsToReplace.length === 0) return; try { const dewarsArray = Array.from(dewarsToCreate.values()); // Perform the creation/update operation await handleDewarCreation(dewarsArray); console.log("Dewars replaced successfully"); } catch (error: any) { console.error("Error replacing dewars:", error); let errorMessage = error?.message || "Unexpected error occurred while replacing dewars."; setErrorMessages((prevMessages) => [...prevMessages, errorMessage]); setErrorDialogOpen(true); } // Reset controls after either success or failure setShowUpdateDialog(false); setDewarsToReplace([]); setDewarsToCreate(new Map()); }; const handleCancelUpdate = () => { setShowUpdateDialog(false); setDewarsToReplace([]); setDewarsToCreate(new Map()); }; const handleDewarCreation = async (dewarsArray: any[]) => { const errorMessages: string[] = []; // Collect error messages for display for (const dewar of dewarsArray) { try { const { number_of_pucks, number_of_samples, ...payload } = dewar; // Attempt to create or update a dewar await DewarsService.createOrUpdateDewarProtectedDewarsPost(selectedShipment.id, payload); console.log(`Dewar "${dewar.dewar_name}" created/updated successfully.`); } catch (error: any) { // Log the full error object for debugging purposes console.error("Full error object:", error); let backendReason = "Unexpected error occurred."; // Default fallback message if (error instanceof ApiError && error.body) { // API error response (similar to delete route) console.error("API error body:", error.body); backendReason = error.body.detail || backendReason; } else if (error?.response?.data?.detail) { // Fallback for Axios-like errors backendReason = error.response.data.detail; } else if (error?.response) { // Unexpected HTTP response (no error detail) backendReason = `Unknown error occurred (Status: ${error.response.status}).`; console.error("Error Response Data:", error.response.data); } else if (error?.message) { // Client-side error (e.g., network issues) backendReason = error.message; } // Append the detailed error message to the list errorMessages.push(`Dewar "${dewar.dewar_name}": ${backendReason}`); } } // Notify the user if there are error messages if (errorMessages.length > 0) { setErrorMessages(errorMessages); // Set state to render an error dialog setErrorDialogOpen(true); // Open the error dialog // Re-throw the error to let the parent function (e.g., handleConfirmUpdate) know something went wrong throw new Error("Error(s) occurred while creating/updating dewars."); } else { console.log("All dewars processed successfully."); alert("All dewars created successfully!"); // Show success message } }; const handleSubmit = async () => { if (isSubmitting) return; 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...'); await createOrUpdateDewarsFromSheet( raw_data, selectedShipment?.contact, selectedShipment?.return_address ); setIsSubmitting(false); } else { console.log('There are validation errors in the dataset. Please correct them before submission.'); } }; const downloadCorrectedSpreadsheet = async () => { const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(fileBlob); const worksheet = workbook.getWorksheet(1); raw_data.forEach((row, rowIndex) => { row.data.forEach((value, colIndex) => { worksheet.getRow(row.row_num).getCell(colIndex + 1).value = value; }); }); workbook.xlsx.writeBuffer().then((buffer) => { const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); saveAs(blob, 'corrected_spreadsheet.xlsx'); }); }; return ( Default Assigned (Light Green) Corrected (Light Yellow) {headers.map((header, index) => ( {header} ))} {enhancedRawData.map((row, rowIndex) => ( {headers.map((header, colIndex) => { const key = `${row.row_num}-${header}`; const errorMessage = errorMap.get(key); const isInvalid = !!errorMessage; const isDuplicateError = errorMessage?.toLowerCase().includes("duplicate position"); // Detect duplicate-specific messages const cellValue = row.data[colIndex]; const editingValue = editingCell[`${rowIndex}-${colIndex}`]; const isCellCorrected = row.corrected_columns?.includes(header); // Use corrected metadata const isDefaultAssigned = row.default_set_columns?.includes(header); // Dynamically match header name return ( {isInvalid ? ( setEditingCell({ ...editingCell, [`${rowIndex}-${colIndex}`]: e.target.value, }) } onBlur={() => handleCellBlur(rowIndex, colIndex)} error={isInvalid} fullWidth variant="outlined" size="small" /> ) : ( cellValue )} ); })} ))}
Replace Dewars The following dewars already exist: {dewarsToReplace.map(dewar => dewar.dewar_name).join(', ')}. Would you like to replace them? setErrorDialogOpen(false)} aria-labelledby="error-dialog-title" aria-describedby="error-dialog-description" > Error Creating/Updating Dewars The following errors occurred while processing dewars:
    {errorMessages.map((message, index) => (
  • {message}
  • ))}
); }; export default SpreadsheetTable;