import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:lanebow/theme/badges.dart'; import 'package:lanebow/utils/enum_mappings.dart'; class PublicUserNameRow extends StatelessWidget { final String name; final String? gender; final String? memberType; final int? userHandicap; final num? userAverage; final int? rank; // 1: 금, 2: 은, 3: 동 const PublicUserNameRow({ super.key, required this.name, this.gender, this.memberType, this.userHandicap, this.userAverage, this.rank, }); @override Widget build(BuildContext context) { return Row( children: [ if (rank != null) ...[_rankBadge(rank!), const SizedBox(width: 6)], Expanded( child: RichText( text: TextSpan( style: const TextStyle( fontWeight: FontWeight.bold, color: Colors .black, // DefaultTextStyle.of(context).style.color 사용도 가능 ), children: [ TextSpan(text: name), const WidgetSpan(child: SizedBox(width: 4)), // 이름-아이콘 간격 WidgetSpan( alignment: PlaceholderAlignment.middle, // 베이스라인 정렬 child: _genderIcon(gender), ), ], ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 6), _hcBadge(userHandicap), if (userAverage != null) ...[ const SizedBox(width: 4), _avgBadge(userAverage), ], ], ); } Widget _genderIcon(String? gender) { final g = (gender ?? '').toLowerCase(); final isFemale = g == 'female' || g == 'f' || g == '여' || g == '여성'; final icon = isFemale ? Icons.female : Icons.male; final color = isFemale ? Colors.pink : Colors.blue; return CircleAvatar( radius: 10, backgroundColor: color.withOpacity(0.1), child: Icon(icon, size: 14, color: color), ); } Widget _avgBadge(num? avg) { if (avg == null) return const SizedBox.shrink(); return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.indigo.withOpacity(0.10), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.indigo.withOpacity(0.25)), ), child: Text( (avg is double ? avg : avg.toDouble()).toStringAsFixed(1), style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Colors.indigo, ), ), ); } Widget _hcBadge(int? hc) { if (hc == null || hc == 0) return const SizedBox.shrink(); return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.deepPurple.withOpacity(0.10), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.deepPurple.withOpacity(0.25)), ), child: Text( hc > 0 ? '+$hc' : '$hc', style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Colors.deepPurple, ), ), ); } Widget _rankBadge(int r) { String label; Color bg; Color fg = Colors.black87; if (r == 1) { label = r.toString(); bg = Colors.amber.shade200; } else if (r == 2) { label = r.toString(); bg = Colors.blueGrey.shade200; } else if (r == 3) { label = r.toString(); bg = Colors.brown.shade200; } else { label = r.toString(); bg = Colors.grey.shade300; } return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(12), ), child: Text( label, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: fg), ), ); } } class PublicUserGameEditors extends StatelessWidget { final int gameCount; final String participantId; final List rawScores; final List handicaps; final bool enabled; final String Function(String pid, int gameNumber) keyFor; final Widget Function(String key) stateIcon; final void Function(int gameNumber, int value) onScoreChanged; final void Function(int gameNumber, int value) onHandicapChanged; final double? eventAverage; // optional, to show at right final int? memberHandicap; // optional default per-game handicap const PublicUserGameEditors({ super.key, required this.gameCount, required this.participantId, required this.rawScores, required this.handicaps, required this.enabled, required this.keyFor, required this.stateIcon, required this.onScoreChanged, required this.onHandicapChanged, this.eventAverage, this.memberHandicap, }); bool _hasRaw(int g) => (g - 1 >= 0 && g - 1 < rawScores.length && rawScores[g - 1] != null); int _rawAt(int g) => _hasRaw(g) ? rawScores[g - 1]! : 0; int _hcAt(int g) { int hv = (g - 1 >= 0 && g - 1 < handicaps.length) ? handicaps[g - 1] : 0; if (hv == 0 && (memberHandicap ?? 0) != 0) return memberHandicap!; return hv; } int _overallRawTotal() { int s = 0; for (int g = 1; g <= gameCount; g++) { if (_hasRaw(g)) s += _rawAt(g); } return s; } double? _avgRaw() { int count = 0; int sum = 0; for (int g = 1; g <= gameCount; g++) { if (_hasRaw(g)) { sum += _rawAt(g); count++; } } if (count == 0) return null; return sum / count; } bool _hasAnyPerGameHandicap() { // Consider effective handicap (per-game or member fallback) for (int g = 1; g <= gameCount; g++) { if (_hcAt(g) != 0) return true; } return false; } int _totalWithPerGameHcAt(int g) { if (!_hasRaw(g)) return 0; // Use effective handicap (includes memberHandicap fallback) return _rawAt(g) + _hcAt(g); } int _overallWithPerGameHcTotal() { int s = 0; for (int g = 1; g <= gameCount; g++) { if (_hasRaw(g)) s += _totalWithPerGameHcAt(g); } return s; } double? _avgWithPerGameHc() { int count = 0; int sum = 0; for (int g = 1; g <= gameCount; g++) { if (_hasRaw(g)) { sum += _totalWithPerGameHcAt(g); count++; } } if (count == 0) return null; return sum / count; } Color _valueColor(num v) => v >= 200 ? Colors.red : Colors.black87; int _playedGames() { int c = 0; for (int g = 1; g <= gameCount; g++) { if (_hasRaw(g)) c++; } return c; } Color _totalColor(int total) { final n = _playedGames(); if (n <= 0) return Colors.blueGrey; final thresh = 200 * n; return total >= thresh ? Colors.red : Colors.blueGrey; } @override Widget build(BuildContext context) { if (!enabled) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Flexible( child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ for (int g = 1; g <= gameCount; g++) Padding( padding: const EdgeInsets.only(right: 6), child: SizedBox( width: 60, child: Column( children: [ Container( padding: const EdgeInsets.symmetric( vertical: 2, ), alignment: Alignment.center, child: Text( _hasRaw(g) ? _rawAt(g).toString() : '', style: TextStyle( fontSize: _hcAt(g) != 0 ? 11 : 17, fontWeight: _hcAt(g) != 0 ? FontWeight.w300 : FontWeight.w700, color: _valueColor(_rawAt(g)), ), ), ), // tighter spacing between raw and handicap if (_hcAt(g) != 0) Text( (_hcAt(g) > 0 ? '+${_hcAt(g)}' : _hcAt(g).toString()), style: const TextStyle( fontSize: 11, color: Colors.black54, height: 1.0, ), ), if (_hcAt(g) != 0) ...[ Align( alignment: Alignment.centerRight, child: Transform.rotate( angle: -0.2, child: Container( width: 40, height: 1, color: Colors.black26, ), ), ), Align( alignment: Alignment.centerRight, child: Text( _totalWithPerGameHcAt(g).toString(), style: TextStyle( fontSize: 17, fontWeight: FontWeight.w700, color: _valueColor( _totalWithPerGameHcAt(g), ), ), ), ), ], ], ), ), ), ], ), ), ), const SizedBox(width: 4), Align( alignment: Alignment.topRight, child: IntrinsicWidth( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: Colors.blueGrey.withOpacity(0.10), border: Border.all( color: Colors.blueGrey.withOpacity(0.25), ), borderRadius: BorderRadius.circular(12), ), child: Builder( builder: (context) { final hasGameHc = _hasAnyPerGameHandicap(); final raw = _overallRawTotal(); if (hasGameHc) { final withHc = _overallWithPerGameHcTotal(); if (withHc != raw) { return Text( '$raw ($withHc)', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: _totalColor(withHc), ), ); } } return Text( '$raw', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: _totalColor(raw), ), ); }, ), ), const SizedBox(height: 6), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: Colors.indigo.withOpacity(0.10), border: Border.all( color: Colors.indigo.withOpacity(0.25), ), borderRadius: BorderRadius.circular(12), ), child: Builder( builder: (context) { final hasGameHc = _hasAnyPerGameHandicap(); final rawAvg = (eventAverage ?? _avgRaw() ?? 0) .toStringAsFixed(1); if (hasGameHc) { final withHcAvg = (_avgWithPerGameHc() ?? 0) .toStringAsFixed(1); if (withHcAvg != rawAvg) { return Text( '$rawAvg ($withHcAvg)', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: _valueColor( double.tryParse(withHcAvg) ?? 0, ), ), ); } } return Text( rawAvg, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: _valueColor( double.tryParse(rawAvg) ?? 0, ), ), ); }, ), ), ], ), ), ), ], ), ], ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Flexible( child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ for (int g = 1; g <= gameCount; g++) Padding( padding: const EdgeInsets.only(right: 6), child: SizedBox( width: 60, child: Column( children: [ Stack( clipBehavior: Clip.none, children: [ TextFormField( key: Key( 'cell_${participantId}_${g}_${_rawAt(g)}', ), initialValue: (g - 1 < rawScores.length && rawScores[g - 1] != null) ? rawScores[g - 1].toString() : '', keyboardType: TextInputType.number, style: TextStyle( fontSize: _hcAt(g) != 0 ? 12 : 14, fontWeight: FontWeight.w600, color: _valueColor(_rawAt(g)), ), decoration: InputDecoration( isDense: true, labelText: '${g}G 점수', floatingLabelBehavior: FloatingLabelBehavior.auto, labelStyle: const TextStyle(fontSize: 10), floatingLabelStyle: const TextStyle( fontSize: 10, ), hintText: '${g}G 점수', hintStyle: const TextStyle(fontSize: 9), counterText: '', contentPadding: const EdgeInsets.symmetric( vertical: 6, horizontal: 8, ), ), maxLength: 3, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, ], enabled: enabled, onChanged: (v) { if (v.isEmpty) { onScoreChanged(g, 0); return; } final val = int.tryParse(v); if (val != null && val >= 0 && val <= 300) { onScoreChanged(g, val); } }, ), Positioned( top: -8, right: -8, child: IgnorePointer( child: stateIcon( keyFor(participantId, g), ), ), ), ], ), const SizedBox(height: 4), TextFormField( key: Key( 'cell_hc_${participantId}_${g}_${_hcAt(g)}', ), initialValue: (() { int hv = (g - 1 < handicaps.length) ? handicaps[g - 1] : 0; if (hv == 0 && (memberHandicap ?? 0) != 0) { return memberHandicap!.toString(); } return hv.toString(); })(), keyboardType: TextInputType.number, decoration: InputDecoration( isDense: true, labelText: '${g}G 핸디캡', floatingLabelBehavior: FloatingLabelBehavior.auto, labelStyle: const TextStyle(fontSize: 10), floatingLabelStyle: const TextStyle( fontSize: 10, ), hintText: '${g}G 핸디캡', hintStyle: const TextStyle(fontSize: 9), counterText: '', contentPadding: const EdgeInsets.symmetric( vertical: 4, horizontal: 8, ), ), maxLength: 3, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, ], enabled: enabled, onChanged: (v) { if (v.isEmpty) { onHandicapChanged(g, 0); return; } final val = int.tryParse(v); if (val != null && val >= 0 && val <= 100) { onHandicapChanged(g, val); } }, ), const SizedBox(height: 2), if (_hcAt(g) != 0) ...[ Align( alignment: Alignment.centerRight, child: Transform.rotate( angle: -0.2, child: Container( width: 40, height: 1, color: Colors.black26, ), ), ), Align( alignment: Alignment.centerRight, child: Text( _totalWithPerGameHcAt(g).toString(), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: _valueColor( _totalWithPerGameHcAt(g), ), ), ), ), ], ], ), ), ), ], ), ), ), const SizedBox(width: 4), Align( alignment: Alignment.topRight, child: IntrinsicWidth( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: Colors.blueGrey.withOpacity(0.10), border: Border.all( color: Colors.blueGrey.withOpacity(0.25), ), borderRadius: BorderRadius.circular(12), ), child: Builder( builder: (context) { final hasGameHc = _hasAnyPerGameHandicap(); final raw = _overallRawTotal(); if (hasGameHc) { final withHc = _overallWithPerGameHcTotal(); if (withHc != raw) { return Text( '$raw ($withHc)', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: _totalColor(withHc), ), ); } } return Text( '$raw', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: _totalColor(raw), ), ); }, ), ), const SizedBox(height: 6), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: Colors.indigo.withOpacity(0.10), border: Border.all( color: Colors.indigo.withOpacity(0.25), ), borderRadius: BorderRadius.circular(12), ), child: Builder( builder: (context) { final hasGameHc = _hasAnyPerGameHandicap(); final rawAvg = (eventAverage ?? _avgRaw() ?? 0) .toStringAsFixed(1); if (hasGameHc) { final withHcAvg = (_avgWithPerGameHc() ?? 0) .toStringAsFixed(1); if (withHcAvg != rawAvg) { return Text( '$rawAvg ($withHcAvg)', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: _valueColor( double.tryParse(withHcAvg) ?? 0, ), ), ); } } return Text( rawAvg, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: _valueColor(double.tryParse(rawAvg) ?? 0), ), ); }, ), ), ], ), ), ), ], ), ], ); } } class PublicUserChipsRow extends StatelessWidget { final String? status; final String? paymentStatus; final String? memberType; const PublicUserChipsRow({ super.key, this.status, this.paymentStatus, this.memberType, }); bool _isPaid(String? paymentStatus) { final s = (paymentStatus ?? '').toLowerCase(); return s == 'paid' || s == '납부' || s == '납부완료' || s == '완납'; } @override Widget build(BuildContext context) { return Wrap( spacing: 6, runSpacing: 6, children: [ BadgeStyles.participantStatus(toKoreanParticipantStatus(status)), if ((paymentStatus ?? '').isNotEmpty) BadgeStyles.payment(_isPaid(paymentStatus)), if ((memberType ?? '').isNotEmpty) BadgeStyles.memberType(getMemberTypeLabel(memberType)), ], ); } }