torneos/app/Services/AdvancedScoreSheetService.php

333 lines
15 KiB
PHP

<?php
namespace App\Services;
use App\Core\Database;
use InvalidArgumentException;
use PDO;
final class AdvancedScoreSheetService
{
private PDO $db;
public function __construct()
{
$this->db = Database::connection();
}
public function state(int $matchId): array
{
return [
'rotations' => $this->rows('SELECT r.*, CONCAT(p.first_name, " ", p.last_name) player_name, t.name team_name
FROM rotations r JOIN players p ON p.id = r.player_id JOIN teams t ON t.id = r.team_id
WHERE r.match_id = :id ORDER BY r.set_number, r.team_id, r.position_number', $matchId),
'liberos' => $this->rows('SELECT l.*, CONCAT(p.first_name, " ", p.last_name) player_name, t.name team_name
FROM match_liberos l JOIN players p ON p.id = l.player_id JOIN teams t ON t.id = l.team_id
WHERE l.match_id = :id ORDER BY l.set_number, l.team_id', $matchId),
'substitutions' => $this->rows('SELECT s.*, outp.first_name out_first, outp.last_name out_last, inp.first_name in_first, inp.last_name in_last, t.name team_name
FROM substitutions s JOIN players outp ON outp.id = s.player_out_id JOIN players inp ON inp.id = s.player_in_id JOIN teams t ON t.id = s.team_id
WHERE s.match_id = :id ORDER BY s.id DESC', $matchId),
'timeouts' => $this->rows('SELECT to1.*, t.name team_name FROM timeouts to1 JOIN teams t ON t.id = to1.team_id
WHERE to1.match_id = :id ORDER BY to1.id DESC', $matchId),
'rallies' => $this->rows('SELECT r.*, wt.name winning_team, st.name serving_team
FROM rally_history r LEFT JOIN teams wt ON wt.id = r.winning_team_id LEFT JOIN teams st ON st.id = r.serving_team_id
WHERE r.match_id = :id ORDER BY r.id DESC LIMIT 100', $matchId),
'audit' => $this->rows('SELECT a.*, u.name user_name FROM referee_audit_logs a LEFT JOIN users u ON u.id = a.user_id
WHERE a.match_id = :id ORDER BY a.id DESC LIMIT 100', $matchId),
'signatures' => $this->rows('SELECT * FROM referee_signatures WHERE match_id = :id ORDER BY signed_at DESC', $matchId),
];
}
public function setRotation(int $matchId, array $data, array $user): array
{
$set = $this->currentSet($matchId);
$teamId = $this->validTeam($matchId, (int) $data['team_id']);
$position = (int) $data['position_number'];
if ($position < 1 || $position > 6) {
throw new InvalidArgumentException('La posicion debe estar entre 1 y 6');
}
$this->ensurePlayerInTeam((int) $data['player_id'], $teamId);
$stmt = $this->db->prepare(
'INSERT INTO rotations (match_id, team_id, set_number, position_number, player_id, is_libero)
VALUES (:match_id, :team_id, :set_number, :position_number, :player_id, :is_libero)
ON DUPLICATE KEY UPDATE player_id = VALUES(player_id), is_libero = VALUES(is_libero)'
);
$stmt->execute([
'match_id' => $matchId,
'team_id' => $teamId,
'set_number' => $data['set_number'] ?? $set['set_number'],
'position_number' => $position,
'player_id' => (int) $data['player_id'],
'is_libero' => (int) ($data['is_libero'] ?? 0),
]);
$this->event($matchId, 'rotation', $teamId, $data['player_id'], "Rotacion posicion $position", $user);
$this->audit($matchId, 'rotation.set', $data, $user);
return $this->state($matchId);
}
public function setLibero(int $matchId, array $data, array $user): array
{
$set = $this->currentSet($matchId);
$teamId = $this->validTeam($matchId, (int) $data['team_id']);
$this->ensurePlayerInTeam((int) $data['player_id'], $teamId);
$stmt = $this->db->prepare(
'INSERT IGNORE INTO match_liberos (match_id, team_id, player_id, set_number, is_starting)
VALUES (:match_id, :team_id, :player_id, :set_number, :is_starting)'
);
$stmt->execute([
'match_id' => $matchId,
'team_id' => $teamId,
'player_id' => (int) $data['player_id'],
'set_number' => $data['set_number'] ?? $set['set_number'],
'is_starting' => (int) ($data['is_starting'] ?? 0),
]);
$this->event($matchId, 'libero', $teamId, $data['player_id'], 'Registro de libero', $user);
$this->audit($matchId, 'libero.set', $data, $user);
return $this->state($matchId);
}
public function substitute(int $matchId, array $data, array $user): array
{
$set = $this->currentSet($matchId);
$teamId = $this->validTeam($matchId, (int) $data['team_id']);
$setNumber = (int) ($data['set_number'] ?? $set['set_number']);
$this->ensurePlayerInTeam((int) $data['player_out_id'], $teamId);
$this->ensurePlayerInTeam((int) $data['player_in_id'], $teamId);
if ((int) $data['player_out_id'] === (int) $data['player_in_id']) {
throw new InvalidArgumentException('Los jugadores de entrada y salida deben ser distintos');
}
if ($this->countRows('substitutions', $matchId, $teamId, $setNumber) >= 6) {
throw new InvalidArgumentException('Maximo reglamentario de 6 sustituciones por set');
}
$stmt = $this->db->prepare(
'INSERT INTO substitutions (match_id, team_id, set_number, player_out_id, player_in_id, reason, created_by)
VALUES (:match_id, :team_id, :set_number, :player_out_id, :player_in_id, :reason, :created_by)'
);
$stmt->execute([
'match_id' => $matchId,
'team_id' => $teamId,
'set_number' => $setNumber,
'player_out_id' => (int) $data['player_out_id'],
'player_in_id' => (int) $data['player_in_id'],
'reason' => $data['reason'] ?? null,
'created_by' => $user['sub'] ?? null,
]);
$this->event($matchId, 'substitution', $teamId, $data['player_in_id'], 'Sustitucion reglamentaria', $user);
$this->audit($matchId, 'substitution.create', $data, $user);
return $this->state($matchId);
}
public function timeout(int $matchId, array $data, array $user): array
{
$set = $this->currentSet($matchId);
$teamId = $this->validTeam($matchId, (int) $data['team_id']);
$setNumber = (int) ($data['set_number'] ?? $set['set_number']);
if ($this->countRows('timeouts', $matchId, $teamId, $setNumber) >= 2) {
throw new InvalidArgumentException('Maximo reglamentario de 2 tiempos por set');
}
$stmt = $this->db->prepare(
'INSERT INTO timeouts (match_id, team_id, set_number, points_home, points_away, requested_by, created_by)
VALUES (:match_id, :team_id, :set_number, :points_home, :points_away, :requested_by, :created_by)'
);
$stmt->execute([
'match_id' => $matchId,
'team_id' => $teamId,
'set_number' => $setNumber,
'points_home' => $set['home_points'],
'points_away' => $set['away_points'],
'requested_by' => $data['requested_by'] ?? null,
'created_by' => $user['sub'] ?? null,
]);
$this->event($matchId, 'timeout', $teamId, null, 'Tiempo solicitado', $user);
$this->audit($matchId, 'timeout.create', $data, $user);
return $this->state($matchId);
}
public function rally(int $matchId, array $data, array $user): array
{
$set = $this->currentSet($matchId);
$winningTeamId = isset($data['winning_team_id']) ? $this->validTeam($matchId, (int) $data['winning_team_id']) : null;
$servingTeamId = isset($data['serving_team_id']) ? $this->validTeam($matchId, (int) $data['serving_team_id']) : null;
$rallyNumber = $this->nextRallyNumber($matchId, (int) $set['set_number']);
$stmt = $this->db->prepare(
'INSERT INTO rally_history
(match_id, set_number, rally_number, serving_team_id, winning_team_id, result_type, points_home, points_away, notes, created_by)
VALUES (:match_id, :set_number, :rally_number, :serving_team_id, :winning_team_id, :result_type, :points_home, :points_away, :notes, :created_by)'
);
$stmt->execute([
'match_id' => $matchId,
'set_number' => $set['set_number'],
'rally_number' => $rallyNumber,
'serving_team_id' => $servingTeamId,
'winning_team_id' => $winningTeamId,
'result_type' => $data['result_type'] ?? 'point',
'points_home' => $set['home_points'],
'points_away' => $set['away_points'],
'notes' => $data['notes'] ?? null,
'created_by' => $user['sub'] ?? null,
]);
$this->audit($matchId, 'rally.create', $data, $user);
return $this->state($matchId);
}
public function sanction(int $matchId, array $data, array $user): array
{
$teamId = !empty($data['team_id']) ? $this->validTeam($matchId, (int) $data['team_id']) : null;
$playerId = !empty($data['player_id']) ? (int) $data['player_id'] : null;
$card = $data['card_type'] ?? 'yellow';
if (!in_array($card, ['yellow', 'red'], true)) {
throw new InvalidArgumentException('Tarjeta invalida');
}
$stmt = $this->db->prepare(
'INSERT INTO sanctions (match_id, team_id, player_id, card_type, reason)
VALUES (:match_id, :team_id, :player_id, :card_type, :reason)'
);
$stmt->execute([
'match_id' => $matchId,
'team_id' => $teamId,
'player_id' => $playerId,
'card_type' => $card,
'reason' => $data['reason'] ?? null,
]);
$this->event($matchId, $card === 'red' ? 'red_card' : 'yellow_card', $teamId, $playerId, $data['reason'] ?? 'Sancion', $user);
$this->audit($matchId, 'sanction.create', $data, $user);
return $this->state($matchId);
}
public function sign(int $matchId, array $data, array $user): array
{
$payload = [
'match_id' => $matchId,
'signer_name' => $data['signer_name'],
'role' => $data['role'] ?? 'principal',
'user_id' => $user['sub'] ?? null,
'signed_at' => date('c'),
];
$hash = hash('sha256', json_encode($payload));
$stmt = $this->db->prepare(
'INSERT INTO referee_signatures (match_id, referee_id, signer_name, role, signature_hash, signed_payload)
VALUES (:match_id, :referee_id, :signer_name, :role, :signature_hash, :signed_payload)
ON DUPLICATE KEY UPDATE signer_name = VALUES(signer_name), signature_hash = VALUES(signature_hash), signed_payload = VALUES(signed_payload), signed_at = CURRENT_TIMESTAMP'
);
$stmt->execute([
'match_id' => $matchId,
'referee_id' => $data['referee_id'] ?? null,
'signer_name' => $data['signer_name'],
'role' => $payload['role'],
'signature_hash' => $hash,
'signed_payload' => json_encode($payload),
]);
$this->event($matchId, 'signature', null, null, 'Firma digital arbitral', $user);
$this->audit($matchId, 'signature.create', ['hash' => $hash] + $data, $user);
return ['signature_hash' => $hash, 'advanced' => $this->state($matchId)];
}
private function event(int $matchId, string $type, ?int $teamId, mixed $playerId, string $notes, array $user): void
{
$set = $this->currentSet($matchId);
$stmt = $this->db->prepare(
'INSERT INTO match_events (match_id, set_number, team_id, player_id, event_type, points_home, points_away, notes, created_by)
VALUES (:match_id, :set_number, :team_id, :player_id, :event_type, :points_home, :points_away, :notes, :created_by)'
);
$stmt->execute([
'match_id' => $matchId,
'set_number' => $set['set_number'],
'team_id' => $teamId,
'player_id' => $playerId ?: null,
'event_type' => $type,
'points_home' => $set['home_points'],
'points_away' => $set['away_points'],
'notes' => $notes,
'created_by' => $user['sub'] ?? null,
]);
}
private function audit(int $matchId, string $action, array $payload, array $user): void
{
$stmt = $this->db->prepare('INSERT INTO referee_audit_logs (match_id, user_id, action, payload) VALUES (:match_id, :user_id, :action, :payload)');
$stmt->execute([
'match_id' => $matchId,
'user_id' => $user['sub'] ?? null,
'action' => $action,
'payload' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
]);
}
private function validTeam(int $matchId, int $teamId): int
{
$match = $this->match($matchId);
if (!in_array($teamId, [(int) $match['home_team_id'], (int) $match['away_team_id']], true)) {
throw new InvalidArgumentException('El equipo no participa en este partido');
}
return $teamId;
}
private function ensurePlayerInTeam(int $playerId, int $teamId): void
{
$stmt = $this->db->prepare('SELECT id FROM players WHERE id = :id AND team_id = :team_id');
$stmt->execute(['id' => $playerId, 'team_id' => $teamId]);
if (!$stmt->fetch()) {
throw new InvalidArgumentException('El jugador no pertenece al equipo');
}
}
private function currentSet(int $matchId): array
{
$stmt = $this->db->prepare('SELECT * FROM match_sets WHERE match_id = :id AND winner_team_id IS NULL ORDER BY set_number LIMIT 1');
$stmt->execute(['id' => $matchId]);
$set = $stmt->fetch();
if ($set) {
return $set;
}
$this->db->prepare('INSERT INTO match_sets (match_id, set_number) VALUES (:id, 1)')->execute(['id' => $matchId]);
$stmt = $this->db->prepare('SELECT * FROM match_sets WHERE id = :id');
$stmt->execute(['id' => $this->db->lastInsertId()]);
return $stmt->fetch();
}
private function match(int $matchId): array
{
$stmt = $this->db->prepare('SELECT * FROM matches WHERE id = :id');
$stmt->execute(['id' => $matchId]);
$match = $stmt->fetch();
if (!$match) {
throw new InvalidArgumentException('Partido no encontrado');
}
return $match;
}
private function countRows(string $table, int $matchId, int $teamId, int $setNumber): int
{
$stmt = $this->db->prepare("SELECT COUNT(*) total FROM $table WHERE match_id = :match_id AND team_id = :team_id AND set_number = :set_number");
$stmt->execute(['match_id' => $matchId, 'team_id' => $teamId, 'set_number' => $setNumber]);
return (int) $stmt->fetch()['total'];
}
private function nextRallyNumber(int $matchId, int $setNumber): int
{
$stmt = $this->db->prepare('SELECT COALESCE(MAX(rally_number), 0) + 1 next_number FROM rally_history WHERE match_id = :match_id AND set_number = :set_number');
$stmt->execute(['match_id' => $matchId, 'set_number' => $setNumber]);
return (int) $stmt->fetch()['next_number'];
}
private function rows(string $sql, int $matchId): array
{
$stmt = $this->db->prepare($sql);
$stmt->execute(['id' => $matchId]);
return $stmt->fetchAll();
}
}