diff --git a/src/pages/StatementCreation.jsx b/src/pages/StatementCreation.jsx index bf344d9..078f13f 100644 --- a/src/pages/StatementCreation.jsx +++ b/src/pages/StatementCreation.jsx @@ -1,6 +1,6 @@ import { useEffect, useState, useRef, useCallback, useMemo } from 'react'; 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'; // ─── Word map helpers ───────────────────────────────────────────────────────── @@ -34,6 +34,32 @@ function withPlaceholders(text, wordMap, objectAssignments = {}) { 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 ────────────────────────────────────────────────────── function HighlightedTextarea({ value, onChange, wordMap, rows = 3, placeholder, onSelectionChange }) { const taRef = useRef(null); @@ -530,9 +556,306 @@ function PairForm({ objectId, allObjects = [], onPairSaved }) { ); } -// ─── PairsPanel (right 2/5) ─────────────────────────────────────────────────── -function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onPairSaved }) { +// ─── EditPairForm ───────────────────────────────────────────────────────────── +function EditPairForm({ pair, allObjects, onSaved, onCancel }) { 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 ( +
+ {[1,2,3].map(i =>
)} +
+ ); + + return ( +
+ {/* Type */} +
+ + +
+ +
+ {/* Word creation banner */} + {selection && ( +
+ {selection}" + +
+ )} + + {type === 'text' && ( +
+ + +
+ )} + + {type === 'yes_no' && (<> +
+ + +
+
+

Antwort

+
+ {[[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]) => ( + + ))} +
+
+ )} + + {type === 'question' && (<> +
+ + +
+
+ + +
+
+ + +
+ )} + + {type === 'word' && (<> +
+ + +
+
+

Positive Wörter

+
+ {positiveWords.map(w => setPositiveWords(prev => prev.filter(x => x.id !== id))} />)} +
+ w.id)} + onAdd={w => setPositiveWords(prev => prev.some(x => x.id === w.id) ? prev : [...prev, w])} placeholder="Positives Wort suchen…" /> +
+
+

Negative Wörter

+
+ {negativeWords.map(w => setNegativeWords(prev => prev.filter(x => x.id !== id))} />)} +
+ w.id)} + onAdd={w => setNegativeWords(prev => prev.some(x => x.id === w.id) ? prev : [...prev, w])} placeholder="Negatives Wort suchen…" /> +
+ )} + + {/* Detected words with object assignment */} + {Object.keys(wordMap).length > 0 && ( +
+ Erkannte Wörter + {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 ( +
+ {title} + {matchingObjs.length > 0 ? ( + + ) : ( + kein Objekt + )} +
+ ); + })} +
+ )} +
+ + {/* Save / Cancel */} +
+ + +
+
+ ); +} + +// ─── PairsPanel (right 2/5) ─────────────────────────────────────────────────── +function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onPairSaved, onPairsReload }) { + const lang = getUserLang(); + const [editingId, setEditingId] = useState(null); if (!selectedObject) { return ( @@ -558,49 +881,56 @@ function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onP { onPairSaved(pair); }} />
{/* Existing pairs */}
- {loadingPairs && ( -
- {[1,2].map(i =>
)} -
- )} - {!loadingPairs && objectPairs.length === 0 && ( -

Noch keine Pairs

- )} - {objectPairs.map((pair, i) => ( -
-
- #{i + 1} - - {pair.status} - - {pair.answer_type} + {loadingPairs && ( +
+ {[1,2].map(i =>
)}
- {pair.question && ( -

- F: - {pair.question[`sentence_${lang}`] || pair.question.sentence_de || '—'} -

- )} - {pair.positive_statement && ( -

- + - {pair.positive_statement[`positive_sentence_${lang}`] || pair.positive_statement.positive_sentence_de || '—'} -

- )} - {pair.negative_statement && ( -

- - {pair.negative_statement[`negative_sentence_${lang}`] || pair.negative_statement.negative_sentence_de || '—'} -

- )} -
- ))} + )} + {!loadingPairs && objectPairs.length === 0 && ( +

Noch keine Pairs

+ )} + {objectPairs.map((pair, i) => ( +
+ {editingId === pair.id ? ( + { setEditingId(null); onPairsReload(); }} + onCancel={() => setEditingId(null)} + /> + ) : ( +
+
+ #{i + 1} + + {pair.status} + + {pair.answer_type} + +
+ {pair.question_id && ( +

F: {pair.question_id.slice(0,8)}…

+ )} + {pair.positive_statement_id && ( +

+ {pair.positive_statement_id.slice(0,8)}…

+ )} + {pair.negative_statement_id && ( +

− {pair.negative_statement_id.slice(0,8)}…

+ )} +
+ )} +
+ ))}
@@ -816,6 +1146,15 @@ export default function StatementCreation() { 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 (
@@ -870,6 +1209,7 @@ export default function StatementCreation() { objectPairs={objectPairs} loadingPairs={loadingPairs} onPairSaved={handlePairSaved} + onPairsReload={reloadPairs} />