Jury Results System Documentation

Version: 2.0 | Last Updated: 2026-01-28 | Status: βœ… Fully Implemented


πŸ“‹ Table of Contents

  1. Overview
  2. Concept & Motivation
  3. Database Architecture
  4. Formula System
  5. Automatic Calculation
  6. Data Flow
  7. API Integration
  8. UI Components
  9. Configuration
  10. Troubleshooting

Overview

Jury Results ist ein feldbasiertes Bewertungssystem fΓΌr Turnen-WettkΓ€mpfe (besonders P1-P9 Programme), das: - βœ… Einzelne Bewertungsfelder erfasst (z.B. "Stufe", "Abzug AusfΓΌhrung") - βœ… Formeln verwendet um den finalen Score zu berechnen - βœ… Automatisch fehlende Endwerte berechnet und speichert - βœ… Transparent die Berechnung in der UI anzeigt

Beispiel P-Wettkampf "Boden m. P1-P9":

Stufe:         6.0 Punkte  (= A)
Abzug Ausf.:   2.0 Punkte  (= B)
─────────────────────────────────
Formel:        (10 + A) - B
Endwert:       (10 + 6) - 2 = 14.0 βœ… (automatisch berechnet)


Concept & Motivation

Warum Jury Results?

Problem der alten Implementierung: - Nur ein Wert pro Disziplin gespeichert (tfx_wertungen_details.rel_leistung) - Keine Transparenz ΓΌber wie der Score berechnet wurde - Keine Einzelfeld-Informationen fΓΌr Jury-Mitglieder - Schwierig zu debuggen bei falschen Berechnungen

LΓΆsung mit Jury Results: - Mehrere Felder pro Disziplin mΓΆglich (z.B. Stufe, Abzug, Bonus, Endwert) - Nachvollziehbar: Jedes Feld wird einzeln gespeichert - Flexibel: Unterschiedliche Felder fΓΌr unterschiedliche Disziplinen - Transparent: UI zeigt die Formel und Berechnung - Kompatibel: tfx_wertungen_details.rel_leistung wird weiterhin aktualisiert fΓΌr Legacy-Code

Wann wird Jury Results verwendet?

Aktiviert durch: Configuration > Score Capture > useJuryResults = true (Standard)

Typische Disziplinen: - P-WettkΓ€mpfe (P1-P9): Stufe + Abzug β†’ Endwert - KΓΌr-Programme: D-Note + E-Note β†’ Endwert - Turn10 Programme: Verschiedene Komponenten

WICHTIG: Einstellung darf NICHT wΓ€hrend eines aktiven Wettkampfes geΓ€ndert werden!


Database Architecture

Table Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   tfx_disziplinen       β”‚  Discipline Definition
β”‚  ─────────────────────  β”‚
β”‚  int_disziplinenid (PK) β”‚  ID: 111 = "Boden m. P1-P9"
β”‚  var_name               β”‚  Name: "Boden m. P1-P9"
β”‚  var_formel             β”‚  Formula (direct): "(10 + A) - B" oder NULL
β”‚  int_formelid (FK)      β”‚  Formula Reference: 4 = "P-Wettkampf"
β”‚  int_sportid            β”‚  Sport: 11 = "P-Stufen"
β”‚  ...                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚
        β”‚ 1:N
        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ tfx_disziplinen_felder          β”‚  Field Definitions per Discipline
β”‚ ─────────────────────────────── β”‚
β”‚ int_disziplinen_felderid (PK)   β”‚  ID: 220, 221, 222
β”‚ int_disziplinenid (FK)          β”‚  β†’ 111 (Boden)
β”‚ var_name                        β”‚  "Stufe", "AbzugAusf.", "Endwert"
β”‚ int_sortierung                  β”‚  Sort Order: 1, 2, 3
β”‚ bol_endwert                     β”‚  Is Final Score: false, false, TRUE
β”‚ bol_ausgangswert                β”‚  Is Starting Score: false
β”‚ int_gruppe                      β”‚  Group: 1
β”‚ bol_enabled                     β”‚  Enabled: true
β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚
        β”‚ 1:N
        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ tfx_jury_results                 β”‚  Actual Performance Values
