From 2386721e419d384f8a8036d399c0ef20c114a527 Mon Sep 17 00:00:00 2001 From: s4luorth Date: Fri, 18 Sep 2026 08:05:23 +0200 Subject: [PATCH] =?UTF-8?q?Motiv-Freigabe:=20=C3=B6ffentliche=20Seite=20/f?= =?UTF-8?q?reigabe/:token=20(A6-Entwurf=20ansehen=20+=20freigeben)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kunde öffnet den Einmal-Link (kein Login), sieht den A6-Drucklayout-Entwurf und gibt frei oder hinterlegt einen Änderungswunsch. API: fetchFreigabe/sendFreigabe/ freigabeLayoutUrl (öffentlich, token-gated). Co-Authored-By: Claude Opus 4.8 --- src/App.tsx | 3 ++ src/lib/api.ts | 23 +++++++++ src/routes/Freigabe.tsx | 105 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 src/routes/Freigabe.tsx diff --git a/src/App.tsx b/src/App.tsx index 3f25f1c..eee0c13 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import Stammdaten from './routes/Stammdaten'; import Configurator from './routes/Configurator'; import MusterSeite from './routes/Muster'; import AngebotSeite from './routes/Angebot'; +import FreigabeSeite from './routes/Freigabe'; import Textmenge from './routes/Textmenge'; import { loadSession } from './lib/api'; @@ -40,6 +41,8 @@ export default function App() { {/* Muster & individuelles Angebot als eigene Seiten (kein Popup). */} } /> } /> + {/* Motiv-Freigabe über Einmal-Link (kein Login nötig). */} + } /> {/* Textmengen-Tool – öffentlich, Direktlink /textmenge. */} } /> { + return request(`/skrift-orders/freigabe/${encodeURIComponent(token)}`, { auth: false }); +} + +export async function sendFreigabe(token: string, action: 'approve' | 'changes', note?: string) { + return request(`/skrift-orders/freigabe/${encodeURIComponent(token)}`, { method: 'POST', body: { action, note }, auth: false }); +} + +/** Direkte URL zum A6-Entwurfs-SVG (Token-gated, kein Login nötig). */ +export function freigabeLayoutUrl(token: string) { + return `${BASE}/skrift-orders/freigabe/${encodeURIComponent(token)}/layout`; +} + /* ── Statusverlauf & Benachrichtigungen ───────────────────────────────────── */ export type StatusEvent = { diff --git a/src/routes/Freigabe.tsx b/src/routes/Freigabe.tsx new file mode 100644 index 0000000..5469df6 --- /dev/null +++ b/src/routes/Freigabe.tsx @@ -0,0 +1,105 @@ +import { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { ApiError, fetchFreigabe, sendFreigabe, freigabeLayoutUrl, type Freigabe } from '../lib/api'; + +/** + * Öffentliche Motiv-Freigabe (Direktlink mit Einmal-Token, kein Login nötig). + * Zeigt den A6-Entwurf und lässt den Kunden freigeben oder einen Änderungswunsch + * hinterlegen. Erst nach Freigabe geht die Karte in den Druck. + */ +export default function FreigabeSeite() { + const { token = '' } = useParams(); + const [data, setData] = useState(null); + const [status, setStatus] = useState<'laedt' | 'da' | 'fehler'>('laedt'); + const [fehler, setFehler] = useState(''); + const [modus, setModus] = useState<'ansicht' | 'aendern'>('ansicht'); + const [note, setNote] = useState(''); + const [sendet, setSendet] = useState(false); + const [ergebnis, setErgebnis] = useState<'approved' | 'changes' | null>(null); + + useEffect(() => { + fetchFreigabe(token) + .then((d) => { setData(d); setStatus('da'); if (d.status === 'approved' || d.status === 'changes') setErgebnis(d.status); }) + .catch((e) => { setStatus('fehler'); setFehler(e instanceof ApiError ? e.message : 'Freigabe-Link ungültig oder abgelaufen.'); }); + }, [token]); + + async function absenden(action: 'approve' | 'changes') { + if (action === 'changes' && !note.trim()) { setFehler('Bitte beschreiben Sie kurz Ihren Änderungswunsch.'); return; } + setFehler(''); setSendet(true); + try { + await sendFreigabe(token, action, action === 'changes' ? note.trim() : undefined); + setErgebnis(action === 'approve' ? 'approved' : 'changes'); + } catch (e) { + setFehler(e instanceof ApiError ? e.message : 'Konnte nicht gespeichert werden.'); + } finally { setSendet(false); } + } + + if (status === 'laedt') return

Lädt …

; + if (status === 'fehler') return ( +
+

Freigabe

+

{fehler}

+
+ ); + + const d = data!; + const vorschau = ( +
+ {d.has_layout + ? A6-Entwurf + :
Entwurf wird noch erstellt …
} +
+ ); + + if (ergebnis === 'approved') return ( +
+

Vielen Dank!

+

Sie haben den Entwurf zu {d.order_number} freigegeben. Ihre Karte geht jetzt in den Druck.

+ {vorschau} +
+ ); + if (ergebnis === 'changes') return ( +
+

Änderungswunsch erhalten

+

Danke – wir haben Ihren Änderungswunsch zu {d.order_number} notiert und melden uns mit einem neuen Entwurf.

+ {vorschau} +
+ ); + + return ( +
+

Ihr Postkarten-Entwurf

+

Bestellung {d.order_number} · {d.product}. Bitte prüfen Sie den Druck-Entwurf (A6). Erst nach Ihrer Freigabe geht die Karte in den Druck.

+ {vorschau} + + {fehler &&

{fehler}

} + + {modus === 'ansicht' ? ( +
+ + +
+ ) : ( +
+ +