validate([ 'round_number' => 'required|integer|min:1', 'name' => 'nullable|string|max:50', 'start_time_seconds' => 'nullable|integer|min:0', ]); // Check if user owns the video if (Auth::id() !== $video->user_id) { return response()->json(['success' => false, 'message' => 'Unauthorized'], 403); } $round = MatchRound::create([ 'video_id' => $video->id, 'round_number' => $request->round_number, 'name' => $request->name ?? 'ROUND '.$request->round_number, 'start_time_seconds' => $request->start_time_seconds, ]); return response()->json([ 'success' => true, 'round' => $round, 'message' => 'Round added successfully!', ]); } public function updateRound(Request $request, MatchRound $round) { $request->validate([ 'round_number' => 'sometimes|integer|min:1', 'name' => 'required|string|max:50', 'start_time_seconds' => 'nullable|integer|min:0', ]); // Check if user owns the video if (Auth::id() !== $round->video->user_id) { return response()->json(['success' => false, 'message' => 'Unauthorized'], 403); } $round->update([ 'round_number' => $request->round_number ?? $round->round_number, 'name' => $request->name, 'start_time_seconds' => $request->start_time_seconds, ]); return response()->json([ 'success' => true, 'round' => $round, 'message' => 'Round updated successfully!', ]); } public function destroyRound(MatchRound $round) { // Check if user owns the video if (Auth::id() !== $round->video->user_id) { return response()->json(['success' => false, 'message' => 'Unauthorized'], 403); } $round->delete(); return response()->json([ 'success' => true, 'message' => 'Round deleted successfully!', ]); } // ==================== POINTS ==================== public function storePoint(Request $request, Video $video) { $request->validate([ 'round_id' => 'required|exists:match_rounds,id', 'timestamp_seconds' => 'required|integer|min:0', 'action' => 'required|string|max:255', 'points' => 'required|integer|min:1', 'competitor' => 'required|in:blue,red', 'notes' => 'nullable|string|max:500', ]); // Check if user owns the video if (Auth::id() !== $video->user_id) { return response()->json(['success' => false, 'message' => 'Unauthorized'], 403); } // 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, 'timestamp_seconds' => $request->timestamp_seconds, 'action' => $request->action, 'points' => $request->points, 'competitor' => $request->competitor, 'notes' => $request->notes, 'score_blue' => 0, 'score_red' => 0, ]); $this->recomputeRoundScores($request->round_id); return response()->json([ 'success' => true, '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([ 'timestamp_seconds' => 'required|integer|min:0', 'action' => 'required|string|max:255', 'points' => 'required|integer|min:1', 'competitor' => 'required|in:blue,red', 'notes' => 'nullable|string|max:500', ]); // Check if user owns the video if (Auth::id() !== $point->video->user_id) { return response()->json(['success' => false, 'message' => 'Unauthorized'], 403); } $roundId = $point->match_round_id; $point->update([ 'timestamp_seconds' => $request->timestamp_seconds, 'action' => $request->action, 'points' => $request->points, 'competitor' => $request->competitor, 'notes' => $request->notes, ]); $this->recomputeRoundScores($roundId); return response()->json([ 'success' => true, 'point' => $point->fresh(), 'message' => 'Point updated successfully!', ]); } public function destroyPoint(MatchPoint $point) { // Check if user owns the video if (Auth::id() !== $point->video->user_id) { return response()->json(['success' => false, 'message' => 'Unauthorized'], 403); } $roundId = $point->match_round_id; $point->delete(); $this->recomputeRoundScores($roundId); return response()->json([ 'success' => true, 'message' => 'Point deleted successfully!', ]); } // ==================== COACH REVIEWS ==================== public function storeReview(Request $request, Video $video) { $request->validate([ 'start_time_seconds' => 'required|integer|min:0', 'end_time_seconds' => 'nullable|integer|min:0', 'note' => 'required|string|max:1000', 'coach_name' => 'required|string|max:100', 'emoji' => 'nullable|string|max:10', ]); // Check if user owns the video if (Auth::id() !== $video->user_id) { return response()->json(['success' => false, 'message' => 'Unauthorized'], 403); } $review = CoachReview::create([ 'video_id' => $video->id, 'user_id' => Auth::id(), 'start_time_seconds' => $request->start_time_seconds, 'end_time_seconds' => $request->end_time_seconds, 'note' => $request->note, 'coach_name' => $request->coach_name, 'emoji' => $request->emoji ?? '🔥', ]); return response()->json([ 'success' => true, 'review' => $review, 'message' => 'Coach note added successfully!', ]); } public function updateReview(Request $request, CoachReview $review) { $request->validate([ 'start_time_seconds' => 'required|integer|min:0', 'end_time_seconds' => 'nullable|integer|min:0', 'note' => 'required|string|max:1000', 'coach_name' => 'required|string|max:100', 'emoji' => 'nullable|string|max:10', ]); // Check if user owns the video if (Auth::id() !== $review->video->user_id) { return response()->json(['success' => false, 'message' => 'Unauthorized'], 403); } $review->update([ 'start_time_seconds' => $request->start_time_seconds, 'end_time_seconds' => $request->end_time_seconds, 'note' => $request->note, 'coach_name' => $request->coach_name, 'emoji' => $request->emoji, ]); return response()->json([ 'success' => true, 'review' => $review, 'message' => 'Coach note updated successfully!', ]); } public function destroyReview(CoachReview $review) { // Check if user owns the video if (Auth::id() !== $review->video->user_id) { return response()->json(['success' => false, 'message' => 'Unauthorized'], 403); } $review->delete(); return response()->json([ 'success' => true, 'message' => 'Coach note deleted successfully!', ]); } // ==================== GET DATA ==================== public function getMatchData(Video $video) { // Check if user can view this video if (! $video->canView(Auth::user())) { return response()->json(['success' => false, 'message' => 'Unauthorized'], 403); } $rounds = MatchRound::where('video_id', $video->id) ->with('points') ->orderBy('round_number') ->get(); $reviews = CoachReview::where('video_id', $video->id) ->orderBy('start_time_seconds') ->get(); return response()->json([ 'success' => true, 'rounds' => $rounds, 'reviews' => $reviews, ]); } }