feat: edit existing pairs inline in PairsPanel
- EditPairForm: loads question + statement text (resolves {{uuid}} back
to word titles via resolvePlaceholders), pre-fills all fields
- Per-type editing: text/yes_no/question/word with same UX as create
- Word link diff on save (add new, remove removed via apiUnlink)
- Creates missing question/statement records if type or content added
- ✏️ button per pair card opens EditPairForm, replaces card inline
- onPairsReload: re-fetches pairs list after edit saved
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
||||||
import Layout from '../components/Layout';
|
import Layout from '../components/Layout';
|
||||||
import { apiFetch, apiPost, apiLink, getUserLang, langField } from '../lib/api';
|
import { apiFetch, apiPost, apiPatch, apiLink, apiUnlink, getUserLang, langField } from '../lib/api';
|
||||||
import { STATUS_COLORS } from '../lib/tables';
|
import { STATUS_COLORS } from '../lib/tables';
|
||||||
|
|
||||||
// ─── Word map helpers ─────────────────────────────────────────────────────────
|
// ─── Word map helpers ─────────────────────────────────────────────────────────
|
||||||
@@ -34,6 +34,32 @@ function withPlaceholders(text, wordMap, objectAssignments = {}) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resolve {{uuid}} placeholders back to word titles / object labels for display in edit mode */
|
||||||
|
async function resolvePlaceholders(text, allObjects) {
|
||||||
|
if (!text) return '';
|
||||||
|
const matches = [...text.matchAll(/\{\{([^}]+)\}\}/g)];
|
||||||
|
if (!matches.length) return text;
|
||||||
|
const uuids = [...new Set(matches.map(m => m[1]))];
|
||||||
|
const idMap = {};
|
||||||
|
// Objects first (already in memory)
|
||||||
|
allObjects.forEach(obj => {
|
||||||
|
if (uuids.includes(obj.id)) {
|
||||||
|
const label = (obj._words || []).slice(0, 2).map(w => w.titel_de).filter(Boolean).join('/') || 'Objekt';
|
||||||
|
idMap[obj.id] = label;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Remaining UUIDs → fetch as words
|
||||||
|
await Promise.all(
|
||||||
|
uuids.filter(id => !idMap[id]).map(async id => {
|
||||||
|
try {
|
||||||
|
const w = await apiFetch(`/words/${id}`);
|
||||||
|
if (w) idMap[id] = w.titel_de || w.titel_en || id;
|
||||||
|
} catch { idMap[id] = id; }
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return text.replace(/\{\{([^}]+)\}\}/g, (_, id) => idMap[id] || id);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── HighlightedTextarea ──────────────────────────────────────────────────────
|
// ─── HighlightedTextarea ──────────────────────────────────────────────────────
|
||||||
function HighlightedTextarea({ value, onChange, wordMap, rows = 3, placeholder, onSelectionChange }) {
|
function HighlightedTextarea({ value, onChange, wordMap, rows = 3, placeholder, onSelectionChange }) {
|
||||||
const taRef = useRef(null);
|
const taRef = useRef(null);
|
||||||
@@ -530,9 +556,306 @@ function PairForm({ objectId, allObjects = [], onPairSaved }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── PairsPanel (right 2/5) ───────────────────────────────────────────────────
|
// ─── EditPairForm ─────────────────────────────────────────────────────────────
|
||||||
function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onPairSaved }) {
|
function EditPairForm({ pair, allObjects, onSaved, onCancel }) {
|
||||||
const lang = getUserLang();
|
const lang = getUserLang();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [type, setType] = useState(pair.answer_type || '');
|
||||||
|
const [question, setQuestion] = useState('');
|
||||||
|
const [positive, setPositive] = useState('');
|
||||||
|
const [negative, setNegative] = useState('');
|
||||||
|
const [yesNoAnswer, setYesNoAnswer] = useState(null);
|
||||||
|
const [positiveWords, setPositiveWords] = useState([]);
|
||||||
|
const [negativeWords, setNegativeWords] = useState([]);
|
||||||
|
const [wordMap, setWordMap] = useState({});
|
||||||
|
const [objectAssignments, setObjectAssignments] = useState({});
|
||||||
|
const [selection, setSelection] = useState('');
|
||||||
|
const [creatingWord, setCreatingWord] = useState(false);
|
||||||
|
|
||||||
|
// Load existing question / statement data
|
||||||
|
useEffect(() => {
|
||||||
|
async function load() {
|
||||||
|
try {
|
||||||
|
if (pair.question_id) {
|
||||||
|
const q = await apiFetch(`/questions/${pair.question_id}`);
|
||||||
|
const raw = q[`sentence_${lang}`] || q.sentence_de || '';
|
||||||
|
setQuestion(await resolvePlaceholders(raw, allObjects));
|
||||||
|
}
|
||||||
|
if (pair.positive_statement_id) {
|
||||||
|
const s = await apiFetch(`/statements/${pair.positive_statement_id}`);
|
||||||
|
const raw = s[`positive_sentence_${lang}`] || s.positive_sentence_de || '';
|
||||||
|
if (raw) setPositive(await resolvePlaceholders(raw, allObjects));
|
||||||
|
if (s.answer !== null && s.answer !== undefined) setYesNoAnswer(s.answer);
|
||||||
|
if (s.positive_word_ids?.length) {
|
||||||
|
const words = await Promise.all(s.positive_word_ids.map(id => apiFetch(`/words/${id}`).catch(() => null)));
|
||||||
|
setPositiveWords(words.filter(Boolean));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pair.negative_statement_id) {
|
||||||
|
const s = await apiFetch(`/statements/${pair.negative_statement_id}`);
|
||||||
|
const raw = s[`negative_sentence_${lang}`] || s.negative_sentence_de || '';
|
||||||
|
if (raw) setNegative(await resolvePlaceholders(raw, allObjects));
|
||||||
|
if (s.negative_word_ids?.length) {
|
||||||
|
const words = await Promise.all(s.negative_word_ids.map(id => apiFetch(`/words/${id}`).catch(() => null)));
|
||||||
|
setNegativeWords(words.filter(Boolean));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
finally { setLoading(false); }
|
||||||
|
}
|
||||||
|
load();
|
||||||
|
}, [pair.id]);
|
||||||
|
|
||||||
|
// Word auto-detection
|
||||||
|
const allText = `${question} ${positive} ${negative}`;
|
||||||
|
useEffect(() => {
|
||||||
|
if (!allText.trim()) { setWordMap({}); return; }
|
||||||
|
const t = setTimeout(async () => {
|
||||||
|
const tokens = [...new Set(allText.split(/[\s.,!?;:()\[\]"']+/).filter(w => w.length >= 2))];
|
||||||
|
if (!tokens.length) return;
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
tokens.map(w => apiFetch(`/words?titel_${lang}=${encodeURIComponent(w)}&limit=1`)
|
||||||
|
.then(d => Array.isArray(d) && d.length ? { key: w.toLowerCase(), word: d[0] } : null))
|
||||||
|
);
|
||||||
|
const map = {};
|
||||||
|
results.forEach(r => { if (r.status === 'fulfilled' && r.value) map[r.value.key] = r.value.word; });
|
||||||
|
setWordMap(map);
|
||||||
|
}, 600);
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}, [allText, lang]);
|
||||||
|
|
||||||
|
async function handleCreateWord() {
|
||||||
|
if (!selection.trim()) return;
|
||||||
|
setCreatingWord(true);
|
||||||
|
try {
|
||||||
|
const w = await apiPost('/words', { [`titel_${lang}`]: selection.trim() });
|
||||||
|
setWordMap(prev => ({ ...prev, [selection.trim().toLowerCase()]: w }));
|
||||||
|
} catch (e) { alert('Fehler: ' + e.message); }
|
||||||
|
finally { setCreatingWord(false); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const ph = (t) => withPlaceholders(t, wordMap, objectAssignments);
|
||||||
|
let patchPair = {};
|
||||||
|
|
||||||
|
// Question
|
||||||
|
if (pair.question_id) {
|
||||||
|
if (question.trim()) await apiPatch('/questions', pair.question_id, { [`sentence_${lang}`]: ph(question) });
|
||||||
|
} else if (question.trim() && type !== 'text') {
|
||||||
|
const q = await apiPost('/questions', { [`sentence_${lang}`]: ph(question), status: 'draft' });
|
||||||
|
patchPair.question_id = q.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positive statement
|
||||||
|
if (pair.positive_statement_id) {
|
||||||
|
const posBody = {};
|
||||||
|
if (type === 'text' || type === 'question') posBody[`positive_sentence_${lang}`] = ph(positive);
|
||||||
|
if (type === 'yes_no') posBody.answer = yesNoAnswer;
|
||||||
|
if (Object.keys(posBody).length) await apiPatch('/statements', pair.positive_statement_id, posBody);
|
||||||
|
if (type === 'word') {
|
||||||
|
const cur = await apiFetch(`/statements/${pair.positive_statement_id}`);
|
||||||
|
const curIds = new Set(cur.positive_word_ids || []);
|
||||||
|
const newIds = new Set(positiveWords.map(w => w.id));
|
||||||
|
await Promise.all([...curIds].filter(id => !newIds.has(id)).map(id =>
|
||||||
|
apiUnlink(`/statements/${pair.positive_statement_id}/positive-words/${id}`)
|
||||||
|
));
|
||||||
|
await Promise.all([...newIds].filter(id => !curIds.has(id)).map(id =>
|
||||||
|
apiLink(`/statements/${pair.positive_statement_id}/positive-words/${id}`)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if (type === 'word' && positiveWords.length) {
|
||||||
|
const s = await apiPost('/statements', { status: 'draft' });
|
||||||
|
await Promise.all(positiveWords.map(w => apiLink(`/statements/${s.id}/positive-words/${w.id}`)));
|
||||||
|
patchPair.positive_statement_id = s.id;
|
||||||
|
} else if ((type === 'text' || type === 'question') && positive.trim()) {
|
||||||
|
const posBody = { status: 'draft', [`positive_sentence_${lang}`]: ph(positive) };
|
||||||
|
const s = await apiPost('/statements', posBody);
|
||||||
|
patchPair.positive_statement_id = s.id;
|
||||||
|
} else if (type === 'yes_no') {
|
||||||
|
const s = await apiPost('/statements', { status: 'draft', answer: yesNoAnswer });
|
||||||
|
patchPair.positive_statement_id = s.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negative statement
|
||||||
|
if (pair.negative_statement_id) {
|
||||||
|
if (type === 'question' && negative.trim())
|
||||||
|
await apiPatch('/statements', pair.negative_statement_id, { [`negative_sentence_${lang}`]: ph(negative) });
|
||||||
|
if (type === 'word') {
|
||||||
|
const cur = await apiFetch(`/statements/${pair.negative_statement_id}`);
|
||||||
|
const curIds = new Set(cur.negative_word_ids || []);
|
||||||
|
const newIds = new Set(negativeWords.map(w => w.id));
|
||||||
|
await Promise.all([...curIds].filter(id => !newIds.has(id)).map(id =>
|
||||||
|
apiUnlink(`/statements/${pair.negative_statement_id}/negative-words/${id}`)
|
||||||
|
));
|
||||||
|
await Promise.all([...newIds].filter(id => !curIds.has(id)).map(id =>
|
||||||
|
apiLink(`/statements/${pair.negative_statement_id}/negative-words/${id}`)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if (type === 'question' && negative.trim()) {
|
||||||
|
const s = await apiPost('/statements', { status: 'draft', [`negative_sentence_${lang}`]: ph(negative) });
|
||||||
|
patchPair.negative_statement_id = s.id;
|
||||||
|
} else if (type === 'word' && negativeWords.length) {
|
||||||
|
const s = await apiPost('/statements', { status: 'draft' });
|
||||||
|
await Promise.all(negativeWords.map(w => apiLink(`/statements/${s.id}/negative-words/${w.id}`)));
|
||||||
|
patchPair.negative_statement_id = s.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pair type
|
||||||
|
if (pair.answer_type !== type) patchPair.answer_type = type;
|
||||||
|
if (Object.keys(patchPair).length) await apiPatch('/pairs', pair.id, patchPair);
|
||||||
|
|
||||||
|
onSaved();
|
||||||
|
} catch (e) { alert('Fehler: ' + e.message); }
|
||||||
|
finally { setSaving(false); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) return (
|
||||||
|
<div className="space-y-2 p-1 animate-pulse">
|
||||||
|
{[1,2,3].map(i => <div key={i} className="h-8 bg-amber-100 rounded" />)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-amber-200 rounded-xl p-3 bg-amber-50/20 space-y-3">
|
||||||
|
{/* Type */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">Typ</label>
|
||||||
|
<select value={type} onChange={e => setType(e.target.value)}
|
||||||
|
className="w-full border border-slate-300 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-amber-400 bg-white">
|
||||||
|
{PAIR_TYPES.map(opt => (
|
||||||
|
<option key={opt.value} value={opt.value}>{opt.label} — {opt.hint}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2.5 pt-1 border-t border-amber-100">
|
||||||
|
{/* Word creation banner */}
|
||||||
|
{selection && (
|
||||||
|
<div className="flex items-center gap-2 bg-white border border-amber-200 rounded-lg px-2.5 py-1.5">
|
||||||
|
<span className="text-xs text-slate-600 flex-1 truncate">„<strong className="text-amber-700">{selection}</strong>"</span>
|
||||||
|
<button onClick={handleCreateWord} disabled={creatingWord}
|
||||||
|
className="text-xs bg-amber-600 hover:bg-amber-700 text-white px-2 py-0.5 rounded font-medium disabled:opacity-50 whitespace-nowrap">
|
||||||
|
{creatingWord ? '…' : 'Als Wort erstellen'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === 'text' && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">Statement <span className="text-red-400">*</span></label>
|
||||||
|
<HighlightedTextarea value={positive} onChange={setPositive} wordMap={wordMap} rows={2} placeholder="Das ist ein Hund." onSelectionChange={setSelection} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === 'yes_no' && (<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">Frage <span className="text-slate-400 font-normal normal-case">(optional)</span></label>
|
||||||
|
<HighlightedTextarea value={question} onChange={setQuestion} wordMap={wordMap} rows={2} placeholder="Ist das ein Hund?" onSelectionChange={setSelection} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5">Antwort</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{[[null,'— Offen','bg-slate-100 text-slate-600 border-slate-200'],[true,'✓ Ja','bg-green-50 text-green-700 border-green-300'],[false,'✗ Nein','bg-red-50 text-red-600 border-red-300']].map(([val, label, cls]) => (
|
||||||
|
<button key={String(val)} onClick={() => setYesNoAnswer(val)}
|
||||||
|
className={`flex-1 py-1.5 text-xs font-medium rounded-lg border transition-all ${yesNoAnswer === val ? `${cls} ring-2 ring-offset-1 ring-amber-400` : 'bg-white text-slate-500 border-slate-200 hover:border-slate-300'}`}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>)}
|
||||||
|
|
||||||
|
{type === 'question' && (<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">Frage <span className="text-red-400">*</span></label>
|
||||||
|
<HighlightedTextarea value={question} onChange={setQuestion} wordMap={wordMap} rows={2} placeholder="Was ist das?" onSelectionChange={setSelection} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">Positive Aussage <span className="text-red-400">*</span></label>
|
||||||
|
<HighlightedTextarea value={positive} onChange={setPositive} wordMap={wordMap} rows={2} placeholder="Das ist ein Hund." onSelectionChange={setSelection} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">Negative Aussage <span className="text-slate-400 font-normal normal-case">(optional)</span></label>
|
||||||
|
<HighlightedTextarea value={negative} onChange={setNegative} wordMap={wordMap} rows={2} placeholder="Das ist keine Katze." onSelectionChange={setSelection} />
|
||||||
|
</div>
|
||||||
|
</>)}
|
||||||
|
|
||||||
|
{type === 'word' && (<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">Frage <span className="text-slate-400 font-normal normal-case">(optional)</span></label>
|
||||||
|
<HighlightedTextarea value={question} onChange={setQuestion} wordMap={wordMap} rows={2} placeholder="Was ist das?" onSelectionChange={setSelection} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5">Positive Wörter</p>
|
||||||
|
<div className="flex flex-wrap gap-1 mb-1.5">
|
||||||
|
{positiveWords.map(w => <WordTag key={w.id} word={w} onRemove={id => setPositiveWords(prev => prev.filter(x => x.id !== id))} />)}
|
||||||
|
</div>
|
||||||
|
<WordSearch existingIds={positiveWords.map(w => w.id)}
|
||||||
|
onAdd={w => setPositiveWords(prev => prev.some(x => x.id === w.id) ? prev : [...prev, w])} placeholder="Positives Wort suchen…" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5">Negative Wörter</p>
|
||||||
|
<div className="flex flex-wrap gap-1 mb-1.5">
|
||||||
|
{negativeWords.map(w => <WordTag key={w.id} word={w} onRemove={id => setNegativeWords(prev => prev.filter(x => x.id !== id))} />)}
|
||||||
|
</div>
|
||||||
|
<WordSearch existingIds={negativeWords.map(w => w.id)}
|
||||||
|
onAdd={w => setNegativeWords(prev => prev.some(x => x.id === w.id) ? prev : [...prev, w])} placeholder="Negatives Wort suchen…" />
|
||||||
|
</div>
|
||||||
|
</>)}
|
||||||
|
|
||||||
|
{/* Detected words with object assignment */}
|
||||||
|
{Object.keys(wordMap).length > 0 && (
|
||||||
|
<div className="space-y-1.5 pt-1">
|
||||||
|
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Erkannte Wörter</span>
|
||||||
|
{Object.entries(wordMap).map(([title, w]) => {
|
||||||
|
const matchingObjs = allObjects.filter(o => o._words?.some(ow => ow.id === w.id));
|
||||||
|
const assigned = objectAssignments[w.id] || '';
|
||||||
|
return (
|
||||||
|
<div key={w.id} className="flex items-center gap-2">
|
||||||
|
<span className="text-xs bg-amber-100 text-amber-700 rounded px-2 py-0.5 font-medium shrink-0">{title}</span>
|
||||||
|
{matchingObjs.length > 0 ? (
|
||||||
|
<select value={assigned}
|
||||||
|
onChange={e => setObjectAssignments(prev => ({ ...prev, [w.id]: e.target.value || null }))}
|
||||||
|
className={`flex-1 text-xs border rounded px-1.5 py-0.5 bg-white focus:outline-none focus:ring-1 focus:ring-amber-400 ${assigned ? 'border-amber-400 text-amber-700 bg-amber-50' : 'border-slate-200 text-slate-500'}`}>
|
||||||
|
<option value="">— nur Wort</option>
|
||||||
|
{matchingObjs.map(obj => {
|
||||||
|
const idx = allObjects.indexOf(obj);
|
||||||
|
const labels = (obj._words || []).slice(0, 3).map(ow => ow.titel_de || ow.id).join(', ');
|
||||||
|
return <option key={obj.id} value={obj.id}>Objekt #{idx + 1}{labels ? ` — ${labels}` : ''}</option>;
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-slate-300 italic">kein Objekt</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Save / Cancel */}
|
||||||
|
<div className="flex gap-2 pt-2 border-t border-amber-100">
|
||||||
|
<button onClick={onCancel}
|
||||||
|
className="flex-1 py-1.5 text-xs font-medium rounded-lg border border-slate-300 text-slate-600 hover:bg-slate-50 transition-colors">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button onClick={handleSave} disabled={saving}
|
||||||
|
className="flex-1 py-1.5 text-xs font-medium rounded-lg bg-amber-500 hover:bg-amber-600 disabled:opacity-40 text-white transition-colors">
|
||||||
|
{saving ? 'Speichern…' : '✓ Änderungen speichern'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── PairsPanel (right 2/5) ───────────────────────────────────────────────────
|
||||||
|
function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onPairSaved, onPairsReload }) {
|
||||||
|
const lang = getUserLang();
|
||||||
|
const [editingId, setEditingId] = useState(null);
|
||||||
|
|
||||||
if (!selectedObject) {
|
if (!selectedObject) {
|
||||||
return (
|
return (
|
||||||
@@ -558,7 +881,7 @@ function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onP
|
|||||||
<PairForm
|
<PairForm
|
||||||
objectId={selectedObject.id}
|
objectId={selectedObject.id}
|
||||||
allObjects={allObjects}
|
allObjects={allObjects}
|
||||||
onPairSaved={onPairSaved}
|
onPairSaved={pair => { onPairSaved(pair); }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -573,31 +896,38 @@ function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onP
|
|||||||
<p className="text-xs text-slate-400 text-center mt-4">Noch keine Pairs</p>
|
<p className="text-xs text-slate-400 text-center mt-4">Noch keine Pairs</p>
|
||||||
)}
|
)}
|
||||||
{objectPairs.map((pair, i) => (
|
{objectPairs.map((pair, i) => (
|
||||||
<div key={pair.id} className="rounded-lg border border-slate-200 p-3 bg-slate-50 space-y-1.5">
|
<div key={pair.id}>
|
||||||
|
{editingId === pair.id ? (
|
||||||
|
<EditPairForm
|
||||||
|
pair={pair}
|
||||||
|
allObjects={allObjects}
|
||||||
|
onSaved={() => { setEditingId(null); onPairsReload(); }}
|
||||||
|
onCancel={() => setEditingId(null)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-lg border border-slate-200 p-3 bg-slate-50 space-y-1.5">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs font-bold text-slate-400">#{i + 1}</span>
|
<span className="text-xs font-bold text-slate-400">#{i + 1}</span>
|
||||||
<span className={`text-xs px-1.5 py-0.5 rounded-full font-medium ${STATUS_COLORS[pair.status] || 'bg-slate-100 text-slate-600'}`}>
|
<span className={`text-xs px-1.5 py-0.5 rounded-full font-medium ${STATUS_COLORS[pair.status] || 'bg-slate-100 text-slate-600'}`}>
|
||||||
{pair.status}
|
{pair.status}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs bg-slate-200 text-slate-600 rounded px-1.5 py-0.5">{pair.answer_type}</span>
|
<span className="text-xs bg-slate-200 text-slate-600 rounded px-1.5 py-0.5">{pair.answer_type}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setEditingId(pair.id)}
|
||||||
|
className="ml-auto text-xs text-slate-400 hover:text-indigo-600 px-1.5 py-0.5 rounded hover:bg-indigo-50 transition-colors"
|
||||||
|
title="Bearbeiten"
|
||||||
|
>✏️</button>
|
||||||
</div>
|
</div>
|
||||||
{pair.question && (
|
{pair.question_id && (
|
||||||
<p className="text-xs text-slate-600">
|
<p className="text-xs text-slate-500 font-mono opacity-60 truncate">F: {pair.question_id.slice(0,8)}…</p>
|
||||||
<span className="font-semibold text-slate-400">F: </span>
|
|
||||||
{pair.question[`sentence_${lang}`] || pair.question.sentence_de || '—'}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
{pair.positive_statement && (
|
{pair.positive_statement_id && (
|
||||||
<p className="text-xs text-green-700">
|
<p className="text-xs text-green-600 font-mono opacity-60 truncate">+ {pair.positive_statement_id.slice(0,8)}…</p>
|
||||||
<span className="font-semibold">+ </span>
|
|
||||||
{pair.positive_statement[`positive_sentence_${lang}`] || pair.positive_statement.positive_sentence_de || '—'}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
{pair.negative_statement && (
|
{pair.negative_statement_id && (
|
||||||
<p className="text-xs text-red-600">
|
<p className="text-xs text-red-500 font-mono opacity-60 truncate">− {pair.negative_statement_id.slice(0,8)}…</p>
|
||||||
<span className="font-semibold">− </span>
|
)}
|
||||||
{pair.negative_statement[`negative_sentence_${lang}`] || pair.negative_statement.negative_sentence_de || '—'}
|
</div>
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -816,6 +1146,15 @@ export default function StatementCreation() {
|
|||||||
setObjectPairs(prev => [pair, ...prev]);
|
setObjectPairs(prev => [pair, ...prev]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reloadPairs() {
|
||||||
|
if (!selectedObjectId) return;
|
||||||
|
setLoadingPairs(true);
|
||||||
|
apiFetch(`/objects/${selectedObjectId}/pairs`)
|
||||||
|
.then(data => setObjectPairs(Array.isArray(data) ? data : []))
|
||||||
|
.catch(() => setObjectPairs([]))
|
||||||
|
.finally(() => setLoadingPairs(false));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout back="/content" fullHeight>
|
<Layout back="/content" fullHeight>
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
@@ -870,6 +1209,7 @@ export default function StatementCreation() {
|
|||||||
objectPairs={objectPairs}
|
objectPairs={objectPairs}
|
||||||
loadingPairs={loadingPairs}
|
loadingPairs={loadingPairs}
|
||||||
onPairSaved={handlePairSaved}
|
onPairSaved={handlePairSaved}
|
||||||
|
onPairsReload={reloadPairs}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user