/// Lifecycle state of a single bracket match. enum MatchStatus { scheduled, inProgress, completed } /// Lightweight team reference stored inline on a bracket match. The full team /// record (roster, record, etc.) lives in the teams feature; brackets only /// need an id, a display name, and an optional logo. class BracketTeam { const BracketTeam({ required this.id, required this.name, this.logoUrl, }); final String id; final String name; final String? logoUrl; BracketTeam copyWith({String? id, String? name, String? logoUrl}) { return BracketTeam( id: id ?? this.id, name: name ?? this.name, logoUrl: logoUrl ?? this.logoUrl, ); } factory BracketTeam.fromMap(Map data) { return BracketTeam( id: (data['id'] as String?) ?? '', name: (data['name'] as String?) ?? '', logoUrl: data['logo_url'] as String?, ); } Map toMap() { return { 'id': id, 'name': name, 'logo_url': logoUrl, }; } @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is BracketTeam && other.id == id && other.name == name && other.logoUrl == logoUrl; } @override int get hashCode => Object.hash(id, name, logoUrl); } /// A single match within a bracket. Either team may be null while previous /// rounds are still being decided (a `null` team renders as "TBD"). class BracketMatch { const BracketMatch({ required this.id, required this.status, this.teamA, this.teamB, this.scoreA, this.scoreB, this.scheduledAt, this.winnerId, }); final String id; final BracketTeam? teamA; final BracketTeam? teamB; final int? scoreA; final int? scoreB; final MatchStatus status; final DateTime? scheduledAt; final String? winnerId; bool get isTeamAWinner => winnerId != null && teamA != null && winnerId == teamA!.id; bool get isTeamBWinner => winnerId != null && teamB != null && winnerId == teamB!.id; BracketMatch copyWith({ String? id, BracketTeam? teamA, BracketTeam? teamB, int? scoreA, int? scoreB, MatchStatus? status, DateTime? scheduledAt, String? winnerId, }) { return BracketMatch( id: id ?? this.id, teamA: teamA ?? this.teamA, teamB: teamB ?? this.teamB, scoreA: scoreA ?? this.scoreA, scoreB: scoreB ?? this.scoreB, status: status ?? this.status, scheduledAt: scheduledAt ?? this.scheduledAt, winnerId: winnerId ?? this.winnerId, ); } factory BracketMatch.fromMap(Map data) { return BracketMatch( id: (data['id'] as String?) ?? '', teamA: data['team_a'] is Map ? BracketTeam.fromMap(data['team_a'] as Map) : null, teamB: data['team_b'] is Map ? BracketTeam.fromMap(data['team_b'] as Map) : null, scoreA: (data['score_a'] as num?)?.toInt(), scoreB: (data['score_b'] as num?)?.toInt(), status: _readStatus(data['status']), scheduledAt: _readTimestamp(data['scheduled_at']), winnerId: data['winner_id'] as String?, ); } Map toMap() { return { 'id': id, 'team_a': teamA?.toMap(), 'team_b': teamB?.toMap(), 'score_a': scoreA, 'score_b': scoreB, 'status': status.name, 'scheduled_at': scheduledAt?.toIso8601String(), 'winner_id': winnerId, }; } static MatchStatus _readStatus(Object? value) { if (value is String) { for (final s in MatchStatus.values) { if (s.name == value) return s; } } return MatchStatus.scheduled; } static DateTime? _readTimestamp(Object? value) { if (value is String && value.isNotEmpty) return DateTime.tryParse(value); if (value is DateTime) return value; return null; } @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is BracketMatch && other.id == id && other.teamA == teamA && other.teamB == teamB && other.scoreA == scoreA && other.scoreB == scoreB && other.status == status && other.scheduledAt == scheduledAt && other.winnerId == winnerId; } @override int get hashCode => Object.hash( id, teamA, teamB, scoreA, scoreB, status, scheduledAt, winnerId, ); } /// A round (column) in a bracket — quarterfinals, semifinals, final, etc. class BracketRound { const BracketRound({ required this.roundNumber, required this.label, required this.matches, }); final int roundNumber; final String label; final List matches; BracketRound copyWith({ int? roundNumber, String? label, List? matches, }) { return BracketRound( roundNumber: roundNumber ?? this.roundNumber, label: label ?? this.label, matches: matches ?? this.matches, ); } factory BracketRound.fromMap(Map data) { final rawMatches = (data['matches'] as List?) ?? const []; return BracketRound( roundNumber: (data['round_number'] as num?)?.toInt() ?? 0, label: (data['label'] as String?) ?? '', matches: rawMatches .whereType>() .map(BracketMatch.fromMap) .toList(growable: false), ); } Map toMap() { return { 'round_number': roundNumber, 'label': label, 'matches': matches.map((m) => m.toMap()).toList(growable: false), }; } @override bool operator ==(Object other) { if (identical(this, other)) return true; if (other is! BracketRound) return false; if (other.roundNumber != roundNumber) return false; if (other.label != label) return false; if (other.matches.length != matches.length) return false; for (var i = 0; i < matches.length; i++) { if (other.matches[i] != matches[i]) return false; } return true; } @override int get hashCode => Object.hash(roundNumber, label, Object.hashAll(matches)); } /// Top-level bracket. A single event may have multiple brackets (e.g. main /// draw + consolation), so brackets carry an [eventId]. class Bracket { const Bracket({ required this.id, required this.eventId, required this.name, required this.rounds, required this.createdAt, }); final String id; final String eventId; final String name; final List rounds; final DateTime createdAt; Bracket copyWith({ String? id, String? eventId, String? name, List? rounds, DateTime? createdAt, }) { return Bracket( id: id ?? this.id, eventId: eventId ?? this.eventId, name: name ?? this.name, rounds: rounds ?? this.rounds, createdAt: createdAt ?? this.createdAt, ); } factory Bracket.fromJson(Map data) { final rawRounds = (data['rounds'] as List?) ?? const []; return Bracket( id: (data['id'] as String?) ?? '', eventId: (data['event_id'] as String?) ?? '', name: (data['name'] as String?) ?? '', rounds: rawRounds .whereType>() .map(BracketRound.fromMap) .toList(growable: false), createdAt: _readDate(data['created_at']) ?? DateTime.now(), ); } Map toJson() { return { 'event_id': eventId, 'name': name, 'rounds': rounds.map((r) => r.toMap()).toList(growable: false), 'created_at': createdAt.toIso8601String(), }; } static DateTime? _readDate(Object? value) { if (value is String && value.isNotEmpty) return DateTime.tryParse(value); if (value is DateTime) return value; return null; } @override bool operator ==(Object other) { if (identical(this, other)) return true; if (other is! Bracket) return false; if (other.id != id) return false; if (other.eventId != eventId) return false; if (other.name != name) return false; if (other.createdAt != createdAt) return false; if (other.rounds.length != rounds.length) return false; for (var i = 0; i < rounds.length; i++) { if (other.rounds[i] != rounds[i]) return false; } return true; } @override int get hashCode => Object.hash(id, eventId, name, createdAt, Object.hashAll(rounds)); @override String toString() => 'Bracket(id: $id, name: $name, rounds: ${rounds.length})'; }