
Updated Dewar API methods to use protected endpoints for enhanced security and consistency. Added `pgroups` handling in various frontend components and modified the LogisticsView contact field for clarity. Simplified backend router imports for better readability.
685 lines
30 KiB
TypeScript
685 lines
30 KiB
TypeScript
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<string[]>([]); // 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 (
|
|
<TableContainer component={Paper}>
|
|
<Box display="flex" justifyContent="space-between" mb={2}>
|
|
<Typography variant="body2" style={{ backgroundColor: "#e6fbe6", padding: "4px 8px", borderRadius: "4px" }}>
|
|
Default Assigned (Light Green)
|
|
</Typography>
|
|
<Typography variant="body2" style={{ backgroundColor: "#fff8e1", padding: "4px 8px", borderRadius: "4px" }}>
|
|
Corrected (Light Yellow)
|
|
</Typography>
|
|
</Box>
|
|
<Table>
|
|
<TableHead>
|
|
<TableRow>
|
|
{headers.map((header, index) => (
|
|
<TableCell key={index} align="center">
|
|
<Typography variant="body2">{header}</Typography>
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
</TableHead>
|
|
<Box display="flex" justifyContent="space-between" mt={2}>
|
|
<Button variant="contained" color="secondary" onClick={onCancel}>
|
|
Cancel
|
|
</Button>
|
|
<Button variant="contained" color="primary" onClick={handleSubmit} disabled={!allCellsValid() || isSubmitting}>
|
|
Submit
|
|
</Button>
|
|
<Button variant="contained" onClick={downloadCorrectedSpreadsheet} disabled={!allCellsValid()}>
|
|
Download Corrected Spreadsheet
|
|
</Button>
|
|
</Box>
|
|
<TableBody>
|
|
{enhancedRawData.map((row, rowIndex) => (
|
|
<TableRow key={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 (
|
|
<TableCell
|
|
key={colIndex}
|
|
align="center"
|
|
style={{
|
|
backgroundColor: isDefaultAssigned
|
|
? "#e6fbe6" // Light green for default values
|
|
: isCellCorrected
|
|
? "#fff8e1" // Light yellow for corrections
|
|
: isDuplicateError
|
|
? "#fdecea" // Light red for duplicate errors
|
|
: "transparent", // No specific color for valid cells
|
|
color: isDefaultAssigned
|
|
? "#1b5e20" // Dark green for default values
|
|
: isDuplicateError
|
|
? "#d32f2f" // Bright red for duplicates
|
|
: "inherit",
|
|
fontWeight: isDefaultAssigned || isCellCorrected || isDuplicateError
|
|
? "bold"
|
|
: "normal",
|
|
cursor: isInvalid ? "pointer" : "default", // Allow editing invalid cells
|
|
}}
|
|
>
|
|
<Tooltip
|
|
title={
|
|
isDefaultAssigned
|
|
? "This value was automatically assigned as a default."
|
|
: isCellCorrected
|
|
? `Field "${header}" was auto-corrected.`
|
|
: isDuplicateError
|
|
? `Duplicate value detected in "${header}". Please ensure values are unique in this column.`
|
|
: errorMessage || ""
|
|
}
|
|
arrow
|
|
disableHoverListener={!isDefaultAssigned && !isCellCorrected && !isInvalid}
|
|
>
|
|
{isInvalid ? (
|
|
<TextField
|
|
value={editingValue !== undefined ? editingValue : cellValue}
|
|
onChange={(e) =>
|
|
setEditingCell({
|
|
...editingCell,
|
|
[`${rowIndex}-${colIndex}`]: e.target.value,
|
|
})
|
|
}
|
|
onBlur={() => handleCellBlur(rowIndex, colIndex)}
|
|
error={isInvalid}
|
|
fullWidth
|
|
variant="outlined"
|
|
size="small"
|
|
/>
|
|
) : (
|
|
cellValue
|
|
)}
|
|
</Tooltip>
|
|
</TableCell>
|
|
);
|
|
})}
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
|
|
<Dialog
|
|
open={showUpdateDialog}
|
|
onClose={handleCancelUpdate}
|
|
aria-labelledby="alert-dialog-title"
|
|
aria-describedby="alert-dialog-description"
|
|
>
|
|
<DialogTitle id="alert-dialog-title">Replace Dewars</DialogTitle>
|
|
<DialogContent>
|
|
<DialogContentText id="alert-dialog-description">
|
|
The following dewars already exist: {dewarsToReplace.map(dewar => dewar.dewar_name).join(', ')}. Would you like to replace them?
|
|
</DialogContentText>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={handleCancelUpdate} color="primary">
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleConfirmUpdate} color="primary" autoFocus>
|
|
Replace
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
|
|
<Dialog
|
|
open={errorDialogOpen}
|
|
onClose={() => setErrorDialogOpen(false)}
|
|
aria-labelledby="error-dialog-title"
|
|
aria-describedby="error-dialog-description"
|
|
>
|
|
<DialogTitle>Error Creating/Updating Dewars</DialogTitle>
|
|
<DialogContent>
|
|
<DialogContentText>
|
|
The following errors occurred while processing dewars:
|
|
<ul>
|
|
{errorMessages.map((message, index) => (
|
|
<li key={index}>{message}</li>
|
|
))}
|
|
</ul>
|
|
</DialogContentText>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => setErrorDialogOpen(false)} color="primary">
|
|
Close
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
|
|
</TableContainer>
|
|
|
|
);
|
|
};
|
|
|
|
export default SpreadsheetTable; |