1004 lines
40 KiB
Dart
1004 lines
40 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:data_table_2/data_table_2.dart';
|
|
import 'package:lanebow/models/public_event.dart';
|
|
import 'package:lanebow/models/public_participant.dart';
|
|
import 'package:lanebow/models/public_team.dart';
|
|
import 'package:lanebow/widgets/public_user_widgets.dart';
|
|
import 'package:lanebow/theme/badges.dart';
|
|
|
|
class PersonalCardView extends StatelessWidget {
|
|
final PublicEvent? event;
|
|
final List<PublicParticipant> participants;
|
|
final Map<String, Map> partRankMap;
|
|
final bool canInputScore;
|
|
final List<PublicTeam> teams;
|
|
final String Function(String pid, int gameNumber) keyFor;
|
|
final Widget Function(String key) stateIcon;
|
|
final void Function(PublicParticipant p, int gameNumber, int value)
|
|
saveInlineScore;
|
|
final void Function(PublicParticipant p, int gameNumber, int value)
|
|
saveInlineHandicap;
|
|
final num? Function(List<int?> rawScores) calcAvg;
|
|
final int? Function(String pid, Map<String, Map> map) rankForParticipant;
|
|
|
|
const PersonalCardView({
|
|
super.key,
|
|
required this.event,
|
|
required this.participants,
|
|
required this.partRankMap,
|
|
required this.canInputScore,
|
|
required this.teams,
|
|
required this.keyFor,
|
|
required this.stateIcon,
|
|
required this.saveInlineScore,
|
|
required this.saveInlineHandicap,
|
|
required this.calcAvg,
|
|
required this.rankForParticipant,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// 팀의 per-game 핸디 우선 적용을 위해 map 빌드
|
|
final Map<String, List<int>> teamHcByPid = {};
|
|
for (final t in teams) {
|
|
for (final m in t.members) {
|
|
if ((m.participantId ?? '').isNotEmpty) {
|
|
teamHcByPid[m.participantId!] = List<int>.from(m.handicaps);
|
|
}
|
|
}
|
|
}
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'참가자',
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
key: Key('section_participants'),
|
|
),
|
|
Text('총 ${participants.length}명', key: const Key('participants_count')),
|
|
const SizedBox(height: 8),
|
|
Expanded(
|
|
child: Card(
|
|
child: participants.isEmpty
|
|
? const ListTile(
|
|
key: Key('empty_participants'),
|
|
title: Text('참가자가 없습니다'),
|
|
)
|
|
: ListView.separated(
|
|
padding: EdgeInsets.zero,
|
|
itemCount: participants.length,
|
|
separatorBuilder: (_, i) => const Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
|
child: Divider(height: 12, thickness: 0.6),
|
|
),
|
|
itemBuilder: (_, i) {
|
|
final p = participants[i];
|
|
return ListTile(
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
key: Key('participant_tile_${p.participantId}'),
|
|
title: PublicUserNameRow(
|
|
name: p.name,
|
|
gender: p.gender,
|
|
memberType: p.memberType,
|
|
userHandicap: p.handicap,
|
|
userAverage: p.average,
|
|
rank: rankForParticipant(
|
|
p.participantId,
|
|
partRankMap,
|
|
),
|
|
),
|
|
subtitle: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
PublicUserChipsRow(
|
|
status: p.status,
|
|
paymentStatus: p.paymentStatus,
|
|
memberType: p.memberType,
|
|
),
|
|
if (event != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8),
|
|
child: PublicUserGameEditors(
|
|
gameCount: event!.gameCount,
|
|
participantId: p.participantId,
|
|
rawScores: p.rawScores,
|
|
handicaps:
|
|
teamHcByPid[p.participantId] ??
|
|
p.handicaps,
|
|
enabled: canInputScore,
|
|
keyFor: keyFor,
|
|
stateIcon: stateIcon,
|
|
onScoreChanged: (g, val) =>
|
|
saveInlineScore(p, g, val),
|
|
onHandicapChanged: (g, val) =>
|
|
saveInlineHandicap(p, g, val),
|
|
eventAverage: calcAvg(
|
|
p.rawScores,
|
|
)?.toDouble(),
|
|
memberHandicap: p.handicap,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class PersonalTableView extends StatefulWidget {
|
|
final PublicEvent event;
|
|
final List<PublicParticipant> participants;
|
|
final bool canInputScore;
|
|
final List<PublicTeam> teams;
|
|
final void Function(PublicParticipant p, int gameNumber, int value)
|
|
saveInlineScore;
|
|
final void Function(PublicParticipant p, int gameNumber, int value)
|
|
saveInlineHandicap;
|
|
final Map<String, Map>? partRankMap;
|
|
final int? Function(String pid, Map<String, Map> map)? rankForParticipant;
|
|
|
|
const PersonalTableView({
|
|
super.key,
|
|
required this.event,
|
|
required this.participants,
|
|
required this.canInputScore,
|
|
required this.teams,
|
|
required this.saveInlineScore,
|
|
required this.saveInlineHandicap,
|
|
this.partRankMap,
|
|
this.rankForParticipant,
|
|
});
|
|
|
|
@override
|
|
State<PersonalTableView> createState() => _PersonalTableViewState();
|
|
}
|
|
|
|
class _PersonalTableViewState extends State<PersonalTableView> {
|
|
int? _sortColumnIndex = 0;
|
|
bool _sortAscending = true;
|
|
List<PublicParticipant> _rows = [];
|
|
|
|
Future<void> _openEditSheet({
|
|
required PublicParticipant participant,
|
|
required int gameNumber,
|
|
required int? currentRaw,
|
|
required int currentHc,
|
|
}) async {
|
|
final rawCtrl = TextEditingController(
|
|
text: currentRaw == null || currentRaw == 0 ? '' : '$currentRaw',
|
|
);
|
|
final hcCtrl = TextEditingController(
|
|
text: currentHc == 0 ? '' : '$currentHc',
|
|
);
|
|
Future<void> _commit(BuildContext ctx) async {
|
|
FocusScope.of(ctx).unfocus();
|
|
final rawVal = int.tryParse(rawCtrl.text.trim()) ?? 0;
|
|
final hcVal = int.tryParse(hcCtrl.text.trim()) ?? 0;
|
|
final prevRaw = currentRaw ?? 0;
|
|
final prevHc = currentHc;
|
|
final changedRaw = rawVal != prevRaw;
|
|
final changedHc = hcVal != prevHc;
|
|
|
|
if (changedRaw && !changedHc) {
|
|
widget.saveInlineScore(participant, gameNumber, rawVal);
|
|
} else if (!changedRaw && changedHc) {
|
|
widget.saveInlineHandicap(participant, gameNumber, hcVal);
|
|
} else if (changedRaw && changedHc) {
|
|
// Debouncer key is the same for score/handicap; sequence to avoid cancellation
|
|
widget.saveInlineScore(participant, gameNumber, rawVal);
|
|
await Future.delayed(const Duration(milliseconds: 650));
|
|
widget.saveInlineHandicap(participant, gameNumber, hcVal);
|
|
}
|
|
Navigator.pop(ctx, true);
|
|
}
|
|
|
|
final res = await showModalBottomSheet<bool>(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
builder: (ctx) {
|
|
return Padding(
|
|
padding: EdgeInsets.only(
|
|
bottom: MediaQuery.of(ctx).viewInsets.bottom,
|
|
left: 16,
|
|
right: 16,
|
|
top: 16,
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextFormField(
|
|
controller: rawCtrl,
|
|
keyboardType: TextInputType.number,
|
|
textInputAction: TextInputAction.done,
|
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
|
decoration: const InputDecoration(labelText: '점수'),
|
|
onFieldSubmitted: (_) => _commit(ctx),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: TextFormField(
|
|
controller: hcCtrl,
|
|
keyboardType: TextInputType.number,
|
|
textInputAction: TextInputAction.done,
|
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
|
decoration: const InputDecoration(labelText: '핸디'),
|
|
onFieldSubmitted: (_) => _commit(ctx),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx, false),
|
|
child: const Text('취소'),
|
|
),
|
|
const SizedBox(width: 8),
|
|
ElevatedButton(
|
|
onPressed: () => _commit(ctx),
|
|
child: const Text('저장'),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
if (res == true) {
|
|
setState(() {});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_rows = List.of(widget.participants);
|
|
final rf = widget.rankForParticipant;
|
|
final prm = widget.partRankMap;
|
|
if (rf != null && prm != null) {
|
|
_rows.sort((a, b) {
|
|
final ra = (rf(a.participantId, prm) ?? 99999);
|
|
final rb = (rf(b.participantId, prm) ?? 99999);
|
|
return ra.compareTo(rb);
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant PersonalTableView oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (!identical(oldWidget.participants, widget.participants) ||
|
|
oldWidget.participants.length != widget.participants.length) {
|
|
_rows = List.of(widget.participants);
|
|
final rf = widget.rankForParticipant;
|
|
final prm = widget.partRankMap;
|
|
if (rf != null && prm != null) {
|
|
_rows.sort((a, b) {
|
|
final ra = (rf(a.participantId, prm) ?? 99999);
|
|
final rb = (rf(b.participantId, prm) ?? 99999);
|
|
return ra.compareTo(rb);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final event = widget.event;
|
|
final participants = widget.participants;
|
|
final bool enabled = widget.canInputScore && (event.isActive == true);
|
|
final teams = widget.teams;
|
|
final partRankMap = widget.partRankMap;
|
|
final rankForParticipant = widget.rankForParticipant;
|
|
|
|
final List<PublicParticipant> rows = _rows;
|
|
final Map<String, List<int>> teamHcByPid = {};
|
|
final Map<String, String> teamLabelByPid = {};
|
|
for (final t in teams) {
|
|
for (final m in t.members) {
|
|
if ((m.participantId ?? '').isNotEmpty) {
|
|
teamHcByPid[m.participantId!] = List<int>.from(m.handicaps);
|
|
teamLabelByPid[m.participantId!] = '팀 ${t.teamNumber}';
|
|
}
|
|
}
|
|
}
|
|
|
|
const double headH = 40.0;
|
|
const double rowH = 100.0;
|
|
|
|
int rawAt(List<int?> raw, int g) =>
|
|
(g - 1 >= 0 && g - 1 < raw.length && raw[g - 1] != null)
|
|
? raw[g - 1]!
|
|
: 0;
|
|
bool hasRaw(List<int?> raw, int g) =>
|
|
(g - 1 >= 0 && g - 1 < raw.length && raw[g - 1] != null);
|
|
int hcAt(List<int> perGame, int? memberHc, int g) {
|
|
int hv = (g - 1 >= 0 && g - 1 < perGame.length) ? perGame[g - 1] : 0;
|
|
if (hv == 0 && (memberHc ?? 0) != 0) return memberHc!;
|
|
return hv;
|
|
}
|
|
|
|
int overallRawTotal(List<int?> raw, int gameCount) {
|
|
int s = 0;
|
|
for (int g = 1; g <= gameCount; g++) {
|
|
if (hasRaw(raw, g)) s += rawAt(raw, g);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
int overallWithHcTotal(
|
|
List<int?> raw,
|
|
List<int> perGame,
|
|
int? memberHc,
|
|
int gameCount,
|
|
) {
|
|
int s = 0;
|
|
for (int g = 1; g <= gameCount; g++) {
|
|
if (hasRaw(raw, g)) s += rawAt(raw, g) + hcAt(perGame, memberHc, g);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
double? avgRaw(List<int?> raw, int gameCount) {
|
|
int count = 0, sum = 0;
|
|
for (int g = 1; g <= gameCount; g++) {
|
|
if (hasRaw(raw, g)) {
|
|
sum += rawAt(raw, g);
|
|
count++;
|
|
}
|
|
}
|
|
if (count == 0) return null;
|
|
return sum / count;
|
|
}
|
|
|
|
double? avgWithHc(
|
|
List<int?> raw,
|
|
List<int> perGame,
|
|
int? memberHc,
|
|
int gameCount,
|
|
) {
|
|
int count = 0, sum = 0;
|
|
for (int g = 1; g <= gameCount; g++) {
|
|
if (hasRaw(raw, g)) {
|
|
sum += rawAt(raw, g) + hcAt(perGame, memberHc, g);
|
|
count++;
|
|
}
|
|
}
|
|
if (count == 0) return null;
|
|
return sum / count;
|
|
}
|
|
|
|
Color valueColor(num v) => v >= 200 ? Colors.red : Colors.black87;
|
|
int playedGames(List<int?> raw, int gameCount) {
|
|
int c = 0;
|
|
for (int g = 1; g <= gameCount; g++) {
|
|
if (hasRaw(raw, g)) c++;
|
|
}
|
|
return c;
|
|
}
|
|
|
|
Color totalColor(int total, int nGames) {
|
|
if (nGames <= 0) return Colors.blueGrey;
|
|
final thresh = 200 * nGames;
|
|
return total >= thresh ? Colors.red : Colors.blueGrey;
|
|
}
|
|
|
|
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: 8,
|
|
backgroundColor: color.withValues(alpha: 0.1),
|
|
child: Icon(icon, size: 12, color: color),
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'개인별 (표 보기)',
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
key: Key('participants_table_title'),
|
|
),
|
|
Text(
|
|
'총 ${participants.length}명',
|
|
key: const Key('participants_table_count'),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Expanded(
|
|
child: DataTable2(
|
|
headingRowHeight: headH,
|
|
horizontalMargin: 0,
|
|
columnSpacing: 8,
|
|
dataRowHeight: rowH,
|
|
minWidth: 600,
|
|
fixedTopRows: 1,
|
|
fixedLeftColumns: 2,
|
|
sortColumnIndex: _sortColumnIndex,
|
|
sortAscending: _sortAscending,
|
|
border: TableBorder.all(color: Colors.black12, width: 0.5),
|
|
columns: [
|
|
DataColumn2(
|
|
fixedWidth: 55,
|
|
label: const Center(child: Text('순위')),
|
|
onSort: (i, asc) {
|
|
setState(() {
|
|
_sortColumnIndex = i;
|
|
_sortAscending = asc;
|
|
rows.sort((a, b) {
|
|
final ra =
|
|
(rankForParticipant?.call(
|
|
a.participantId,
|
|
partRankMap ?? const {},
|
|
) ??
|
|
99999);
|
|
final rb =
|
|
(rankForParticipant?.call(
|
|
b.participantId,
|
|
partRankMap ?? const {},
|
|
) ??
|
|
99999);
|
|
return asc ? ra.compareTo(rb) : rb.compareTo(ra);
|
|
});
|
|
});
|
|
},
|
|
numeric: true,
|
|
),
|
|
DataColumn2(
|
|
size: ColumnSize.L,
|
|
label: const Text('이름'),
|
|
onSort: (i, asc) {
|
|
setState(() {
|
|
_sortColumnIndex = i;
|
|
_sortAscending = asc;
|
|
rows.sort(
|
|
(a, b) => asc
|
|
? a.name.compareTo(b.name)
|
|
: b.name.compareTo(a.name),
|
|
);
|
|
});
|
|
},
|
|
),
|
|
DataColumn2(
|
|
size: ColumnSize.S,
|
|
label: const Text('팀'),
|
|
onSort: (i, asc) {
|
|
setState(() {
|
|
_sortColumnIndex = i;
|
|
_sortAscending = asc;
|
|
rows.sort((a, b) {
|
|
final ta = teamLabelByPid[a.participantId] ?? '';
|
|
final tb = teamLabelByPid[b.participantId] ?? '';
|
|
return asc ? ta.compareTo(tb) : tb.compareTo(ta);
|
|
});
|
|
});
|
|
},
|
|
),
|
|
for (int g = 1; g <= event.gameCount; g++)
|
|
DataColumn(
|
|
label: Text('${g}G'),
|
|
onSort: (i, asc) {
|
|
setState(() {
|
|
_sortColumnIndex = i;
|
|
_sortAscending = asc;
|
|
rows.sort((a, b) {
|
|
final perA =
|
|
teamHcByPid[a.participantId] ?? a.handicaps;
|
|
final perB =
|
|
teamHcByPid[b.participantId] ?? b.handicaps;
|
|
final sa = (hasRaw(a.rawScores, g)
|
|
? rawAt(a.rawScores, g) + hcAt(perA, a.handicap, g)
|
|
: -999999);
|
|
final sb = (hasRaw(b.rawScores, g)
|
|
? rawAt(b.rawScores, g) + hcAt(perB, b.handicap, g)
|
|
: -999999);
|
|
return asc ? sa.compareTo(sb) : sb.compareTo(sa);
|
|
});
|
|
});
|
|
},
|
|
numeric: true,
|
|
),
|
|
DataColumn(
|
|
label: const Text('총점'),
|
|
onSort: (i, asc) {
|
|
setState(() {
|
|
_sortColumnIndex = i;
|
|
_sortAscending = asc;
|
|
rows.sort((a, b) {
|
|
final perGameA =
|
|
teamHcByPid[a.participantId] ?? a.handicaps;
|
|
final perGameB =
|
|
teamHcByPid[b.participantId] ?? b.handicaps;
|
|
final ta = overallWithHcTotal(
|
|
a.rawScores,
|
|
perGameA,
|
|
a.handicap,
|
|
event.gameCount,
|
|
);
|
|
final tb = overallWithHcTotal(
|
|
b.rawScores,
|
|
perGameB,
|
|
b.handicap,
|
|
event.gameCount,
|
|
);
|
|
return asc ? ta.compareTo(tb) : tb.compareTo(ta);
|
|
});
|
|
});
|
|
},
|
|
numeric: true,
|
|
),
|
|
DataColumn(
|
|
label: const Text('에버리지'),
|
|
onSort: (i, asc) {
|
|
setState(() {
|
|
_sortColumnIndex = i;
|
|
_sortAscending = asc;
|
|
rows.sort((a, b) {
|
|
final perGameA =
|
|
teamHcByPid[a.participantId] ?? a.handicaps;
|
|
final perGameB =
|
|
teamHcByPid[b.participantId] ?? b.handicaps;
|
|
final aa =
|
|
(avgWithHc(
|
|
a.rawScores,
|
|
perGameA,
|
|
a.handicap,
|
|
event.gameCount,
|
|
) ??
|
|
0);
|
|
final ab =
|
|
(avgWithHc(
|
|
b.rawScores,
|
|
perGameB,
|
|
b.handicap,
|
|
event.gameCount,
|
|
) ??
|
|
0);
|
|
return asc ? aa.compareTo(ab) : ab.compareTo(aa);
|
|
});
|
|
});
|
|
},
|
|
numeric: true,
|
|
),
|
|
],
|
|
rows: [
|
|
for (final p in rows)
|
|
DataRow(
|
|
cells: [
|
|
DataCell(
|
|
SizedBox(
|
|
width: 28,
|
|
child: Center(
|
|
child: Text(
|
|
(rankForParticipant?.call(
|
|
p.participantId,
|
|
partRankMap ?? const {},
|
|
) ??
|
|
0)
|
|
.toString(),
|
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
DataCell(
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
genderIcon(p.gender),
|
|
const SizedBox(width: 6),
|
|
Flexible(
|
|
child: Text(
|
|
p.name,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 2),
|
|
Builder(
|
|
builder: (context) {
|
|
final String? avgText = (p.average == null)
|
|
? null
|
|
: ((p.average is double)
|
|
? (p.average as double)
|
|
: (p.average as num).toDouble())
|
|
.toStringAsFixed(1);
|
|
final String? hcText = (p.handicap == 0)
|
|
? null
|
|
: (p.handicap > 0
|
|
? '+${p.handicap}'
|
|
: '${p.handicap}');
|
|
return Wrap(
|
|
spacing: 4,
|
|
runSpacing: 2,
|
|
children: [
|
|
if (hcText != null)
|
|
Chip(
|
|
label: Text(
|
|
hcText,
|
|
style: const TextStyle(
|
|
fontSize: 10,
|
|
height: 1.0,
|
|
),
|
|
),
|
|
backgroundColor: BadgeStyles.successBg,
|
|
padding: EdgeInsets.zero,
|
|
labelPadding: const EdgeInsets.symmetric(
|
|
horizontal: 4,
|
|
vertical: 0,
|
|
),
|
|
materialTapTargetSize:
|
|
MaterialTapTargetSize.shrinkWrap,
|
|
visualDensity: const VisualDensity(
|
|
horizontal: -4,
|
|
vertical: -4,
|
|
),
|
|
),
|
|
if (avgText != null)
|
|
Chip(
|
|
label: Text(
|
|
avgText,
|
|
style: const TextStyle(
|
|
fontSize: 10,
|
|
height: 1.0,
|
|
),
|
|
),
|
|
backgroundColor: BadgeStyles.primaryBg,
|
|
padding: EdgeInsets.zero,
|
|
labelPadding: const EdgeInsets.symmetric(
|
|
horizontal: 4,
|
|
vertical: 0,
|
|
),
|
|
materialTapTargetSize:
|
|
MaterialTapTargetSize.shrinkWrap,
|
|
visualDensity: const VisualDensity(
|
|
horizontal: -4,
|
|
vertical: -4,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
DataCell(
|
|
Text(
|
|
teamLabelByPid[p.participantId] ?? '',
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
for (int g = 1; g <= event.gameCount; g++)
|
|
DataCell(
|
|
Builder(
|
|
builder: (context) {
|
|
final perGame =
|
|
teamHcByPid[p.participantId] ?? p.handicaps;
|
|
final effHc = hcAt(perGame, p.handicap, g);
|
|
final has = hasRaw(p.rawScores, g);
|
|
if (!has) {
|
|
return const SizedBox.expand(
|
|
child: Center(child: Text('')),
|
|
);
|
|
}
|
|
final raw = rawAt(p.rawScores, g);
|
|
final sum = raw + effHc;
|
|
if (effHc == 0) {
|
|
return SizedBox.expand(
|
|
child: Center(
|
|
child: Text(
|
|
'$raw',
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 18,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return SizedBox.expand(
|
|
child: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: CustomPaint(
|
|
painter: _DiagonalSlashPainter(
|
|
Colors.black26,
|
|
),
|
|
),
|
|
),
|
|
Align(
|
|
alignment: Alignment.topLeft,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(
|
|
left: 2,
|
|
top: 2,
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
'$raw',
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
Text(
|
|
'+$effHc',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey.shade700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Align(
|
|
alignment: Alignment.bottomRight,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(
|
|
right: 2,
|
|
bottom: 2,
|
|
),
|
|
child: Text(
|
|
'$sum',
|
|
textAlign: TextAlign.right,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 18,
|
|
color: valueColor(sum),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
onTap: enabled
|
|
? () {
|
|
final perGame = teamHcByPid[p.participantId] ?? p.handicaps;
|
|
final hc = hcAt(perGame, p.handicap, g);
|
|
final has = hasRaw(p.rawScores, g);
|
|
final current = has ? rawAt(p.rawScores, g) : null;
|
|
_openEditSheet(
|
|
participant: p,
|
|
gameNumber: g,
|
|
currentRaw: current,
|
|
currentHc: hc,
|
|
);
|
|
}
|
|
: null,
|
|
),
|
|
DataCell(
|
|
Builder(
|
|
builder: (context) {
|
|
final perGame =
|
|
teamHcByPid[p.participantId] ?? p.handicaps;
|
|
final raw = overallRawTotal(
|
|
p.rawScores,
|
|
event.gameCount,
|
|
);
|
|
final withHc = overallWithHcTotal(
|
|
p.rawScores,
|
|
perGame,
|
|
p.handicap,
|
|
event.gameCount,
|
|
);
|
|
final n = playedGames(p.rawScores, event.gameCount);
|
|
if (withHc != raw) {
|
|
return SizedBox.expand(
|
|
child: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: CustomPaint(
|
|
painter: _DiagonalSlashPainter(
|
|
Colors.black26,
|
|
),
|
|
),
|
|
),
|
|
Align(
|
|
alignment: Alignment.topLeft,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(
|
|
left: 2,
|
|
top: 2,
|
|
),
|
|
child: Text(
|
|
'$raw',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
color: totalColor(raw, n),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Align(
|
|
alignment: Alignment.bottomRight,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(
|
|
right: 2,
|
|
bottom: 2,
|
|
),
|
|
child: Text(
|
|
'$withHc',
|
|
textAlign: TextAlign.right,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 18,
|
|
color: totalColor(withHc, n),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
return SizedBox.expand(
|
|
child: Center(
|
|
child: Text(
|
|
'$raw',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 18,
|
|
color: totalColor(raw, n),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
DataCell(
|
|
Builder(
|
|
builder: (context) {
|
|
final perGame =
|
|
teamHcByPid[p.participantId] ?? p.handicaps;
|
|
final rawAvg =
|
|
(avgRaw(p.rawScores, event.gameCount) ?? 0)
|
|
.toStringAsFixed(1);
|
|
final withHcAvg =
|
|
(avgWithHc(
|
|
p.rawScores,
|
|
perGame,
|
|
p.handicap,
|
|
event.gameCount,
|
|
) ??
|
|
0)
|
|
.toStringAsFixed(1);
|
|
final avgNum =
|
|
double.tryParse(withHcAvg) ??
|
|
double.tryParse(rawAvg) ??
|
|
0;
|
|
if (withHcAvg != rawAvg) {
|
|
return SizedBox.expand(
|
|
child: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: CustomPaint(
|
|
painter: _DiagonalSlashPainter(
|
|
Colors.black26,
|
|
),
|
|
),
|
|
),
|
|
Align(
|
|
alignment: Alignment.topLeft,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(
|
|
left: 2,
|
|
top: 2,
|
|
),
|
|
child: Text(
|
|
rawAvg,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
color: valueColor(
|
|
double.tryParse(rawAvg) ?? 0,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Align(
|
|
alignment: Alignment.bottomRight,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(
|
|
right: 2,
|
|
bottom: 2,
|
|
),
|
|
child: Text(
|
|
withHcAvg,
|
|
textAlign: TextAlign.right,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 18,
|
|
color: valueColor(
|
|
double.tryParse(withHcAvg) ?? 0,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
return SizedBox.expand(
|
|
child: Center(
|
|
child: Text(
|
|
rawAvg,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 18,
|
|
color: valueColor(avgNum),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DiagonalSlashPainter extends CustomPainter {
|
|
final Color color;
|
|
const _DiagonalSlashPainter(this.color);
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final paint = Paint()
|
|
..color = color
|
|
..strokeWidth = 1.0
|
|
..style = PaintingStyle.stroke;
|
|
// Draw diagonal from bottom-left to top-right
|
|
canvas.drawLine(Offset(0, size.height), Offset(size.width, 0), paint);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant _DiagonalSlashPainter oldDelegate) {
|
|
return oldDelegate.color != color;
|
|
}
|
|
}
|