Entry の表示改善

This commit is contained in:
2024-09-08 21:18:45 +09:00
parent e6328f84b1
commit a22c2ea730
10 changed files with 321 additions and 18 deletions

7
TODO.txt Normal file
View File

@ -0,0 +1,7 @@
不参加の過去エントリーは削除できるようにする。
バグ:
履歴の写真:アクセスエラー
バックアップをイベントごとに保存・レストア
ログインした際に、イベントが選択されていなければ、イベントを選択するように促す。
事前チェックインした写真が履歴に表示されない。

View File

@ -11,6 +11,8 @@ class Entry {
final DateTime? date; final DateTime? date;
final int zekkenNumber; // 新しく追加 final int zekkenNumber; // 新しく追加
final String owner; final String owner;
bool hasParticipated;
bool hasGoaled;
Entry({ Entry({
required this.id, required this.id,
@ -20,6 +22,8 @@ class Entry {
required this.date, required this.date,
required this.zekkenNumber, required this.zekkenNumber,
required this.owner, required this.owner,
this.hasParticipated = false,
this.hasGoaled = false,
}); });
factory Entry.fromJson(Map<String, dynamic> json) { factory Entry.fromJson(Map<String, dynamic> json) {

View File

@ -8,7 +8,7 @@ import 'package:gifunavi/model/team.dart';
import 'package:gifunavi/model/category.dart'; import 'package:gifunavi/model/category.dart';
import 'package:gifunavi/services/api_service.dart'; import 'package:gifunavi/services/api_service.dart';
import '../index/index_controller.dart'; import 'package:gifunavi/pages/index/index_controller.dart';
import 'package:timezone/timezone.dart' as tz; import 'package:timezone/timezone.dart' as tz;
class EntryController extends GetxController { class EntryController extends GetxController {
@ -27,6 +27,8 @@ class EntryController extends GetxController {
final currentEntry = Rx<Entry?>(null); final currentEntry = Rx<Entry?>(null);
final isLoading = true.obs; final isLoading = true.obs;
final activeEvents = <Event>[].obs; //有効なイベントリスト
@override @override
void onInit() async { void onInit() async {
super.onInit(); super.onInit();
@ -52,14 +54,15 @@ class EntryController extends GetxController {
fetchTeams(), fetchTeams(),
fetchCategories(), fetchCategories(),
]); ]);
updateActiveEvents(); // イベント取得後にアクティブなイベントを更新
if (Get.arguments != null && Get.arguments['entry'] != null) { if (Get.arguments != null && Get.arguments['entry'] != null) {
currentEntry.value = Get.arguments['entry']; currentEntry.value = Get.arguments['entry'];
initializeEditMode(currentEntry.value!); initializeEditMode(currentEntry.value!);
} else { } else {
// 新規作成モードの場合、最初のイベントを選択 // 新規作成モードの場合、最初のイベントを選択
if (events.isNotEmpty) { if (events.isNotEmpty) {
selectedEvent.value = events.first; selectedEvent.value = activeEvents.first;
selectedDate.value = events.first.startDatetime; selectedDate.value = activeEvents.first.startDatetime;
} }
} }
} catch(e) { } catch(e) {
@ -70,6 +73,10 @@ class EntryController extends GetxController {
} }
} }
void updateActiveEvents() {
final now = tz.TZDateTime.now(tz.getLocation('Asia/Tokyo'));
activeEvents.assignAll(events.where((event) => event.deadlineDateTime.isAfter(now)));
}
void initializeEditMode(Entry entry) { void initializeEditMode(Entry entry) {
currentEntry.value = entry; currentEntry.value = entry;
@ -143,6 +150,7 @@ class EntryController extends GetxController {
deadlineDateTime: deadlineDateTime, deadlineDateTime: deadlineDateTime,
); );
}).toList()); }).toList());
updateActiveEvents();
} catch (e) { } catch (e) {
print('Error fetching events: $e'); print('Error fetching events: $e');
Get.snackbar('Error', 'Failed to fetch events'); Get.snackbar('Error', 'Failed to fetch events');

View File

@ -39,10 +39,10 @@ class EntryDetailPage extends GetView<EntryController> {
children: [ children: [
_buildDropdown<Event>( _buildDropdown<Event>(
label: 'イベント', label: 'イベント',
items: controller.events, items: controller.activeEvents,
selectedId: controller.selectedEvent.value?.id, selectedId: controller.selectedEvent.value?.id,
onChanged: (eventId) => controller.updateEvent( onChanged: (eventId) => controller.updateEvent(
controller.events.firstWhere((e) => e.id == eventId) controller.activeEvents.firstWhere((e) => e.id == eventId)
), ),
getDisplayName: (event) => event.eventName, getDisplayName: (event) => event.eventName,
getId: (event) => event.id, getId: (event) => event.id,

View File

@ -7,6 +7,8 @@ import 'package:gifunavi/pages/entry/entry_controller.dart';
import 'package:gifunavi/routes/app_pages.dart'; import 'package:gifunavi/routes/app_pages.dart';
import 'package:timezone/timezone.dart' as tz; import 'package:timezone/timezone.dart' as tz;
import 'package:gifunavi/model/entry.dart';
class EntryListPage extends GetView<EntryController> { class EntryListPage extends GetView<EntryController> {
const EntryListPage({super.key}); const EntryListPage({super.key});
@ -28,11 +30,41 @@ class EntryListPage extends GetView<EntryController> {
child: Text('表示するエントリーがありません。'), child: Text('表示するエントリーがありません。'),
); );
} }
final sortedEntries = controller.entries.toList()
..sort((b, a) => a.date!.compareTo(b.date!));
return ListView.builder( return ListView.builder(
itemCount: controller.entries.length, itemCount: sortedEntries.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final entry = controller.entries[index]; final entry = sortedEntries[index];
//final now = DateTime.now();
//print("now=$now");
//final isEntryInFuture = _compareDatesOnly(entry.date!, now) >= 0;
final now = tz.TZDateTime.now(tz.getLocation('Asia/Tokyo'));
final entryDate = tz.TZDateTime.from(entry.date!, tz.getLocation('Asia/Tokyo'));
// 日付のみを比較(時間を無視)
final isEntryInFuture = _compareDatesOnly(entryDate, now) >= 0;
//final isEntryInFuture = entry.date!.isAfter(now) || entry.date!.isAtSameMomentAs(now);
Widget? leadingIcon;
if (!isEntryInFuture) {
if (entry.hasParticipated) {
if (entry.hasGoaled) {
leadingIcon =
const Icon(Icons.check_circle, color: Colors.green);
} else {
leadingIcon = const Icon(Icons.warning, color: Colors.yellow);
}
} else {
leadingIcon = const Icon(Icons.cancel, color: Colors.red);
}
}
return ListTile( return ListTile(
leading: leadingIcon,
title: Row( title: Row(
children: [ children: [
Expanded( Expanded(
@ -41,17 +73,24 @@ class EntryListPage extends GetView<EntryController> {
Text(entry.team.teamName, style: const TextStyle(fontWeight: FontWeight.bold)), Text(entry.team.teamName, style: const TextStyle(fontWeight: FontWeight.bold)),
], ],
), ),
subtitle: Row( subtitle: Row(
children: [ children: [
Expanded( Expanded(
child: Text('カテゴリー: ${entry.category.categoryName}'), child: Text('カテゴリー: ${entry.category.categoryName}'),
), ),
Text('ゼッケン: ${entry.zekkenNumber ?? "未設定"}'), Text('ゼッケン: ${entry.zekkenNumber ?? "未設定"}'),
], ],
), ),
onTap: () => onTap: () {
Get.toNamed(AppPages.ENTRY_DETAIL, if (isEntryInFuture) {
arguments: {'mode': 'edit', 'entry': entry}), Get.toNamed(AppPages.ENTRY_DETAIL,
arguments: {'mode': 'edit', 'entry': entry});
} else if (entry.hasParticipated) {
Get.toNamed(AppPages.EVENT_RESULT, arguments: {'entry': entry});
} else {
_showNonParticipationDialog(context, entry);
}
}
); );
}, },
); );
@ -59,6 +98,11 @@ class EntryListPage extends GetView<EntryController> {
); );
} }
// 新しく追加するメソッド
int _compareDatesOnly(DateTime a, DateTime b) {
return DateTime(a.year, a.month, a.day).compareTo(DateTime(b.year, b.month, b.day));
}
String _formatDate(DateTime? date) { String _formatDate(DateTime? date) {
if (date == null) { if (date == null) {
return '日時未設定'; return '日時未設定';
@ -66,6 +110,26 @@ class EntryListPage extends GetView<EntryController> {
final jstDate = tz.TZDateTime.from(date, tz.getLocation('Asia/Tokyo')); final jstDate = tz.TZDateTime.from(date, tz.getLocation('Asia/Tokyo'));
return DateFormat('yyyy-MM-dd').format(jstDate); return DateFormat('yyyy-MM-dd').format(jstDate);
} }
void _showNonParticipationDialog(BuildContext context, Entry entry) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(entry.event.eventName),
content: Text('${_formatDate(entry.date)}\n\n不参加でした'),
actions: <Widget>[
TextButton(
child: const Text('閉じる'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
} }
class EntryListPage_old extends GetView<EntryController> { class EntryListPage_old extends GetView<EntryController> {

View File

@ -0,0 +1,153 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gifunavi/model/entry.dart';
import 'package:gifunavi/pages/gps/gps_controller.dart';
import 'package:gifunavi/pages/history/history_controller.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:intl/intl.dart';
class EventResultPage extends StatefulWidget {
final Entry entry;
const EventResultPage({super.key, required this.entry});
@override
State<EventResultPage> createState() => _EventResultPageState();
}
class _EventResultPageState extends State<EventResultPage> {
late GpsController gpsController;
late HistoryController historyController;
@override
void initState() {
super.initState();
gpsController = Get.put(GpsController());
historyController = Get.put(HistoryController());
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: Text('${widget.entry.event.eventName} 結果'),
bottom: const TabBar(
tabs: [
Tab(text: 'ランキング'),
Tab(text: '走行経路'),
Tab(text: 'チェックポイント'),
],
),
),
body: TabBarView(
children: [
_buildFutureTab(), //_buildRankingTab(),
_buildFutureTab(), //_buildRouteTab(),
_buildFutureTab(), //_buildCheckpointTab(),
],
),
),
);
}
Widget _buildFutureTab() {
// ランキングの表示ロジックを実装
return const Center(child: Text('近日公開予定当面、HPを参照ください。'));
}
Widget _buildRankingTab() {
// ランキングの表示ロジックを実装
return const Center(child: Text('ランキング表示(実装が必要)'));
}
Widget _buildRouteTab() {
return Obx(() {
if (gpsController.gpsData.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return FlutterMap(
options: MapOptions(
center: LatLng(gpsController.gpsData[0].lat, gpsController.gpsData[0].lon),
zoom: 13.0,
),
children: [
TileLayer(
urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
subdomains: ['a', 'b', 'c'],
),
PolylineLayer(
polylines: [
Polyline(
points: gpsController.gpsData
.map((data) => LatLng(data.lat, data.lon))
.toList(),
color: Colors.red,
strokeWidth: 3.0,
),
],
),
MarkerLayer(
markers: gpsController.gpsData
.map((data) => Marker(
width: 80.0,
height: 80.0,
point: LatLng(data.lat, data.lon),
child: const Icon(Icons.location_on, color: Colors.red),
))
.toList(),
),
],
);
});
}
Widget _buildCheckpointTab() {
return Obx(() {
if (historyController.checkpoints.isEmpty) {
return const Center(child: Text('チェックポイント履歴がありません'));
}
return ListView.builder(
itemCount: historyController.checkpoints.length,
itemBuilder: (context, index) {
final checkpoint = historyController.checkpoints[index];
return ListTile(
title: Text('CP ${checkpoint.cp_number ?? 'Unknown'}'),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('通過時刻: ${_formatDateTime(checkpoint.checkintime)}'),
Text('チーム: ${checkpoint.team_name ?? 'Unknown'}'),
Text('イベント: ${checkpoint.event_code ?? 'Unknown'}'),
],
),
leading: checkpoint.image != null
? Image.file(
File(checkpoint.image!),
width: 50,
height: 50,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
print('Error loading image: $error');
return const Icon(Icons.error);
},
)
: const Icon(Icons.image_not_supported),
);
}
);
});
}
}
String _formatDateTime(int? microsecondsSinceEpoch) {
if (microsecondsSinceEpoch == null) return 'Unknown';
final dateTime = DateTime.fromMicrosecondsSinceEpoch(microsecondsSinceEpoch);
return DateFormat('yyyy-MM-dd HH:mm:ss').format(dateTime);
}

View File

@ -0,0 +1,22 @@
import 'package:get/get.dart';
import 'package:gifunavi/model/gps_data.dart';
import 'package:gifunavi/utils/database_gps.dart';
import 'package:gifunavi/pages/index/index_controller.dart';
class GpsController extends GetxController {
final gpsData = <GpsData>[].obs;
@override
void onInit() {
super.onInit();
loadGpsData();
}
void loadGpsData() async {
final teamName = Get.find<IndexController>().currentUser[0]["user"]['team_name'];
final eventCode = Get.find<IndexController>().currentUser[0]["user"]["event_code"];
GpsDatabaseHelper db = GpsDatabaseHelper.instance;
var data = await db.getGPSData(teamName, eventCode);
gpsData.value = data;
}
}

View File

@ -0,0 +1,22 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:gifunavi/model/rog.dart';
import 'package:gifunavi/utils/database_helper.dart';
class HistoryController extends GetxController {
final checkpoints = <Rog>[].obs; // Rog オブジェクトのリストに変更
@override
void onInit() {
super.onInit();
loadCheckpoints();
}
void loadCheckpoints() async {
DatabaseHelper db = DatabaseHelper.instance;
var data = await db.allRogianing();
checkpoints.value = data;
}
}

View File

@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get/get_navigation/src/routes/get_route.dart'; import 'package:get/get_navigation/src/routes/get_route.dart';
import 'package:gifunavi/pages/changepassword/change_password_page.dart'; import 'package:gifunavi/pages/changepassword/change_password_page.dart';
@ -34,6 +35,9 @@ import 'package:gifunavi/pages/entry/event_entries_page.dart';
import 'package:gifunavi/pages/entry/event_entries_binding.dart'; import 'package:gifunavi/pages/entry/event_entries_binding.dart';
import 'package:gifunavi/pages/register/user_detail_page.dart'; import 'package:gifunavi/pages/register/user_detail_page.dart';
import 'package:gifunavi/pages/entry/event_result_page.dart';
import 'package:gifunavi/model/entry.dart';
part 'app_routes.dart'; part 'app_routes.dart';
class AppPages { class AppPages {
@ -70,6 +74,7 @@ class AppPages {
static const EVENT_ENTRY = Routes.EVENT_ENTRIES; static const EVENT_ENTRY = Routes.EVENT_ENTRIES;
static const USER_DETAILS_EDIT = Routes.USER_DETAILS_EDIT; static const USER_DETAILS_EDIT = Routes.USER_DETAILS_EDIT;
static const EVENT_RESULT = Routes.EVENT_RESULT;
static final routes = [ static final routes = [
GetPage( GetPage(
@ -175,6 +180,23 @@ class AppPages {
name: Routes.USER_DETAILS_EDIT, name: Routes.USER_DETAILS_EDIT,
page: () => const UserDetailsEditPage(), page: () => const UserDetailsEditPage(),
), ),
GetPage(
name: Routes.EVENT_RESULT,
page: () {
final args = Get.arguments;
if (args is Map<String, dynamic> && args.containsKey('entry')) {
return EventResultPage(entry: args['entry'] as Entry);
} else {
// エントリーが提供されていない場合のフォールバック
// 例: エラーページを表示するか、ホームページにリダイレクトする
return const Scaffold(
body: Center(
child: Text('エラー: イベント結果を表示できません。'),
),
);
}
},
),
]; ];
} }

View File

@ -38,4 +38,5 @@ abstract class Routes {
static const EVENT_ENTRIES = '/event-entries'; static const EVENT_ENTRIES = '/event-entries';
static const USER_DETAILS_EDIT = '/user-details-edit'; static const USER_DETAILS_EDIT = '/user-details-edit';
static const EVENT_RESULT = '/event-result';
} }