From 402c6dc89ffdc9d90c5575478dd5af1619ea84f6 Mon Sep 17 00:00:00 2001 From: ghassan Date: Sat, 8 Aug 2026 05:31:26 +0300 Subject: [PATCH] Point capture in the player: inline strip, grouped entries, correct scores MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the point Add/Edit popup with an in-player capture strip and fixes the underlying running-score bug. Backend (MatchEventController) - storePoint used pluck('points','competitor'), which collapses same-side points into a single row (last value wins) and excluded same-timestamp points (< instead of <=). Same-moment Blue+Red produced 1-0 / 0-1 instead of 1-1. Replaced with recomputeRoundScores($roundId), a single pass over the round ordered by (timestamp_seconds, id) that walks a running total and updates each row. Called after create/update/delete so edits ripple. Player-embedded capture strip (match.blade.php) - Adding / editing points no longer opens a modal; the video's chrome bottom-bar is swapped for a strip inside #ytpWrap that survives fullscreen. - Editable mm:ss field + draggable red marker + −1s/+1s nudges. Zoom control (1x / 5x / 20x) narrows the slider window around the current time for frame-level positioning; nudges scale with zoom. Auto-recentre on edge. - Inline form: Blue / Red / Both toggle (equal-width segments), action text, points number. Both mode splits into two per-side rows. Values carry over when switching modes so nothing is retyped. - Enter saves, Esc cancels. Save button auto-disables during in-flight requests. On mobile the buttons collapse to icon-only (× / ✓). Grouped highlights entry - Points sharing a timestamp within a round collapse into ONE card with a chip per side (equal-width Blue/Red pills), a single ✏️/🗑 pair, and a meta line coloured by role: ROUND N in amber, blue score in blue, red score in red. - ✏️ on a grouped entry opens the strip in Both mode with both sides pre-filled. Saving PUTs both rows; switching to a single competitor keeps that side and deletes the other. - 🗑 on a grouped entry deletes both rows via the shared custom-confirm. - loadMatchData now writes back to window.matchRounds so subsequent edit/lookup handlers find newly-added points without a page reload. Removed - #pointModal HTML block and the openAddPointModal / savePoint / confirmDeletePoint helpers (superseded by the capture strip). Co-Authored-By: Claude Opus 4.7 (1M context) --- app/Http/Controllers/MatchEventController.php | 67 +- resources/views/videos/types/match.blade.php | 1224 +++++++++++++---- 2 files changed, 983 insertions(+), 308 deletions(-) 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 @@