aaredb/frontend/src/components/SpreadsheetTable.tsx
2024-11-12 14:00:32 +01:00

353 lines
13 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Tooltip,
TextField,
Typography,
Button,
Box
} 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 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: []
};
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 initialNonEditableCells = new Set();
raw_data.forEach((row, rowIndex) => {
headers.forEach((_, colIndex) => {
const key = `${row.row_num}-${headers[colIndex]}`;
if (!errorMap.has(key)) {
initialNonEditableCells.add(`${rowIndex}-${colIndex}`);
}
});
});
setNonEditableCells(initialNonEditableCells);
}, [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;
if (!currentRow.data) {
currentRow.data = [];
}
currentRow.data[colIndex] = newValue;
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.is_valid !== undefined) {
if (response.is_valid) {
const updatedErrors = localErrors.filter(
(error) => !(error.row === currentRow.row_num && error.cell === colIndex)
);
setLocalErrors(updatedErrors);
setNonEditableCells((prev) => new Set([...prev, `${rowIndex}-${colIndex}`]));
} else {
const updatedErrors = [
...localErrors,
{ row: currentRow.row_num, cell: colIndex, message: response.message || 'Invalid value.' }
];
setLocalErrors(updatedErrors);
}
}
setRawData(updatedRawData);
} catch (error) {
console.error('Validation 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,
// Add other fields as needed
};
const createDewarsFromSheet = 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 dewarNameIdx = fieldToCol['dewarname'];
const puckNameIdx = fieldToCol['puckname'];
const puckTypeIdx = fieldToCol['pucktype'];
let puckPositionInDewar = 1;
for (const row of data) {
if (!row.data) {
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 puckType = typeof row.data[puckTypeIdx] === 'string' ? row.data[puckTypeIdx] : 'Unipuck';
console.log(`Processing Dewar: ${dewarName}, Puck: ${puckName}, Type: ${puckType}`);
if (dewarName) {
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);
puckPositionInDewar = 1;
console.log(`Created new dewar: ${dewarName}`);
} 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);
console.log(`Added puck: ${JSON.stringify(puck)}`);
} else {
console.error('Dewar name is missing in the row');
}
}
const dewarsArray = Array.from(dewars.values());
for (const dewar of dewarsArray) {
try {
// Call to create the dewar
const createdDewar = await DewarsService.createDewarDewarsPost(dewar);
console.log(`Created dewar: ${createdDewar.id}`);
// 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}`);
}
} 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);
}
}
}
return dewarsArray;
};
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...');
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 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;
});
});
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
saveAs(blob, 'corrected_data.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 <div>Loading...</div>;
}
return (
<TableContainer component={Paper}>
<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>
{raw_data.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 cellValue = (row.data && row.data[colIndex]) || "";
const editingValue = editingCell[`${rowIndex}-${colIndex}`];
const isReadonly = !isInvalid;
return (
<TableCell key={colIndex} align="center">
<Tooltip title={errorMessage || ""} arrow disableHoverListener={!isInvalid}>
<TextField
value={editingValue !== undefined ? editingValue : cellValue}
onChange={(e) => 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}
/>
</Tooltip>
</TableCell>
);
})}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
export default SpreadsheetTable;