- {s.modus}
+ {s.modus}
{s.fragenset}
{s.date ? new Date(s.date).toLocaleDateString('de') : ''}
@@ -745,6 +1400,9 @@ function App() {
case 'solo-select': return
;
case 'solo-quiz': return
;
case 'solo-result': return
;
+ case 'wettkampf-select': return
;
+ case 'wettkampf-quiz': return
;
+ case 'wettkampf-result': return
;
case 'my-results': return
;
default: return
;
}
diff --git a/server/baserow.js b/server/baserow.js
index 683bdb1..e47d6a1 100644
--- a/server/baserow.js
+++ b/server/baserow.js
@@ -61,4 +61,12 @@ async function batchCreateRows(tableId, rows) {
}
}
-module.exports = { listRows, createRow, batchCreateRows };
\ No newline at end of file
+async function updateRow(tableId, rowId, data) {
+ console.log(`[baserow] Update row ${rowId} in table ${tableId}`);
+ return baserowFetch(`/database/rows/table/${tableId}/${rowId}/?user_field_names=true`, {
+ method: 'PATCH',
+ body: JSON.stringify(data),
+ });
+}
+
+module.exports = { listRows, createRow, batchCreateRows, updateRow };
\ No newline at end of file
diff --git a/server/index.js b/server/index.js
index acb10a6..b06d867 100644
--- a/server/index.js
+++ b/server/index.js
@@ -2,7 +2,7 @@ const express = require('express');
const http = require('http');
const path = require('path');
const { getEnvConfig, readConfig, writeConfig } = require('./config');
-const { listRows, createRow, batchCreateRows } = require('./baserow');
+const { listRows, createRow, batchCreateRows, updateRow } = require('./baserow');
const { setupWebSocket, getSessions } = require('./live-session');
const { extractFieldValue } = require('./quiz-logic');
@@ -106,19 +106,77 @@ app.get('/api/sets/:id/questions', async (req, res) => {
}
});
-/* ---- Solo: Save single answer immediately ---- */
+app.get('/api/sets/:id/count', async (req, res) => {
+ try {
+ const config = readConfig();
+ const set = config.sets.find((s) => s.id === req.params.id);
+ if (!set) return res.status(404).json({ error: 'Fragenset nicht gefunden' });
+ const rows = await listRows(set.tableId);
+ res.json({ count: rows.length });
+ } catch (e) {
+ res.status(500).json({ error: e.message });
+ }
+});
+
+/* ---- Solo/Wettkampf: Save single answer immediately ---- */
app.post('/api/results/single', async (req, res) => {
try {
const config = readConfig();
if (!config.answersTableId) return res.status(400).json({ error: 'Antworten-Tabelle nicht konfiguriert' });
- await createRow(config.answersTableId, req.body);
- res.json({ success: true });
+ const row = await createRow(config.answersTableId, req.body);
+ res.json({ success: true, id: row.id });
} catch (e) {
console.error('[api] Single save error:', e.message);
res.status(500).json({ error: e.message });
}
});
+/* ---- Wettkampf: Update existing answer ---- */
+app.put('/api/results/update', async (req, res) => {
+ try {
+ const config = readConfig();
+ if (!config.answersTableId) return res.status(400).json({ error: 'Antworten-Tabelle nicht konfiguriert' });
+ const { sessionId, frageId, nutzername, antwort } = req.body;
+ if (!sessionId || !frageId || !nutzername) return res.status(400).json({ error: 'sessionId, frageId und nutzername erforderlich' });
+
+ // Find the existing row
+ const rows = await listRows(config.answersTableId, {
+ Session_ID: sessionId,
+ Nutzername: nutzername,
+ });
+ const existing = rows.find((r) => r['Frage_ID'] === frageId);
+ if (!existing) return res.status(404).json({ error: 'Antwort nicht gefunden' });
+
+ await updateRow(config.answersTableId, existing.id, { Antwort: antwort });
+ res.json({ success: true });
+ } catch (e) {
+ console.error('[api] Update answer error:', e.message);
+ res.status(500).json({ error: e.message });
+ }
+});
+
+/* ---- Wettkampf: Finalize scores ---- */
+app.put('/api/results/finalize', async (req, res) => {
+ try {
+ const config = readConfig();
+ if (!config.answersTableId) return res.status(400).json({ error: 'Antworten-Tabelle nicht konfiguriert' });
+ const { results } = req.body;
+ if (!results || !results.length) return res.status(400).json({ error: 'Keine Ergebnisse' });
+
+ // results is array of { rowId, richtig, punkte }
+ for (const r of results) {
+ await updateRow(config.answersTableId, r.rowId, {
+ Richtig: r.richtig,
+ Punkte: r.punkte,
+ });
+ }
+ res.json({ success: true });
+ } catch (e) {
+ console.error('[api] Finalize error:', e.message);
+ res.status(500).json({ error: e.message });
+ }
+});
+
/* ---- Solo: Check for incomplete session ---- */
app.get('/api/progress/:name/:setId', async (req, res) => {
try {
@@ -187,6 +245,72 @@ app.get('/api/progress/:name/:setId', async (req, res) => {
}
});
+/* ---- Wettkampf: Check for incomplete session ---- */
+app.get('/api/wettkampf-progress/:name/:setId', async (req, res) => {
+ try {
+ const config = readConfig();
+ const set = config.sets.find((s) => s.id === req.params.setId);
+ if (!set) return res.status(404).json({ error: 'Set nicht gefunden' });
+ if (!config.answersTableId) return res.json({ hasProgress: false });
+
+ const allAnswers = await listRows(config.answersTableId, {
+ Nutzername: req.params.name,
+ Fragenset: set.name,
+ });
+
+ // Filter to Wettkampf only
+ const wkAnswers = allAnswers.filter((a) => {
+ return extractFieldValue(a['Modus'], '') === 'Wettkampf';
+ });
+
+ // Group by Session_ID
+ const bySession = {};
+ wkAnswers.forEach((a) => {
+ const sid = a['Session_ID'] || '';
+ if (!bySession[sid]) bySession[sid] = [];
+ bySession[sid].push(a);
+ });
+
+ // Find the most recent session where NOT all answers are finalized (Punkte is still 0 for all)
+ let best = null;
+ for (const [sid, answers] of Object.entries(bySession)) {
+ // A session is "incomplete" if all Punkte are 0 and all Richtig are false (not yet confirmed)
+ const allUnscored = answers.every((a) => {
+ const punkte = Number(a['Punkte']) || 0;
+ const richtig = a['Richtig'];
+ return punkte === 0 && (richtig === false || richtig === 'false' || !richtig);
+ });
+ if (allUnscored && answers.length > 0) {
+ const latest = answers.reduce((max, a) => {
+ const ts = a['Zeitstempel'] || '';
+ return ts > max ? ts : max;
+ }, '');
+ if (!best || latest > best.latest) {
+ best = { sid, answers, latest };
+ }
+ }
+ }
+
+ if (best) {
+ res.json({
+ hasProgress: true,
+ sessionId: best.sid,
+ answers: best.answers.map((a) => ({
+ rowId: a.id,
+ Frage_ID: a['Frage_ID'],
+ Antwort: a['Antwort'] || '',
+ })),
+ totalAnswered: best.answers.length,
+ });
+ } else {
+ res.json({ hasProgress: false });
+ }
+ } catch (e) {
+ console.error('[api] Wettkampf progress error:', e.message);
+ res.status(500).json({ error: e.message });
+ }
+});
+
/* ---- Batch save (still used by live sessions) ---- */
app.post('/api/results', async (req, res) => {
try {