import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../../models/event_model.dart'; import '../../../models/team_model.dart'; import '../../../models/participant_model.dart'; import '../../../models/score_model.dart'; import '../../../theme/badges.dart'; import '../../../utils/enum_mappings.dart'; class TeamsTab extends StatelessWidget { final Event event; final bool isLoading; final List teams; final List participants; final List scores; final int teamGenerationMethod; final TextEditingController teamSizeController; final TextEditingController teamCountController; final List unassignedParticipants; final ValueChanged onChangeTeamGenerationMethod; final VoidCallback onGenerateTeams; final VoidCallback onSaveTeams; final void Function(Team team, Participant participant) onRemoveFromTeam; final void Function(Team team) onShowParticipantSelectionDialog; final void Function(Participant participant) onShowTeamSelectionDialog; final double Function(Team team) calculateTeamAverage; final VoidCallback onShareEvent; final void Function( Participant participant, Team fromTeam, Team toTeam, int toIndex, )? onMoveParticipant; const TeamsTab({ super.key, required this.event, required this.isLoading, required this.teams, required this.participants, required this.scores, required this.teamGenerationMethod, required this.teamSizeController, required this.teamCountController, required this.unassignedParticipants, required this.onChangeTeamGenerationMethod, required this.onGenerateTeams, required this.onSaveTeams, required this.onRemoveFromTeam, required this.onShowParticipantSelectionDialog, required this.onShowTeamSelectionDialog, required this.calculateTeamAverage, required this.onShareEvent, this.onMoveParticipant, }); @override Widget build(BuildContext context) { if (teams.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.groups, size: 64, color: Colors.blue), const SizedBox(height: 16), const Text( '생성된 팀이 없습니다.', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 24), Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '팀 생성 옵션', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), Row( children: [ Radio( value: 1, groupValue: teamGenerationMethod, onChanged: (value) { if (value != null) { onChangeTeamGenerationMethod(value); } }, ), const Text('팀 인원수로 나누기'), ], ), if (teamGenerationMethod == 1) Padding( padding: const EdgeInsets.only(left: 32.0), child: Row( children: [ SizedBox( width: 80, child: TextFormField( controller: teamSizeController, decoration: const InputDecoration( labelText: '인원수', suffixText: '명', ), keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, ], ), ), const SizedBox(width: 16), Text('총 ${participants.length}명 참가자'), ], ), ), const SizedBox(height: 8), Row( children: [ Radio( value: 2, groupValue: teamGenerationMethod, onChanged: (value) { if (value != null) { onChangeTeamGenerationMethod(value); } }, ), const Text('팀 개수로 나누기'), ], ), if (teamGenerationMethod == 2) Padding( padding: const EdgeInsets.only(left: 32.0), child: Row( children: [ SizedBox( width: 80, child: TextFormField( controller: teamCountController, decoration: const InputDecoration( labelText: '팀 수', suffixText: '개', ), keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, ], ), ), const SizedBox(width: 16), Text('총 ${participants.length}명 참가자'), ], ), ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton.icon( onPressed: isLoading ? null : onGenerateTeams, icon: isLoading ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Icon(Icons.group_add), label: Text(isLoading ? '생성 중...' : '팀 생성하기'), style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 12, ), ), ), ], ), ], ), ), ), ], ), ), ); } return Column( children: [ if (event.description != null && event.description!.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '이벤트 설명', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Text(event.description!), ], ), ), if (unassignedParticipants.isNotEmpty) Card( margin: const EdgeInsets.all(8), color: Colors.amber.shade50, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.all(8.0), child: Row( children: [ Icon(Icons.person_off, color: Colors.amber), SizedBox(width: 8), Text( '미배정 참가자', style: TextStyle(fontWeight: FontWeight.bold), ), ], ), ), const Divider(height: 1), // 2열 그리드 + 검색/정렬/필터 Padding( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 6, ), child: Builder( builder: (context) { final query = ValueNotifier(''); final sortKey = ValueNotifier( 'name', ); // name|hcp|avg final genderFilter = ValueNotifier( 'all', ); // all|male|female List apply(List src) { var list = List.from(src); // search if (query.value.isNotEmpty) { list = list .where( (p) => (p.name ?? '').toLowerCase().contains( query.value, ), ) .toList(); } // gender filter if (genderFilter.value != 'all') { list = list.where((p) { final g = (p.gender ?? '').toLowerCase(); final isFemale = g == 'female' || g == 'f' || g == '여' || g == '여성'; return genderFilter.value == 'female' ? isFemale : !isFemale; }).toList(); } // sort list.sort((a, b) { if (sortKey.value == 'hcp') { int ah = a.handicap ?? (scores .where( (s) => s.memberId == a.memberId && s.handicap != null, ) .fold( null, (prev, s) => prev == null || s.date.isAfter(prev.date) ? s : prev, ) ?.handicap ?? 0); int bh = b.handicap ?? (scores .where( (s) => s.memberId == b.memberId && s.handicap != null, ) .fold( null, (prev, s) => prev == null || s.date.isAfter(prev.date) ? s : prev, ) ?.handicap ?? 0); return bh.compareTo(ah); } else if (sortKey.value == 'avg') { double aa = a.average ?? 0; double bb = b.average ?? 0; return bb.compareTo(aa); } else { return (a.name ?? '').compareTo(b.name ?? ''); } }); return list; } Widget chipFor(Participant participant) { return Draggable>( data: { 'participant': participant, 'fromTeam': Team( id: 'unassigned', eventId: event.id, name: '미배정', memberIds: const [], ), }, feedback: Material( type: MaterialType.transparency, child: _UnassignedChip( participant: participant, scores: scores, ), ), childWhenDragging: const Opacity( opacity: 0.3, child: SizedBox.shrink(), ), child: _UnassignedChip( participant: participant, scores: scores, ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: TextField( decoration: const InputDecoration( hintText: '이름 검색', prefixIcon: Icon(Icons.search), isDense: true, border: OutlineInputBorder(), ), onChanged: (v) => query.value = v.trim().toLowerCase(), ), ), const SizedBox(width: 8), DropdownButton( value: sortKey.value, items: const [ DropdownMenuItem( value: 'name', child: Text('이름순'), ), DropdownMenuItem( value: 'hcp', child: Text('핸디 높은순'), ), DropdownMenuItem( value: 'avg', child: Text('AVG 높은순'), ), ], onChanged: (v) { if (v != null) sortKey.value = v; }, ), const SizedBox(width: 8), DropdownButton( value: genderFilter.value, items: const [ DropdownMenuItem( value: 'all', child: Text('전체 성별'), ), DropdownMenuItem( value: 'male', child: Text('남성'), ), DropdownMenuItem( value: 'female', child: Text('여성'), ), ], onChanged: (v) { if (v != null) genderFilter.value = v; }, ), ], ), const SizedBox(height: 8), SizedBox( height: 200, child: AnimatedBuilder( animation: Listenable.merge([ query, sortKey, genderFilter, ]), builder: (context, _) { final list = apply(unassignedParticipants); return GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 6, crossAxisSpacing: 6, childAspectRatio: 3.8, ), itemCount: list.length, itemBuilder: (context, i) => chipFor(list[i]), ); }, ), ), ], ); }, ), ), ], ), ), Expanded( child: ListView.builder( itemCount: teams.length, itemBuilder: (context, index) { final team = teams[index]; final teamAverage = calculateTeamAverage(team); final memberCount = team.memberIds.length; return DragTarget>( onWillAcceptWithDetails: (details) => details.data['participant'] is Participant, onAcceptWithDetails: (details) { final Participant p = details.data['participant'] as Participant; final Team from = details.data['fromTeam'] as Team; if (onMoveParticipant != null) { onMoveParticipant!(p, from, team, team.memberIds.length); } }, builder: (context, candidateData, rejected) => Card( margin: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), child: ExpansionTile( leading: CircleAvatar( backgroundColor: Colors.blue.shade700, child: Text( '${index + 1}', style: const TextStyle(color: Colors.white), ), ), title: Row( children: [ Text( '팀 ${team.name}', style: const TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: Colors.blue.shade100, borderRadius: BorderRadius.circular(12), ), child: Text('$memberCount명'), ), ], ), subtitle: Wrap( spacing: 4, crossAxisAlignment: WrapCrossAlignment.center, children: [ const Icon( Icons.bar_chart, size: 16, color: Colors.blue, ), Text('팀 에버리지: ${teamAverage.toStringAsFixed(1)}'), ], ), children: [ // 상단 드롭존: 리스트 맨 앞(인덱스 0)에 삽입 DragTarget>( onWillAcceptWithDetails: (details) => details.data['participant'] is Participant, onAcceptWithDetails: (details) { final Participant dp = details.data['participant'] as Participant; final Team from = details.data['fromTeam'] as Team; if (onMoveParticipant != null) { onMoveParticipant!(dp, from, team, 0); } }, builder: (context, candidateData, rejected) { final highlight = candidateData.isNotEmpty; return AnimatedContainer( duration: const Duration(milliseconds: 150), curve: Curves.easeOut, height: highlight ? 16 : 8, color: highlight ? Colors.blue.shade100 : Colors.transparent, ); }, ), ...team.memberIds.asMap().entries.map((entry) { final memberIndex = entry.key; final memberId = entry.value; // 표시는 간단히 이름만. 상세 평균은 외부 계산을 유지하려면 상위에서 데이터를 내려주거나 콜백을 추가. final p = participants.firstWhere( (pp) => pp.memberId == memberId, orElse: () => Participant( id: 'unknown_$memberId', eventId: event.id, memberId: memberId, ), ); // subtitle는 배지 UI로 대체됨 // 행 자체를 드롭 타깃으로 만들어 해당 위치로 삽입(같은 팀 내 재정렬 및 타팀에서 특정 위치 삽입 지원) return DragTarget>( onWillAcceptWithDetails: (details) => details.data['participant'] is Participant, onAcceptWithDetails: (details) { final Participant dp = details.data['participant'] as Participant; final Team from = details.data['fromTeam'] as Team; if (onMoveParticipant != null) { onMoveParticipant!(dp, from, team, memberIndex); } }, builder: (context, candidateData, rejected) { final highlight = candidateData.isNotEmpty; return AnimatedContainer( duration: const Duration(milliseconds: 150), curve: Curves.easeOut, decoration: highlight ? BoxDecoration( color: Colors.blue.shade50, border: Border( top: BorderSide( color: Colors.blue.shade200, width: 2, ), bottom: BorderSide( color: Colors.blue.shade200, width: 2, ), ), ) : const BoxDecoration(), child: Dismissible( key: ValueKey( 'team_${team.id}_member_${p.memberId}', ), direction: DismissDirection.endToStart, background: const SizedBox.shrink(), secondaryBackground: Container( padding: const EdgeInsets.symmetric( horizontal: 16, ), alignment: Alignment.centerRight, color: Colors.red.shade50, child: const Icon( Icons.delete, color: Colors.red, ), ), onDismissed: (_) => onRemoveFromTeam(team, p), child: ListTile( leading: Builder( builder: (_) { final gender = (p.gender ?? '') .toLowerCase(); final bool isFemale = gender == 'female' || gender == 'f' || gender == '여' || gender == '여성'; final icon = isFemale ? Icons.female : Icons.male; final color = isFemale ? Colors.pink : Colors.blue; return CircleAvatar( backgroundColor: color.withOpacity(0.1), child: Icon( icon, size: 18, color: color, ), ); }, ), title: Row( children: [ Expanded( child: Text( p.name ?? '알 수 없음', maxLines: 1, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 6), // Guest small badge next to name if ((p.memberType ?? '').toLowerCase() == 'guest') Padding( padding: const EdgeInsets.only(right: 2), child: BadgeStyles.guestSmall(size: 16), ), // Compact badges next to name Builder( builder: (_) { int? hcp = p.handicap; if (hcp == null) { Score? latest = scores .where( (s) => s.memberId == p.memberId && s.handicap != null, ) .fold( null, (prev, s) => prev == null || s.date.isAfter( prev.date, ) ? s : prev, ); hcp = latest?.handicap; } if (hcp != null && hcp != 0) { final String txt = hcp > 0 ? '+$hcp' : '$hcp'; 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( txt, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Colors.deepPurple, ), ), ); } return const SizedBox.shrink(); }, ), // member type badge (non-guest types only) if ((p.memberType ?? '').isNotEmpty && (p.memberType ?? '').toLowerCase() != 'guest') ...[ const SizedBox(width: 4), BadgeStyles.memberType(getMemberTypeLabel(p.memberType)), ], if (p.average != null) Padding( padding: const EdgeInsets.only( left: 4, ), child: Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: Colors.indigo.withValues( alpha: 0.10, ), borderRadius: BorderRadius.circular(12), border: Border.all( color: Colors.indigo.withValues( alpha: 0.25, ), ), ), child: Text( p.average!.toStringAsFixed(1), style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Colors.indigo, ), ), ), ), ], ), subtitle: const SizedBox.shrink(), trailing: Draggable>( data: {'participant': p, 'fromTeam': team}, feedback: Material( type: MaterialType.transparency, child: ConstrainedBox( constraints: const BoxConstraints( maxWidth: 320, ), child: Transform.scale( scale: 1.05, child: Card( elevation: 6, clipBehavior: Clip.antiAlias, child: ListTile( leading: Builder( builder: (_) { final gender = (p.gender ?? '') .toLowerCase(); final bool isFemale = gender == 'female' || gender == 'f' || gender == '여' || gender == '여성'; final icon = isFemale ? Icons.female : Icons.male; final color = isFemale ? Colors.pink : Colors.blue; return CircleAvatar( backgroundColor: color .withOpacity(0.1), child: Icon( icon, size: 18, color: color, ), ); }, ), title: Row( children: [ Expanded( child: Text( p.name ?? '알 수 없음', maxLines: 1, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 6), Builder( builder: (_) { int? hcp = p.handicap; if (hcp == null || hcp <= 0) { Score? latest = scores .where( (s) => s.memberId == p.memberId && s.handicap != null && s.handicap! > 0, ) .fold( null, (prev, s) => prev == null || s.date.isAfter( prev.date, ) ? s : prev, ); hcp = latest?.handicap; } if (hcp != null && hcp > 0) { return Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: Colors .deepPurple .withValues( alpha: 0.10, ), borderRadius: BorderRadius.circular( 12, ), border: Border.all( color: Colors .deepPurple .withValues( alpha: 0.25, ), ), ), child: Text( 'HCP $hcp', style: const TextStyle( fontSize: 11, fontWeight: FontWeight .w600, color: Colors .deepPurple, ), ), ); } return const SizedBox.shrink(); }, ), if (p.average != null) Padding( padding: const EdgeInsets.only( left: 4, ), child: Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: Colors.indigo .withValues( alpha: 0.10, ), borderRadius: BorderRadius.circular( 12, ), border: Border.all( color: Colors.indigo .withValues( alpha: 0.25, ), ), ), child: Text( p.average! .toStringAsFixed( 1, ), style: const TextStyle( fontSize: 11, fontWeight: FontWeight .w600, color: Colors .indigo, ), ), ), ), ], ), subtitle: Wrap( spacing: 6, runSpacing: -6, children: [ Builder( builder: (_) { int? hcp = p.handicap; if (hcp == null || hcp <= 0) { Score? latest = scores .where( (s) => s.memberId == p.memberId && s.handicap != null && s.handicap! > 0, ) .fold( null, (prev, s) => prev == null || s.date.isAfter( prev.date, ) ? s : prev, ); hcp = latest?.handicap; } if (hcp != null && hcp > 0) { return Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: Colors .deepPurple .withValues( alpha: 0.10, ), borderRadius: BorderRadius.circular( 12, ), border: Border.all( color: Colors .deepPurple .withValues( alpha: 0.25, ), ), ), child: Text( 'HCP $hcp', style: const TextStyle( fontSize: 12, fontWeight: FontWeight .w600, color: Colors .deepPurple, ), ), ); } return const SizedBox.shrink(); }, ), if (p.average != null) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: Colors.indigo .withValues( alpha: 0.10, ), borderRadius: BorderRadius.circular( 12, ), border: Border.all( color: Colors.indigo .withValues( alpha: 0.25, ), ), ), child: Text( p.average! .toStringAsFixed(1), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.indigo, ), ), ), ], ), ), ), ), ), ), childWhenDragging: const Opacity( opacity: 0.3, child: Icon( Icons.drag_indicator, color: Colors.blueGrey, ), ), child: const Icon( Icons.drag_indicator, color: Colors.blueGrey, ), ), ), ), ); }, ); }), // 하단 드롭존: 리스트 맨 끝(인덱스 length)에 삽입 DragTarget>( onWillAcceptWithDetails: (details) => details.data['participant'] is Participant, onAcceptWithDetails: (details) { final Participant dp = details.data['participant'] as Participant; final Team from = details.data['fromTeam'] as Team; if (onMoveParticipant != null) { onMoveParticipant!( dp, from, team, team.memberIds.length, ); } }, builder: (context, candidateData, rejected) { final highlight = candidateData.isNotEmpty; return AnimatedContainer( duration: const Duration(milliseconds: 150), curve: Curves.easeOut, height: highlight ? 16 : 8, color: highlight ? Colors.blue.shade100 : Colors.transparent, ); }, ), if (unassignedParticipants.isNotEmpty) ListTile( leading: const Icon( Icons.add_circle, color: Colors.green, ), title: const Text('팀원 추가하기'), onTap: () => onShowParticipantSelectionDialog(team), ), ], ), ), ); }, ), ), ], ); } } class _UnassignedChip extends StatelessWidget { final Participant participant; final List scores; const _UnassignedChip({required this.participant, required this.scores}); @override Widget build(BuildContext context) { final gender = (participant.gender ?? '').toLowerCase(); final bool isFemale = gender == 'female' || gender == 'f' || gender == '여' || gender == '여성'; final icon = isFemale ? Icons.female : Icons.male; final color = isFemale ? Colors.pink : Colors.blue; int? hcp = participant.handicap; if (hcp == null) { final latest = scores .where( (s) => s.memberId == participant.memberId && s.handicap != null, ) .fold( null, (prev, s) => prev == null || s.date.isAfter(prev.date) ? s : prev, ); hcp = latest?.handicap; } return Chip( avatar: CircleAvatar( backgroundColor: color.withOpacity(0.1), child: Icon(icon, size: 16, color: color), ), label: Row( mainAxisSize: MainAxisSize.max, children: [ Expanded( child: Text( participant.name ?? '알 수 없음', maxLines: 1, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 4), Flexible( child: FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerRight, child: Row( mainAxisSize: MainAxisSize.min, children: [ if (hcp != null && hcp != 0) 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( hcp > 0 ? '+$hcp' : '$hcp', style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Colors.deepPurple, ), ), ), if (participant.average != null) Padding( padding: const EdgeInsets.only(left: 4), child: 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( participant.average!.toStringAsFixed(1), style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Colors.indigo, ), ), ), ), ], ), ), ), ], ), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ); } }