β”‚ ────────────────────────────────│
β”‚ int_juryresultsid (PK)           β”‚  ID: 1, 2, 3
β”‚ int_wertungenid (FK)             β”‚  β†’ tfx_wertungen.int_wertungenid
β”‚ int_disziplinen_felderid (FK)    β”‚  β†’ 220 (Stufe)
β”‚ rel_leistung                     β”‚  Value: 6.0, 2.0, 14.0
β”‚ int_versuch                      β”‚  Attempt: 1
β”‚ int_kp                           β”‚  Control Point: 0
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   tfx_formeln           β”‚  Formula Definitions (Lookup Table)
β”‚  ─────────────────────  β”‚
β”‚  int_formelid (PK)      β”‚  ID: 4
β”‚  var_name               β”‚  Name: "P-Wettkampf"
β”‚  var_formel             β”‚  Formula: "(10 + A) - B"
β”‚  int_typ                β”‚  Type: (unused)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ tfx_wertungen               β”‚  Base Score Record
β”‚ ─────────────────────────── β”‚
β”‚ int_wertungenid (PK)        β”‚  ID: 116 (Luis Bader)
β”‚ int_teilnehmerid (FK)       β”‚  β†’ Participant
β”‚ int_wettkaempfeid (FK)      β”‚  β†’ Competition
β”‚ var_riege                   β”‚  Squad: "zz"
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β”‚ 1:N
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ tfx_wertungen_details       β”‚  Score per Discipline (Legacy Compatible)
β”‚ ─────────────────────────── β”‚
β”‚ int_wertungen_detailsid(PK) β”‚
β”‚ int_wertungenid (FK)        β”‚  β†’ 116
β”‚ int_disziplinenid (FK)      β”‚  β†’ 111 (Boden)
β”‚ rel_leistung                β”‚  Final Score: 14.0 (auto-updated!)
β”‚ int_versuch                 β”‚  Attempt: 1
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Relationships

  1. Discipline β†’ Fields: Eine Disziplin hat mehrere Felder (1:N)
  2. Beispiel: "Boden m. P1-P9" hat "Stufe", "AbzugAusf.", "Endwert"

  3. Field β†’ Jury Results: Ein Feld hat viele Wertungen (1:N)

  4. Beispiel: Feld "Stufe" (220) hat Werte fΓΌr alle Turner

  5. Wertung β†’ Jury Results: Eine Wertung hat mehrere Feldwerte (1:N)

  6. Beispiel: Luis Bader (wertungenId 116) hat 3 Feldwerte

  7. Discipline β†’ Formula: Formel wird ΓΌber int_formelid referenziert ODER direkt in var_formel gespeichert

  8. PrioritΓ€t: tfx_formeln.var_formel (via int_formelid) > tfx_disziplinen.var_formel

Field Types

Jedes Feld hat Flags die seine Funktion definieren:

Flag Bedeutung Beispiel
bol_endwert = true Final Score Field - wird berechnet "Endwert"
bol_ausgangswert = true Starting Score - Basis fΓΌr Berechnung "D-Note"
int_sortierung Sort Order - Reihenfolge in UI 1, 2, 3
bol_enabled = true Enabled - aktiv oder inaktiv true
int_gruppe Group - Gruppierung in UI 1

Formula System

Formula Storage

Formeln kΓΆnnen an zwei Stellen gespeichert werden:

  1. In tfx_formeln Tabelle (bevorzugt):
    SELECT var_formel FROM tfx_formeln WHERE int_formelid = 4;
    -- Result: "(10 + A) - B"
    
  2. VerknΓΌpft ΓΌber tfx_disziplinen.int_formelid = 4
  3. Vorteil: Wiederverwendbar, zentral gepflegt

  4. Direkt in tfx_disziplinen.var_formel:

    SELECT var_formel FROM tfx_disziplinen WHERE int_disziplinenid = 111;
    -- Result: "(10 + A) - B" oder NULL
    

  5. Vorteil: Disziplin-spezifische Formel

