This commit is contained in:
2025-10-29 01:18:29 +09:00
parent 1047d3089d
commit 7677007b49
6 changed files with 338 additions and 61 deletions
@@ -32,6 +32,79 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
final Map<String, Timer> _postTimers = {};
_ResultSort _resultSort = _ResultSort.total;
String _statusLabel(EventStatus s) {
switch (s) {
case EventStatus.ready:
return 'ready';
case EventStatus.active:
return 'active';
case EventStatus.completed:
return 'completed';
case EventStatus.canceled:
return 'canceled';
default:
return 'unknown';
}
}
Color _statusColor(EventStatus s) {
switch (s) {
case EventStatus.ready:
return Colors.blue.shade50;
case EventStatus.active:
return Colors.green.shade50;
case EventStatus.completed:
return Colors.grey.shade200;
case EventStatus.canceled:
return Colors.red.shade50;
default:
return Colors.grey.shade100;
}
}
String _typeLabel(EventType t) {
switch (t) {
case EventType.regular:
return 'regular';
case EventType.tournament:
return 'tournament';
case EventType.practice:
return 'practice';
case EventType.league:
return 'league';
default:
return 'unknown';
}
}
String _participantStatusLabel(ParticipantStatus s) {
switch (s) {
case ParticipantStatus.pending:
return 'pending';
case ParticipantStatus.confirmed:
return 'confirmed';
case ParticipantStatus.canceled:
return 'canceled';
default:
return 'unknown';
}
}
String _paymentLabel(PaymentStatus s) {
switch (s) {
case PaymentStatus.paid:
return 'paid';
case PaymentStatus.unpaid:
return 'unpaid';
case PaymentStatus.refunded:
return 'refunded';
case PaymentStatus.partial:
return 'partial';
default:
return 'unknown';
}
}
@override
void initState() {
super.initState();
@@ -385,10 +458,13 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
spacing: 8,
runSpacing: 8,
children: [
if (event.status != null)
Chip(key: const Key('status_chip'), label: Text(event.status!)),
Chip(
key: const Key('status_chip'),
label: Text(_statusLabel(event.statusEnum)),
backgroundColor: _statusColor(event.statusEnum),
),
if (event.eventType != null && event.eventType!.isNotEmpty)
Chip(key: const Key('type_chip'), label: Text(event.eventType!)),
Chip(key: const Key('type_chip'), label: Text(_typeLabel(event.typeEnum))),
if (event.startDate != null || event.endDate != null)
Chip(
key: const Key('period_chip'),
@@ -402,32 +478,52 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
Chip(key: const Key('deadline_chip'), label: Text('신청마감 ' + event.registrationDeadline!.toLocal().toString().split('.').first)),
if (event.participantFee != null)
Chip(key: const Key('fee_chip'), label: Text('참가비 ₩' + event.participantFee!.toString())),
if (event.location != null && event.location!.isNotEmpty)
Chip(key: const Key('location_chip'), label: Text(event.location!)),
],
),
const SizedBox(height: 8),
],
const SizedBox(height: 8),
const Text('참가자', style: TextStyle(fontWeight: FontWeight.bold), key: Key('section_participants')),
Text('' + participants.length.toString() + '', key: const Key('participants_count')),
const SizedBox(height: 8),
Card(
child: participants.isEmpty
? const ListTile(
key: Key('empty_participants'),
title: Text('참가자가 없습니다'),
)
: Column(
children: [
for (final p in participants)
ListTile(
if (teams.isEmpty) ...[
const SizedBox(height: 8),
const Text('참가자', style: TextStyle(fontWeight: FontWeight.bold), key: Key('section_participants')),
Text('' + participants.length.toString() + '', key: const Key('participants_count')),
const SizedBox(height: 8),
Card(
child: participants.isEmpty
? const ListTile(
key: Key('empty_participants'),
title: Text('참가자가 없습니다'),
)
: Column(
children: [
for (final p in participants)
ListTile(
key: Key('participant_tile_' + p.participantId),
title: Text(p.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
(p.status ?? '-') + ' · avg:' + (p.average?.toString() ?? '-') + ' · hc:' + p.handicap.toString(),
key: Key('participant_info_' + p.participantId),
Wrap(
spacing: 6,
runSpacing: 6,
children: [
Chip(
key: Key('participant_status_' + p.participantId),
label: Text(_participantStatusLabel(p.statusEnum)),
),
if (p.paymentStatus != null && p.paymentStatus!.isNotEmpty)
Chip(
key: Key('participant_payment_' + p.participantId),
label: Text(_paymentLabel(p.paymentEnum)),
),
if (p.memberType != null && p.memberType!.isNotEmpty)
Chip(
key: Key('participant_type_' + p.participantId),
label: Text(p.memberType!),
),
Text('avg:' + (p.average?.toString() ?? '-') + ' · hc:' + p.handicap.toString(), key: Key('participant_info_' + p.participantId)),
],
),
if (canInputScore && event != null)
Padding(
@@ -438,7 +534,7 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
Padding(
padding: const EdgeInsets.only(right: 6),
child: SizedBox(
width: 54,
width: 60,
child: Column(
children: [
Row(
@@ -466,7 +562,7 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
isDense: true,
hintText: '---',
counterText: '',
contentPadding: EdgeInsets.symmetric(vertical: 6, horizontal: 6),
contentPadding: EdgeInsets.symmetric(vertical: 6, horizontal: 8),
),
maxLength: 3,
onChanged: (v) => _onScoreChanged(p, g, v),
@@ -480,10 +576,25 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
),
],
),
trailing: Text(
'R ' + (partRankMap[p.participantId]?['rank']?.toString() ?? '-') +
' · 총 ' + PublicRankingUtils.participantTotalWithHc(p).toString(),
key: Key('participant_total_' + p.participantId),
trailing: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(
key: Key('participant_rank_' + p.participantId),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(10),
),
child: Text('R ' + (partRankMap[p.participantId]?['rank']?.toString() ?? '-'), style: const TextStyle(fontSize: 12)),
),
const SizedBox(height: 6),
Text(
'' + PublicRankingUtils.participantTotalWithHc(p).toString(),
key: Key('participant_total_' + p.participantId),
),
],
),
onTap: () {
if (!canInputScore) {
@@ -492,10 +603,11 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
}
_openScoreDialog(p.participantId, initial: p.rawScores.isNotEmpty ? p.rawScores[0] : null);
},
),
],
),
),
),
],
),
),
],
const SizedBox(height: 24),
const Text('', style: TextStyle(fontWeight: FontWeight.bold), key: Key('section_teams')),
Text('' + teams.length.toString() + '', key: const Key('teams_count')),
@@ -509,41 +621,88 @@ class _PublicEventScreenState extends State<PublicEventScreen> {
: Column(
children: [
for (final t in teams)
ListTile(
title: Row(
Padding(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('' + t.teamNumber.toString()),
const SizedBox(width: 6),
Container(
key: Key('team_rank_' + t.teamNumber.toString()),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(10),
),
child: Text('R ' + (teamRankMap[t.teamNumber]?['rank']?.toString() ?? '-'), style: const TextStyle(fontSize: 12)),
Row(
children: [
Text('' + t.teamNumber.toString()),
const SizedBox(width: 6),
Container(
key: Key('team_rank_' + t.teamNumber.toString()),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(10),
),
child: Text('R ' + (teamRankMap[t.teamNumber]?['rank']?.toString() ?? '-'), style: const TextStyle(fontSize: 12)),
),
const Spacer(),
SizedBox(
width: 100,
child: TextFormField(
key: Key('team_hc_input_' + t.teamNumber.toString()),
initialValue: t.handicap.toString(),
keyboardType: TextInputType.number,
decoration: const InputDecoration(
isDense: true,
labelText: '팀 HC',
counterText: '',
),
maxLength: 3,
onChanged: (v) async {
final val = int.tryParse(v);
if (val == null || val < 0 || val > 300) return;
try {
await _service.updateTeamHandicap(widget.publicHash, teamNumber: t.teamNumber, handicap: val);
// 로컬 반영
final idx = _teams.indexWhere((tt) => tt.teamNumber == t.teamNumber);
if (idx >= 0) {
_teams[idx] = PublicTeam(teamId: _teams[idx].teamId, teamNumber: _teams[idx].teamNumber, handicap: val, members: _teams[idx].members);
setState(() {});
}
} catch (_) {}
},
),
),
],
),
const SizedBox(height: 6),
Builder(builder: (context) {
int membersTotal = 0;
for (final m in t.members) {
int mt = 0;
for (int i = 0; i < m.rawScores.length; i++) {
final raw = m.rawScores[i];
final hc = (i < m.handicaps.length) ? m.handicaps[i] : 0;
if (raw == null) continue;
mt += PublicRankingUtils.capScore(raw + hc);
}
membersTotal += mt;
}
final total = membersTotal + t.handicap;
final avg = t.members.isNotEmpty ? (total / t.members.length).toStringAsFixed(1) : '-';
return Text(
'' + total.toString() + ' · 평균 ' + avg + ' (팀 HC ' + t.handicap.toString() + ')',
key: Key('team_summary_' + t.teamNumber.toString()),
);
}),
const SizedBox(height: 6),
// 팀 구성원 리스트
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final m in t.members)
Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text('' + m.name),
),
],
),
],
),
subtitle: Builder(builder: (context) {
int membersTotal = 0;
for (final m in t.members) {
int mt = 0;
for (int i = 0; i < m.rawScores.length; i++) {
final raw = m.rawScores[i];
final hc = (i < m.handicaps.length) ? m.handicaps[i] : 0;
if (raw == null) continue;
mt += PublicRankingUtils.capScore(raw + hc);
}
membersTotal += mt;
}
final total = membersTotal + t.handicap;
final avg = t.members.isNotEmpty ? (total / t.members.length).toStringAsFixed(1) : '-';
return Text(
'' + total.toString() + ' · 평균 ' + avg + ' (팀 HC ' + t.handicap.toString() + ')',
key: Key('team_summary_' + t.teamNumber.toString()),
);
}),
),
],
),