diff --git a/src/pages/StatementCreation.jsx b/src/pages/StatementCreation.jsx index c5e1189..4ebbf38 100644 --- a/src/pages/StatementCreation.jsx +++ b/src/pages/StatementCreation.jsx @@ -78,31 +78,119 @@ function HighlightedTextarea({ value, onChange, wordMap, rows = 3, placeholder, ); } -// ─── PairForm ───────────────────────────────────────────────────────────────── -function PairForm({ objectId, onPairSaved, onCancel }) { +// ─── WordSearch (for word pickers) ─────────────────────────────────────────── +function WordSearch({ existingIds = [], onAdd, placeholder = 'Wort suchen…' }) { + const lang = getUserLang(); + const [q, setQ] = useState(''); + const [results, setResults] = useState([]); + const [open, setOpen] = useState(false); + const [creating, setCreating] = useState(false); + const ref = useRef(null); + + useEffect(() => { + if (!q.trim()) { setResults([]); return; } + const t = setTimeout(async () => { + try { + const data = await apiFetch(`/words?search=${encodeURIComponent(q.trim())}&limit=10`); + setResults(Array.isArray(data) ? data.filter(w => !existingIds.includes(w.id)) : []); + } catch {} + }, 300); + return () => clearTimeout(t); + }, [q, existingIds]); + + useEffect(() => { + function out(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); } + document.addEventListener('mousedown', out); + return () => document.removeEventListener('mousedown', out); + }, []); + + async function handleCreate() { + setCreating(true); + try { + const w = await apiPost('/words', { [`titel_${lang}`]: q.trim() }); + onAdd(w); setQ(''); setResults([]); setOpen(false); + } catch (e) { alert('Fehler: ' + e.message); } + finally { setCreating(false); } + } + + return ( +
+ { setQ(e.target.value); setOpen(true); }} + onFocus={() => setOpen(true)} + placeholder={placeholder} + 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-indigo-400 bg-white" + /> + {open && q.trim() && ( +
+ {results.map(w => ( + + ))} + {results.length === 0 && ( + + )} +
+ )} +
+ ); +} + +function WordTag({ word, onRemove }) { + return ( + + {word.titel_de || word.id} + + + ); +} + +// ─── PairForm ───────────────────────────────────────────────────────────────── +function PairForm({ objectId, onPairSaved }) { const lang = getUserLang(); - const [answerType, setAnswerType] = useState('word'); const [showForm, setShowForm] = useState(false); + + // Type checkboxes + const [typeText, setTypeText] = useState(false); + const [typeYesNo, setTypeYesNo] = useState(false); + const [typeWord, setTypeWord] = useState(false); + + // Text fields const [question, setQuestion] = useState(''); const [positive, setPositive] = useState(''); const [negative, setNegative] = useState(''); - const [wordMap, setWordMap] = useState({}); // { 'hund': {id, titel_de, ...} } + const [wordMap, setWordMap] = useState({}); const [selection, setSelection] = useState(''); - const [saving, setSaving] = useState(false); const [creatingWord, setCreatingWord] = useState(false); - // Auto-detect words in all three fields combined - const allText = `${question} ${positive} ${negative}`; + // Yes/No answer + const [yesNoAnswer, setYesNoAnswer] = useState(null); // null | true | false + // Word pickers + const [positiveWords, setPositiveWords] = useState([]); + const [negativeWords, setNegativeWords] = useState([]); + + const [saving, setSaving] = useState(false); + + // Auto-detect words from text fields (only when Text type active) + const allText = typeText ? `${question} ${positive} ${negative}` : ''; useEffect(() => { + if (!allText.trim()) { setWordMap({}); return; } const t = setTimeout(async () => { - const words = [...new Set( - allText.split(/[\s.,!?;:()\[\]"']+/).filter(w => w.length >= 2) - )]; - if (!words.length) return; - + const tokens = [...new Set(allText.split(/[\s.,!?;:()\[\]"']+/).filter(w => w.length >= 2))]; + if (!tokens.length) return; const results = await Promise.allSettled( - words.map(w => + 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) ) @@ -118,151 +206,251 @@ function PairForm({ objectId, onPairSaved, onCancel }) { if (!selection.trim()) return; setCreatingWord(true); try { - const body = { [`titel_${lang}`]: selection.trim() }; - const w = await apiPost('/words', body); + 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); } } + // Validation + const atLeastOne = typeText || typeYesNo || typeWord; + const textOk = !typeText || positive.trim().length > 0; + const negativeOk = !negative.trim() || (question.trim().length > 0 && positive.trim().length > 0); + const canSave = atLeastOne && textOk && negativeOk; + + function validationHint() { + if (!atLeastOne) return 'Wähle mindestens einen Typ (Text / Ja·Nein / Wort).'; + if (!textOk) return 'Text: Positive Aussage ist Pflicht.'; + if (!negativeOk) return 'Negative Aussage erfordert Frage + Positive Aussage.'; + return ''; + } + + function reset() { + setTypeText(false); setTypeYesNo(false); setTypeWord(false); + setQuestion(''); setPositive(''); setNegative(''); + setWordMap({}); setYesNoAnswer(null); + setPositiveWords([]); setNegativeWords([]); + setShowForm(false); + } + async function handleSave() { - if (!question.trim() || !positive.trim() || !negative.trim()) { - alert('Bitte alle drei Felder ausfüllen.'); - return; - } + if (!canSave) return; setSaving(true); try { - const qField = `sentence_${lang}`; - const posProcField = `positive_sentence_${lang}`; - const negProcField = `negative_sentence_${lang}`; + const answerTypes = [ + ...(typeText ? ['text'] : []), + ...(typeYesNo ? ['yes_no'] : []), + ...(typeWord ? ['word'] : []), + ]; - // Insert word placeholders - const qProcessed = withPlaceholders(question, wordMap); - const posProcessed = withPlaceholders(positive, wordMap); - const negProcessed = withPlaceholders(negative, wordMap); + // Question (only if Text + question text filled) + let questionId = null; + if (typeText && question.trim()) { + const q = await apiPost('/questions', { + [`sentence_${lang}`]: withPlaceholders(question, wordMap), + status: 'draft', + }); + questionId = q.id; + } - // 1. Create question - const q = await apiPost('/questions', { [qField]: qProcessed, status: 'draft' }); + // Positive statement + const posBody = { status: 'draft' }; + if (typeText && positive.trim()) + posBody[`positive_sentence_${lang}`] = withPlaceholders(positive, wordMap); + if (typeYesNo && yesNoAnswer !== null) + posBody.answer = yesNoAnswer; - // 2. Create positive statement (separate record) - const posStmt = await apiPost('/statements', { [posProcField]: posProcessed, status: 'draft' }); + const posStmt = await apiPost('/statements', posBody); - // 3. Create negative statement (separate record) - const negStmt = await apiPost('/statements', { [negProcField]: negProcessed, status: 'draft' }); + // Link positive words to positive statement + if (typeWord && positiveWords.length) + await Promise.all(positiveWords.map(w => apiLink(`/statements/${posStmt.id}/positive-words/${w.id}`))); - // 4. Create pair + // Negative statement (Text + negative text, OR Word + negativeWords) + let negStmtId = null; + const hasNegText = typeText && negative.trim(); + const hasNegWords = typeWord && negativeWords.length > 0; + + if (hasNegText || hasNegWords) { + const negBody = { status: 'draft' }; + if (hasNegText) + negBody[`negative_sentence_${lang}`] = withPlaceholders(negative, wordMap); + const negStmt = await apiPost('/statements', negBody); + negStmtId = negStmt.id; + if (hasNegWords) + await Promise.all(negativeWords.map(w => apiLink(`/statements/${negStmt.id}/negative-words/${w.id}`))); + } + + // Pair const pair = await apiPost('/pairs', { - answer_type: answerType, - question_id: q.id, + answer_type: answerTypes, + question_id: questionId, positive_statement_id: posStmt.id, - negative_statement_id: negStmt.id, + negative_statement_id: negStmtId, status: 'draft', }); - // 5. Link pair to object await apiLink(`/objects/${objectId}/pairs/${pair.id}`); - // Reset - setQuestion(''); setPositive(''); setNegative(''); - setWordMap({}); setShowForm(false); - onPairSaved({ ...pair, question: q, positive_statement: posStmt, negative_statement: negStmt }); + reset(); + onPairSaved({ ...pair, question: questionId ? { [`sentence_${lang}`]: question } : null, + positive_statement: posStmt, negative_statement: null }); } catch (e) { alert('Fehler: ' + e.message); } finally { setSaving(false); } } return ( -
- {/* Answer type + toggle button */} -
- - -
+
+ {/* Toggle button */} + {showForm && ( -
- {/* "Als Wort erstellen" — floating */} - {selection && ( -
- - Ausgewählt: „{selection}" - - +
+ + {/* ── Type checkboxes ── */} +
+ {[['typeText', typeText, setTypeText, 'Text'], + ['typeYesNo', typeYesNo, setTypeYesNo, 'Ja / Nein'], + ['typeWord', typeWord, setTypeWord, 'Wort'], + ].map(([key, val, setter, label]) => ( + + ))} +
+ + {/* ── Text section ── */} + {typeText && ( +
+ {/* "Als Wort erstellen" banner */} + {selection && ( +
+ + „{selection}" + + +
+ )} + +
+ + +
+ +
+ + +
+ +
+ + + {negative.trim() && !negativeOk && ( +

Frage + Positive Aussage erforderlich.

+ )} +
+ + {Object.keys(wordMap).length > 0 && ( +
+ Erkannte Wörter: + {Object.entries(wordMap).map(([title, w]) => ( + {title} + ))} +
+ )}
)} -
- - -
- -
- - -
- -
- - -
- - {Object.keys(wordMap).length > 0 && ( -
- Erkannte Wörter: - {Object.entries(wordMap).map(([title, w]) => ( - - {title} - - ))} + {/* ── Ja/Nein section ── */} + {typeYesNo && ( +
+

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]) => ( + + ))} +
)} - + {/* ── Wort section ── */} + {typeWord && ( +
+
+

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…" + /> +
+
+ )} + + {/* ── Save ── */} +
+ {!canSave &&

{validationHint()}

} + +
)}