이벤트 개발완료
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../models/score_model.dart';
|
||||
import '../../models/participant_model.dart';
|
||||
import '../../services/event_service.dart';
|
||||
|
||||
class ScoreBatchEditScreen extends StatefulWidget {
|
||||
final String eventId;
|
||||
final Participant participant;
|
||||
final List<Score> scores; // 해당 참가자의 기존 점수들 (게임번호 기준 정렬 권장)
|
||||
final int gameCount; // 이벤트 전체 게임 수
|
||||
|
||||
const ScoreBatchEditScreen({
|
||||
super.key,
|
||||
required this.eventId,
|
||||
required this.participant,
|
||||
required this.scores,
|
||||
required this.gameCount,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ScoreBatchEditScreen> createState() => _ScoreBatchEditScreenState();
|
||||
}
|
||||
|
||||
class _ScoreBatchEditScreenState extends State<ScoreBatchEditScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isSaving = false;
|
||||
late List<TextEditingController> _scoreCtrls;
|
||||
late List<TextEditingController> _handicapCtrls;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 기존 점수 맵: gameNumber -> Score
|
||||
final byGame = <int, Score>{};
|
||||
for (final s in widget.scores) {
|
||||
final gn = s.gameNumber ?? 0;
|
||||
if (gn > 0) byGame[gn] = s;
|
||||
}
|
||||
|
||||
final count = widget.gameCount > 0 ? widget.gameCount : (byGame.keys.isEmpty ? 1 : byGame.keys.reduce((a,b)=>a>b?a:b));
|
||||
_scoreCtrls = List.generate(count, (i) {
|
||||
final gn = i + 1;
|
||||
final s = byGame[gn];
|
||||
return TextEditingController(text: s != null ? s.totalScore.toString() : '');
|
||||
});
|
||||
_handicapCtrls = List.generate(count, (i) {
|
||||
final gn = i + 1;
|
||||
final s = byGame[gn];
|
||||
return TextEditingController(text: s != null ? (s.handicap ?? 0).toString() : '');
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final c in _scoreCtrls) {
|
||||
c.dispose();
|
||||
}
|
||||
for (final c in _handicapCtrls) {
|
||||
c.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _saveAll() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
setState(() => _isSaving = true);
|
||||
try {
|
||||
final eventService = Provider.of<EventService>(context, listen: false);
|
||||
// 기존 점수 맵 구성
|
||||
final byGame = <int, Score>{};
|
||||
for (final s in widget.scores) {
|
||||
final gn = s.gameNumber ?? 0;
|
||||
if (gn > 0) byGame[gn] = s;
|
||||
}
|
||||
final count = _scoreCtrls.length;
|
||||
for (int i = 0; i < count; i++) {
|
||||
final gameNo = i + 1;
|
||||
final scoreText = _scoreCtrls[i].text.trim();
|
||||
final hText = _handicapCtrls[i].text.trim();
|
||||
final hasAny = scoreText.isNotEmpty || hText.isNotEmpty;
|
||||
if (!hasAny) continue; // 완전 비어있으면 건너뜀
|
||||
|
||||
final newScore = int.tryParse(scoreText) ?? 0;
|
||||
final newHdc = int.tryParse(hText.isEmpty ? '0' : hText) ?? 0;
|
||||
final existing = byGame[gameNo];
|
||||
if (existing != null) {
|
||||
// 업데이트: 날짜 보존
|
||||
await eventService.updateScore(widget.eventId, existing.id, {
|
||||
'gameNumber': gameNo,
|
||||
'score': newScore,
|
||||
'totalScore': newScore, // 호환 목적
|
||||
'handicap': newHdc,
|
||||
'date': existing.date.toIso8601String(),
|
||||
});
|
||||
} else {
|
||||
// 신규 추가
|
||||
await eventService.addScore(widget.eventId, {
|
||||
'eventId': widget.eventId,
|
||||
'participantId': widget.participant.id,
|
||||
'gameNumber': gameNo,
|
||||
'score': newScore,
|
||||
'handicap': newHdc,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (mounted) Navigator.of(context).pop(true);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _isSaving = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('일괄 수정 실패: $e')));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final count = _scoreCtrls.length;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('${widget.participant.name ?? '참가자'} - 점수 일괄 수정'),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: _isSaving ? null : _saveAll,
|
||||
icon: const Icon(Icons.save),
|
||||
tooltip: '저장',
|
||||
)
|
||||
],
|
||||
),
|
||||
body: _isSaving
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Form(
|
||||
key: _formKey,
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: count,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, i) {
|
||||
// 기존 점수 찾기
|
||||
final gn = i + 1;
|
||||
final s = widget.scores.firstWhere(
|
||||
(e) => (e.gameNumber ?? 0) == gn,
|
||||
orElse: () => Score(
|
||||
id: '',
|
||||
memberId: widget.participant.memberId,
|
||||
eventId: widget.eventId,
|
||||
clubId: '',
|
||||
frames: const [],
|
||||
totalScore: 0,
|
||||
handicap: 0,
|
||||
gameNumber: gn,
|
||||
date: DateTime.now(),
|
||||
),
|
||||
);
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 14,
|
||||
child: Text(gn.toString()),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text('기록일: ${DateFormat('yyyy-MM-dd').format(s.date)}'),
|
||||
const Spacer(),
|
||||
Text('합계: ${(_scoreCtrls[i].text.isEmpty ? 0 : int.tryParse(_scoreCtrls[i].text) ?? 0) + (int.tryParse(_handicapCtrls[i].text.isEmpty ? '0' : _handicapCtrls[i].text) ?? 0)}'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _scoreCtrls[i],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '점수 (0-300)',
|
||||
prefixIcon: Icon(Icons.score),
|
||||
),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) return null; // 신규 입력은 비워둘 수 있음
|
||||
final n = int.tryParse(v);
|
||||
if (n == null) return '숫자만';
|
||||
if (n < 0 || n > 300) return '0-300';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _handicapCtrls[i],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '핸디캡',
|
||||
prefixIcon: Icon(Icons.add_circle_outline),
|
||||
),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) return null;
|
||||
final n = int.tryParse(v);
|
||||
if (n == null) return '숫자만';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.save),
|
||||
label: const Text('모든 게임 저장'),
|
||||
onPressed: _isSaving ? null : _saveAll,
|
||||
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 14)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user