4 Commits

Author SHA1 Message Date
7976f4c75b Fixed Android issues 2024-09-10 16:16:04 +09:00
8ed6a4e8bf Fix Android issues 2024-09-10 08:12:33 +09:00
a22c2ea730 Entry の表示改善 2024-09-08 21:18:45 +09:00
e6328f84b1 Fix iOS background issue 2024-09-08 19:28:08 +09:00
26 changed files with 815 additions and 199 deletions

16
TODO.txt Normal file
View File

@ -0,0 +1,16 @@
不参加の過去エントリーは削除できるようにする。
バグ:
履歴の写真:アクセスエラー
バックアップをイベントごとに保存・レストア
ログインした際に、イベントが選択されていなければ、イベントを選択するように促す。
事前チェックインした写真が履歴に表示されない。
ユーザー名間違えたらログインできなくなる。
起動時に最後の参加イベントが過去日だったら、
チェックポイントをクリアする。
当日なら、参加処理?をしてタイトルを変える。
チーム構成とエントリーの相関が難しいのでは??

8
android/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,8 @@
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int i(...);
public static int w(...);
public static int d(...);
public static int e(...);
}

View File

@ -13,7 +13,7 @@ import CoreMotion
//GeneratedPluginRegistrant.register(with: self) //GeneratedPluginRegistrant.register(with: self)
//return super.application(application, didFinishLaunchingWithOptions: launchOptions) //return super.application(application, didFinishLaunchingWithOptions: launchOptions)
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let motionChannel = FlutterMethodChannel(name: "com.yourcompany.app/motion", let motionChannel = FlutterMethodChannel(name: "net.sumasen.gifunavi/motion",
binaryMessenger: controller.binaryMessenger) binaryMessenger: controller.binaryMessenger)
motionChannel.setMethodCallHandler({ motionChannel.setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in

View File

@ -19,26 +19,34 @@
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="岐阜ナビ" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WPs-nj-CIV"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="岐阜ナビ" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WPs-nj-CIV">
<rect key="frame" x="46" y="123" width="314" height="59"/> <rect key="frame" x="46" y="122.99999999999999" width="301" height="38.333333333333329"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="32"/> <fontDescription key="fontDescription" type="system" pointSize="32"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="NPO 岐阜aiネットワーク" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TM1-SD-6RA"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" misplaced="YES" text="NPO 岐阜aiネットワーク" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TM1-SD-6RA">
<rect key="frame" x="46" y="722" width="314" height="41"/> <rect key="frame" x="46" y="708" width="301" height="40"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="hCH-Iu-4S2"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="hCH-Iu-4S2">
<rect key="frame" x="83" y="314" width="240" height="224"/> <rect key="frame" x="46" y="241.33333333333334" width="301" height="341.33333333333326"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</imageView> </imageView>
</subviews> </subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="xbc-2k-c8Z" firstAttribute="top" secondItem="TM1-SD-6RA" secondAttribute="bottom" constant="70" id="DbA-tx-JAk"/>
<constraint firstAttribute="trailingMargin" secondItem="hCH-Iu-4S2" secondAttribute="trailing" constant="30" id="DsC-fI-z7h"/>
<constraint firstAttribute="trailingMargin" secondItem="TM1-SD-6RA" secondAttribute="trailing" constant="30" id="Spt-qL-bHo"/>
<constraint firstItem="WPs-nj-CIV" firstAttribute="top" secondItem="Ydg-fD-yQy" secondAttribute="bottom" constant="64" id="Wf4-J5-CP7"/>
<constraint firstItem="hCH-Iu-4S2" firstAttribute="top" secondItem="WPs-nj-CIV" secondAttribute="bottom" constant="80" id="eIO-ZH-rp5"/>
<constraint firstItem="hCH-Iu-4S2" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leadingMargin" constant="30" id="oZr-ky-01l"/>
<constraint firstItem="WPs-nj-CIV" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leadingMargin" constant="30" id="pEC-jM-hHf"/>
<constraint firstItem="TM1-SD-6RA" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leadingMargin" constant="30" id="pRM-dy-ucg"/>
<constraint firstAttribute="trailingMargin" secondItem="WPs-nj-CIV" secondAttribute="trailing" constant="30" id="ugk-x4-Wgc"/>
</constraints>
</view> </view>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>

View File

@ -88,6 +88,9 @@ Future<void> saveGameState() async {
pref.setBool( pref.setBool(
"rogaining_counted", destinationController.rogainingCounted.value); "rogaining_counted", destinationController.rogainingCounted.value);
pref.setBool("ready_for_goal", DestinationController.ready_for_goal); pref.setBool("ready_for_goal", DestinationController.ready_for_goal);
// 最後のゲーム日時を保存
pref.setString('lastGameDate', DateTime.now().toIso8601String());
} }
@ -108,6 +111,20 @@ Future<void> restoreGame() async {
if (indexController.currentUser.isNotEmpty && if (indexController.currentUser.isNotEmpty &&
indexController.currentUser[0]["user"]["id"] == savedUserId) { indexController.currentUser[0]["user"]["id"] == savedUserId) {
// 最後のゲーム日時を取得
final lastGameDateString = pref.getString('lastGameDate');
if (lastGameDateString != null) {
final lastGameDate = DateTime.parse(lastGameDateString);
final now = DateTime.now();
// 最後のゲームが昨日以前の場合
if (lastGameDate.isBefore(DateTime(now.year, now.month, now.day))) {
// ゲームの状態をクリア
await resetGameState();
return; // ここで関数を終了
}
}
final dateString = pref.getString('eventDate'); final dateString = pref.getString('eventDate');
if (dateString != null) { if (dateString != null) {
final parsedDate = DateTime.parse(dateString); final parsedDate = DateTime.parse(dateString);
@ -143,6 +160,24 @@ Future<void> restoreGame() async {
} }
} }
// ゲームの状態をリセットする関数
Future<void> resetGameState() async {
SharedPreferences pref = await SharedPreferences.getInstance();
await pref.remove("is_in_rog");
await pref.remove("rogaining_counted");
await pref.remove("ready_for_goal");
DestinationController destinationController = Get.find<DestinationController>();
destinationController.isInRog.value = false;
destinationController.rogainingCounted.value = false;
DestinationController.ready_for_goal = false;
// チェックポイントをクリア
destinationController.deleteDBDestinations();
debugPrint("Game state has been reset due to outdated last game date");
}
/* /*
void restoreGame_new() async { void restoreGame_new() async {
SharedPreferences pref = await SharedPreferences.getInstance(); SharedPreferences pref = await SharedPreferences.getInstance();
@ -216,10 +251,7 @@ void main() async {
}; };
try { try {
await initServices(); await initServices();
runApp(const ProviderScope(child: MyApp())); runApp(const ProviderScope(child: MyApp()));
}catch(e, stackTrace){ }catch(e, stackTrace){
print('Error during initialization: $e'); print('Error during initialization: $e');
@ -244,19 +276,6 @@ Future<void> initServices() async {
Get.put(LocationController(), permanent: true); Get.put(LocationController(), permanent: true);
debugPrint("2: Controllers initialized"); debugPrint("2: Controllers initialized");
/*
// すべてのコントローラーとサービスを非同期で初期化
Get.lazyPut(() => IndexController(apiService: Get.find<ApiService>()));
debugPrint("2: start IndexController");
// その他のコントローラーを遅延初期化
Get.lazyPut(() => SettingsController());
debugPrint("2: start SettingsController");
Get.lazyPut(() => DestinationController());
debugPrint("3: start DestinationController");
Get.lazyPut(() => LocationController());
debugPrint("4: start LocationController");
*/
// 非同期処理を並列実行 // 非同期処理を並列実行
await Future.wait([ await Future.wait([
@ -273,6 +292,7 @@ Future<void> initServices() async {
}catch(e){ }catch(e){
print('Error initializing : $e'); print('Error initializing : $e');
rethrow;
} }
print('All services started...'); print('All services started...');
@ -667,11 +687,14 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
} }
void _checkMemoryUsage() async { void _checkMemoryUsage() async {
/*
final memoryInfo = await _getMemoryInfo(); final memoryInfo = await _getMemoryInfo();
//debugPrint('Current memory usage: ${memoryInfo['used']} MB'); //debugPrint('Current memory usage: ${memoryInfo['used']} MB');
if (memoryInfo['used']! > 100) { // 100MB以上使用している場合 if (memoryInfo['used']! > 100) { // 100MB以上使用している場合
_performMemoryCleanup(); _performMemoryCleanup();
} }
*/
} }
Future<Map<String, int>> _getMemoryInfo() async { Future<Map<String, int>> _getMemoryInfo() async {

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

@ -44,6 +44,9 @@ import 'package:path_provider/path_provider.dart';
// 目的地に関連する状態管理とロジックを担当するクラスです。 // 目的地に関連する状態管理とロジックを担当するクラスです。
// //
class DestinationController extends GetxController { class DestinationController extends GetxController {
Timer? _checkForCheckinTimer;
final int _checkInterval = 3000; // ミリ秒単位でチェック間隔を設定
late LocationSettings locationSettings; // 位置情報の設定を保持する変数です。 late LocationSettings locationSettings; // 位置情報の設定を保持する変数です。
//late TeamController teamController = TeamController(); //late TeamController teamController = TeamController();
@ -1019,15 +1022,16 @@ class DestinationController extends GetxController {
// 2024-8-24 ... 佐伯呼び出しが必要なのか? // 2024-8-24 ... 佐伯呼び出しが必要なのか?
// //
Future<void> checkForCheckin() async { Future<void> checkForCheckin() async {
//print("--- Start of checkForCheckin function ---"); if (!game_started) {
dbService.updateDatabase(); game_started = true;
await Future.delayed(const Duration(milliseconds: 3000)); dbService.updateDatabase();
game_started = true; }
try { try {
// ここで、エラー // ここで、エラー
if( indexController.locations.isNotEmpty ) { if( indexController.locations.isNotEmpty ) {
indexController.locations[0].features.forEach((fs) async { for (var fs in indexController.locations[0].features) {
//indexController.locations[0].features.forEach((fs) async {
GeoJSONMultiPoint mp = fs!.geometry as GeoJSONMultiPoint; GeoJSONMultiPoint mp = fs!.geometry as GeoJSONMultiPoint;
LatLng pt = LatLng(mp.coordinates[0][1], mp.coordinates[0][0]); LatLng pt = LatLng(mp.coordinates[0][1], mp.coordinates[0][0]);
@ -1045,10 +1049,11 @@ class DestinationController extends GetxController {
await startTimerLocation(fs, distFs); await startTimerLocation(fs, distFs);
// Note: You cannot break out of forEach. If you need to stop processing, you might have to reconsider using forEach. // Note: You cannot break out of forEach. If you need to stop processing, you might have to reconsider using forEach.
} }
}); }
if (gps_push_started == false) { if (gps_push_started == false) {
unawaited(pushGPStoServer()); pushGPStoServer();
//unawaited(pushGPStoServer());
} }
} }
//print("--- 123 ---- $skip_gps----"); //print("--- 123 ---- $skip_gps----");
@ -1071,14 +1076,19 @@ class DestinationController extends GetxController {
// "^^^^^^^^ ${DateFormat('kk:mm:ss \n EEE d MMM').format(DateTime.now())}"); // "^^^^^^^^ ${DateFormat('kk:mm:ss \n EEE d MMM').format(DateTime.now())}");
try { try {
gps_push_started = true; gps_push_started = true;
ExternalService().pushGPS(); await ExternalService().pushGPS();
} catch (e) { } catch (e) {
print("An error occurred in pushGPStoServer: $e");
//print("An error occurred: $e"); //print("An error occurred: $e");
//await pushGPStoServer(); //await pushGPStoServer();
} finally { } finally {
//print("--- End of pushGPStoServer function, calling recursively ---"); if (gps_push_started) {
await Future.delayed(const Duration(seconds: 5 * 60)); Future.delayed(Duration(minutes: 5), pushGPStoServer);
await pushGPStoServer(); }
//print("--- End of pushGPStoServer function, calling recursively ---");
//await Future.delayed(const Duration(seconds: 5 * 60));
//await pushGPStoServer();
} }
} }
@ -1332,12 +1342,13 @@ class DestinationController extends GetxController {
@override @override
void onInit() async { void onInit() async {
super.onInit(); super.onInit();
startCheckForCheckinTimer();
/*
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
await PermissionController.checkAndRequestPermissions(); await PermissionController.checkAndRequestPermissions();
}); });
*/
startGPSCheckTimer(); startGPSCheckTimer();
@ -1415,6 +1426,18 @@ class DestinationController extends GetxController {
//checkGPSDataReceived(); //checkGPSDataReceived();
} }
void startCheckForCheckinTimer() {
_checkForCheckinTimer = Timer.periodic(Duration(milliseconds: _checkInterval), (_) {
checkForCheckin();
});
}
void stopCheckForCheckinTimer() {
_checkForCheckinTimer?.cancel();
_checkForCheckinTimer = null;
}
void restartGPS(){ void restartGPS(){
// GPSデータのListenを再開する処理を追加 // GPSデータのListenを再開する処理を追加
Future.delayed(const Duration(seconds: 5), () { Future.delayed(const Duration(seconds: 5), () {
@ -1427,8 +1450,9 @@ class DestinationController extends GetxController {
// //
@override @override
void onClose() { void onClose() {
gpsCheckTimer?.cancel(); stopCheckForCheckinTimer();
locationController.stopPositionStream(); //gpsCheckTimer?.cancel();
//locationController.stopPositionStream();
super.onClose(); super.onClose();
} }

View File

@ -8,9 +8,11 @@ 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;
import '../../model/user.dart';
class EntryController extends GetxController { class EntryController extends GetxController {
late ApiService _apiService; late ApiService _apiService;
@ -27,6 +29,12 @@ 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; //有効なイベントリスト
final teamMembers = <User>[].obs;
final hasError = false.obs;
final errorMessage = "".obs;
@override @override
void onInit() async { void onInit() async {
super.onInit(); super.onInit();
@ -39,7 +47,7 @@ class EntryController extends GetxController {
_apiService = await Get.putAsync(() => ApiService().init()); _apiService = await Get.putAsync(() => ApiService().init());
} catch (e) { } catch (e) {
print('Error initializing ApiService: $e'); print('Error initializing ApiService: $e');
Get.snackbar('Error', 'Failed to initialize API service'); Get.snackbar('Error', 'APIサービスの初期化に失敗しました');
} }
} }
@ -52,24 +60,31 @@ 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 (activeEvents.isNotEmpty) {
selectedEvent.value = events.first; selectedEvent.value = activeEvents.first;
selectedDate.value = events.first.startDatetime; selectedDate.value = activeEvents.first.startDatetime;
} }
} }
} catch(e) { } catch(e) {
print('Error initializing data: $e'); print('Error initializing data: $e');
Get.snackbar('Error', 'Failed to load initial data'); // エラー状態を設定
hasError.value = true;
Get.snackbar('Error', '初期データの読み込みに失敗しました');
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
} }
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;
@ -91,12 +106,64 @@ class EntryController extends GetxController {
} }
} }
void updateTeam(Team? value) { Future<void> fetchTeamMembers(int teamId) async {
try {
final members = await _apiService.getTeamMembers(teamId);
teamMembers.assignAll(members);
} catch (e) {
print('Error fetching team members: $e');
Get.snackbar('Error', 'Failed to fetch team members');
}
}
List<NewCategory> getFilteredCategories() {
if (selectedTeam.value == null) return [];
if (teamMembers.isEmpty) {
// ソロの場合
String baseCategory = selectedTeam.value!.members.first.female ? 'ソロ女子' : 'ソロ男子';
return categories.where((c) => c.categoryName.startsWith(baseCategory)).toList();
} else if (teamMembers.length == 1) {
// チームメンバーが1人の場合ソロ
String baseCategory = teamMembers.first.female ? 'ソロ女子' : 'ソロ男子';
return categories.where((c) => c.categoryName.startsWith(baseCategory)).toList();
} else {
// 複数人の場合
bool hasElementaryOrYounger = teamMembers.any(isElementarySchoolOrYounger);
String baseCategory = hasElementaryOrYounger ? 'ファミリー' : '一般';
return categories.where((c) => c.categoryName.startsWith(baseCategory)).toList();
}
}
bool isElementarySchoolOrYounger(User user) {
final now = DateTime.now();
final age = now.year - user.dateOfBirth!.year;
return age <= 12;
}
void updateTeam(Team? value) async {
selectedTeam.value = value;
if (value != null) {
await fetchTeamMembers(value.id);
final filteredCategories = getFilteredCategories();
if (filteredCategories.isNotEmpty) {
selectedCategory.value = filteredCategories.first;
} else {
selectedCategory.value = null;
}
} else {
teamMembers.clear();
selectedCategory.value = null;
}
}
void updateTeam_old(Team? value) {
selectedTeam.value = value; selectedTeam.value = value;
if (value != null) { if (value != null) {
selectedCategory.value = value.category; selectedCategory.value = value.category;
} }
} }
//void updateTeam(Team? value) => selectedTeam.value = value; //void updateTeam(Team? value) => selectedTeam.value = value;
void updateCategory(NewCategory? value) => selectedCategory.value = value; void updateCategory(NewCategory? value) => selectedCategory.value = value;
//void updateDate(DateTime value) => selectedDate.value = value; //void updateDate(DateTime value) => selectedDate.value = value;
@ -143,6 +210,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

@ -7,6 +7,7 @@ import 'package:gifunavi/model/event.dart';
import 'package:gifunavi/model/category.dart'; import 'package:gifunavi/model/category.dart';
import 'package:gifunavi/model/team.dart'; import 'package:gifunavi/model/team.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:gifunavi/widgets/error_widget.dart';
import 'package:timezone/timezone.dart' as tz; import 'package:timezone/timezone.dart' as tz;
@ -31,36 +32,72 @@ class EntryDetailPage extends GetView<EntryController> {
if (controller.isLoading.value) { if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
return Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView( if (controller.hasError.value) {
child: Column( return CustomErrorWidget(
crossAxisAlignment: CrossAxisAlignment.start, errorMessage: controller.errorMessage.value,
children: [ onRetry: () => controller.loadInitialData(),
_buildDropdown<Event>( );
label: 'イベント', }
items: controller.events,
selectedId: controller.selectedEvent.value?.id,
try {
return Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDropdown<Event>(
label: 'イベント',
items: controller.activeEvents,
selectedId: controller.selectedEvent.value?.id,
onChanged: (eventId) {
final event = controller.activeEvents.firstWhereOrNull((
e) => e.id == eventId);
if (event != null) {
controller.updateEvent(event);
} else {
print('Event with id $eventId not found');
// 必要に応じてエラー処理を追加
}
},
/*
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, */
getId: (event) => event.id, getDisplayName: (event) => event.eventName,
), getId: (event) => event.id,
const SizedBox(height: 16), ),
_buildDropdown<Team>( const SizedBox(height: 16),
label: 'チーム', _buildDropdown<Team>(
items: controller.teams, label: 'チーム',
selectedId: controller.selectedTeam.value?.id, items: controller.teams,
selectedId: controller.selectedTeam.value?.id,
onChanged: (teamId) {
final team = controller.teams.firstWhereOrNull((t) =>
t.id == teamId);
if (team != null) {
controller.updateTeam(team);
} else {
print('Team with id $teamId not found');
// 必要に応じてエラー処理を追加
}
},
/*
onChanged: (teamId) => controller.updateTeam( onChanged: (teamId) => controller.updateTeam(
controller.teams.firstWhere((t) => t.id == teamId) controller.teams.firstWhere((t) => t.id == teamId)
), ),
getDisplayName: (team) => team.teamName, */
getId: (team) => team.id, getDisplayName: (team) => team.teamName,
), getId: (team) => team.id,
const SizedBox(height: 16), ),
_buildCategoryDropdown(), const SizedBox(height: 16),
/* _buildCategoryDropdown(),
/*
_buildDropdown<NewCategory>() _buildDropdown<NewCategory>()
label: 'カテゴリ', label: 'カテゴリ',
items: controller.categories, items: controller.categories,
@ -73,84 +110,104 @@ class EntryDetailPage extends GetView<EntryController> {
), ),
*/ */
const SizedBox(height: 16), const SizedBox(height: 16),
ListTile( ListTile(
title: const Text('日付'), title: const Text('日付'),
subtitle: Text( subtitle: Text(
controller.selectedDate.value != null controller.selectedDate.value != null
? DateFormat('yyyy-MM-dd').format(tz.TZDateTime.from(controller.selectedDate.value!, tz.getLocation('Asia/Tokyo'))) ? DateFormat('yyyy-MM-dd').format(tz.TZDateTime.from(
: '日付を選択してください', controller.selectedDate.value!,
), tz.getLocation('Asia/Tokyo')))
onTap: () async { : '日付を選択してください',
if (controller.selectedEvent.value == null) {
Get.snackbar('Error', 'Please select an event first');
return;
}
final tz.TZDateTime now = tz.TZDateTime.now(tz.getLocation('Asia/Tokyo'));
final tz.TZDateTime eventStart = tz.TZDateTime.from(controller.selectedEvent.value!.startDatetime, tz.getLocation('Asia/Tokyo'));
final tz.TZDateTime eventEnd = tz.TZDateTime.from(controller.selectedEvent.value!.endDatetime, tz.getLocation('Asia/Tokyo'));
final tz.TZDateTime initialDate = controller.selectedDate.value != null
? tz.TZDateTime.from(controller.selectedDate.value!, tz.getLocation('Asia/Tokyo'))
: (now.isAfter(eventStart) ? now : eventStart);
// 選択可能な最初の日付を設定(今日かイベント開始日のうち、より後の日付)
final tz.TZDateTime firstDate = now.isAfter(eventStart) ? now : eventStart;
final DateTime? picked = await showDatePicker(
context: context,
initialDate: initialDate.isAfter(firstDate) ? initialDate : firstDate,
firstDate: firstDate,
lastDate: eventEnd,
);
if (picked != null) {
controller.updateDate(tz.TZDateTime.from(picked, tz.getLocation('Asia/Tokyo')));
}
},
),
const SizedBox(height: 32),
if (mode == 'new')
ElevatedButton(
onPressed: () => controller.createEntry(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 50),
), ),
child: const Text('エントリーを作成'), onTap: () async {
) if (controller.selectedEvent.value == null) {
else Get.snackbar('Error', 'Please select an event first');
Row( return;
children: [ }
Expanded( final tz.TZDateTime now = tz.TZDateTime.now(tz
child: ElevatedButton( .getLocation('Asia/Tokyo'));
onPressed: () => controller.deleteEntry(), final tz.TZDateTime eventStart = tz.TZDateTime.from(
style: ElevatedButton.styleFrom( controller.selectedEvent.value!.startDatetime, tz
backgroundColor: Colors.red, .getLocation('Asia/Tokyo'));
foregroundColor: Colors.white, final tz.TZDateTime eventEnd = tz.TZDateTime.from(
minimumSize: const Size(0, 50), controller.selectedEvent.value!.endDatetime, tz
), .getLocation('Asia/Tokyo'));
child: const Text('エントリーを削除'),
), final tz.TZDateTime initialDate = controller.selectedDate
), .value != null
const SizedBox(width: 16), ? tz.TZDateTime.from(controller.selectedDate.value!,
Expanded( tz.getLocation('Asia/Tokyo'))
child: ElevatedButton( : (now.isAfter(eventStart) ? now : eventStart);
onPressed: () => controller.updateEntry(),
style: ElevatedButton.styleFrom( // 選択可能な最初の日付を設定(今日かイベント開始日のうち、より後の日付)
backgroundColor: Colors.lightBlue, final tz.TZDateTime firstDate = now.isAfter(eventStart)
foregroundColor: Colors.white, ? now
minimumSize: const Size(0, 50), : eventStart;
),
child: const Text('エントリーを更新'), final DateTime? picked = await showDatePicker(
), context: context,
), initialDate: initialDate.isAfter(firstDate)
], ? initialDate
: firstDate,
firstDate: firstDate,
lastDate: eventEnd,
);
if (picked != null) {
controller.updateDate(tz.TZDateTime.from(
picked, tz.getLocation('Asia/Tokyo')));
}
},
), ),
], const SizedBox(height: 32),
if (mode == 'new')
ElevatedButton(
onPressed: () => controller.createEntry(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 50),
),
child: const Text('エントリーを作成'),
)
else
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => controller.deleteEntry(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
minimumSize: const Size(0, 50),
),
child: const Text('エントリーを削除'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: () => controller.updateEntry(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.lightBlue,
foregroundColor: Colors.white,
minimumSize: const Size(0, 50),
),
child: const Text('エントリーを更新'),
),
),
],
),
],
),
), ),
), );
); } catch (e) {
print('Error in EntryDetailPage: $e');
return const Center(
child: Text('エラーが発生しました。もう一度お試しください。'),
);
}
}), }),
); );
} }
@ -166,15 +223,42 @@ class EntryDetailPage extends GetView<EntryController> {
return DropdownButtonFormField<int>( return DropdownButtonFormField<int>(
decoration: InputDecoration(labelText: label), decoration: InputDecoration(labelText: label),
value: selectedId, value: selectedId,
items: items.map((item) => DropdownMenuItem<int>( items: items.isNotEmpty ? items.map((item) => DropdownMenuItem<int>(
//items: items.map((item) => DropdownMenuItem<int>(
value: getId(item), value: getId(item),
child: Text(getDisplayName(item)), child: Text(getDisplayName(item)),
)).toList(), )).toList() : null,
onChanged: onChanged, onChanged: (value) {
if (value != null) {
onChanged(value);
}
},
//onChanged: onChanged,
); );
} }
Widget _buildCategoryDropdown() { Widget _buildCategoryDropdown() {
final eligibleCategories = controller.getFilteredCategories();
return DropdownButtonFormField<NewCategory>(
decoration: InputDecoration(labelText: 'カテゴリ'),
value: controller.selectedCategory.value,
items: eligibleCategories.isNotEmpty ? eligibleCategories.map((category) => DropdownMenuItem<NewCategory>(
//items: eligibleCategories.map((category) => DropdownMenuItem<NewCategory>(
value: category,
child: Text(category.categoryName),
)).toList() : null,
onChanged: (value) {
if (value != null) {
controller.updateCategory(value);
}
},
//onChanged: (value) => controller.updateCategory(value),
);
}
Widget _buildCategoryDropdown_old() {
final eligibleCategories = controller.categories.where((c) => final eligibleCategories = controller.categories.where((c) =>
c.baseCategory == controller.selectedCategory.value?.baseCategory c.baseCategory == controller.selectedCategory.value?.baseCategory
).toList(); ).toList();

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

@ -240,14 +240,14 @@ class IndexController extends GetxController with WidgetsBindingObserver {
@override @override
void onInit() { void onInit() {
super.onInit();
try { try {
super.onInit();
initConnectivity(); initConnectivity();
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateConnectionStatus); _connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
WidgetsBinding.instance.addPostFrameCallback((_) async { //WidgetsBinding.instance.addPostFrameCallback((_) async {
await PermissionController.checkAndRequestPermissions(); // await PermissionController.checkAndRequestPermissions();
}); //});
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
_startLocationService(); // アプリ起動時にLocationServiceを開始する _startLocationService(); // アプリ起動時にLocationServiceを開始する

View File

@ -40,7 +40,7 @@ class _IndexPageState extends State<IndexPage> {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
await _ensureControllersAreInitialized(); await _ensureControllersAreInitialized();
await PermissionController.checkAndRequestPermissions(); //await PermissionController.checkAndRequestPermissions();
}); });
} }

View File

@ -167,6 +167,17 @@ class PermissionController {
} }
static Future<bool> showLocationDisclosure() async { static Future<bool> showLocationDisclosure() async {
if (Platform.isIOS) {
return await _showLocationDisclosureIOS();
} else if (Platform.isAndroid) {
return await _showLocationDisclosureAndroid();
} else {
// その他のプラットフォームの場合はデフォルトの処理を行う
return await _showLocationDisclosureIOS();
}
}
static Future<bool> _showLocationDisclosureIOS() async {
if (Get.context == null) { if (Get.context == null) {
print('Context is null, cannot show dialog'); print('Context is null, cannot show dialog');
return false; return false;
@ -214,6 +225,41 @@ class PermissionController {
} }
} }
static Future<bool> _showLocationDisclosureAndroid() async {
return await showDialog<bool>(
context: Get.overlayContext ?? Get.context ?? (throw Exception('No valid context found')),
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('位置情報の使用について'),
content: const SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text('このアプリでは、以下の目的で位置情報を使用します:'),
Text('• チェックポイントの自動チェックイン(アプリが閉じているときも含む)'),
Text('• 移動履歴の記録(バックグラウンドでも継続)'),
Text('• 現在地周辺の情報表示'),
Text('\nバックグラウンドでも位置情報を継続的に取得します。'),
Text('これにより、バッテリーの消費が増加する可能性があります。'),
Text('同意しない場合には、アプリは終了します。'),
],
),
),
actions: <Widget>[
TextButton(
child: const Text('同意しない'),
onPressed: () => Navigator.of(context).pop(false),
),
TextButton(
child: const Text('同意する'),
onPressed: () => Navigator.of(context).pop(true),
),
],
);
},
) ?? false;
}
static void showPermissionDeniedDialog(String title,String message) { static void showPermissionDeniedDialog(String title,String message) {
Get.dialog( Get.dialog(
AlertDialog( AlertDialog(

View File

@ -329,13 +329,6 @@ class TeamController extends GetxController {
} }
List<NewCategory> getFilteredCategories_old() { List<NewCategory> getFilteredCategories_old() {
//List<User> teamMembers = getCurrentTeamMembers();
return categories.where((category) {
return isCategoryValid(category, teamMembers);
}).toList();
}
List<NewCategory> getFilteredCategories() {
if (teamMembers.isEmpty && currentUser.value != null) { if (teamMembers.isEmpty && currentUser.value != null) {
// ソロの場合 // ソロの場合
String baseCategory = currentUser.value!.female ? 'ソロ女子' : 'ソロ男子'; String baseCategory = currentUser.value!.female ? 'ソロ女子' : 'ソロ男子';
@ -347,6 +340,24 @@ class TeamController extends GetxController {
} }
} }
List<NewCategory> getFilteredCategories() {
if (teamMembers.isEmpty && currentUser.value != null) {
// ソロの場合
String baseCategory = currentUser.value!.female ? 'ソロ女子' : 'ソロ男子';
return categories.where((c) => c.categoryName.startsWith(baseCategory)).toList();
} else if (teamMembers.length == 1) {
// チームメンバーが1人の場合ソロ
String baseCategory = teamMembers.first.female ? 'ソロ女子' : 'ソロ男子';
return categories.where((c) => c.categoryName.startsWith(baseCategory)).toList();
} else {
// 複数人の場合
bool hasElementaryOrYounger = teamMembers.any(isElementarySchoolOrYounger);
String baseCategory = hasElementaryOrYounger ? 'ファミリー' : '一般';
return categories.where((c) => c.categoryName.startsWith(baseCategory)).toList();
}
}
bool isElementarySchoolOrYounger(User user) { bool isElementarySchoolOrYounger(User user) {
final now = DateTime.now(); final now = DateTime.now();
final age = now.year - user.dateOfBirth!.year; final age = now.year - user.dateOfBirth!.year;
@ -379,17 +390,7 @@ class TeamController extends GetxController {
// メンバーリストの最新状態を取得 // メンバーリストの最新状態を取得
await fetchTeamMembers(selectedTeam.value!.id); await fetchTeamMembers(selectedTeam.value!.id);
List<NewCategory> eligibleCategories = []; List<NewCategory> eligibleCategories = getFilteredCategories();
if (teamMembers.isEmpty || teamMembers.length == 1) {
if (currentUser.value != null) {
String baseCategory = currentUser.value!.female ? 'ソロ女子' : 'ソロ男子';
eligibleCategories = categories.where((c) => c.baseCategory == baseCategory).toList();
}
} else {
bool hasElementaryOrYounger = teamMembers.any(isElementarySchoolOrYounger);
String baseCategory = hasElementaryOrYounger ? 'ファミリー' : '一般';
eligibleCategories = categories.where((c) => c.baseCategory == baseCategory).toList();
}
// 同じ時間のカテゴリを優先的に選択 // 同じ時間のカテゴリを優先的に選択
NewCategory? newCategory = eligibleCategories.firstWhereOrNull((c) => c.time == oldTime); NewCategory? newCategory = eligibleCategories.firstWhereOrNull((c) => c.time == oldTime);

View File

@ -120,15 +120,19 @@ class _TeamDetailPageState extends State<TeamDetailPage> {
} }
final filteredCategories = controller.getFilteredCategories(); final filteredCategories = controller.getFilteredCategories();
final categoriesToDisplay = filteredCategories.isEmpty // 選択されているカテゴリが表示リストに含まれていない場合、最初の項目を選択する
? controller.categories if (controller.selectedCategory.value == null ||
: filteredCategories; !filteredCategories.contains(controller.selectedCategory.value)) {
controller.updateCategory(filteredCategories.isNotEmpty
? filteredCategories.first
: null);
}
// 選択されているカテゴリが表示リストに含まれていない場合、最初の項目を選択する // 選択されているカテゴリが表示リストに含まれていない場合、最初の項目を選択する
if (controller.selectedCategory.value == null || if (controller.selectedCategory.value == null ||
!categoriesToDisplay.contains(controller.selectedCategory.value)) { !filteredCategories.contains(controller.selectedCategory.value)) {
controller.updateCategory(categoriesToDisplay.isNotEmpty controller.updateCategory(filteredCategories.isNotEmpty
? categoriesToDisplay.first ? filteredCategories.first
: null); : null);
} }
@ -147,7 +151,7 @@ class _TeamDetailPageState extends State<TeamDetailPage> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
if (categoriesToDisplay.isEmpty) if (filteredCategories.isEmpty)
const Text('カテゴリデータを読み込めませんでした。', const Text('カテゴリデータを読み込めませんでした。',
style: TextStyle(color: Colors.red)) style: TextStyle(color: Colors.red))
else else
@ -156,7 +160,7 @@ class _TeamDetailPageState extends State<TeamDetailPage> {
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'カテゴリ'), labelText: 'カテゴリ'),
value: controller.selectedCategory.value, value: controller.selectedCategory.value,
items: categoriesToDisplay.map((category) => items: filteredCategories.map((category) =>
DropdownMenuItem( DropdownMenuItem(
value: category, value: category,
child: Text(category.categoryName), child: Text(category.categoryName),

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';
} }

View File

@ -87,7 +87,7 @@ class ApiService extends GetxService{
Future<dynamic> _handleRequest(Future<http.Response> Function() request) async { Future<dynamic> _handleRequest(Future<http.Response> Function() request) async {
try { try {
final response = await request(); final response = await request();
if (response.statusCode == 200) { if (response.statusCode == 200 || response.statusCode == 201) {
return json.decode(utf8.decode(response.bodyBytes)); return json.decode(utf8.decode(response.bodyBytes));
} else if (response.statusCode == 401) { } else if (response.statusCode == 401) {
await _handleUnauthorized(); await _handleUnauthorized();

View File

@ -370,6 +370,15 @@ class ExternalService {
//int userId = indexController.currentUser[0]["user"]["id"]; //int userId = indexController.currentUser[0]["user"]["id"];
//print("--- Pressed -----"); //print("--- Pressed -----");
if( indexController.currentUser[0]["user"]==null ){
return Future.value(false);
}
if( indexController.currentUser[0]["user"]['team_name']==null ){
return Future.value(false);
}
if( indexController.currentUser[0]["user"]["event_code"]==null ){
return Future.value(false);
}
String team = indexController.currentUser[0]["user"]['team_name']; String team = indexController.currentUser[0]["user"]['team_name'];
//print("--- _team : ${_team}-----"); //print("--- _team : ${_team}-----");
String eventCode = indexController.currentUser[0]["user"]["event_code"]; String eventCode = indexController.currentUser[0]["user"]["event_code"];

View File

@ -248,7 +248,7 @@ class LocationController extends GetxController {
return; return;
} }
await PermissionController.checkAndRequestPermissions(); //await PermissionController.checkAndRequestPermissions();
// 位置情報の設定を行います。z11 // 位置情報の設定を行います。z11
// Set up the location options // Set up the location options

View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class CustomErrorWidget extends StatelessWidget {
final String errorMessage;
final VoidCallback onRetry;
const CustomErrorWidget({
Key? key,
required this.errorMessage,
required this.onRetry,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'エラーが発生しました',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(errorMessage),
const SizedBox(height: 16),
ElevatedButton(
onPressed: onRetry,
child: const Text('再試行'),
),
],
),
);
}
}

View File

@ -54,9 +54,11 @@ class _MapWidgetState extends State<MapWidget> with WidgetsBindingObserver {
super.initState(); super.initState();
// 追加 // 追加
/*
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
PermissionController.checkAndRequestPermissions(); PermissionController.checkAndRequestPermissions();
}); });
*/
debugPrint('MapWidget: initState called'); debugPrint('MapWidget: initState called');
SettingsBinding().dependencies(); // これを追加 SettingsBinding().dependencies(); // これを追加
@ -83,7 +85,7 @@ class _MapWidgetState extends State<MapWidget> with WidgetsBindingObserver {
}); });
// MapControllerの初期化が完了したら、IndexControllerのonInitを呼び出す // MapControllerの初期化が完了したら、IndexControllerのonInitを呼び出す
//indexController.checkPermission(); //indexController.checkPermission();
PermissionController.checkAndRequestPermissions(); //PermissionController.checkAndRequestPermissions();
}); });
late MapResetController mapResetController = MapResetController(); late MapResetController mapResetController = MapResetController();

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 4.8.19+499 version: 4.8.20+500
environment: environment:
sdk: ^3.5.0 sdk: ^3.5.0