Ladestrategie (Server: scores.ts):

// 1. Try loading from tfx_formeln via int_formelid (preferred)
const formulaResult = await prisma.$queryRawUnsafe(`
  SELECT 
    d.var_formel as "disciplineFormula",
    f.var_formel as "tableFormula"
  FROM tfx_disziplinen d
  LEFT JOIN tfx_formeln f ON d.int_formelid = f.int_formelid
  WHERE d.int_disziplinenid = $1
`, disciplineId);

// 2. Prioritize tfx_formeln over discipline direct field
formula = formulaResult[0].tableFormula || formulaResult[0].disciplineFormula;

Formula Syntax

Format: Mathematischer Ausdruck mit Variablen A, B, C, ...

Beispiele:

Disziplin Formel Bedeutung
P-Wettkampf (10 + A) - B 10 + Stufe - Abzug
AK A - B - C Stufe - Abzug1 - Abzug2
LK A + B - C D-Note + E-Note - Abzug
D+E-Neutral 1*x Direkter Wert (kein Jury Result)

Variable Mapping: - Variablen werden in alphabetischer Reihenfolge den nicht-finalen Feldern zugeordnet - Reihenfolge basiert auf int_sortierung der Felder

Beispiel "Boden m. P1-P9":

Felder (sortiert nach int_sortierung):
  1. Stufe (220)        β†’ A
  2. AbzugAusf. (221)   β†’ B
  3. Endwert (222)      β†’ (berechnet, nicht in Formel)

Formel: "(10 + A) - B"
Mapping: A = 6.0, B = 2.0
Berechnung: (10 + 6.0) - 2.0 = 14.0

Starting Value Extraction

StartValue ist die Basis-Punktzahl (meist 10) die aus der Formel extrahiert wird:

