333 lines
15 KiB
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();
|
|
}
|
|
}
|