Skip to content
Merged
6 changes: 5 additions & 1 deletion application/frontend/src/pages/MyOpenCRE/MyOpenCRE.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.myopencre-container {
margin-top: 3rem;
}

.myopencre-section {
margin-top: 2rem;
}
Expand All @@ -8,4 +12,4 @@

.myopencre-disabled {
opacity: 0.7;
}
}
76 changes: 66 additions & 10 deletions application/frontend/src/pages/MyOpenCRE/MyOpenCRE.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -13,8 +27,9 @@ export const MyOpenCRE = () => {

const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [error, setError] = useState<ImportErrorResponse | null>(null);
const [success, setSuccess] = useState<any | null>(null);
const [info, setInfo] = useState<string | null>(null);

/* ------------------ CSV DOWNLOAD ------------------ */

Expand Down Expand Up @@ -54,13 +69,18 @@ export const MyOpenCRE = () => {
const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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;
Expand All @@ -77,6 +97,7 @@ export const MyOpenCRE = () => {
setLoading(true);
setError(null);
setSuccess(null);
setInfo(null);

const formData = new FormData();
formData.append('cre_csv', selectedFile);
Expand All @@ -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 (
<Message negative>
<strong>Import failed due to validation errors</strong>
<ul>
{error.errors.map((e, idx) => (
<li key={idx}>
<strong>Row {e.row}:</strong> {e.message}
</li>
))}
</ul>
</Message>
);
}

return <Message negative>{error.message || 'Import failed'}</Message>;
};

/* ------------------ UI ------------------ */

return (
<Container style={{ marginTop: '3rem' }}>
<Container className="myopencre-container">
<Header as="h1">MyOpenCRE</Header>

<p>
Expand All @@ -120,7 +175,7 @@ export const MyOpenCRE = () => {
</p>

<p>
Start by downloading the CRE catalogue below, then map your standards 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.
</p>

Expand All @@ -143,7 +198,8 @@ export const MyOpenCRE = () => {
</Message>
)}

{error && <Message negative>{error}</Message>}
{renderErrorMessage()}
{info && <Message info>{info}</Message>}

{success && (
<Message positive>
Expand Down
4 changes: 2 additions & 2 deletions application/utils/spreadsheet_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
5 changes: 5 additions & 0 deletions application/web/web_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
)

Expand Down