diff --git a/frontend/src/components/ExportDataModal.jsx b/frontend/src/components/ExportDataModal.jsx new file mode 100644 index 0000000..3e0e2c1 --- /dev/null +++ b/frontend/src/components/ExportDataModal.jsx @@ -0,0 +1,108 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { experimentsApi } from '../api/client'; +import Button from './ui/Button'; +import Alert from './ui/Alert'; +import { listExportableParams, buildMatrix, toCSV, csvFilename } from '../lib/dataExport'; + +const GROUP_LABELS = { metrics: 'Session metrics', daily: 'Daily record fields' }; + +function triggerDownload(filename, text) { + const blob = new Blob([text], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +export default function ExportDataModal({ experimentTitle, experimentId, dailyTemplate, animals, onClose }) { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [statuses, setStatuses] = useState([]); + const [selectedId, setSelectedId] = useState(null); + + useEffect(() => { + let alive = true; + setLoading(true); + setError(null); + experimentsApi.getDailyStatuses(experimentId, {}) + .then((data) => { if (alive) setStatuses(data); }) + .catch((err) => { if (alive) setError(err.message); }) + .finally(() => { if (alive) setLoading(false); }); + return () => { alive = false; }; + }, [experimentId]); + + const params = useMemo(() => listExportableParams(dailyTemplate, statuses), [dailyTemplate, statuses]); + + // Default the selection to the first parameter once params are available. + useEffect(() => { + if (selectedId === null && params.length > 0) setSelectedId(params[0].id); + }, [params, selectedId]); + + const selectedParam = params.find((p) => p.id === selectedId) ?? null; + + const matrix = useMemo( + () => (selectedParam ? buildMatrix(statuses, animals, selectedParam) : { columns: [], rows: [] }), + [statuses, animals, selectedParam], + ); + + const hasData = matrix.rows.length > 0; + + // Group params for rendering, preserving order. + const grouped = useMemo(() => { + const out = []; + for (const p of params) { + let g = out.find((x) => x.group === p.group); + if (!g) { g = { group: p.group, items: [] }; out.push(g); } + g.items.push(p); + } + return out; + }, [params]); + + function handleExport() { + if (!hasData || !selectedParam) return; + triggerDownload(csvFilename(experimentTitle, selectedParam.label), toCSV(matrix)); + onClose(); + } + + if (loading) return

Loading data…

; + if (error) return ; + + return ( +
+

+ Export one parameter as a CSV file — each row is a date, each column is a subject. +

+ +
+ + +
+ +

+ {hasData + ? `${matrix.rows.length} date${matrix.rows.length !== 1 ? 's' : ''} × ${matrix.columns.length} subject${matrix.columns.length !== 1 ? 's' : ''}` + : 'No data for this parameter yet.'} +

+ +
+ + +
+
+ ); +}