feat(dokumente): DOC-001 Duplikat-Erkennung + Fix Online-Ansicht (Popup-Blocker)
Duplikat-Erkennung: SHA-256-Hash je Dokument (Migration 0024), Upload wird mit 409 abgelehnt, wenn dieselbe Datei bereits an derselben Entität liegt (Vergleich bewusst pro Entität, nicht global - gleiche Datei an zwei Objekten ist kein Duplikat). Frontend zeigt die Backend-Fehlermeldung (Dateiname/Datum des bestehenden Dokuments) statt generischem Text. Nebenbei gefunden+gefixt: "Ansehen" (PDF/JPG/PNG online statt Download) funktionierte nicht zuverlässig, weil window.open() erst nach einem await fetch() aufgerufen wurde - Popup-Blocker werten das nicht mehr als direkte Nutzeraktion. Jetzt öffnet der Tab sofort synchron, die Blob-URL wird nachgeladen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -111,17 +111,35 @@ export async function ladeDokumentHerunter(dokumentId: string, dateiname: string
|
||||
// speichern). Auch hier: Auth-Header nötig, daher Blob statt einfachem
|
||||
// <a href> auf den Download-Endpunkt. Object-URL wird bewusst NICHT sofort
|
||||
// revoked (Tab braucht sie noch); der Browser räumt sie beim Tab-Schließen auf.
|
||||
//
|
||||
// window.open() MUSS synchron im Klick-Handler passieren, sonst greift die
|
||||
// Popup-Blocker-Heuristik (window.open nach einem await gilt nicht mehr als
|
||||
// direkte Nutzeraktion) - Tab wird also SOFORT leer geöffnet, die Blob-URL
|
||||
// erst danach nachgeladen. Kein "noopener", weil sonst keine Referenz zum
|
||||
// nachträglichen Setzen von location.href zurückkäme; unkritisch, da das
|
||||
// Ziel eine selbst erzeugte same-origin blob:-URL ist, keine fremde Seite.
|
||||
export async function ladeDokumentAnsehen(dokumentId: string): Promise<void> {
|
||||
const headers: Record<string, string> = {};
|
||||
if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
|
||||
const tab = window.open("", "_blank");
|
||||
try {
|
||||
const headers: Record<string, string> = {};
|
||||
if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
|
||||
|
||||
const response = await fetch(`${BASE_URL}/dokumente/${dokumentId}/download`, { headers });
|
||||
if (!response.ok) {
|
||||
throw new ApiError(response.status, null);
|
||||
const response = await fetch(`${BASE_URL}/dokumente/${dokumentId}/download`, { headers });
|
||||
if (!response.ok) {
|
||||
throw new ApiError(response.status, null);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
if (tab) {
|
||||
tab.location.href = url;
|
||||
} else {
|
||||
// Popup wurde trotzdem blockiert - Fallback auf denselben Tab.
|
||||
window.location.href = url;
|
||||
}
|
||||
} catch (fehler) {
|
||||
tab?.close();
|
||||
throw fehler;
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, "_blank", "noopener");
|
||||
}
|
||||
|
||||
export async function login(username: string, password: string): Promise<string> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { apiRequest, ladeDokumentAnsehen, ladeDokumentHerunter, ladeDokumentHoch } from "../api/client";
|
||||
import { ApiError, apiRequest, ladeDokumentAnsehen, ladeDokumentHerunter, ladeDokumentHoch } from "../api/client";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import type { Dokument } from "../api/types";
|
||||
|
||||
@@ -56,8 +56,13 @@ export function DokumentePanel({ entitaetTyp, entitaetId, onFehler }: Props) {
|
||||
setBeschreibung("");
|
||||
e.target.value = "";
|
||||
await laden();
|
||||
} catch {
|
||||
onFehler("Datei konnte nicht hochgeladen werden (Typ erlaubt: PDF/JPEG/PNG/WebP, max. 25 MB).");
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof ApiError && fehler.status === 409) {
|
||||
const detail = (fehler.detail as { detail?: string } | null)?.detail;
|
||||
onFehler(detail ?? "Diese Datei wurde hier bereits hochgeladen.");
|
||||
} else {
|
||||
onFehler("Datei konnte nicht hochgeladen werden (Typ erlaubt: PDF/JPEG/PNG/WebP, max. 25 MB).");
|
||||
}
|
||||
} finally {
|
||||
setWirdHochgeladen(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user