Point capture in the player: inline strip, grouped entries, correct scores

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) <noreply@anthropic.com>
This commit is contained in:
ghassan 2026-08-08 05:31:26 +03:00
parent ad6cbaf742
commit 402c6dc89f
2 changed files with 983 additions and 308 deletions

View File

@ -99,31 +99,10 @@ class MatchEventController extends Controller
return response()->json(['success' => false, 'message' => 'Unauthorized'], 403); return response()->json(['success' => false, 'message' => 'Unauthorized'], 403);
} }
// Get ALL previous points in this round (ordered by timestamp) // Create the row first with placeholder scores, then recompute the whole
$previousPoints = MatchPoint::where('match_round_id', $request->round_id) // round's running scores. This is the only correct approach when two
->where('timestamp_seconds', '<', $request->timestamp_seconds) // points can share a timestamp (Blue + Red at the same moment) or when
->orderBy('timestamp_seconds', 'asc') // a point is inserted between existing ones.
->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;
}
$point = MatchPoint::create([ $point = MatchPoint::create([
'video_id' => $video->id, 'video_id' => $video->id,
'match_round_id' => $request->round_id, 'match_round_id' => $request->round_id,
@ -132,17 +111,42 @@ class MatchEventController extends Controller
'points' => $request->points, 'points' => $request->points,
'competitor' => $request->competitor, 'competitor' => $request->competitor,
'notes' => $request->notes, 'notes' => $request->notes,
'score_blue' => $scoreBlue, 'score_blue' => 0,
'score_red' => $scoreRed, 'score_red' => 0,
]); ]);
$this->recomputeRoundScores($request->round_id);
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'point' => $point, 'point' => $point->fresh(),
'message' => 'Point added successfully!', '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) public function updatePoint(Request $request, MatchPoint $point)
{ {
$request->validate([ $request->validate([
@ -158,6 +162,7 @@ class MatchEventController extends Controller
return response()->json(['success' => false, 'message' => 'Unauthorized'], 403); return response()->json(['success' => false, 'message' => 'Unauthorized'], 403);
} }
$roundId = $point->match_round_id;
$point->update([ $point->update([
'timestamp_seconds' => $request->timestamp_seconds, 'timestamp_seconds' => $request->timestamp_seconds,
'action' => $request->action, 'action' => $request->action,
@ -166,9 +171,11 @@ class MatchEventController extends Controller
'notes' => $request->notes, 'notes' => $request->notes,
]); ]);
$this->recomputeRoundScores($roundId);
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'point' => $point, 'point' => $point->fresh(),
'message' => 'Point updated successfully!', 'message' => 'Point updated successfully!',
]); ]);
} }
@ -180,7 +187,9 @@ class MatchEventController extends Controller
return response()->json(['success' => false, 'message' => 'Unauthorized'], 403); return response()->json(['success' => false, 'message' => 'Unauthorized'], 403);
} }
$roundId = $point->match_round_id;
$point->delete(); $point->delete();
$this->recomputeRoundScores($roundId);
return response()->json([ return response()->json([
'success' => true, 'success' => true,

File diff suppressed because it is too large Load Diff