diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..141bc06 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,7 @@ +不参加の過去エントリーは削除できるようにする。 +バグ: +履歴の写真:アクセスエラー +バックアップをイベントごとに保存・レストア + +ログインした際に、イベントが選択されていなければ、イベントを選択するように促す。 +事前チェックインした写真が履歴に表示されない。 \ No newline at end of file diff --git a/lib/model/entry.dart b/lib/model/entry.dart index 6e84524..3c9bc68 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -11,6 +11,8 @@ class Entry { final DateTime? date; final int zekkenNumber; // 新しく追加 final String owner; + bool hasParticipated; + bool hasGoaled; Entry({ required this.id, @@ -20,6 +22,8 @@ class Entry { required this.date, required this.zekkenNumber, required this.owner, + this.hasParticipated = false, + this.hasGoaled = false, }); factory Entry.fromJson(Map json) { diff --git a/lib/pages/entry/entry_controller.dart b/lib/pages/entry/entry_controller.dart index ea30eca..08f10e0 100644 --- a/lib/pages/entry/entry_controller.dart +++ b/lib/pages/entry/entry_controller.dart @@ -8,7 +8,7 @@ import 'package:gifunavi/model/team.dart'; import 'package:gifunavi/model/category.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; class EntryController extends GetxController { @@ -27,6 +27,8 @@ class EntryController extends GetxController { final currentEntry = Rx(null); final isLoading = true.obs; + final activeEvents = [].obs; //有効なイベントリスト + @override void onInit() async { super.onInit(); @@ -52,14 +54,15 @@ class EntryController extends GetxController { fetchTeams(), fetchCategories(), ]); + updateActiveEvents(); // イベント取得後にアクティブなイベントを更新 if (Get.arguments != null && Get.arguments['entry'] != null) { currentEntry.value = Get.arguments['entry']; initializeEditMode(currentEntry.value!); } else { // 新規作成モードの場合、最初のイベントを選択 if (events.isNotEmpty) { - selectedEvent.value = events.first; - selectedDate.value = events.first.startDatetime; + selectedEvent.value = activeEvents.first; + selectedDate.value = activeEvents.first.startDatetime; } } } 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) { currentEntry.value = entry; @@ -143,6 +150,7 @@ class EntryController extends GetxController { deadlineDateTime: deadlineDateTime, ); }).toList()); + updateActiveEvents(); } catch (e) { print('Error fetching events: $e'); Get.snackbar('Error', 'Failed to fetch events'); diff --git a/lib/pages/entry/entry_detail_page.dart b/lib/pages/entry/entry_detail_page.dart index f16e050..cf4c984 100644 --- a/lib/pages/entry/entry_detail_page.dart +++ b/lib/pages/entry/entry_detail_page.dart @@ -39,10 +39,10 @@ class EntryDetailPage extends GetView { children: [ _buildDropdown( label: 'イベント', - items: controller.events, + items: controller.activeEvents, selectedId: controller.selectedEvent.value?.id, onChanged: (eventId) => controller.updateEvent( - controller.events.firstWhere((e) => e.id == eventId) + controller.activeEvents.firstWhere((e) => e.id == eventId) ), getDisplayName: (event) => event.eventName, getId: (event) => event.id, diff --git a/lib/pages/entry/entry_list_page.dart b/lib/pages/entry/entry_list_page.dart index 1a87226..9cbf95d 100644 --- a/lib/pages/entry/entry_list_page.dart +++ b/lib/pages/entry/entry_list_page.dart @@ -7,6 +7,8 @@ import 'package:gifunavi/pages/entry/entry_controller.dart'; import 'package:gifunavi/routes/app_pages.dart'; import 'package:timezone/timezone.dart' as tz; +import 'package:gifunavi/model/entry.dart'; + class EntryListPage extends GetView { const EntryListPage({super.key}); @@ -28,11 +30,41 @@ class EntryListPage extends GetView { child: Text('表示するエントリーがありません。'), ); } + + final sortedEntries = controller.entries.toList() + ..sort((b, a) => a.date!.compareTo(b.date!)); + return ListView.builder( - itemCount: controller.entries.length, + itemCount: sortedEntries.length, 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( + leading: leadingIcon, title: Row( children: [ Expanded( @@ -41,17 +73,24 @@ class EntryListPage extends GetView { Text(entry.team.teamName, style: const TextStyle(fontWeight: FontWeight.bold)), ], ), - subtitle: Row( - children: [ - Expanded( - child: Text('カテゴリー: ${entry.category.categoryName}'), - ), - Text('ゼッケン: ${entry.zekkenNumber ?? "未設定"}'), - ], - ), - onTap: () => - Get.toNamed(AppPages.ENTRY_DETAIL, - arguments: {'mode': 'edit', 'entry': entry}), + subtitle: Row( + children: [ + Expanded( + child: Text('カテゴリー: ${entry.category.categoryName}'), + ), + Text('ゼッケン: ${entry.zekkenNumber ?? "未設定"}'), + ], + ), + onTap: () { + if (isEntryInFuture) { + 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 { ); } + // 新しく追加するメソッド + 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) { if (date == null) { return '日時未設定'; @@ -66,6 +110,26 @@ class EntryListPage extends GetView { final jstDate = tz.TZDateTime.from(date, tz.getLocation('Asia/Tokyo')); 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: [ + TextButton( + child: const Text('閉じる'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } } class EntryListPage_old extends GetView { diff --git a/lib/pages/entry/event_result_page.dart b/lib/pages/entry/event_result_page.dart new file mode 100644 index 0000000..73f3ca3 --- /dev/null +++ b/lib/pages/entry/event_result_page.dart @@ -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 createState() => _EventResultPageState(); + +} + +class _EventResultPageState extends State { + 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); +} diff --git a/lib/pages/gps/gps_controller.dart b/lib/pages/gps/gps_controller.dart new file mode 100644 index 0000000..f490972 --- /dev/null +++ b/lib/pages/gps/gps_controller.dart @@ -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 = [].obs; + + @override + void onInit() { + super.onInit(); + loadGpsData(); + } + + void loadGpsData() async { + final teamName = Get.find().currentUser[0]["user"]['team_name']; + final eventCode = Get.find().currentUser[0]["user"]["event_code"]; + GpsDatabaseHelper db = GpsDatabaseHelper.instance; + var data = await db.getGPSData(teamName, eventCode); + gpsData.value = data; + } +} \ No newline at end of file diff --git a/lib/pages/history/history_controller.dart b/lib/pages/history/history_controller.dart new file mode 100644 index 0000000..2cd9f4a --- /dev/null +++ b/lib/pages/history/history_controller.dart @@ -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 = [].obs; // Rog オブジェクトのリストに変更 + + @override + void onInit() { + super.onInit(); + loadCheckpoints(); + } + + void loadCheckpoints() async { + DatabaseHelper db = DatabaseHelper.instance; + var data = await db.allRogianing(); + checkpoints.value = data; + } +} \ No newline at end of file diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 3ce5171..c8d0df8 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get/get_navigation/src/routes/get_route.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/register/user_detail_page.dart'; +import 'package:gifunavi/pages/entry/event_result_page.dart'; +import 'package:gifunavi/model/entry.dart'; + part 'app_routes.dart'; class AppPages { @@ -70,6 +74,7 @@ class AppPages { static const EVENT_ENTRY = Routes.EVENT_ENTRIES; static const USER_DETAILS_EDIT = Routes.USER_DETAILS_EDIT; + static const EVENT_RESULT = Routes.EVENT_RESULT; static final routes = [ GetPage( @@ -175,6 +180,23 @@ class AppPages { name: Routes.USER_DETAILS_EDIT, page: () => const UserDetailsEditPage(), ), + GetPage( + name: Routes.EVENT_RESULT, + page: () { + final args = Get.arguments; + if (args is Map && args.containsKey('entry')) { + return EventResultPage(entry: args['entry'] as Entry); + } else { + // エントリーが提供されていない場合のフォールバック + // 例: エラーページを表示するか、ホームページにリダイレクトする + return const Scaffold( + body: Center( + child: Text('エラー: イベント結果を表示できません。'), + ), + ); + } + }, + ), ]; } diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 9c3bb03..d5759a4 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -38,4 +38,5 @@ abstract class Routes { static const EVENT_ENTRIES = '/event-entries'; static const USER_DETAILS_EDIT = '/user-details-edit'; + static const EVENT_RESULT = '/event-result'; }