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 }) => { 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 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', pucks: [] // Ensure 'pucks' array exists }; 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(); if (Array.isArray(errorsList)) { errorsList.forEach((error) => { const key = `${error.row}-${headers[error.cell]}`; 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) { // If valid, update the value (and use corrected_value if returned) 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) ); 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; 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) => { 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 proteinNameIdx = fieldToCol['proteinname']; const samplePositionIdx = fieldToCol['positioninpuck']; const priorityIdx = fieldToCol['priority']; const commentsIdx = fieldToCol['comments']; for (let rowIndex = 0; rowIndex < data.length; rowIndex++) { const row = data[rowIndex]; if (!row.data) { console.error('Row data is missing'); continue; } // Extract values from the appropriate columns const dewarName = typeof row.data[dewarNameIdx] === 'string' ? row.data[dewarNameIdx].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 proteinName = typeof row.data[proteinNameIdx] === 'string' ? row.data[proteinNameIdx].trim() : null; const samplePosition = row.data[samplePositionIdx] !== undefined && row.data[samplePositionIdx] !== null ? Number(row.data[samplePositionIdx]) : null; const priority = row?.data?.[priorityIdx] ? Number(row.data[priorityIdx]) : null; const comments = typeof row.data[commentsIdx] === 'string' ? row.data[commentsIdx].trim() : null; // Create data_collection_parameters object const dataCollectionParameters = { directory: row.data[fieldToCol['directory']], oscillation: row.data[fieldToCol['oscillation']] ? parseFloat(row.data[fieldToCol['oscillation']]) : undefined, aperture: row.data[fieldToCol['aperture']] ? row.data[fieldToCol['aperture']].trim() : undefined, exposure: row.data[fieldToCol['exposure']] ? parseFloat(row.data[fieldToCol['exposure']]) : undefined, totalrange: row.data[fieldToCol['totalrange']] ? parseInt(row.data[fieldToCol['totalrange']], 10) : undefined, transmission: row.data[fieldToCol['transmission']] ? parseInt(row.data[fieldToCol['transmission']], 10) : undefined, dose: row.data[fieldToCol['dose']] ? parseFloat(row.data[fieldToCol['dose']]) : undefined, targetresolution: row.data[fieldToCol['targetresolution']] ? parseFloat(row.data[fieldToCol['targetresolution']]) : undefined, datacollectiontype: row.data[fieldToCol['datacollectiontype']], processingpipeline: row.data[fieldToCol['processingpipeline']], spacegroupnumber: row.data[fieldToCol['spacegroupnumber']] ? parseInt(row.data[fieldToCol['spacegroupnumber']], 10) : undefined, cellparameters: row.data[fieldToCol['cellparameters']], rescutkey: row.data[fieldToCol['rescutkey']], rescutvalue: row.data[fieldToCol['rescutvalue']] ? parseFloat(row.data[fieldToCol['rescutvalue']]) : undefined, userresolution: row.data[fieldToCol['userresolution']] ? parseFloat(row.data[fieldToCol['userresolution']]) : undefined, pdbid: row.data[fieldToCol['pdbid']], 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: row.data[fieldToCol['trustedhigh']] ? parseFloat(row.data[fieldToCol['trustedhigh']]) : undefined, autoprocextraparams: row.data[fieldToCol['autoprocextraparams']], chiphiangles: row.data[fieldToCol['chiphiangles']] ? parseFloat(row.data[fieldToCol['chiphiangles']]) : undefined, }; if (dewarName && puckName) { let dewar; if (!dewars.has(dewarName)) { dewar = { ...initialNewDewarState, dewar_name: dewarName, contact_person_id: contactPerson.id, return_address_id: returnAddress.id, pucks: [] }; dewars.set(dewarName, dewar); 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); } let puckPositions = puckPositionMap.get(dewarName); if (!puckPositions.has(puckName)) { puckPositions.set(puckName, puckPositions.size + 1); } const puckPosition = puckPositions.get(puckName); 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, proteinname: proteinName, position: samplePosition, priority: priority, comments: comments, data_collection_parameters: dataCollectionParameters, // Attach the parameters 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 { 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 { 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); if (createdDewar && selectedShipment) { await ShipmentsService.addDewarToShipmentShipmentsShipmentIdAddDewarPost( selectedShipment.id, 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); if (error instanceof ApiError && error.body) { console.error('Validation errors:', error.body.detail); } else { console.error('Unexpected error:', error); } } } }; 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_person, 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 ( {headers.map((header, index) => ( {header} ))} {raw_data.map((row, rowIndex) => ( {headers.map((header, colIndex) => { const key = `${row.row_num}-${header}`; const errorMessage = errorMap.get(key); const isInvalid = !!errorMessage; const cellValue = row.data[colIndex]; const editingValue = editingCell[`${rowIndex}-${colIndex}`]; const isCellCorrected = row.corrected_columns?.includes(header); // Check if this column is marked as corrected const isDefaultAssigned = colIndex === 7 && row.default_set; // Default-assigned field exists and is true // Dynamic styles for corrected cells const cellStyle = { backgroundColor: isDefaultAssigned ? "#e6fbe6" // Light green for default values : isCellCorrected ? "#fff8e1" // Light yellow for corrected values : "transparent", // Default for others color: isDefaultAssigned ? "#1b5e20" // Dark green for default values : "inherit", // Default for others fontWeight: (isCellCorrected || isDefaultAssigned) ? "bold" : "normal", // Bold text for any change cursor: isInvalid ? "pointer" : "default", // Mouse pointer indicates interactive error cells }; 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?
); }; export default SpreadsheetTable;