Files
snakkimo-cmt/src/components/PairReviewModal.jsx
admin 2a6d203d1c fix: Negativ-Zeile im Review-Modal immer zeigen (fehlt sichtbar machen)
question/word-Pairs zeigen die Negativ-Zeile jetzt auch wenn leer ('fehlt'
statt stilles Ausblenden), damit eine fehlende Negativ-Antwort auffaellt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 22:14:28 +02:00

192 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { apiPost, apiPatch } from '../lib/api';
const LANGS = [
{ code: 'de', flag: '🇩🇪' },
{ code: 'en', flag: '🇬🇧' },
{ code: 'sv', flag: '🇸🇪' },
];
// Platzhalter {{label.type:uuid}} → nur das Label anzeigen.
function strip(text) {
if (!text) return '';
return text.replace(/\{\{([^}]+)\}\}/g, (_, inner) => {
const m = inner.match(/^(.+?)\.[a-z]+:[0-9a-f-]{36}$/i);
return m ? m[1] : inner;
});
}
// Baut die Anzeigezeilen je nach answer_type. Jede Zeile ist entweder
// - { kind: 'lang', label, color, cell(l) } → eine Zelle pro Sprache (übersetzbar)
// - { kind: 'single', label, color, value } → ein einzelner Wert (nicht sprachabhängig)
function buildRows(content) {
if (!content) return [];
const type = content?.answer_type;
const rows = [];
const wordsCell = (stmt) => (l) =>
(stmt?.words || []).map(w => w[`titel_${l}`] || '—').join(', ');
const sentenceCell = (stmt, prefix) => (l) =>
strip(stmt?.sentence?.[`${prefix}_${l}`] || '');
// Frage (yes_no / question / word)
if (content.question) {
rows.push({ kind: 'lang', label: 'Frage', color: 'text-slate-700',
cell: l => strip(content.question[`sentence_${l}`] || '') });
}
if (type === 'yes_no') {
// Ja/Nein-Antwort ist ein boolescher Wert, keine Übersetzung
const a = content.positive?.answer;
rows.push({ kind: 'single', label: 'Antwort', color: 'text-green-700',
value: a === true ? '✓ Ja' : a === false ? '✗ Nein' : null });
} else if (type === 'word') {
rows.push({ kind: 'lang', label: 'Positiv-Wörter', color: 'text-green-700',
cell: wordsCell(content.positive) });
// 'word' braucht laut Datenmodell Negativ-Wörter → Zeile immer zeigen, fehlende sichtbar machen
rows.push({ kind: 'lang', label: 'Negativ-Wörter', color: 'text-red-600',
cell: wordsCell(content.negative) });
} else {
// text / question → Sätze
if (content.positive)
rows.push({ kind: 'lang', label: 'Positiv', color: 'text-green-700',
cell: sentenceCell(content.positive, 'positive_sentence') });
// 'question' = Frage + Positiv + Negativ → Negativ-Zeile immer zeigen, auch wenn leer
// ('fehlt' statt stilles Ausblenden, damit eine fehlende Negativ-Antwort auffällt).
// 'text' hat per Definition kein Negativ.
if (type === 'question')
rows.push({ kind: 'lang', label: 'Negativ', color: 'text-red-600',
cell: sentenceCell(content.negative, 'negative_sentence') });
}
return rows;
}
export default function PairReviewModal({ pair, content, onClose, onDone, onRetranslate }) {
const [busy, setBusy] = useState(null); // 'review' | 'block' | 'retranslate'
const [missing, setMissing] = useState(null);
const [error, setError] = useState(null);
const rows = buildRows(content);
async function handleRetranslate() {
setBusy('retranslate'); setMissing(null); setError(null);
try {
await onRetranslate();
} catch (e) {
setError(e.message);
} finally { setBusy(null); }
}
async function handleReview() {
setBusy('review'); setMissing(null); setError(null);
try {
await apiPost(`/pairs/${pair.id}/review`, {});
onDone();
} catch (e) {
const m = e.payload?.missing;
if (Array.isArray(m) && m.length) setMissing(m);
else setError(e.message);
} finally { setBusy(null); }
}
async function handleBlock() {
setBusy('block'); setMissing(null); setError(null);
try {
await apiPatch('/pairs', pair.id, { status: 'blocked' });
onDone();
} catch (e) {
setError(e.message);
} finally { setBusy(null); }
}
return (
<div className="fixed inset-0 z-50 flex items-start justify-center bg-black/40 backdrop-blur-sm p-4 overflow-y-auto">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-2xl my-8 flex flex-col">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-200">
<div className="flex items-center gap-2">
<span className="text-xl">🔍</span>
<span className="font-semibold text-slate-800">Pair prüfen</span>
<span className="text-xs bg-slate-200 text-slate-600 rounded px-1.5 py-0.5">{pair.answer_type}</span>
<span className="font-mono text-xs text-slate-400">{pair.id?.slice(0, 8)}</span>
</div>
<button onClick={onClose} className="text-slate-400 hover:text-slate-600 text-2xl leading-none" aria-label="Schließen">×</button>
</div>
{/* Body — Übersetzungen zum Gegenprüfen */}
<div className="px-6 py-5 overflow-y-auto">
<div className="grid gap-3 items-center" style={{ gridTemplateColumns: '7rem repeat(3, 1fr)' }}>
<div />
{LANGS.map(l => (
<div key={l.code} className="text-center text-sm font-medium text-slate-500">{l.flag} {l.code.toUpperCase()}</div>
))}
{rows.map(row => (
<Row key={row.label} row={row} />
))}
</div>
{rows.length === 0 && (
<p className="text-sm text-slate-400 text-center py-6">Kein Inhalt gefunden.</p>
)}
</div>
{/* Fehler / fehlende Übersetzungen */}
{missing && (
<div className="mx-6 mb-2 bg-amber-50 border border-amber-200 text-amber-800 text-sm rounded-xl px-4 py-2">
<b>Reviewed nicht möglich Übersetzung unvollständig.</b> Fehlt: {missing.join(', ')}
</div>
)}
{error && (
<div className="mx-6 mb-2 bg-red-50 border border-red-200 text-red-700 text-sm rounded-xl px-4 py-2">{error}</div>
)}
{/* Footer — Aktionen */}
<div className="flex items-center gap-2 px-6 py-4 border-t border-slate-200">
{onRetranslate && (
<button onClick={handleRetranslate} disabled={!!busy}
className="px-4 py-2 text-sm font-medium rounded-lg bg-indigo-50 text-indigo-700 hover:bg-indigo-100 disabled:opacity-40"
title="Alle Zielsprachen neu übersetzen (überschreibt vorhandene)">
{busy === 'retranslate' ? 'Läuft …' : '🔄 Neu übersetzen'}
</button>
)}
<div className="flex-1" />
<button onClick={onClose} disabled={!!busy}
className="px-4 py-2 text-sm rounded-lg text-slate-500 hover:bg-slate-100 disabled:opacity-40">Abbrechen</button>
<button onClick={handleBlock} disabled={!!busy}
className="px-4 py-2 text-sm font-medium rounded-lg bg-red-100 text-red-700 hover:bg-red-200 disabled:opacity-40">
{busy === 'block' ? 'Läuft …' : '🚫 Blocked'}
</button>
<button onClick={handleReview} disabled={!!busy}
className="px-4 py-2 text-sm font-medium rounded-lg bg-emerald-600 text-white hover:bg-emerald-500 disabled:opacity-40">
{busy === 'review' ? 'Läuft …' : '✓ Reviewed'}
</button>
</div>
</div>
</div>
);
}
function Row({ row }) {
if (row.kind === 'single') {
return (
<>
<div className="text-xs font-semibold text-slate-400">{row.label}</div>
<div className={`col-span-3 text-sm rounded-lg border px-2.5 py-1.5 ${row.value ? `bg-slate-50 border-slate-200 ${row.color}` : 'bg-red-50 border-red-200 text-red-400 italic'}`}>
{row.value || 'fehlt'}
</div>
</>
);
}
return (
<>
<div className="text-xs font-semibold text-slate-400">{row.label}</div>
{LANGS.map(l => {
const val = row.cell(l.code);
return (
<div key={l.code} className={`text-sm rounded-lg border px-2.5 py-1.5 ${val ? `bg-slate-50 border-slate-200 ${row.color}` : 'bg-red-50 border-red-200 text-red-400 italic'}`}>
{val || 'fehlt'}
</div>
);
})}
</>
);
}