// Regex extracts first number (with optional parenthesis)
const startValueMatch = formula.match(/^[(\s]*(\d+\.?\d*)/);
// "(10 + A) - B" β†’ matches "10"
// "A - B" β†’ no match, use default 10.0

const startValue = startValueMatch ? parseFloat(startValueMatch[1]) : 10.0;

Verwendung in UI: - Anzeige in blauem Info-Box: "Ausgehend von 10.0 Punkten" - Hilft dem User die Formel zu verstehen


Automatic Calculation

When is Endwert Calculated?

Trigger: Server erkennt automatisch wenn: 1. βœ… Disziplin hat ein Feld mit bol_endwert = true (Final Score Field) 2. βœ… Es gibt Jury Results fΓΌr nicht-finale Felder (A, B, C, ...) 3. βœ… KEIN Jury Result fΓΌr das Final Score Field existiert

Beispiel:

Jury Results in DB:
  - Stufe (220): 6.0 βœ“
  - AbzugAusf. (221): 2.0 βœ“
  - Endwert (222): βœ— FEHLT!

β†’ Server berechnet automatisch: (10 + 6) - 2 = 14.0
β†’ Speichert in tfx_jury_results fΓΌr Feld 222
β†’ Aktualisiert tfx_wertungen_details.rel_leistung = 14.0

Calculation Logic

Server: newWebBased/server/src/routes/scores.ts (lines ~220-280)

// 1. Detect missing final score
const hasFinalScore = juryResults.some(jr => jr.isFinalScore);
if (!hasFinalScore && formula && endwertFieldId) {
  needsEndwertCalculation = true;
}

// 2. Build value map from non-final jury results
const valueMap: { [key: string]: number } = {};
const letters = formula.match(/[A-Z]/g) || [];

letters.forEach((letter, index) => {
  if (nonFinalScores[index]) {
    valueMap[letter] = parseFloat(nonFinalScores[index].performance);
  }
});
// Result: { A: 6.0, B: 2.0 }

// 3. Replace variables in formula
let evalFormula = formula; // "(10 + A) - B"
Object.keys(valueMap).forEach(letter => {
  evalFormula = evalFormula.replace(new RegExp(letter, 'g'), 
    valueMap[letter].toString());
});
// Result: "(10 + 6.0) - 2.0"

// 4. Evaluate using JavaScript Function constructor
const calculatedScore = new Function(`return ${evalFormula}`)();
// Result: 14.0

// 5. Insert into tfx_jury_results
await prisma.$executeRawUnsafe(`
  INSERT INTO tfx_jury_results 
    (int_wertungenid, int_disziplinen_felderid, rel_leistung, int_versuch, int_kp)
  VALUES ($1, $2, $3, $4, $5)
`, wertungenId, endwertFieldId, calculatedScore, 1, 0);

// 6. Update legacy table for compatibility
await prisma.$executeRawUnsafe(`
  UPDATE tfx_wertungen_details 
  SET rel_leistung = $1
  WHERE int_wertungenid = $2 AND int_disziplinenid = $3
`, calculatedScore, wertungenId, disciplineId);

Safety & Validation

Fehlerbehandlung:

try {
  const calculatedScore = new Function(`return ${evalFormula}`)();

  // Validation
  if (isNaN(calculatedScore) || !isFinite(calculatedScore)) {
    throw new Error('Invalid calculation result');
  }

  console.log('βœ… Calculated final score:', calculatedScore);
} catch (error) {
  console.error('❌ Error calculating final score:', error);
  // Falls back to manual entry or existing value
}

EinschrÀnkungen: - ⚠️ Nur einfache mathematische Operationen (+, -, *, /, ()) - ⚠️ Keine komplexen Funktionen (Math.max, if-else, etc.) - ⚠️ Variablen A-Z müssen in Formel vorhanden sein


Data Flow

Score Capture β†’ Database

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Score Capture UI   β”‚
β”‚  (Client)           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚ POST /api/scores/capture
           β”‚ {
           β”‚   wertungenId: 116,
           β”‚   fieldValues: [
           β”‚     { fieldId: 220, value: 6.0 },
           β”‚     { fieldId: 221, value: 2.0 }
           β”‚   ]
           β”‚ }
           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Server: scores.ts      β”‚
β”‚  (Backend)              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 1. Validate fields      β”‚
β”‚ 2. Insert into          β”‚
β”‚    tfx_jury_results     β”‚
β”‚ 3. Check for missing    β”‚
β”‚    final score          β”‚
β”‚ 4. ✨ AUTO-CALCULATE ✨ β”‚
β”‚ 5. Insert Endwert       β”‚
β”‚ 6. Update               β”‚
β”‚    tfx_wertungen_detailsβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Database               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ tfx_jury_results:       β”‚
β”‚   Row 1: Field 220 = 6  β”‚
β”‚   Row 2: Field 221 = 2  β”‚
β”‚   Row 3: Field 222 = 14 │← Auto-inserted!
β”‚                         β”‚
β”‚ tfx_wertungen_details:  β”‚
β”‚   rel_leistung = 14     │← Auto-updated!
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Database β†’ Results Display

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Database               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚ GET /api/scores?eventId=1&squadName=zz
           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Server: scores.ts                  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 1. Load tfx_wertungen + details     β”‚
β”‚ 2. For each score:                  β”‚
β”‚    a. Load jury results from        β”‚
β”‚       tfx_jury_results              β”‚
β”‚    b. Load formula from             β”‚
β”‚       tfx_formeln or                β”‚
β”‚       tfx_disziplinen               β”‚
β”‚    c. Extract startValue            β”‚
β”‚ 3. Map to camelCase                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚ Response:
           β”‚ {
           β”‚   formula: "(10 + A) - B",
           β”‚   startValue: 10,
           β”‚   score: 14,
           β”‚   juryResults: [
           β”‚     { fieldName: "Stufe", performance: 6 },
           β”‚     { fieldName: "AbzugAusf.", performance: 2 },
           β”‚     { fieldName: "Endwert", performance: 14, isFinalScore: true }
           β”‚   ]
           β”‚ }
           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Results Page UI        β”‚
β”‚  (Client)               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ JuryResultsDisplay:     β”‚
β”‚                         β”‚
β”‚ πŸ“Š 6.0 (A) - 2.0 (B)    β”‚
β”‚ = 14.00                 β”‚
β”‚                         β”‚
β”‚ Formel: (10 + A) - B    β”‚
β”‚ Ausgehend von 10 Punktenβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

API Integration

GET /api/scores

Request:

GET /api/scores?eventId=1&squadName=zz&limit=200

Response (relevant fields):

{
  "results": [
    {
      "id": 116,
      "participantId": 100,
      "disciplineId": 111,
      "score": 14.0,
      "formula": "(10 + A) - B",
      "startValue": 10,
      "juryResults": [
        {
          "id": 1,
          "disciplineFieldId": 220,
          "fieldName": "Stufe",
          "performance": 6.0,
          "isFinalScore": false,
          "isStartingScore": false,
          "sortOrder": 1
        },
        {
          "id": 2,
          "disciplineFieldId": 221,
          "fieldName": "AbzugAusf.",
          "performance": 2.0,
          "isFinalScore": false,
          "isStartingScore": false,
          "sortOrder": 2
        },
        {
          "id": 3,
          "disciplineFieldId": 222,
          "fieldName": "Endwert",
          "performance": 14.0,
          "isFinalScore": true,
          "isStartingScore": false,
          "sortOrder": 3
        }
      ]
    }
  ]
}

Field Mapping (Database β†’ API): | Database Field | API Field | Type | |----------------|-----------|------| | d.var_formel / f.var_formel | formula | string | | (extracted from formula) | startValue | number | | jr.rel_leistung | juryResults[].performance | number | | df.var_name | juryResults[].fieldName | string | | df.bol_endwert | juryResults[].isFinalScore | boolean | | df.int_sortierung | juryResults[].sortOrder | number |

POST /api/scores/capture

Request:

{
  "wertungenId": 116,
  "fieldValues": [
    { "fieldId": 220, "value": 6.0 },
    { "fieldId": 221, "value": 2.0 }
  ]
}

Logic: 1. Validate all fieldIds exist in tfx_disziplinen_felder 2. Insert/Update tfx_jury_results for each fieldId 3. Check if final score field exists but has no value 4. If yes: Auto-calculate using formula 5. Update tfx_wertungen_details.rel_leistung with final score

Response:

{
  "success": true,
  "calculatedScore": 14.0,
  "message": "Score saved and final score calculated"
}


UI Components

1. JuryResultsDisplay Component

Location: client/src/pages/Results/components/JuryResultsDisplay.tsx

Purpose: Zeigt Jury Results mit Formel-Berechnung

Features: - πŸ“Š Anzeige aller nicht-finalen Felder mit Werten - πŸ”’ Formel-Darstellung mit dynamischen Symbolen (A, B, C) - βœ… Finaler Score hervorgehoben - πŸ“˜ Blau-Box mit StartValue Hinweis - 🎯 Kompakt 2-zeilig fΓΌr Table View

Example Output:

6.0 (A) - 2.0 (B) = 14.00
Formel: (10 + A) - B

[ℹ️ Ausgehend von 10.0 Punkten]

Props Interface:

interface JuryResultsDisplayProps {
  juryResults: JuryResult[];     // All field values
  finalScore: number;            // Total score
  formula?: string;              // Formula like "(10 + A) - B"
  startValue?: number;           // Starting value (e.g., 10)
  isCompact?: boolean;           // Compact mode for tables
}

Dynamic Symbol Extraction:

const getFormulaSymbol = (index: number): string => {
  if (!formula) return String.fromCharCode(65 + index); // A, B, C

  // Extract letters from formula in order
  const letterMatches = formula.match(/[A-Z]/g);
  return letterMatches?.[index] || String.fromCharCode(65 + index);
};

2. Results Page Integration

Location: client/src/pages/Results.tsx

Conditional Rendering:

{score.juryResults && score.juryResults.length > 0 ? (
  <JuryResultsDisplay
    juryResults={score.juryResults}
    finalScore={score.score}
    formula={score.formula}
    startValue={score.startValue}
    isCompact={true}
  />
) : (
  <span className="text-lg font-semibold">
    {score.score.toFixed(2)}
  </span>
)}

3. Score Capture Page

Location: client/src/pages/ScoreCapture.tsx

Features: - Multi-field input fΓΌr jedes definierte Feld - Automatische Feld-Liste basierend auf tfx_disziplinen_felder - Real-time Validierung - Automatische Berechnung beim Speichern


Configuration

Central Setting

Location: Configuration Page β†’ Score Capture Section

Setting:

{
  key: 'useJuryResults',
  label: 'Jury-Results verwenden',
  description: 'Feldspezifische Jury-Results in Wertungserfassung, Ergebnisanzeige, Live-Results und PDF-Export verwenden',
  type: 'boolean',
  value: true  // DEFAULT: ENABLED
}

⚠️ Warning Box:

WICHTIG: Datenbankstruktur

Diese Einstellung darf NICHT wΓ€hrend eines aktiven Wettkampfes geΓ€ndert werden!

β€’ Jury-Results verwenden unterschiedliche Datenbanktabellen
β€’ Γ„nderungen wΓ€hrend laufendem Wettkampf fΓΌhren zu Dateninkonsistenz
β€’ Standard: Aktiviert (empfohlen)

Storage

Settings werden gespeichert in: - Client: localStorage β†’ appSettings.scoreCapture.useJuryResults - Server: Nutzt nur Client-Config (keine Server-side Einstellung nΓΆtig)

Migration Path

Von altem System (nur tfx_wertungen_details) zu Jury Results:

  1. βœ… useJuryResults = true setzen
  2. βœ… Felder in tfx_disziplinen_felder definieren
  3. βœ… Formel in tfx_formeln oder tfx_disziplinen.var_formel hinterlegen
  4. βœ… Beim nΓ€chsten Score-Eintrag: Automatisch Jury Results erstellt
  5. βœ… tfx_wertungen_details.rel_leistung wird parallel aktualisiert (KompatibilitΓ€t!)

Zurück zu altem System: ⚠️ NUR zwischen WettkÀmpfen! 1. useJuryResults = false setzen 2. System verwendet wieder tfx_wertungen_details.rel_leistung 3. Jury Results bleiben in DB (werden nicht gelâscht)


Troubleshooting

Problem: "Formula not loading"

Symptom: formula: null in API response

Checks: 1. βœ… Hat Disziplin int_formelid gesetzt?

SELECT int_formelid FROM tfx_disziplinen WHERE int_disziplinenid = 111;

  1. βœ… Existiert Formel in tfx_formeln?

    SELECT var_formel FROM tfx_formeln WHERE int_formelid = 4;
    

  2. βœ… Oder ist Formel direkt in Disziplin?

    SELECT var_formel FROM tfx_disziplinen WHERE int_disziplinenid = 111;
    

  3. βœ… Server-Logs prΓΌfen:

    pm2 logs turnfix-server | grep "Formula query"
    

Solution: Siehe Formula System fΓΌr Details zur Formel-Speicherung


Problem: "Endwert not calculated automatically"

Symptom: Nur Stufe + Abzug gespeichert, kein Endwert

Checks: 1. βœ… Existiert Feld mit bol_endwert = true?

SELECT * FROM tfx_disziplinen_felder 
WHERE int_disziplinenid = 111 AND bol_endwert = true;

  1. βœ… Ist Formel geladen? (siehe oben)

  2. βœ… Sind alle nicht-finalen Felder befΓΌllt?

    SELECT * FROM tfx_jury_results 
    WHERE int_wertungenid = 116;
    

  3. βœ… Server-Logs prΓΌfen:

    pm2 logs turnfix-server | grep "Calculating final score"
    

Solution: - Falls Feld fehlt: In tfx_disziplinen_felder anlegen mit bol_endwert = true - Falls Formel fehlt: Siehe Formula System - Falls Felder fehlen: Score Capture nochmal aufrufen


Problem: "DATABASE_URL not found (PM2)"

Symptom:

PrismaClientInitializationError:
error: Error validating datasource `db`: 
the URL must start with the protocol `postgresql://`

Cause: PM2 liest .env nicht automatisch

Solution: DATABASE_URL in ecosystem.config.js eintragen:

{
  name: 'turnfix-server',
  env: {
    DATABASE_URL: 'postgresql://user:pass@localhost:5432/dbname',
    PORT: 3001,
    DEBUG: 'true'
  }
}


Problem: "Formula in response but not displayed in UI"

Symptom: API gibt formula: "(10 + A) - B" zurΓΌck, aber UI zeigt nichts

Checks: 1. βœ… Client-State aktualisiert? - Hard-Refresh: Ctrl+Shift+R - Check Network Tab: Formula in response?

  1. βœ… Component rendering conditional?

    {score.formula && (
      <div className="text-xs text-gray-600">
        Formel: {score.formula}
      </div>
    )}
    

  2. βœ… TypeScript types erweitert?

    interface Score {
      formula?: string;
      startValue?: number;
      juryResults: JuryResult[];
    }
    


Problem: "Wrong final score calculated"

Symptom: Endwert = 13.3 statt 14.0

Possible Causes: 1. Alte Daten: Endwert wurde VOR Auto-Calculation gespeichert - Solution: Endwert-Eintrag lΓΆschen, GET Request β†’ Auto-Recalculation

  1. Falsche Formel: Disziplin verwendet falsche Formel
  2. Check: SELECT var_formel FROM tfx_formeln WHERE int_formelid = ...
  3. Solution: Formel korrigieren oder richtige Formel zuweisen

  4. Falsche Werte: Eingabe-Felder haben falsche Werte

  5. Check: SELECT * FROM tfx_jury_results WHERE int_wertungenid = 116
  6. Solution: Werte korrigieren in Score Capture

  7. Variable Mapping falsch: Reihenfolge der Felder stimmt nicht

  8. Check: SELECT * FROM tfx_disziplinen_felder WHERE int_disziplinenid = 111 ORDER BY int_sortierung
  9. Solution: int_sortierung korrigieren

Development Notes

Server Implementation Details

Key Files: - server/src/routes/scores.ts - Main GET /api/scores endpoint with auto-calculation - server/src/routes/disciplineFields.ts - Field definitions API - server/src/routes/formulas.ts - Formula lookup API

Important Functions:

// Formula loading (lines ~180-210)
if (disciplineId) {
  const formulaResult = await prisma.$queryRawUnsafe(`
    SELECT 
      d.var_formel as "disciplineFormula",
      f.var_formel as "tableFormula"
    FROM tfx_disziplinen d
    LEFT JOIN tfx_formeln f ON d.int_formelid = f.int_formelid
    WHERE d.int_disziplinenid = $1
  `, disciplineId);

  formula = formulaResult[0].tableFormula || formulaResult[0].disciplineFormula;
}

// Auto-calculation (lines ~220-280)
if (needsEndwertCalculation && formula && endwertFieldId) {
  const valueMap = buildValueMap(juryResults, formula);
  const evalFormula = replaceVariables(formula, valueMap);
  const calculatedScore = evaluate(evalFormula);

  await insertJuryResult(wertungenId, endwertFieldId, calculatedScore);
  await updateWertungenDetails(wertungenId, disciplineId, calculatedScore);
}

Client Implementation Details

Key Files: - client/src/pages/Results/components/JuryResultsDisplay.tsx - Display component - client/src/pages/Results.tsx - Integration in results table - client/src/pages/Configuration.tsx - Central toggle

State Management:

// Results page loads scores with jury results
const { data: scores } = useQuery(['/api/scores', filters], {
  queryKey: ['/api/scores', eventId, squadName],
  queryFn: () => fetchScores(eventId, squadName)
});

// Each score has juryResults array
scores.results.forEach(score => {
  if (score.juryResults && score.juryResults.length > 0) {
    // Display with JuryResultsDisplay component
  }
});


Future Enhancements

Planned Features

  1. Live Score Calculation Preview
  2. Show calculated final score in real-time wΓ€hrend Score Capture
  3. Validation feedback bevor Speichern

  4. PDF Export with Jury Results

  5. Extended result sheets mit allen Feld-Werten
  6. Formula display in Urkunden

  7. Complex Formula Support

  8. Support fΓΌr Math.max(), Math.min()
  9. Conditional logic: if (A > 10) then X else Y
  10. Bonus/Penalty calculations

  11. Jury Member Assignment

  12. Tracking welcher Juror welches Feld bewertet hat
  13. Multi-jury averaging fΓΌr fair scoring

  14. Historical Tracking

  15. Γ„nderungshistorie fΓΌr Jury Results
  16. Audit trail: Wer hat wann welchen Wert geΓ€ndert

Known Limitations

  1. Formula Syntax: Nur einfache math (+ - * / ())
  2. Variable Names: Nur A-Z (26 Felder maximum)
  3. No Field Dependencies: Felder kΓΆnnen sich nicht gegenseitig referenzieren
  4. Single Attempt: Derzeit nur 1 Versuch pro Feld (int_versuch = 1)

Appendix: Example Data

Example 1: Luis Bader (Boden m. P1-P9)

-- Discipline
SELECT * FROM tfx_disziplinen WHERE int_disziplinenid = 111;
-- Result: "Boden m. P1-P9", int_formelid = 4

-- Formula
SELECT * FROM tfx_formeln WHERE int_formelid = 4;
-- Result: "P-Wettkampf", "(10 + A) - B"

-- Fields
SELECT * FROM tfx_disziplinen_felder WHERE int_disziplinenid = 111;
-- Results:
--   220: "Stufe", sortierung=1, endwert=false
--   221: "AbzugAusf.", sortierung=2, endwert=false
--   222: "Endwert", sortierung=3, endwert=TRUE

-- Jury Results
SELECT * FROM tfx_jury_results WHERE int_wertungenid = 116;
-- Results:
--   Field 220: 6.0
--   Field 221: 2.0
--   Field 222: 14.0 ← Auto-calculated!

-- Wertungen Details (Legacy)
SELECT * FROM tfx_wertungen_details 
WHERE int_wertungenid = 116 AND int_disziplinenid = 111;
-- Result: rel_leistung = 14.0 ← Auto-updated!

Example 2: API Response Structure

{
  "results": [
    {
      "id": 116,
      "participantId": 100,
      "disciplineId": 111,
      "competitionId": 14,
      "score": 14.0,
      "formula": "(10 + A) - B",
      "startValue": 10,
      "participant": {
        "firstName": "Luis",
        "lastName": "Bader"
      },
      "discipline": {
        "name": "Boden m. P1-P9"
      },
      "juryResults": [
        {
          "id": 1,
          "disciplineFieldId": 220,
          "fieldName": "Stufe",
          "performance": 6.0,
          "isFinalScore": false,
          "sortOrder": 1
        },
        {
          "id": 2,
          "disciplineFieldId": 221,
          "fieldName": "AbzugAusf.",
          "performance": 2.0,
          "isFinalScore": false,
          "sortOrder": 2
        },
        {
          "id": 3,
          "disciplineFieldId": 222,
          "fieldName": "Endwert",
          "performance": 14.0,
          "isFinalScore": true,
          "sortOrder": 3
        }
      ]
    }
  ],
  "pagination": {
    "total": 1,
    "limit": 200,
    "offset": 0,
    "hasMore": false
  }
}

Summary

Jury Results System ist ein vollstΓ€ndig implementiertes, feldbasiertes Bewertungssystem mit:

βœ… Flexible Feld-Definitionen pro Disziplin βœ… Automatische Endwert-Berechnung mit Formeln βœ… Transparente Anzeige in UI mit Formel-Rendering βœ… Legacy-KompatibilitΓ€t mit tfx_wertungen_details βœ… Zentrale Konfiguration mit Warnung vor Γ„nderungen βœ… Robuste Fehlerbehandlung und Logging

Das System ist produktionsbereit und wird bereits verwendet. Diese Dokumentation dient als Referenz fΓΌr weitere Entwicklung und Troubleshooting.


Document Version: 1.0
Last Updated: 2026-01-28
Author: AI Assistant (GitHub Copilot)
Review Status: βœ… Complete