diff --git a/app/Http/Controllers/MatchEventController.php b/app/Http/Controllers/MatchEventController.php index 0471a27..c2e7c33 100644 --- a/app/Http/Controllers/MatchEventController.php +++ b/app/Http/Controllers/MatchEventController.php @@ -99,31 +99,10 @@ class MatchEventController extends Controller return response()->json(['success' => false, 'message' => 'Unauthorized'], 403); } - // Get ALL previous points in this round (ordered by timestamp) - $previousPoints = MatchPoint::where('match_round_id', $request->round_id) - ->where('timestamp_seconds', '<', $request->timestamp_seconds) - ->orderBy('timestamp_seconds', 'asc') - ->pluck('points', 'competitor') - ->toArray(); - - // Calculate cumulative scores by summing each point value - $scoreBlue = 0; - $scoreRed = 0; - - if (isset($previousPoints['blue'])) { - $scoreBlue += $previousPoints['blue']; - } - if (isset($previousPoints['red'])) { - $scoreRed += $previousPoints['red']; - } - - // Add current point - if ($request->competitor === 'blue') { - $scoreBlue += $request->points; - } else { - $scoreRed += $request->points; - } - + // Create the row first with placeholder scores, then recompute the whole + // round's running scores. This is the only correct approach when two + // points can share a timestamp (Blue + Red at the same moment) or when + // a point is inserted between existing ones. $point = MatchPoint::create([ 'video_id' => $video->id, 'match_round_id' => $request->round_id, @@ -132,17 +111,42 @@ class MatchEventController extends Controller 'points' => $request->points, 'competitor' => $request->competitor, 'notes' => $request->notes, - 'score_blue' => $scoreBlue, - 'score_red' => $scoreRed, + 'score_blue' => 0, + 'score_red' => 0, ]); + $this->recomputeRoundScores($request->round_id); + return response()->json([ 'success' => true, - 'point' => $point, + 'point' => $point->fresh(), 'message' => 'Point added successfully!', ]); } + /** + * Recompute cumulative score_blue/score_red for every point in a round, + * ordered by (timestamp_seconds, id). Handles same-timestamp ties by + * insertion order, so Blue and Red saved at the same moment both end up + * reflecting the running score after that moment. + */ + private function recomputeRoundScores(int $roundId): void + { + $blue = 0; + $red = 0; + MatchPoint::where('match_round_id', $roundId) + ->orderBy('timestamp_seconds', 'asc') + ->orderBy('id', 'asc') + ->get() + ->each(function (MatchPoint $p) use (&$blue, &$red) { + if ($p->competitor === 'blue') $blue += (int) $p->points; + else $red += (int) $p->points; + if ((int) $p->score_blue !== $blue || (int) $p->score_red !== $red) { + $p->update(['score_blue' => $blue, 'score_red' => $red]); + } + }); + } + public function updatePoint(Request $request, MatchPoint $point) { $request->validate([ @@ -158,6 +162,7 @@ class MatchEventController extends Controller return response()->json(['success' => false, 'message' => 'Unauthorized'], 403); } + $roundId = $point->match_round_id; $point->update([ 'timestamp_seconds' => $request->timestamp_seconds, 'action' => $request->action, @@ -166,9 +171,11 @@ class MatchEventController extends Controller 'notes' => $request->notes, ]); + $this->recomputeRoundScores($roundId); + return response()->json([ 'success' => true, - 'point' => $point, + 'point' => $point->fresh(), 'message' => 'Point updated successfully!', ]); } @@ -180,7 +187,9 @@ class MatchEventController extends Controller return response()->json(['success' => false, 'message' => 'Unauthorized'], 403); } + $roundId = $point->match_round_id; $point->delete(); + $this->recomputeRoundScores($roundId); return response()->json([ 'success' => true, diff --git a/resources/views/videos/types/match.blade.php b/resources/views/videos/types/match.blade.php index c49fd52..f44ab6b 100644 --- a/resources/views/videos/types/match.blade.php +++ b/resources/views/videos/types/match.blade.php @@ -1219,13 +1219,23 @@ color: var(--text-secondary); line-height: 1.4; } + .event-meta .meta-round { color: #eab308; font-weight: 700; letter-spacing: .02em; } + .event-meta .meta-sep { margin: 0 4px; opacity: .5; } + .event-meta .meta-score-blue { color: #3b82f6; font-weight: 700; font-variant-numeric: tabular-nums; } + .event-meta .meta-score-red { color: #ef4444; font-weight: 700; font-variant-numeric: tabular-nums; } + .event-meta .meta-score-sep { margin: 0 4px; color: var(--text-secondary); opacity: .7; } .pill { + display: inline-block; + min-width: 42px; /* equal width for Blue/Red — no visual bias */ + text-align: center; font-size: 10px; padding: 2px 6px; border-radius: 4px; font-weight: 600; text-transform: uppercase; + font-variant-numeric: tabular-nums; + box-sizing: border-box; } .pill-blue { @@ -1238,6 +1248,27 @@ color: white; } + /* Point entry (single or grouped) — one card, inline chips per side */ + .event-item-point .event-body { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1; } + .event-chips { + display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; + } + .event-chip { + display: inline-flex; align-items: center; gap: 6px; min-width: 0; + padding: 3px 4px 3px 3px; border-radius: 6px; + transition: background .12s; + } + .event-chip:hover { background: rgba(255,255,255,.04); } + .chip-action { + font-size: 13px; color: var(--text-primary); + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + max-width: 220px; + } + .chip-pt { + font-size: 11px; font-weight: 700; color: var(--text-secondary); + background: rgba(255,255,255,.06); padding: 1px 6px; border-radius: 3px; + } + /* ===== Coach Review Tab Specific Styles ===== */ #tab-review .event-item { position: relative; @@ -2411,9 +2442,6 @@