diff --git a/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.scss b/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.scss index ad0ed9258..072c10e8c 100644 --- a/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.scss +++ b/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.scss @@ -1,3 +1,7 @@ +.myopencre-container { + margin-top: 3rem; +} + .myopencre-section { margin-top: 2rem; } @@ -8,4 +12,4 @@ .myopencre-disabled { opacity: 0.7; -} +} \ No newline at end of file diff --git a/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.tsx b/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.tsx index 0523ad028..10da0ac98 100644 --- a/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.tsx +++ b/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.tsx @@ -5,6 +5,20 @@ import { Button, Container, Form, Header, Message } from 'semantic-ui-react'; import { useEnvironment } from '../../hooks'; +type RowValidationError = { + row: number; + code: string; + message: string; + column?: string; +}; + +type ImportErrorResponse = { + success: false; + type: string; + message?: string; + errors?: RowValidationError[]; +}; + export const MyOpenCRE = () => { const { apiUrl } = useEnvironment(); @@ -13,8 +27,9 @@ export const MyOpenCRE = () => { const [selectedFile, setSelectedFile] = useState(null); const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const [success, setSuccess] = useState(null); + const [info, setInfo] = useState(null); /* ------------------ CSV DOWNLOAD ------------------ */ @@ -54,13 +69,18 @@ export const MyOpenCRE = () => { const onFileChange = (e: React.ChangeEvent) => { setError(null); setSuccess(null); + setInfo(null); if (!e.target.files || e.target.files.length === 0) return; const file = e.target.files[0]; if (!file.name.toLowerCase().endsWith('.csv')) { - setError('Please upload a valid CSV file.'); + setError({ + success: false, + type: 'FILE_ERROR', + message: 'Please upload a valid CSV file.', + }); e.target.value = ''; setSelectedFile(null); return; @@ -77,6 +97,7 @@ export const MyOpenCRE = () => { setLoading(true); setError(null); setSuccess(null); + setInfo(null); const formData = new FormData(); formData.append('cre_csv', selectedFile); @@ -93,25 +114,59 @@ export const MyOpenCRE = () => { ); } + const payload = await response.json(); + if (!response.ok) { - const text = await response.text(); - throw new Error(text || 'CSV import failed'); + setError(payload); + return; } - const result = await response.json(); - setSuccess(result); + if (payload.import_type === 'noop') { + setInfo( + 'Import completed successfully, but no new CREs or standards were added because all mappings already exist.' + ); + } else { + setSuccess(payload); + } setSelectedFile(null); } catch (err: any) { - setError(err.message || 'Unexpected error during import'); + setError({ + success: false, + type: 'CLIENT_ERROR', + message: err.message || 'Unexpected error during import', + }); } finally { setLoading(false); } }; + /* ------------------ ERROR RENDERING ------------------ */ + + const renderErrorMessage = () => { + if (!error) return null; + + if (error.errors && error.errors.length > 0) { + return ( + + Import failed due to validation errors +
    + {error.errors.map((e, idx) => ( +
  • + Row {e.row}: {e.message} +
  • + ))} +
+
+ ); + } + + return {error.message || 'Import failed'}; + }; + /* ------------------ UI ------------------ */ return ( - +
MyOpenCRE

@@ -120,7 +175,7 @@ export const MyOpenCRE = () => {

- Start by downloading the CRE catalogue below, then map your standard’s controls or sections to CRE IDs + Start by downloading the CRE catalogue below, then map your standard's controls or sections to CRE IDs in the spreadsheet.

@@ -143,7 +198,8 @@ export const MyOpenCRE = () => { )} - {error && {error}} + {renderErrorMessage()} + {info && {info}} {success && ( diff --git a/application/utils/spreadsheet_parsers.py b/application/utils/spreadsheet_parsers.py index ffe72a64b..4c5397156 100644 --- a/application/utils/spreadsheet_parsers.py +++ b/application/utils/spreadsheet_parsers.py @@ -149,8 +149,8 @@ def validate_import_csv_rows(rows: List[Dict[str, Any]]) -> List[Dict[str, Any]] headers = list(rows[0].keys()) - if not headers: - raise ValueError("CSV header row is missing") + if not headers or len(headers) < 2: + raise ValueError("Invalid CSV format or missing header row") if not any(h.startswith("CRE") for h in headers): raise ValueError("At least one CRE column is required") diff --git a/application/web/web_main.py b/application/web/web_main.py index 6738a0109..3837792d9 100644 --- a/application/web/web_main.py +++ b/application/web/web_main.py @@ -948,11 +948,16 @@ def import_from_cre_csv() -> Any: calculate_gap_analysis=calculate_gap_analysis, ) + import_type = "created" + if not new_cres and not standards: + import_type = "noop" + return jsonify( { "status": "success", "new_cres": [c.external_id for c in new_cres], "new_standards": len(standards), + "import_type": import_type, } )