Added downloading back the excel sheet with corrected values
This commit is contained in:
parent
dbebfd6d5a
commit
744a365bfc
1009
frontend/package-lock.json
generated
1009
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -26,6 +26,8 @@
|
|||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"chokidar": "^4.0.1",
|
"chokidar": "^4.0.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"exceljs": "^4.4.0",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"openapi-typescript-codegen": "^0.29.0",
|
"openapi-typescript-codegen": "^0.29.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-big-calendar": "^1.15.0",
|
"react-big-calendar": "^1.15.0",
|
||||||
|
@ -10,13 +10,17 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
TextField,
|
TextField,
|
||||||
Typography,
|
Typography,
|
||||||
Button
|
Button,
|
||||||
|
Box
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { SpreadsheetService } from '../../openapi';
|
import { SpreadsheetService } from '../../openapi';
|
||||||
|
import * as ExcelJS from 'exceljs';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
|
||||||
const SpreadsheetTable = ({ raw_data, errors, headers, setRawData }) => {
|
const SpreadsheetTable = ({ raw_data, errors, headers, setRawData, onCancel, fileBlob }) => {
|
||||||
const [localErrors, setLocalErrors] = useState(errors || []);
|
const [localErrors, setLocalErrors] = useState(errors || []);
|
||||||
const [editingCell, setEditingCell] = useState({});
|
const [editingCell, setEditingCell] = useState({});
|
||||||
|
const [nonEditableCells, setNonEditableCells] = useState(new Set());
|
||||||
|
|
||||||
const generateErrorMap = (errorsList) => {
|
const generateErrorMap = (errorsList) => {
|
||||||
const errorMap = new Map();
|
const errorMap = new Map();
|
||||||
@ -31,6 +35,21 @@ const SpreadsheetTable = ({ raw_data, errors, headers, setRawData }) => {
|
|||||||
|
|
||||||
const errorMap = generateErrorMap(localErrors);
|
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 handleCellEdit = async (rowIndex, colIndex) => {
|
||||||
const updatedRawData = [...raw_data];
|
const updatedRawData = [...raw_data];
|
||||||
const columnName = headers[colIndex];
|
const columnName = headers[colIndex];
|
||||||
@ -39,14 +58,10 @@ const SpreadsheetTable = ({ raw_data, errors, headers, setRawData }) => {
|
|||||||
|
|
||||||
if (newValue === undefined) return;
|
if (newValue === undefined) return;
|
||||||
|
|
||||||
console.log(`Editing cell at row ${rowIndex}, column ${colIndex}: new value: ${newValue}`);
|
|
||||||
|
|
||||||
// Ensure currentRow.data exists before accessing it
|
|
||||||
if (!currentRow.data) {
|
if (!currentRow.data) {
|
||||||
currentRow.data = [];
|
currentRow.data = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the relevant cell value
|
|
||||||
currentRow.data[colIndex] = newValue;
|
currentRow.data[colIndex] = newValue;
|
||||||
|
|
||||||
setEditingCell(prev => {
|
setEditingCell(prev => {
|
||||||
@ -62,15 +77,13 @@ const SpreadsheetTable = ({ raw_data, errors, headers, setRawData }) => {
|
|||||||
value: newValue
|
value: newValue
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Validation response:', response);
|
|
||||||
|
|
||||||
if (response.is_valid !== undefined) {
|
if (response.is_valid !== undefined) {
|
||||||
if (response.is_valid) {
|
if (response.is_valid) {
|
||||||
// Remove error if it passes validation
|
|
||||||
const updatedErrors = localErrors.filter(
|
const updatedErrors = localErrors.filter(
|
||||||
error => !(error.row === currentRow.row_num && error.cell === colIndex)
|
error => !(error.row === currentRow.row_num && error.cell === colIndex)
|
||||||
);
|
);
|
||||||
setLocalErrors(updatedErrors);
|
setLocalErrors(updatedErrors);
|
||||||
|
setNonEditableCells(prev => new Set([...prev, `${rowIndex}-${colIndex}`]));
|
||||||
} else {
|
} else {
|
||||||
const updatedErrors = [
|
const updatedErrors = [
|
||||||
...localErrors,
|
...localErrors,
|
||||||
@ -78,8 +91,6 @@ const SpreadsheetTable = ({ raw_data, errors, headers, setRawData }) => {
|
|||||||
];
|
];
|
||||||
setLocalErrors(updatedErrors);
|
setLocalErrors(updatedErrors);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.error('Unexpected response structure:', response);
|
|
||||||
}
|
}
|
||||||
setRawData(updatedRawData);
|
setRawData(updatedRawData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -91,39 +102,33 @@ const SpreadsheetTable = ({ raw_data, errors, headers, setRawData }) => {
|
|||||||
handleCellEdit(rowIndex, colIndex);
|
handleCellEdit(rowIndex, colIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateBeforeSubmit = () => {
|
const allCellsValid = () => nonEditableCells.size === raw_data.length * headers.length;
|
||||||
let isValid = true;
|
|
||||||
raw_data.forEach((row, rowIndex) => {
|
|
||||||
headers.forEach((header, colIndex) => {
|
|
||||||
const key = `${row.row_num}-${header}`;
|
|
||||||
const newValue = editingCell[`${rowIndex}-${colIndex}`];
|
|
||||||
|
|
||||||
if (newValue !== undefined) {
|
|
||||||
// Perform cell validation on each edit
|
|
||||||
handleCellEdit(rowIndex, colIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorMessage = errorMap.get(key);
|
|
||||||
if (errorMessage) {
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return isValid;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const isValid = validateBeforeSubmit();
|
if (allCellsValid()) {
|
||||||
|
|
||||||
if (isValid) {
|
|
||||||
console.log('All data is valid. Proceeding with submission...');
|
console.log('All data is valid. Proceeding with submission...');
|
||||||
// Use validated data to populate the database
|
|
||||||
// You can call a dedicated API endpoint to process the entire dataset as needed
|
|
||||||
} else {
|
} else {
|
||||||
console.log('There are validation errors in the dataset. Please correct them before submission.');
|
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(() => {
|
useEffect(() => {
|
||||||
console.log('Raw data:', raw_data);
|
console.log('Raw data:', raw_data);
|
||||||
console.log('Errors:', localErrors);
|
console.log('Errors:', localErrors);
|
||||||
@ -146,6 +151,17 @@ const SpreadsheetTable = ({ raw_data, errors, headers, setRawData }) => {
|
|||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</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()}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={downloadCorrectedSpreadsheet} disabled={!allCellsValid()}>
|
||||||
|
Download Corrected Spreadsheet
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{raw_data.map((row, rowIndex) => (
|
{raw_data.map((row, rowIndex) => (
|
||||||
<TableRow key={rowIndex}>
|
<TableRow key={rowIndex}>
|
||||||
@ -155,6 +171,7 @@ const SpreadsheetTable = ({ raw_data, errors, headers, setRawData }) => {
|
|||||||
const isInvalid = !!errorMessage;
|
const isInvalid = !!errorMessage;
|
||||||
const cellValue = (row.data && row.data[colIndex]) || "";
|
const cellValue = (row.data && row.data[colIndex]) || "";
|
||||||
const editingValue = editingCell[`${rowIndex}-${colIndex}`];
|
const editingValue = editingCell[`${rowIndex}-${colIndex}`];
|
||||||
|
const isReadonly = !isInvalid;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableCell key={colIndex} align="center">
|
<TableCell key={colIndex} align="center">
|
||||||
@ -172,6 +189,7 @@ const SpreadsheetTable = ({ raw_data, errors, headers, setRawData }) => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
|
disabled={isReadonly}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@ -181,9 +199,6 @@ const SpreadsheetTable = ({ raw_data, errors, headers, setRawData }) => {
|
|||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
<Button variant="contained" color="primary" onClick={handleSubmit}>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
@ -7,7 +7,8 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Typography,
|
Typography,
|
||||||
IconButton,
|
IconButton,
|
||||||
Box
|
Box,
|
||||||
|
CircularProgress
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import DownloadIcon from '@mui/icons-material/Download';
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
@ -16,7 +17,8 @@ import logo from '../assets/Heidi-logo.png';
|
|||||||
import { OpenAPI, SpreadsheetService } from '../../openapi';
|
import { OpenAPI, SpreadsheetService } from '../../openapi';
|
||||||
import type { Body_upload_file_upload_post } from '../../openapi/models/Body_upload_file_upload_post';
|
import type { Body_upload_file_upload_post } from '../../openapi/models/Body_upload_file_upload_post';
|
||||||
import SpreadsheetTable from './SpreadsheetTable';
|
import SpreadsheetTable from './SpreadsheetTable';
|
||||||
import Modal from './Modal'; // Import the custom Modal component
|
import Modal from './Modal'; // Ensure correct import paths
|
||||||
|
import * as ExcelJS from 'exceljs';
|
||||||
|
|
||||||
interface UploadDialogProps {
|
interface UploadDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -37,7 +39,10 @@ const UploadDialog: React.FC<UploadDialogProps> = ({ open, onClose }) => {
|
|||||||
samples: string[];
|
samples: string[];
|
||||||
headers: string[]; // Headers must be part of this object
|
headers: string[]; // Headers must be part of this object
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [fileBlob, setFileBlob] = useState<Blob | null>(null); // New state to store the file blob
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
OpenAPI.BASE = 'http://127.0.0.1:8000';
|
OpenAPI.BASE = 'http://127.0.0.1:8000';
|
||||||
@ -49,44 +54,48 @@ const UploadDialog: React.FC<UploadDialogProps> = ({ open, onClose }) => {
|
|||||||
|
|
||||||
setUploadError(null);
|
setUploadError(null);
|
||||||
setFileSummary(null);
|
setFileSummary(null);
|
||||||
|
setFileBlob(file); // Store the file blob
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
if (!file.name.endsWith('.xlsx')) {
|
if (!file.name.endsWith('.xlsx')) {
|
||||||
setUploadError('Invalid file format. Please upload an .xlsx file.');
|
setUploadError('Invalid file format. Please upload an .xlsx file.');
|
||||||
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData: Body_upload_file_upload_post = {
|
const formData: Body_upload_file_upload_post = { file: file } as Body_upload_file_upload_post;
|
||||||
file: file,
|
|
||||||
} as Body_upload_file_upload_post;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await SpreadsheetService.uploadFileUploadPost(formData);
|
const response = await SpreadsheetService.uploadFileUploadPost(formData);
|
||||||
|
|
||||||
// Ensure headers are included in the response
|
|
||||||
const { headers, raw_data, errors } = response;
|
const { headers, raw_data, errors } = response;
|
||||||
setFileSummary({
|
setFileSummary({
|
||||||
data: raw_data,
|
data: raw_data,
|
||||||
errors: errors,
|
errors: errors,
|
||||||
raw_data: raw_data, // keep raw data as is
|
raw_data: raw_data,
|
||||||
headers: headers, // Set headers correctly here
|
headers: headers,
|
||||||
dewars_count: 2, // You might need to adjust according to actual response
|
dewars_count: 2,
|
||||||
dewars: ['Dewar1', 'Dewar2'],
|
dewars: ['Dewar1', 'Dewar2'],
|
||||||
pucks_count: 2, // Adjust accordingly
|
pucks_count: 2,
|
||||||
pucks: ['Puck1', 'Puck2'],
|
pucks: ['Puck1', 'Puck2'],
|
||||||
samples_count: 23, // Adjust accordingly
|
samples_count: 23,
|
||||||
samples: ['Sample1', 'Sample2']
|
samples: ['Sample1', 'Sample2']
|
||||||
});
|
});
|
||||||
setIsModalOpen(true); // Open modal once data is available
|
setIsLoading(false);
|
||||||
|
setIsModalOpen(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setUploadError('Failed to upload file. Please try again.');
|
setUploadError('Failed to upload file. Please try again.');
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRawDataChange = (updatedRawData) => {
|
const handleCancel = () => {
|
||||||
setFileSummary((prevSummary) => ({
|
setIsModalOpen(false);
|
||||||
...prevSummary,
|
setFileSummary(null);
|
||||||
raw_data: updatedRawData
|
|
||||||
}));
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = "";
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -117,29 +126,46 @@ const UploadDialog: React.FC<UploadDialogProps> = ({ open, onClose }) => {
|
|||||||
<Box mt={3}>
|
<Box mt={3}>
|
||||||
<Button variant="contained" component="label" startIcon={<UploadFileIcon />}>
|
<Button variant="contained" component="label" startIcon={<UploadFileIcon />}>
|
||||||
Choose a File
|
Choose a File
|
||||||
<input type="file" hidden onChange={handleFileUpload} />
|
<input type="file" hidden ref={fileInputRef} onChange={handleFileUpload} />
|
||||||
</Button>
|
</Button>
|
||||||
{uploadError && <Typography color="error">{uploadError}</Typography>}
|
{uploadError && <Typography color="error">{uploadError}</Typography>}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<Box display="flex" justifyContent="center" alignItems="center" mt={2}>
|
||||||
|
<CircularProgress />
|
||||||
|
<Typography variant="body2" ml={2}>Processing the file, please wait...</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onClose} color="primary">Close</Button>
|
<Button onClick={onClose} color="primary">Close</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Open modal to display spreadsheet table if file uploaded successfully */}
|
{fileSummary && fileBlob && (
|
||||||
{fileSummary && (
|
|
||||||
<Modal open={isModalOpen} onClose={() => setIsModalOpen(false)} title="File Summary">
|
<Modal open={isModalOpen} onClose={() => setIsModalOpen(false)} title="File Summary">
|
||||||
<Typography color="success.main">File uploaded successfully!</Typography>
|
<Typography color="success.main">File uploaded successfully!</Typography>
|
||||||
<SpreadsheetTable
|
{fileSummary.errors.length > 0 ? (
|
||||||
raw_data={fileSummary.raw_data}
|
<Typography color="error">
|
||||||
errors={fileSummary.errors}
|
The file contains errors that need to be corrected before submission. Please review the highlighted cells below.
|
||||||
headers={fileSummary.headers} // Ensure headers are passed here
|
</Typography>
|
||||||
setRawData={handleRawDataChange}
|
) : (
|
||||||
/>
|
<Typography color="success.main">
|
||||||
|
The file is validated successfully with no errors.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
<Typography>Dewars: {fileSummary.dewars_count}</Typography>
|
<Typography>Dewars: {fileSummary.dewars_count}</Typography>
|
||||||
<Typography>Pucks: {fileSummary.pucks_count}</Typography>
|
<Typography>Pucks: {fileSummary.pucks_count}</Typography>
|
||||||
<Typography>Samples: {fileSummary.samples_count}</Typography>
|
<Typography>Samples: {fileSummary.samples_count}</Typography>
|
||||||
|
<SpreadsheetTable
|
||||||
|
raw_data={fileSummary.raw_data}
|
||||||
|
errors={fileSummary.errors}
|
||||||
|
headers={fileSummary.headers}
|
||||||
|
setRawData={(newRawData) => setFileSummary((prevSummary) => ({ ...prevSummary, raw_data: newRawData }))}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
fileBlob={fileBlob} // Pass the original file blob
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user