) => {
const files = Array.from(event.target.files || []);
@@ -283,7 +289,7 @@ const SubmissionDetailView: React.FC<{
{uploadAllowed ? (
- 1} accept={detail.allowed_file_types.map(type => `.${type.toLowerCase().replace(/^\./, '')}`).join(',')} onChange={chooseFiles} />
+ 1} accept={accept} onChange={chooseFiles} />
inputRef.current?.click()} className="flex w-full flex-col items-center justify-center rounded-xl border border-dashed border-primary-300 px-4 py-7 text-center text-primary-800 transition-colors hover:bg-primary-100/70 dark:border-primary-800 dark:text-primary-200 dark:hover:bg-primary-950/50">
Dateien auswählen
diff --git a/src/services/api.ts b/src/services/api.ts
index 0b925b03..8162c4b6 100644
--- a/src/services/api.ts
+++ b/src/services/api.ts
@@ -685,7 +685,13 @@ export const coursesAPI = {
const response = await apiClient.post(
'/meinunterricht/submissions/upload',
form,
- { headers: { 'X-Session-Token': token }, signal },
+ {
+ headers: {
+ 'X-Session-Token': token,
+ 'Content-Type': 'multipart/form-data',
+ },
+ signal,
+ },
);
return response.data;
},
@@ -705,7 +711,14 @@ export const coursesAPI = {
form.append('password', password);
const response = await apiClient.delete(
'/meinunterricht/submissions/file',
- { headers: { 'X-Session-Token': token }, data: form, signal },
+ {
+ headers: {
+ 'X-Session-Token': token,
+ 'Content-Type': 'multipart/form-data',
+ },
+ data: form,
+ signal,
+ },
);
return response.data;
},
From 0046d16720eeee73eb4ac409d13961834230806c Mon Sep 17 00:00:00 2001
From: Joan Code <172996447+joan-code6@users.noreply.github.com>
Date: Tue, 22 Sep 2026 23:23:30 +0200
Subject: [PATCH 3/7] fix: close submission UI review gaps
---
src/components/submissions/Submissions.tsx | 19 ++++++++++---------
1 file changed, 10 insertions(+), 9 deletions(-)
diff --git a/src/components/submissions/Submissions.tsx b/src/components/submissions/Submissions.tsx
index e874b2c6..ce295cb6 100644
--- a/src/components/submissions/Submissions.tsx
+++ b/src/components/submissions/Submissions.tsx
@@ -137,10 +137,11 @@ const UploadResults: React.FC<{ statuses: SubmissionUploadStatus[] }> = ({ statu
const SubmissionDetailView: React.FC<{
detail: SubmissionDetail;
+ refreshError: string;
token: string;
onBack: () => void;
onRefresh: () => Promise;
-}> = ({ detail, token, onBack, onRefresh }) => {
+}> = ({ detail, refreshError, token, onBack, onRefresh }) => {
const inputRef = useRef(null);
const [selectedFiles, setSelectedFiles] = useState([]);
const [busy, setBusy] = useState(false);
@@ -153,7 +154,7 @@ const SubmissionDetailView: React.FC<{
const maxBytes = parseMaxBytes(detail.max_file_size);
const attemptClosed = detail.allows_multiple_attempts === false && detail.own_files.length > 0;
const uploadAllowed = detail.status === 'open' && detail.can_upload && !attemptClosed;
- const maxFiles = detail.allows_multiple_files === false ? 1 : 5;
+ const multipleFilesAllowed = detail.allows_multiple_files !== false;
const accept = detail.allowed_file_types.length > 0
&& !detail.allowed_file_types.some(type => type.trim().toLowerCase() === 'alle')
? detail.allowed_file_types
@@ -164,8 +165,8 @@ const SubmissionDetailView: React.FC<{
const chooseFiles = (event: React.ChangeEvent) => {
const files = Array.from(event.target.files || []);
setError('');
- if (files.length > maxFiles) {
- setError(`Für diese Abgabe sind höchstens ${maxFiles} Datei${maxFiles === 1 ? '' : 'en'} erlaubt.`);
+ if (!multipleFilesAllowed && files.length > 1) {
+ setError('Für diese Abgabe ist nur eine Datei erlaubt.');
event.target.value = '';
return;
}
@@ -266,7 +267,7 @@ const SubmissionDetailView: React.FC<{
- {error && {error}
}
+ {(error || refreshError) && {error || refreshError}
}
@@ -289,13 +290,13 @@ const SubmissionDetailView: React.FC<{
{uploadAllowed ? (
-
1} accept={accept} onChange={chooseFiles} />
+
inputRef.current?.click()} className="flex w-full flex-col items-center justify-center rounded-xl border border-dashed border-primary-300 px-4 py-7 text-center text-primary-800 transition-colors hover:bg-primary-100/70 dark:border-primary-800 dark:text-primary-200 dark:hover:bg-primary-950/50">
Dateien auswählen
- {detail.allowed_file_types.join(', ') || 'Erlaubte Dateitypen ansehen'} · maximal {maxFiles} Datei{maxFiles === 1 ? '' : 'en'}
+ {detail.allowed_file_types.join(', ') || 'Erlaubte Dateitypen ansehen'} · {multipleFilesAllowed ? 'Mehrere Dateien möglich' : 'Maximal eine Datei'}
- {selectedFiles.length > 0 &&
{selectedFiles.map(file => setSelectedFiles(current => current.filter(item => item !== file))} className="rounded-lg p-2 text-surface-500 hover:bg-surface-200 dark:hover:bg-surface-800" aria-label={`${file.name} entfernen`}> } />)}
}
+ {selectedFiles.length > 0 &&
{selectedFiles.map(file => { setSelectedFiles(current => current.filter(item => item !== file)); if (inputRef.current) inputRef.current.value = ''; }} className="rounded-lg p-2 text-surface-500 hover:bg-surface-200 dark:hover:bg-surface-800" aria-label={`${file.name} entfernen`}> } />)}
}
void upload()} disabled={!selectedFiles.length || busy} className="btn btn-primary mt-3 w-full disabled:cursor-not-allowed disabled:opacity-50">{busy ? 'Wird hochgeladen …' : 'Hochladen'}
) : (
@@ -379,7 +380,7 @@ const Submissions: React.FC = () => {
return (
{isDetail && detail ? (
-
navigate(listPath)} onRefresh={async () => { if (id) await loadDetail(id); }} />
+ navigate(listPath)} onRefresh={async () => { if (id) await loadDetail(id); }} />
) : !isDetail ? (
Mein Unterricht
Abgaben Alle Upload-Aufträge, Fristen und deine abgegebenen Dateien an einem Ort.
void loadList()} className="btn btn-secondary inline-flex items-center gap-2"> Aktualisieren
From afda29f5d0656d84e6bb4c86c89f282b7a8157a3 Mon Sep 17 00:00:00 2001
From: Joan Code <172996447+joan-code6@users.noreply.github.com>
Date: Tue, 22 Sep 2026 23:44:15 +0200
Subject: [PATCH 4/7] fix: address Abgaben UI follow-up review
---
src/components/demo/mockApi.ts | 13 +++++++++++++
src/components/submissions/Submissions.tsx | 2 +-
2 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/src/components/demo/mockApi.ts b/src/components/demo/mockApi.ts
index 1b4ffefd..a630be2b 100644
--- a/src/components/demo/mockApi.ts
+++ b/src/components/demo/mockApi.ts
@@ -339,6 +339,17 @@ const mockCourseDetails: Record
= {
},
};
+const syncMockCourseSubmissionCount = (submission: MockSubmission) => {
+ const course = mockCourseDetails[submission.course_id];
+ const entry = course?.entries?.find(
+ (item: any) => item.entry_id === submission.entry_id,
+ );
+ const upload = entry?.uploads?.find(
+ (item: any) => (item.detail_ref || item.id) === submission.detail_ref,
+ );
+ if (upload) upload.uploaded_count = submission.uploaded_count;
+};
+
const mockEntryDetails: Record = {
b1e1: { id: 'b1e1', title: 'Gedichtvergleich: Stadt und Natur', content: 'Vergleiche Bildsprache und Stimmung der beiden Gedichte.
Arbeitsauftrag: Belege deine Aussage mit je einer Textstelle und einem Fachbegriff.
', date: daysAgo(1), attachments: [{ name: 'Gedichtvergleich-Leitfaden.pdf', url: '/files/gedichtvergleich-leitfaden.pdf' }] },
b2e1: { id: 'b2e1', title: 'Lineare Funktionen und Steigung', content: 'Lies Steigung und y-Achsenabschnitt aus dem Graphen ab und zeichne die Funktion.
Arbeitsauftrag: Notiere jeden Rechenschritt und prüfe einen Punkt durch Einsetzen.
', date: daysAgo(2), attachments: [{ name: 'Funktionsgraphen-Arbeitsblatt.pdf', url: '/files/funktionsgraphen-arbeitsblatt.pdf' }] },
@@ -766,6 +777,7 @@ export function getMockResponse(url: string, method: string, config: any): { dat
return { name, status: 'erfolgreich', message: null };
});
summary.uploaded_count = files.length;
+ syncMockCourseSubmissionCount(summary);
return { status: 200, data: { success: true, all_succeeded: true, files: statuses } };
}
if (u === '/meinunterricht/submissions/file' && method === 'delete') {
@@ -776,6 +788,7 @@ export function getMockResponse(url: string, method: string, config: any): { dat
const index = files.findIndex(file => file.index === fileIndex);
if (index >= 0) files.splice(index, 1);
summary.uploaded_count = files.length;
+ syncMockCourseSubmissionCount(summary);
return { status: 200, data: { success: true, code: '1', message: 'File deleted successfully' } };
}
if (u.startsWith('/meinunterricht/submissions/') && method === 'get') {
diff --git a/src/components/submissions/Submissions.tsx b/src/components/submissions/Submissions.tsx
index ce295cb6..1fc9251a 100644
--- a/src/components/submissions/Submissions.tsx
+++ b/src/components/submissions/Submissions.tsx
@@ -386,7 +386,7 @@ const Submissions: React.FC = () => {
Mein Unterricht
Abgaben Alle Upload-Aufträge, Fristen und deine abgegebenen Dateien an einem Ort.
void loadList()} className="btn btn-secondary inline-flex items-center gap-2"> Aktualisieren
{error && {error}
}
{!loading && !error && Aufträge
{submissions.length}
Abgegeben
{submissions.filter(item => (item.uploaded_count || 0) > 0).length}
}
- {loading ? {Array.from({ length: 4 }).map((_, index) =>
)}
: submissions.length === 0 ? Keine Abgaben Sobald dir ein Upload-Auftrag zugewiesen wurde, erscheint er hier.
: {submissions.map(submission => openSubmission(submission)} />)}
}
+ {loading ? {Array.from({ length: 4 }).map((_, index) =>
)}
: submissions.length === 0 ? !error && Keine Abgaben Sobald dir ein Upload-Auftrag zugewiesen wurde, erscheint er hier.
: {submissions.map(submission => openSubmission(submission)} />)}
}
) : loading ? (
From d35e0a578f007a622c2179d2722fdbdabd55ea84 Mon Sep 17 00:00:00 2001
From: Joan Code <172996447+joan-code6@users.noreply.github.com>
Date: Wed, 23 Sep 2026 10:36:06 +0200
Subject: [PATCH 5/7] fix: address latest Abgaben UI feedback
---
src/components/submissions/Submissions.tsx | 24 ++++++++++++++++------
1 file changed, 18 insertions(+), 6 deletions(-)
diff --git a/src/components/submissions/Submissions.tsx b/src/components/submissions/Submissions.tsx
index 1fc9251a..3fdeb3bc 100644
--- a/src/components/submissions/Submissions.tsx
+++ b/src/components/submissions/Submissions.tsx
@@ -149,6 +149,7 @@ const SubmissionDetailView: React.FC<{
const [error, setError] = useState('');
const [uploadStatuses, setUploadStatuses] = useState([]);
const [deleteTarget, setDeleteTarget] = useState(null);
+ const [deleteError, setDeleteError] = useState('');
const [password, setPassword] = useState('');
const maxBytes = parseMaxBytes(detail.max_file_size);
@@ -234,17 +235,19 @@ const SubmissionDetailView: React.FC<{
if (!deleteTarget || !password || busy) return;
setBusy(true);
setError('');
+ setDeleteError('');
try {
const response = await coursesAPI.deleteSubmissionFile(token, detail, deleteTarget, password);
if (!response.success) {
- setError(response.message || response.error || 'Die Datei konnte nicht gelöscht werden.');
+ setDeleteError(response.message || response.error || 'Die Datei konnte nicht gelöscht werden.');
} else {
setDeleteTarget(null);
setPassword('');
+ setDeleteError('');
await onRefresh();
}
} catch {
- setError('Die Datei konnte nicht gelöscht werden.');
+ setDeleteError('Die Datei konnte nicht gelöscht werden.');
} finally {
setBusy(false);
}
@@ -316,7 +319,7 @@ const SubmissionDetailView: React.FC<{
- {deleteTarget && }
+ {deleteTarget && Datei löschen? Das Schulportal verlangt dein Passwort, um diese Datei endgültig zu löschen.
{deleteError &&
{deleteError}
}
Passwort { setPassword(event.target.value); setDeleteError(''); }} autoFocus className="input mt-1 w-full" autoComplete="current-password" /> { setDeleteTarget(null); setPassword(''); setDeleteError(''); }}>Abbrechen void removeFile()}>Endgültig löschen
}
);
};
@@ -330,6 +333,7 @@ const Submissions: React.FC = () => {
const [detail, setDetail] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
+ const detailRequestRef = useRef(0);
const listPath = `${basePath}/courses/submissions`;
const openSubmission = (submission: Submission) => navigate(`${listPath}/${encodeURIComponent(submission.detail_ref || submission.id)}`);
@@ -351,16 +355,21 @@ const Submissions: React.FC = () => {
const loadDetail = async (detailRef: string, signal?: AbortSignal) => {
if (!token) return;
+ const requestId = detailRequestRef.current + 1;
+ detailRequestRef.current = requestId;
setLoading(true);
setError('');
try {
const response = await coursesAPI.getSubmission(token, detailRef, signal);
+ if (requestId !== detailRequestRef.current || signal?.aborted) return;
if (response.success && response.submission) setDetail(response.submission);
else setError(response.error || 'Die Abgabe konnte nicht geladen werden.');
} catch (loadError) {
- if (!axios.isCancel(loadError)) setError('Die Abgabe konnte nicht geladen werden.');
+ if (requestId === detailRequestRef.current && !axios.isCancel(loadError)) {
+ setError('Die Abgabe konnte nicht geladen werden.');
+ }
} finally {
- if (!signal?.aborted) setLoading(false);
+ if (requestId === detailRequestRef.current && !signal?.aborted) setLoading(false);
}
};
@@ -368,7 +377,10 @@ const Submissions: React.FC = () => {
const controller = new AbortController();
setDetail(null);
if (id) void loadDetail(id, controller.signal);
- else void loadList(controller.signal);
+ else {
+ detailRequestRef.current += 1;
+ void loadList(controller.signal);
+ }
return () => controller.abort();
}, [token, id]);
From 1bb1836284875d87a36c478e28f5ebc60684d37a Mon Sep 17 00:00:00 2001
From: Joan Code <172996447+joan-code6@users.noreply.github.com>
Date: Thu, 24 Sep 2026 09:48:00 +0200
Subject: [PATCH 6/7] fix: guard submission detail refreshes
---
src/components/submissions/Submissions.tsx | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)
diff --git a/src/components/submissions/Submissions.tsx b/src/components/submissions/Submissions.tsx
index 3fdeb3bc..cf44f040 100644
--- a/src/components/submissions/Submissions.tsx
+++ b/src/components/submissions/Submissions.tsx
@@ -143,6 +143,7 @@ const SubmissionDetailView: React.FC<{
onRefresh: () => Promise;
}> = ({ detail, refreshError, token, onBack, onRefresh }) => {
const inputRef = useRef(null);
+ const mountedRef = useRef(true);
const [selectedFiles, setSelectedFiles] = useState([]);
const [busy, setBusy] = useState(false);
const [downloading, setDownloading] = useState(null);
@@ -152,6 +153,10 @@ const SubmissionDetailView: React.FC<{
const [deleteError, setDeleteError] = useState('');
const [password, setPassword] = useState('');
+ useEffect(() => () => {
+ mountedRef.current = false;
+ }, []);
+
const maxBytes = parseMaxBytes(detail.max_file_size);
const attemptClosed = detail.allows_multiple_attempts === false && detail.own_files.length > 0;
const uploadAllowed = detail.status === 'open' && detail.can_upload && !attemptClosed;
@@ -201,7 +206,7 @@ const SubmissionDetailView: React.FC<{
setUploadStatuses(response.files || []);
setSelectedFiles([]);
if (inputRef.current) inputRef.current.value = '';
- await onRefresh();
+ if (mountedRef.current) await onRefresh();
}
} catch (uploadError) {
if (!axios.isCancel(uploadError)) setError('Die Dateien konnten nicht hochgeladen werden.');
@@ -244,7 +249,7 @@ const SubmissionDetailView: React.FC<{
setDeleteTarget(null);
setPassword('');
setDeleteError('');
- await onRefresh();
+ if (mountedRef.current) await onRefresh();
}
} catch {
setDeleteError('Die Datei konnte nicht gelöscht werden.');
@@ -286,7 +291,7 @@ const SubmissionDetailView: React.FC<{
void download(file.download_ref, file.name)} title="Datei herunterladen"> {detail.can_delete && setDeleteTarget(file.index)} title="Datei löschen"> }}
/>
))}
@@ -334,6 +339,8 @@ const Submissions: React.FC = () => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const detailRequestRef = useRef(0);
+ const currentIdRef = useRef(id);
+ currentIdRef.current = id;
const listPath = `${basePath}/courses/submissions`;
const openSubmission = (submission: Submission) => navigate(`${listPath}/${encodeURIComponent(submission.detail_ref || submission.id)}`);
@@ -392,7 +399,7 @@ const Submissions: React.FC = () => {
return (
{isDetail && detail ? (
-
navigate(listPath)} onRefresh={async () => { if (id) await loadDetail(id); }} />
+ navigate(listPath)} onRefresh={async () => { if (id && currentIdRef.current === id) await loadDetail(id); }} />
) : !isDetail ? (
Mein Unterricht
Abgaben Alle Upload-Aufträge, Fristen und deine abgegebenen Dateien an einem Ort.
void loadList()} className="btn btn-secondary inline-flex items-center gap-2"> Aktualisieren
From ba0c560788f270c6b145a64ff1c410aa20b7efb9 Mon Sep 17 00:00:00 2001
From: Joan Code <172996447+joan-code6@users.noreply.github.com>
Date: Fri, 25 Sep 2026 11:22:46 +0200
Subject: [PATCH 7/7] style: align submissions header with attendance
---
src/components/submissions/Submissions.tsx | 30 +++++++++++++++++++---
1 file changed, 27 insertions(+), 3 deletions(-)
diff --git a/src/components/submissions/Submissions.tsx b/src/components/submissions/Submissions.tsx
index cf44f040..29fe9183 100644
--- a/src/components/submissions/Submissions.tsx
+++ b/src/components/submissions/Submissions.tsx
@@ -397,12 +397,36 @@ const Submissions: React.FC = () => {
if (!token) return
Nicht authentifiziert
;
return (
-
+
{isDetail && detail ? (
navigate(listPath)} onRefresh={async () => { if (id && currentIdRef.current === id) await loadDetail(id); }} />
) : !isDetail ? (
-
-
Mein Unterricht
Abgaben Alle Upload-Aufträge, Fristen und deine abgegebenen Dateien an einem Ort.
void loadList()} className="btn btn-secondary inline-flex items-center gap-2"> Aktualisieren
+
+
+
+
navigate(`${basePath}/courses`)}
+ aria-label="Zurück zu Mein Unterricht"
+ >
+
+
+
+
Abgaben
+
Upload-Aufträge und Fristen
+
+
+ void loadList()}
+ disabled={loading}
+ >
+
+ Aktualisieren
+
+
{error &&
{error}
}
{!loading && !error &&
Aufträge
{submissions.length}
Abgegeben
{submissions.filter(item => (item.uploaded_count || 0) > 0).length}
}
{loading ?
{Array.from({ length: 4 }).map((_, index) =>
)}
: submissions.length === 0 ? !error &&
Keine Abgaben Sobald dir ein Upload-Auftrag zugewiesen wurde, erscheint er hier.
:
{submissions.map(submission => openSubmission(submission)} />)}
}