From ee007795b9320056185c69b3cf73bd8b91315095 Mon Sep 17 00:00:00 2001 From: Akira Date: Thu, 1 Aug 2024 20:08:12 +0900 Subject: [PATCH] =?UTF-8?q?=E3=81=BB=E3=81=BC=E5=AE=8C=E6=88=90=EF=BC=9AQR?= =?UTF-8?q?code=E3=83=88=E3=83=A9=E3=82=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- all_combined.txt | 2766 +++++++++++++---- ios/Podfile.lock | 14 +- ios/Runner.xcodeproj/project.pbxproj | 26 +- lib/pages/camera/camera_page.dart | 285 +- .../destination/destination_controller.dart | 2 +- lib/pages/drawer/drawer_page.dart | 9 +- lib/pages/entry/entry_controller.dart | 12 +- lib/pages/entry/entry_detail_page.dart | 47 +- lib/pages/entry/entry_list_page.dart | 36 +- lib/pages/index/index_controller.dart | 41 +- lib/pages/index/index_page.dart | 36 +- lib/pages/login/login_page.dart | 22 +- lib/pages/register/register_page.dart | 269 +- lib/pages/register/user_detail_page.dart | 182 ++ lib/pages/team/member_detail_page.dart | 258 +- lib/pages/team/team_controller.dart | 106 +- lib/pages/team/team_detail_page.dart | 31 +- lib/routes/app_pages.dart | 9 +- lib/routes/app_routes.dart | 2 + lib/services/api_service.dart | 83 +- lib/services/auth_service.dart | 20 + lib/utils/const.dart | 2 + lib/utils/string_values.dart | 2 +- lib/widgets/helper_dialog.dart | 73 + pubspec.yaml | 2 +- 25 files changed, 3214 insertions(+), 1121 deletions(-) create mode 100644 lib/pages/register/user_detail_page.dart create mode 100644 lib/widgets/helper_dialog.dart diff --git a/all_combined.txt b/all_combined.txt index f4e8d90..961bf14 100644 --- a/all_combined.txt +++ b/all_combined.txt @@ -1176,7 +1176,7 @@ class StringValues extends Translations{ 'already_have_account': 'すでにアカウントをお持ちですか?', 'sign_up': 'サインアップ', 'create_account': 'アカウントを無料で作成します', - 'confirm_password': 'パスワードを認証する', + 'confirm_password': '確認用パスワード', 'cancel_checkin': 'チェックイン取消', 'go_here': 'ルート表示', 'cancel_route':'ルート消去', @@ -1907,6 +1907,8 @@ class UtilController extends GetxController{ class ConstValues{ static const container_svr = "http://container.intranet.sumasen.net:8100"; static const server_uri = "https://rogaining.intranet.sumasen.net"; + //static const container_svr = "http://container.sumasen.net:8100"; + //static const server_uri = "https://rogaining.sumasen.net"; static const dev_server = "http://localhost:8100"; static const dev_ip_server = "http://192.168.8.100:8100"; static const dev_home_ip_server = "http://172.20.10.9:8100"; @@ -2573,6 +2575,7 @@ class GpsData { } // lib/models/team.dart +import 'dart:convert'; import 'category.dart'; import 'user.dart'; @@ -2580,9 +2583,10 @@ class Team { final int id; final String zekkenNumber; final String teamName; - final Category category; + final NewCategory category; final User owner; + Team({ required this.id, required this.zekkenNumber, @@ -2593,11 +2597,15 @@ class Team { factory Team.fromJson(Map json) { return Team( - id: json['id'], - zekkenNumber: json['zekken_number'], - teamName: json['team_name'], - category: Category.fromJson(json['category']), - owner: User.fromJson(json['owner']), + id: json['id'] ?? 0, + zekkenNumber: json['zekken_number'] ?? 'Unknown', + teamName: json['team_name'] ?? 'Unknown Team', + category: json['category'] != null + ? NewCategory.fromJson(json['category']) + : NewCategory(id: 0, categoryName: 'Unknown', categoryNumber: 0, duration: Duration.zero, numOfMember: 1, family: false, female: false), + owner: json['owner'] != null + ? User.fromJson(json['owner']) + : User(id: 0, email: 'unknown@example.com', firstname: 'Unknown', lastname: 'User', dateOfBirth: null, female: false, isActive: false), ); } @@ -2662,11 +2670,13 @@ class User { return User( id: json['id'], email: json['email'], - firstname: json['firstname'], - lastname: json['lastname'], - dateOfBirth: json['date_of_birth'] != null ? DateTime.parse(json['date_of_birth']) : null, - female: json['female'], - isActive: json['is_active'], + firstname: json['firstname'] ?? 'Unknown', + lastname: json['lastname'] ?? 'Unknown', + dateOfBirth: json['date_of_birth'] != null + ? DateTime.tryParse(json['date_of_birth']) + : null, + female: json['female'] ?? false, + isActive: json['is_active'] ?? false, ); } @@ -2821,7 +2831,7 @@ class Destination { } // lib/models/category.dart -class Category { +class NewCategory { final int id; final String categoryName; final int categoryNumber; @@ -2830,7 +2840,7 @@ class Category { final bool family; final bool female; - Category({ + NewCategory({ required this.id, required this.categoryName, required this.categoryNumber, @@ -2840,15 +2850,25 @@ class Category { required this.female, }); - factory Category.fromJson(Map json) { - return Category( - id: json['id'], - categoryName: json['category_name'], - categoryNumber: json['category_number'], - duration: Duration(seconds: json['duration']), - numOfMember: json['num_of_member'], - family: json['family'], - female: json['female'], + @override + bool operator ==(Object other) => + identical(this, other) || + other is NewCategory && + runtimeType == other.runtimeType && + id == other.id; + + @override + int get hashCode => id.hashCode; + + factory NewCategory.fromJson(Map json) { + return NewCategory( + id: json['id'] ?? 0, + categoryName: json['category_name'] ?? 'Unknown Category', + categoryNumber: json['category_number'] ?? 0, + duration: Duration(seconds: json['duration'] ?? 0), + numOfMember: json['num_of_member'] ?? 1, + family: json['family'] ?? false, + female: json['female'] ?? false, ); } @@ -3012,8 +3032,8 @@ class Entry { final int id; final Team team; final Event event; - final Category category; - final DateTime date; + final NewCategory category; + final DateTime? date; final String owner; Entry({ @@ -3030,9 +3050,11 @@ class Entry { id: json['id'], team: Team.fromJson(json['team']), event: Event.fromJson(json['event']), - category: Category.fromJson(json['category']), - date: DateTime.parse(json['date']), - owner: json['owner'], + category: NewCategory.fromJson(json['category']), + date: json['date'] != null + ? DateTime.tryParse(json['date']) + : null, + owner: json['owner'] is Map ? json['owner']['name'] ?? '' : json['owner'] ?? '', ); } @@ -3042,7 +3064,7 @@ class Entry { 'team': team.toJson(), 'event': event.toJson(), 'category': category.toJson(), - 'date': date.toIso8601String(), + 'date': date?.toIso8601String(), 'owner': owner, }; } @@ -3500,6 +3522,40 @@ class DrawerPage extends StatelessWidget { ), )), ), + + ListTile( + leading: Icon(Icons.group), + title: Text('チーム管理'), + onTap: () { + Get.back(); + Get.toNamed(AppPages.TEAM_LIST); + }, + ), + ListTile( + leading: Icon(Icons.event), + title: Text('エントリー管理'), + onTap: () { + Get.back(); + Get.toNamed(AppPages.ENTRY_LIST); + }, + ), + ListTile( + leading: Icon(Icons.event), + title: Text('イベント参加'), + onTap: () { + Get.back(); // ドロワーを閉じる + Get.toNamed(AppPages.EVENT_ENTRY); + }, + ), + ListTile( + leading: const Icon(Icons.person), + title: Text("個人情報の修正"), + onTap: () { + Get.back(); // Close the drawer + Get.toNamed(AppPages.USER_DETAILS_EDIT); + }, + ), + Obx(() => indexController.currentUser.isEmpty ? ListTile( leading: const Icon(Icons.login), @@ -3627,22 +3683,7 @@ class DrawerPage extends StatelessWidget { width: 0, height: 0, ), - ListTile( - leading: Icon(Icons.group), - title: Text('チーム管理'), - onTap: () { - Get.back(); - Get.toNamed(AppPages.TEAM_LIST); - }, - ), - ListTile( - leading: Icon(Icons.event), - title: Text('エントリー管理'), - onTap: () { - Get.back(); - Get.toNamed(AppPages.ENTRY_LIST); - }, - ), + ListTile( leading: const Icon(Icons.privacy_tip), title: Text("privacy".tr), @@ -4236,11 +4277,37 @@ class _GpsPageState extends State { ); } } -// lib/pages/entry/entry_detail_page.dart +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:rogapp/pages/entry/event_entries_controller.dart'; + +class EventEntriesPage extends GetView { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('イベント参加')), + body: Obx(() => ListView.builder( + itemCount: controller.entries.length, + itemBuilder: (context, index) { + final entry = controller.entries[index]; + return ListTile( + title: Text(entry.event.eventName), + subtitle: Text('${entry.category.categoryName} - ${entry.date}'), + onTap: () => controller.joinEvent(entry), + ); + }, + )), + ); + } +}// lib/pages/entry/entry_detail_page.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/entry/entry_controller.dart'; +import 'package:rogapp/model/event.dart'; +import 'package:rogapp/model/category.dart'; +import 'package:rogapp/model/team.dart'; +import 'package:intl/intl.dart'; class EntryDetailPage extends GetView { @override @@ -4249,52 +4316,140 @@ class EntryDetailPage extends GetView { final mode = Get.arguments['mode'] as String? ?? 'new'; final entry = Get.arguments['entry']; + if (mode == 'edit' && entry != null) { + controller.initializeEditMode(entry); + } + return Scaffold( appBar: AppBar( title: Text(mode == 'new' ? 'エントリー登録' : 'エントリー詳細'), ), - body: Padding( - padding: EdgeInsets.all(16.0), - child: Obx(() => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DropdownButtonFormField( - decoration: InputDecoration(labelText: 'イベント'), - value: controller.selectedEvent.value, - items: controller.events.map((event) => DropdownMenuItem( - value: event, - child: Text(event.eventName), - )).toList(), - onChanged: (value) => controller.updateEvent(value), + body: Obx(() { + if (controller.isLoading.value) { + return Center(child: CircularProgressIndicator()); + } + return Padding( + padding: EdgeInsets.all(16.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDropdown( + label: 'イベント', + items: controller.events, + selectedId: controller.selectedEvent.value?.id, + onChanged: (eventId) => controller.updateEvent( + controller.events.firstWhere((e) => e.id == eventId) + ), + getDisplayName: (event) => event.eventName, + getId: (event) => event.id, + ), + SizedBox(height: 16), + _buildDropdown( + label: 'チーム', + items: controller.teams, + selectedId: controller.selectedTeam.value?.id, + onChanged: (teamId) => controller.updateTeam( + controller.teams.firstWhere((t) => t.id == teamId) + ), + getDisplayName: (team) => team.teamName, + getId: (team) => team.id, + ), + SizedBox(height: 16), + _buildDropdown( + label: 'カテゴリ', + items: controller.categories, + selectedId: controller.selectedCategory.value?.id, + onChanged: (categoryId) => controller.updateCategory( + controller.categories.firstWhere((c) => c.id == categoryId) + ), + getDisplayName: (category) => category.categoryName, + getId: (category) => category.id, + ), + SizedBox(height: 16), + ListTile( + title: Text('日付'), + subtitle: Text( + controller.selectedDate.value != null + ? DateFormat('yyyy-MM-dd').format(controller.selectedDate.value!) + : '日付を選択してください', + ), + onTap: () async { + if (controller.selectedEvent.value == null) { + Get.snackbar('Error', 'Please select an event first'); + return; + } + final DateTime? picked = await showDatePicker( + context: context, + initialDate: controller.selectedDate.value ?? controller.selectedEvent.value!.startDatetime, + firstDate: controller.selectedEvent.value!.startDatetime, + lastDate: controller.selectedEvent.value!.endDatetime, + ); + if (picked != null) { + controller.updateDate(picked); + } + }, + ), + SizedBox(height: 32), + if (mode == 'new') + ElevatedButton( + child: Text('エントリーを作成'), + onPressed: () => controller.createEntry(), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + minimumSize: Size(double.infinity, 50), + ), + ) + else + Row( + children: [ + Expanded( + child: ElevatedButton( + child: Text('エントリーを削除'), + onPressed: () => controller.deleteEntry(), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + minimumSize: Size(0, 50), + ), + ), + ), + SizedBox(width: 16), + Expanded( + child: ElevatedButton( + child: Text('エントリーを更新'), + onPressed: () => controller.updateEntry(), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.lightBlue, + minimumSize: Size(0, 50), + ), + ), + ), + ], + ), + ], ), - DropdownButtonFormField( - decoration: InputDecoration(labelText: 'チーム'), - value: controller.selectedTeam.value, - items: controller.teams.map((team) => DropdownMenuItem( - value: team, - child: Text(team.teamName), - )).toList(), - onChanged: (value) => controller.updateTeam(value), - ), - DropdownButtonFormField( - decoration: InputDecoration(labelText: 'カテゴリ'), - value: controller.selectedCategory.value, - items: controller.categories.map((category) => DropdownMenuItem( - value: category, - child: Text(category.categoryName), - )).toList(), - onChanged: (value) => controller.updateCategory(value), - ), - // 日付選択ウィジェットを追加 - if (mode == 'edit' && controller.isOwner()) - ElevatedButton( - child: Text('エントリーを削除'), - onPressed: () => controller.deleteEntry(), - ), - ], - ), - ), - ) + ), + ); + }), + ); + } + + Widget _buildDropdown({ + required String label, + required List items, + required int? selectedId, + required void Function(int?) onChanged, + required String Function(T) getDisplayName, + required int Function(T) getId, + }) { + return DropdownButtonFormField( + decoration: InputDecoration(labelText: label), + value: selectedId, + items: items.map((item) => DropdownMenuItem( + value: getId(item), + child: Text(getDisplayName(item)), + )).toList(), + onChanged: onChanged, ); } }// lib/pages/entry/entry_list_page.dart @@ -4317,23 +4472,36 @@ class EntryListPage extends GetView { ), ], ), - body: Obx(() => ListView.builder( - itemCount: controller.entries.length, - itemBuilder: (context, index) { - final entry = controller.entries[index]; - return ListTile( - title: Text(entry.event?.eventName ?? 'イベント未設定'), - subtitle: Text('${entry.team?.teamName ?? 'チーム未設定'} - ${entry.category?.categoryName ?? 'カテゴリ未設定'}'), - trailing: Text(entry.date?.toString().substring(0, 10) ?? '日付未設定'), - onTap: () => Get.toNamed(AppPages.ENTRY_DETAIL, arguments: {'mode': 'edit', 'entry': entry}), - ); - }, - )), + body: Obx((){ + // エントリーを日付昇順にソート + final sortedEntries = controller.entries.toList() + ..sort((a, b) => (a.date ?? DateTime(0)).compareTo(b.date ?? DateTime(0))); + + return ListView.builder( + itemCount: sortedEntries.length, + itemBuilder: (context, index) { + final entry = sortedEntries[index]; + return ListTile( + title: Text(entry.event?.eventName ?? 'イベント未設定'), + subtitle: Text( + '${entry.team?.teamName ?? 'チーム未設定'} - ${entry.category + ?.categoryName ?? 'カテゴリ未設定'}'), + trailing: Text( + entry.date?.toString().substring(0, 10) ?? '日付未設定'), + onTap: () => + Get.toNamed(AppPages.ENTRY_DETAIL, + arguments: {'mode': 'edit', 'entry': entry}), + ); + }, + ); + }), ); + } }// lib/entry/entry_controller.dart import 'package:get/get.dart'; +import 'package:intl/intl.dart'; import 'package:rogapp/model/entry.dart'; import 'package:rogapp/model/event.dart'; import 'package:rogapp/model/team.dart'; @@ -4341,27 +4509,40 @@ import 'package:rogapp/model/category.dart'; import 'package:rogapp/services/api_service.dart'; class EntryController extends GetxController { - late final ApiService _apiService; + late ApiService _apiService; final entries = [].obs; final events = [].obs; final teams = [].obs; - final categories = [].obs; + final categories = [].obs; final selectedEvent = Rx(null); final selectedTeam = Rx(null); - final selectedCategory = Rx(null); + final selectedCategory = Rx(null); final selectedDate = Rx(null); final currentEntry = Rx(null); + final isLoading = true.obs; @override - void onInit() async{ + void onInit() async { super.onInit(); - await Get.putAsync(() => ApiService().init()); - _apiService = Get.find(); + await initializeApiService(); + await loadInitialData(); + } + Future initializeApiService() async { try { + _apiService = await Get.putAsync(() => ApiService().init()); + } catch (e) { + print('Error initializing ApiService: $e'); + Get.snackbar('Error', 'Failed to initialize API service'); + } + } + + Future loadInitialData() async { + try { + isLoading.value = true; await Future.wait([ fetchEntries(), fetchEvents(), @@ -4370,14 +4551,54 @@ class EntryController extends GetxController { ]); if (Get.arguments != null && Get.arguments['entry'] != null) { currentEntry.value = Get.arguments['entry']; - _initializeEntryData(); + initializeEditMode(currentEntry.value!); + } else { + // 新規作成モードの場合、最初のイベントを選択 + if (events.isNotEmpty) { + selectedEvent.value = events.first; + selectedDate.value = events.first.startDatetime; + } } - Get.putAsync(() => ApiService().init()); - }catch(e){ + } catch(e) { print('Error initializing data: $e'); + Get.snackbar('Error', 'Failed to load initial data'); + } finally { + isLoading.value = false; } } + + void initializeEditMode(Entry entry) { + currentEntry.value = entry; + selectedEvent.value = entry.event; + selectedTeam.value = entry.team; + selectedCategory.value = entry.category; + selectedDate.value = entry.date; + } + + void updateEvent(Event? value) { + selectedEvent.value = value; + if (value != null) { + // イベント変更時に日付を調整 + if (selectedDate.value == null || + selectedDate.value!.isBefore(value.startDatetime) || + selectedDate.value!.isAfter(value.endDatetime)) { + selectedDate.value = value.startDatetime; + } + } + } + + void updateTeam(Team? value) => selectedTeam.value = value; + void updateCategory(NewCategory? value) => selectedCategory.value = value; + void updateDate(DateTime value) => selectedDate.value = value; + + /* + void updateDate(DateTime value){ + selectedDate.value = DateFormat('yyyy-MM-dd').format(value!) as DateTime?; + } + + */ + void _initializeEntryData() { if (currentEntry.value != null) { selectedEvent.value = currentEntry.value!.event; @@ -4393,6 +4614,7 @@ class EntryController extends GetxController { entries.assignAll(fetchedEntries); } catch (e) { print('Error fetching entries: $e'); + Get.snackbar('Error', 'Failed to fetch entries'); } } @@ -4402,6 +4624,7 @@ class EntryController extends GetxController { events.assignAll(fetchedEvents); } catch (e) { print('Error fetching events: $e'); + Get.snackbar('Error', 'Failed to fetch events'); } } @@ -4411,6 +4634,7 @@ class EntryController extends GetxController { teams.assignAll(fetchedTeams); } catch (e) { print('Error fetching teams: $e'); + Get.snackbar('Error', 'Failed to fetch team'); } } @@ -4420,6 +4644,7 @@ class EntryController extends GetxController { categories.assignAll(fetchedCategories); } catch (e) { print('Error fetching categories: $e'); + Get.snackbar('Error', 'Failed to fetch categories'); } } @@ -4445,10 +4670,14 @@ class EntryController extends GetxController { } Future updateEntry() async { - if (currentEntry.value == null) return; + if (currentEntry.value == null) { + Get.snackbar('Error', 'No entry selected for update'); + return; + } try { final updatedEntry = await _apiService.updateEntry( currentEntry.value!.id, + currentEntry.value!.team.id, selectedEvent.value!.id, selectedCategory.value!.id, selectedDate.value!, @@ -4465,7 +4694,10 @@ class EntryController extends GetxController { } Future deleteEntry() async { - if (currentEntry.value == null) return; + if (currentEntry.value == null) { + Get.snackbar('Error', 'No entry selected for deletion'); + return; + } try { await _apiService.deleteEntry(currentEntry.value!.id); entries.removeWhere((entry) => entry.id == currentEntry.value!.id); @@ -4476,16 +4708,68 @@ class EntryController extends GetxController { } } - void updateEvent(Event? value) => selectedEvent.value = value; - void updateTeam(Team? value) => selectedTeam.value = value; - void updateCategory(Category? value) => selectedCategory.value = value; - void updateDate(DateTime value) => selectedDate.value = value; bool isOwner() { // Implement logic to check if the current user is the owner of the entry return true; // Placeholder } }import 'package:get/get.dart'; +import 'package:rogapp/model/entry.dart'; +import 'package:rogapp/pages/index/index_controller.dart'; +import 'package:rogapp/services/api_service.dart'; + +class EventEntriesController extends GetxController { + final ApiService _apiService = Get.find(); + final IndexController _indexController = Get.find(); + + final entries = [].obs; + + @override + void onInit() { + super.onInit(); + fetchEntries(); + } + + Future fetchEntries() async { + try { + final fetchedEntries = await _apiService.getEntries(); + entries.assignAll(fetchedEntries); + } catch (e) { + print('Error fetching entries: $e'); + // エラー処理を追加 + } + } + + Future joinEvent(Entry entry) async { + try { + + final userid = _indexController.currentUser[0]["user"]["id"]; + + await _apiService.updateUserInfo(userid,entry); + + _indexController.currentUser[0]["user"]["event_code"] = entry.event.eventName; + _indexController.currentUser[0]["user"]["team_name"] = entry.team.teamName; + _indexController.currentUser[0]["user"]["group"] = entry.team.category.categoryName; + _indexController.currentUser[0]["user"]["zekken_number"] = entry.team.zekkenNumber; + + Get.back(); // エントリー一覧ページを閉じる + //_indexController.isLoading.value = true; + _indexController.reloadMap(entry.event.eventName); // マップをリロード + } catch (e) { + print('Error joining event: $e'); + // エラー処理を追加 + } + } +}import 'package:get/get.dart'; +import 'package:rogapp/pages/entry/event_entries_controller.dart'; + +class EventEntriesBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => EventEntriesController()); + } +} +import 'package:get/get.dart'; import 'package:rogapp/pages/entry/entry_controller.dart'; import 'package:rogapp/pages/team/member_controller.dart'; import 'package:rogapp/services/api_service.dart'; @@ -5737,7 +6021,7 @@ class DestinationMapPage extends StatelessWidget { indexController.currentBound.clear(); indexController.currentBound.add(bounds); if (indexController.currentUser.isEmpty) { - indexController.loadLocationsBound(); + indexController.loadLocationsBound(indexController.currentUser[0]["user"]["event_code"]); } } }); @@ -6813,7 +7097,7 @@ class DestinationController extends GetxController { print("An error occurred: $e"); // await checkForCheckin(); } finally { - await Future.delayed(const Duration(seconds: 5)); // 一定時間待機してから再帰呼び出し + await Future.delayed(const Duration(seconds: 1)); // 一定時間待機してから再帰呼び出し //print("--- End of checkForCheckin function, calling recursively ---"); unawaited( checkForCheckin() ); } @@ -7413,7 +7697,7 @@ class DestinationController extends GetxController { // 地図のイベントリスナーを設定 indexController.mapController.mapEventStream.listen((MapEvent mapEvent) { if (mapEvent is MapEventMoveEnd) { - indexController.loadLocationsBound(); + indexController.loadLocationsBound(indexController.currentUser[0]["user"]["event_code"]); } }); @@ -7438,7 +7722,7 @@ class DestinationController extends GetxController { ); indexController.currentBound.clear(); indexController.currentBound.add(bnds); - indexController.loadLocationsBound(); + indexController.loadLocationsBound(indexController.currentUser[0]["user"]["event_code"]); centerMapToCurrentLocation(); } }); @@ -7899,15 +8183,30 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; +import 'package:rogapp/widgets/helper_dialog.dart'; -class RegisterPage extends StatelessWidget { +class RegisterPage extends StatefulWidget { + @override + _RegisterPageState createState() => _RegisterPageState(); +} + +class _RegisterPageState extends State { final IndexController indexController = Get.find(); - TextEditingController emailController = TextEditingController(); - TextEditingController passwordController = TextEditingController(); - TextEditingController confirmPasswordController = TextEditingController(); + final TextEditingController emailController = TextEditingController(); + final TextEditingController passwordController = TextEditingController(); + final TextEditingController confirmPasswordController = TextEditingController(); - RegisterPage({Key? key}) : super(key: key); + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + showHelperDialog( + '登録メールにアクティベーションメールが送信されます。メールにあるリンクをタップすると正式登録になります。', + 'register_page' + ); + }); + } @override Widget build(BuildContext context) { @@ -7918,150 +8217,53 @@ class RegisterPage extends StatelessWidget { elevation: 0, backgroundColor: Colors.white, leading: IconButton( - onPressed: () { - Navigator.pop(context); - }, - icon: const Icon( - Icons.arrow_back_ios, - size: 20, - color: Colors.black, - )), + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.arrow_back_ios, size: 20, color: Colors.black), + ), ), body: SafeArea( child: SingleChildScrollView( - child: SizedBox( + child: Container( height: MediaQuery.of(context).size.height, width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 40), child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Column( - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Text( - "sign_up".tr, // "サインアップ" - style: const TextStyle( - fontSize: 30, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox( - height: 20, - ), - Text( - "create_account".tr, // アカウントを無料で作成します。 - style: TextStyle( - fontSize: 15, - color: Colors.grey[700], - ), - ), - const SizedBox( - height: 30, - ) - ], - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 40), - child: Column( - children: [ - makeInput(label: "email".tr, controller: emailController), // メールアドレス - makeInput( - label: "password".tr, - controller: passwordController, - obsureText: true), // パスワード - makeInput( - label: "confirm_password".tr, - controller: confirmPasswordController, - obsureText: true) // パスワード再確認 - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 40), - child: Container( - padding: const EdgeInsets.only(top: 3, left: 3), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(40), - border: const Border( - bottom: BorderSide(color: Colors.black), - top: BorderSide(color: Colors.black), - right: BorderSide(color: Colors.black), - left: BorderSide(color: Colors.black))), - child: MaterialButton( - minWidth: double.infinity, - height: 60, - onPressed: () { - if (passwordController.text != - confirmPasswordController.text) { - Get.snackbar( - "no_match".tr, - "password_does_not_match".tr, - backgroundColor: Colors.red, - colorText: Colors.white, - icon: const Icon(Icons.assistant_photo_outlined, - size: 40.0, color: Colors.blue), - snackPosition: SnackPosition.TOP, - duration: const Duration(seconds: 3), - // backgroundColor: Colors.yellow, - //icon:Image(image:AssetImage("assets/images/dora.png")) - ); - } - if (emailController.text.isEmpty || - passwordController.text.isEmpty) { - Get.snackbar( - "no_values".tr, - "email_and_password_required".tr, - backgroundColor: Colors.red, - colorText: Colors.white, - icon: const Icon(Icons.assistant_photo_outlined, - size: 40.0, color: Colors.blue), - snackPosition: SnackPosition.TOP, - duration: const Duration(seconds: 3), - //backgroundColor: Colors.yellow, - //icon:Image(image:AssetImage("assets/images/dora.png")) - ); - return; - } - indexController.isLoading.value = true; - indexController.register(emailController.text, - passwordController.text, context); - }, - color: Colors.redAccent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(40)), - child: Text( - "sign_up".tr, - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16, - ), - ), - ), - ), - ), - const SizedBox( - height: 20, - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible(child: Text("already_have_account".tr)), - TextButton( - onPressed: () { - Get.toNamed(AppPages.LOGIN); - }, - child: Text( - "login".tr, - style: TextStyle( - fontWeight: FontWeight.w600, fontSize: 18), - ), - ), - ], - ) - ], + Text( + "sign_up".tr, + style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold), ), + const SizedBox(height: 20), + Text( + "create_account".tr, + style: TextStyle(fontSize: 15, color: Colors.grey[700]), + ), + const SizedBox(height: 30), + makeInput(label: "email".tr, controller: emailController), + makeInput(label: "password".tr, controller: passwordController, obsureText: true), + makeInput(label: "confirm_password".tr, controller: confirmPasswordController, obsureText: true), + const SizedBox(height: 20), + ElevatedButton( + onPressed: _handleRegister, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.redAccent, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(40)), + minimumSize: const Size(double.infinity, 60), + ), + child: Text("sign_up".tr, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16)), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible(child: Text("already_have_account".tr)), + TextButton( + onPressed: () => Get.toNamed(AppPages.LOGIN), + child: Text("login".tr, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 18)), + ), + ], + ) ], ), ), @@ -8069,43 +8271,248 @@ class RegisterPage extends StatelessWidget { ), ); } + + void _handleRegister() { + if (passwordController.text != confirmPasswordController.text) { + _showErrorSnackbar("no_match".tr, "password_does_not_match".tr); + return; + } + if (emailController.text.isEmpty || passwordController.text.isEmpty) { + _showErrorSnackbar("no_values".tr, "email_and_password_required".tr); + return; + } + + indexController.isLoading.value = true; + try { + indexController.register( + emailController.text, + passwordController.text, + context + ); + // 登録が成功したと仮定し、ログインページに遷移 + Get.offNamed(AppPages.LOGIN); + } catch (error) { + _showErrorSnackbar("registration_error".tr, error.toString()); + } finally { + indexController.isLoading.value = false; + } + } + + void _showErrorSnackbar(String title, String message) { + Get.snackbar( + title, + message, + backgroundColor: Colors.red, + colorText: Colors.white, + icon: const Icon(Icons.error_outline, size: 40.0, color: Colors.white), + snackPosition: SnackPosition.TOP, + duration: const Duration(seconds: 3), + ); + } } -Widget makeInput( - {label, required TextEditingController controller, obsureText = false}) { +Widget makeInput({required String label, required TextEditingController controller, bool obsureText = false}) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - label, - style: const TextStyle( - fontSize: 15, fontWeight: FontWeight.w400, color: Colors.black87), - ), - const SizedBox( - height: 5, - ), + Text(label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w400, color: Colors.black87)), + const SizedBox(height: 5), TextField( controller: controller, obscureText: obsureText, decoration: InputDecoration( - contentPadding: - const EdgeInsets.symmetric(vertical: 0, horizontal: 10), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: (Colors.grey[400])!, - ), - ), - border: OutlineInputBorder( - borderSide: BorderSide(color: (Colors.grey[400])!), - ), + contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10), + enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey[400]!)), + border: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey[400]!)), ), ), - const SizedBox( - height: 30, - ) + const SizedBox(height: 20), ], ); } + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:rogapp/model/user.dart'; +import 'package:rogapp/routes/app_pages.dart'; +import 'package:rogapp/services/api_service.dart'; +import 'package:rogapp/pages/index/index_controller.dart'; + +class UserDetailsEditPage extends StatefulWidget { + @override + _UserDetailsEditPageState createState() => _UserDetailsEditPageState(); +} + +class _UserDetailsEditPageState extends State { + final _formKey = GlobalKey(); + final IndexController indexController = Get.find(); + late User _user; + final TextEditingController _firstnameController = TextEditingController(); + final TextEditingController _lastnameController = TextEditingController(); + final TextEditingController _dateOfBirthController = TextEditingController(); + late bool _female; + + @override + void initState() { + super.initState(); + _user = User.fromJson(indexController.currentUser[0]['user']); + _firstnameController.text = _user.firstname; + _lastnameController.text = _user.lastname; + _dateOfBirthController.text = _user.dateOfBirth != null + ? '${_user.dateOfBirth!.year}/${_user.dateOfBirth!.month.toString().padLeft(2, '0')}/${_user.dateOfBirth!.day.toString().padLeft(2, '0')}' + : ''; + _female = _user.female; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('個人情報の修正'), + automaticallyImplyLeading: false, + ), + body: Form( + key: _formKey, + child: ListView( + padding: EdgeInsets.all(16.0), + children: [ + TextFormField( + controller: _lastnameController, + decoration: InputDecoration( + labelText: '姓', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '姓を入力してください'; + } + return null; + }, + ), + + SizedBox(height: 16), + TextFormField( + controller: _firstnameController, + decoration: InputDecoration( + labelText: '名', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '名を入力してください'; + } + return null; + }, + ), + SizedBox(height: 16), + TextFormField( + controller: _dateOfBirthController, + decoration: InputDecoration( + labelText: '生年月日 (YYYY/MM/DD)', + border: OutlineInputBorder(), + hintText: 'YYYY/MM/DD', + ), + keyboardType: TextInputType.datetime, + validator: (value) { + if (value == null || value.isEmpty) { + return '生年月日を入力してください'; + } + if (!RegExp(r'^\d{4}/\d{2}/\d{2}$').hasMatch(value)) { + return '正しい形式で入力してください (YYYY/MM/DD)'; + } + final date = DateTime.tryParse(value.replaceAll('/', '-')); + if (date == null) { + return '有効な日付を入力してください'; + } + if (date.isAfter(DateTime.now())) { + return '未来の日付は入力できません'; + } + return null; + }, + ), + SizedBox(height: 16), + SwitchListTile( + title: Text('性別'), + subtitle: Text(_female ? '女性' : '男性'), + value: _female, + onChanged: (bool value) { + setState(() { + _female = value; + }); + }, + ), + SizedBox(height: 16), + TextFormField( + initialValue: _user.email, + decoration: InputDecoration( + labelText: 'メールアドレス', + border: OutlineInputBorder(), + ), + enabled: false, + ), + SizedBox(height: 16), + SwitchListTile( + title: Text('アクティブ状態'), + value: _user.isActive, + onChanged: null, + ), + SizedBox(height: 32), + ElevatedButton( + child: Text('更新'), + onPressed: _updateUserDetails, + ), + ], + ), + ), + ); + } + + void _updateUserDetails() async { + if (_formKey.currentState!.validate()) { + final dateOfBirth = DateTime.tryParse(_dateOfBirthController.text.replaceAll('/', '-')); + if (dateOfBirth == null || dateOfBirth.isAfter(DateTime.now())) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('生年月日が無効です', style: TextStyle(color: Colors.red))), + ); + return; + } + + User updatedUser = User( + id: _user.id, + email: _user.email, + firstname: _firstnameController.text, + lastname: _lastnameController.text, + dateOfBirth: dateOfBirth, + female: _female, + isActive: _user.isActive, + ); + + try { + bool success = await ApiService.updateUserDetail(updatedUser, indexController.currentUser[0]['token']); + if (success) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('個人情報が更新されました')), + ); + indexController.updateCurrentUser(updatedUser); + Get.offAllNamed(AppPages.INDEX); + //Get.back(); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('更新に失敗しました', style: TextStyle(color: Colors.red))), + ); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('エラーが発生しました: $e', style: TextStyle(color: Colors.red))), + ); + } + } + } +} + + + + import 'package:get/get.dart'; import 'package:rogapp/pages/team/team_controller.dart'; import 'package:rogapp/services/api_service.dart'; @@ -8124,7 +8531,7 @@ import 'package:rogapp/pages/team/team_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/services/api_service.dart'; -class TeamListPage extends GetView { +class TeamListPage extends GetWidget { @override Widget build(BuildContext context) { return Scaffold( @@ -8137,31 +8544,42 @@ class TeamListPage extends GetView { ), ], ), - body: FutureBuilder( - future: Get.putAsync(() => ApiService().init()), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return Obx(() => ListView.builder( + body: Obx(() { + if (controller.isLoading.value) { + return Center(child: CircularProgressIndicator()); + } else if (controller.error.value.isNotEmpty) { + return Center(child: Text(controller.error.value)); + } else if (controller.teams.isEmpty) { + return Center(child: Text('チームがありません')); + } else { + return RefreshIndicator( + onRefresh: controller.fetchTeams, + child: ListView.builder( itemCount: controller.teams.length, itemBuilder: (context, index) { final team = controller.teams[index]; return ListTile( title: Text(team.teamName), subtitle: Text('${team.category.categoryName} - ${team.zekkenNumber}'), - //trailing: Text('メンバー: ${team.memberCount}'), - trailing: Text('メンバー: (メンバー数表示予定)'), - onTap: () => Get.toNamed(AppPages.TEAM_DETAIL, arguments: {'mode': 'edit', 'team': team}), + onTap: () async { + await Get.toNamed( + AppPages.TEAM_DETAIL, + arguments: {'mode': 'edit', 'team': team}, + ); + controller.fetchTeams(); + }, ); }, - )); - } else { - return Center(child: CircularProgressIndicator()); - } - }, - ), + ), + ); + } + }), ); } -}import 'package:get/get.dart'; +} + + +import 'package:get/get.dart'; import 'package:rogapp/pages/team/member_controller.dart'; import 'package:rogapp/services/api_service.dart'; @@ -8172,7 +8590,8 @@ class MemberBinding extends Bindings { Get.lazyPut(() => MemberController()); } }// lib/controllers/team_controller.dart - +import 'dart:convert'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/model/team.dart'; import 'package:rogapp/model/category.dart'; @@ -8182,44 +8601,101 @@ import 'package:rogapp/services/api_service.dart'; class TeamController extends GetxController { late final ApiService _apiService; - final teams = [].obs; - final categories = [].obs; + final categories = [].obs; final teamMembers = [].obs; - final selectedCategory = Rx(null); + + final selectedCategory = Rx(null); + final selectedTeam = Rx(null); final currentUser = Rx(null); - final isLoading = true.obs; + final teamName = ''.obs; + final isLoading = false.obs; final error = RxString(''); @override void onInit() async{ super.onInit(); try { - //await Get.putAsync(() => ApiService().init()); _apiService = Get.find(); + isLoading.value = true; - await Future.wait([ + await fetchCategories(); + await Future.wait([ fetchTeams(), - fetchCategories(), getCurrentUser(), ]); + print("selectedCategory=$selectedCategory.value"); + // カテゴリが取得できたら、最初のカテゴリを選択状態にする + if (categories.isNotEmpty && selectedCategory.value == null) { + selectedCategory.value = categories.first; + } + }catch(e){ print("Team Controller error: $e"); + error.value = e.toString(); }finally{ isLoading.value = false; } } + void setSelectedTeam(Team team) { + selectedTeam.value = team; + teamName.value = team.teamName; + if (categories.isNotEmpty) { + selectedCategory.value = categories.firstWhere( + (category) => category.id == team.category.id, + orElse: () => categories.first, + ); + } else { + // カテゴリリストが空の場合、teamのカテゴリをそのまま使用 + selectedCategory.value = team.category; + } + fetchTeamMembers(team.id); + } + + void resetForm() { + selectedTeam.value = null; + teamName.value = ''; + if (categories.isNotEmpty) { + selectedCategory.value = categories.first; + } else { + selectedCategory.value = null; + // カテゴリが空の場合、エラーメッセージをセット + error.value = 'カテゴリデータが取得できませんでした。'; + } teamMembers.clear(); + } + + void cleanupForNavigation() { + selectedTeam.value = null; + teamName.value = ''; + selectedCategory.value = categories.isNotEmpty ? categories.first : null; + teamMembers.clear(); + //teamMembersはクリアしない + // 必要に応じて他のクリーンアップ処理を追加 + } + Future fetchTeams() async { try { + isLoading.value = true; final fetchedTeams = await _apiService.getTeams(); teams.assignAll(fetchedTeams); } catch (e) { + error.value = 'チームの取得に失敗しました: $e'; print('Error fetching teams: $e'); + } finally { + isLoading.value = false; + } + } + + bool checkIfUserHasEntryData(){ + if (teams.isEmpty) { + return false; + }else { + return true; } } @@ -8227,6 +8703,8 @@ class TeamController extends GetxController { try { final fetchedCategories = await _apiService.getCategories(); categories.assignAll(fetchedCategories); + print("Fetched categories: ${categories.length}"); // デバッグ用 + } catch (e) { print('Error fetching categories: $e'); } @@ -8241,198 +8719,720 @@ class TeamController extends GetxController { } } - Future createTeam(String teamName, int categoryId) async { - try { - final newTeam = await _apiService.createTeam(teamName, categoryId); - teams.add(newTeam); - } catch (e) { - print('Error creating team: $e'); - } + Future createTeam(String teamName, int categoryId) async { + final newTeam = await _apiService.createTeam(teamName, categoryId); + + // 自分自身をメンバーとして自動登録 + await _apiService.createTeamMember( + newTeam.id, + currentUser.value?.email, + currentUser.value!.firstname, + currentUser.value!.lastname, + currentUser.value?.dateOfBirth, + currentUser.value?.female, + ); + + return newTeam; } - Future updateTeam(int teamId, String teamName, int categoryId) async { - try { - final updatedTeam = await _apiService.updateTeam(teamId, teamName, categoryId); - final index = teams.indexWhere((team) => team.id == teamId); - if (index != -1) { - teams[index] = updatedTeam; - } - } catch (e) { - print('Error updating team: $e'); - } + Future updateTeam(int teamId, String teamName, int categoryId) async { + // APIサービスを使用してチームを更新 + final updatedTeam = await _apiService.updateTeam(teamId, teamName, categoryId); + return updatedTeam; } Future deleteTeam(int teamId) async { - try { - await _apiService.deleteTeam(teamId); - teams.removeWhere((team) => team.id == teamId); - } catch (e) { - print('Error deleting team: $e'); + bool confirmDelete = await Get.dialog( + AlertDialog( + title: Text('チーム削除の確認'), + content: Text('このチームとそのすべてのメンバーを削除しますか?この操作は取り消せません。'), + actions: [ + TextButton( + child: Text('キャンセル'), + onPressed: () => Get.back(result: false), + ), + TextButton( + child: Text('削除'), + onPressed: () => Get.back(result: true), + ), + ], + ), + ) ?? false; + + if (confirmDelete) { + try { + // まず、チームのメンバーを全て削除 + await _apiService.deleteAllTeamMembers(teamId); + + // その後、チームを削除 + await _apiService.deleteTeam(teamId); + + // ローカルのチームリストを更新 + teams.removeWhere((team) => team.id == teamId); + + /* + Get.snackbar( + '成功', + 'チームとそのメンバーが削除されました', + backgroundColor: Colors.green, + colorText: Colors.white, + snackPosition: SnackPosition.BOTTOM, + ); + */ + + // チームリスト画面に戻る + Get.back(); + + } catch (e) { + print('Error deleting team and members: $e'); + Get.snackbar('エラー', 'チームとメンバーの削除に失敗しました'); + } } } Future fetchTeamMembers(int teamId) async { try { + isLoading.value = true; final members = await _apiService.getTeamMembers(teamId); teamMembers.assignAll(members); } catch (e) { + error.value = 'メンバーの取得に失敗しました: $e'; print('Error fetching team members: $e'); + } finally { + isLoading.value = false; } } - void updateTeamName(String value) { - // Update local state + Future updateMember(teamId, User member) async { + try { + isLoading.value = true; + await _apiService.updateTeamMember(teamId,member.id, member.firstname, member.lastname, member.dateOfBirth, member.female); + await fetchTeamMembers(selectedTeam.value!.id); + } catch (e) { + error.value = 'メンバーの更新に失敗しました: $e'; + print('Error updating member: $e'); + } finally { + isLoading.value = false; + } } - void updateCategory(Category? value) { - selectedCategory.value = value; + Future deleteMember(int memberId, int teamId) async { + try { + isLoading.value = true; + await _apiService.deleteTeamMember(teamId,memberId); + await fetchTeamMembers(teamId); + } catch (e) { + error.value = 'メンバーの削除に失敗しました: $e'; + print('Error deleting member: $e'); + } finally { + isLoading.value = false; + } } + + + // Future addMember(User member, int teamId) async { + // try { + // isLoading.value = true; + // await _apiService.createTeamMember(teamId, member.email, member.firstname, member.lastname, member.dateOfBirth); + // await fetchTeamMembers(teamId); + // } catch (e) { + // error.value = 'メンバーの追加に失敗しました: $e'; + // print('Error adding member: $e'); + // } finally { + // isLoading.value = false; + // } + // } + + + void updateTeamName(String value) { + teamName.value = value; + } + + void updateCategory(NewCategory? value) { + if (value != null) { + selectedCategory.value = value; + } + } + //void updateCategory(NewCategory? value) { + // if (value != null) { + // selectedCategory.value = categories.firstWhere( + // (category) => category.id == value.id, + // orElse: () => value, + // ); + // } + //} + + Future saveTeam() async { + try { + isLoading.value = true; + if (selectedCategory.value == null) { + throw Exception('カテゴリを選択してください'); + } + + if (selectedTeam.value == null) { + await createTeam(teamName.value, selectedCategory.value!.id); + } else { + await updateTeam(selectedTeam.value!.id, teamName.value, selectedCategory.value!.id); + } + + // サーバーから最新のデータを再取得 + await fetchTeams(); + update(); // UIを強制的に更新 + } catch (e) { + error.value = 'チームの保存に失敗しました: $e'; + + } finally { + isLoading.value = false; + } + } + + + Future deleteSelectedTeam() async { + if (selectedTeam.value != null) { + await deleteTeam(selectedTeam.value!.id); + selectedTeam.value = null; + } + } + + List getFilteredCategories() { + //List teamMembers = getCurrentTeamMembers(); + return categories.where((category) { + return isCategoryValid(category, teamMembers); + }).toList(); + } + + bool isCategoryValid(NewCategory category, List teamMembers) { + int maleCount = teamMembers.where((member) => !member.female).length; + int femaleCount = teamMembers.where((member) => member.female).length; + int totalCount = teamMembers.length; + + bool isValidGender = category.female ? (femaleCount == totalCount) : true; + bool isValidMemberCount = totalCount == category.numOfMember; + bool isValidFamily = category.family ? areAllMembersFamily(teamMembers) : true; + + return isValidGender && isValidMemberCount && isValidFamily; + } + + bool areAllMembersFamily(List teamMembers) { + // 家族かどうかを判断するロジック(例: 同じ姓を持つメンバーが2人以上いる場合は家族とみなす) + Set familyNames = teamMembers.map((member) => member.lastname).toSet(); + return familyNames.length < teamMembers.length; + } + + }// lib/pages/team/team_detail_page.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/team/team_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; -import 'package:rogapp/services/api_service.dart'; +import 'package:rogapp/model/team.dart'; +import 'package:rogapp/model/category.dart'; + +class TeamDetailPage extends StatefulWidget { + @override + _TeamDetailPageState createState() => _TeamDetailPageState(); +} + +class _TeamDetailPageState extends State { + late TeamController controller; + late TextEditingController _teamNameController = TextEditingController(); + final RxString mode = ''.obs; + final Rx team = Rx(null); + Worker? _teamNameWorker; + + @override + void initState() { + super.initState(); + controller = Get.find(); + _teamNameController = TextEditingController(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _initializeData(); + }); + } + + void _initializeData() { + final args = Get.arguments; + if (args != null && args is Map) { + mode.value = args['mode'] as String? ?? ''; + team.value = args['team'] as Team?; + + if (mode.value == 'edit' && team.value != null) { + controller.setSelectedTeam(team.value!); + } else { + controller.resetForm(); + } + } else { + // 引数がない場合は新規作成モードとして扱う + mode.value = 'new'; + controller.resetForm(); + } + + _teamNameController.text = controller.teamName.value; + + // Use ever instead of direct listener + _teamNameWorker = ever(controller.teamName, (String value) { + if (_teamNameController.text != value) { + _teamNameController.text = value; + } + }); + } + + @override + void dispose() { + _teamNameWorker?.dispose(); + _teamNameController.dispose(); + //controller.resetForm(); // Reset the form when leaving the page + super.dispose(); + } -class TeamDetailPage extends GetView { @override Widget build(BuildContext context) { - final mode = Get.arguments['mode']; - final team = Get.arguments['team']; - return Scaffold( appBar: AppBar( - title: Text(mode == 'new' ? '新規チーム作成' : 'チーム詳細'), + title: Text(Get.arguments['mode'] == 'new' ? '新規チーム作成' : 'チーム詳細'), + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + controller.cleanupForNavigation(); + Get.back(); + }, + ), actions: [ - if (mode == 'edit') - IconButton( - icon: Icon(Icons.add), - onPressed: () => Get.toNamed(AppPages.MEMBER_DETAIL, arguments: {'mode': 'new', 'teamId': team.id}), - ), + IconButton( + icon: Icon(Icons.save), + onPressed: () async { + try { + await controller.saveTeam(); + Get.back(); + } catch (e) { + Get.snackbar('エラー', e.toString(), snackPosition: SnackPosition.BOTTOM); + } + }, + ), + Obx(() { + if (mode.value == 'edit') { + return IconButton( + icon: Icon(Icons.delete), + onPressed: () async { + try { + await controller.deleteSelectedTeam(); + Get.back(); + } catch (e) { + Get.snackbar('エラー', e.toString(), snackPosition: SnackPosition.BOTTOM); + } + }, + ); + } else { + return SizedBox.shrink(); + } + }), ], ), - body: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextField( - decoration: InputDecoration(labelText: 'チーム名'), - onChanged: (value) => controller.updateTeamName(value), - ), - DropdownButtonFormField( - decoration: InputDecoration(labelText: 'カテゴリ'), - value: controller.selectedCategory.value, - items: controller.categories.map((category) => DropdownMenuItem( - value: category, - child: Text(category.categoryName), - )).toList(), - onChanged: (value) => controller.updateCategory(value), - ), - if (mode == 'edit') - Text('ゼッケン番号: ${team.zekkenNumber}'), - Obx(() { - final currentUser = controller.currentUser.value; - return Text('所有者: ${currentUser?.email ?? "未設定"}'); - }), - SizedBox(height: 20), - Text('メンバーリスト', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - Expanded( - child: Obx(() => ListView.builder( - itemCount: controller.teamMembers.length, - itemBuilder: (context, index) { - final member = controller.teamMembers[index]; - return ListTile( - title: Text('${member.lastname}, ${member.firstname}'), - onTap: () => Get.toNamed(AppPages.MEMBER_DETAIL, arguments: {'mode': 'edit', 'member': member}), + body: Obx(() { + if (controller.isLoading.value) { + return Center(child: CircularProgressIndicator()); + } + + return SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + decoration: InputDecoration(labelText: 'チーム名'), + controller: _teamNameController, + onChanged: (value) => controller.updateTeamName(value), + ), + + SizedBox(height: 16), + Obx(() { + if (controller.categories.isEmpty) { + return Text('カテゴリデータを読み込めませんでした。', style: TextStyle(color: Colors.red)); + } + return DropdownButtonFormField( + decoration: InputDecoration(labelText: 'カテゴリ'), + value: controller.selectedCategory.value, + items: controller.categories.map((category) => DropdownMenuItem( + value: category, + child: Text(category.categoryName), + )).toList(), + /* + items: controller.getFilteredCategories().map((category) => DropdownMenuItem( + value: category, + child: Text(category.categoryName), + )).toList(), + */ + onChanged: (value) => controller.updateCategory(value), ); - }, - )), + }), + if (mode.value == 'edit') + Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Text('ゼッケン番号: ${controller.selectedTeam.value?.zekkenNumber ?? ""}'), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Text('所有者: ${controller.currentUser.value?.email ?? "未設定"}'), + ), + + if (mode.value == 'edit') ...[ + SizedBox(height: 24), + Text('メンバーリスト', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + SizedBox(height: 8), + ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: controller.teamMembers.length, + itemBuilder: (context, index) { + final member = controller.teamMembers[index]; + final isDummyEmail = member.email?.startsWith('dummy_') ?? false; + final displayName = isDummyEmail + ? '${member.lastname} ${member.firstname}' + : member.isActive + ? '${member.lastname} ${member.firstname}' + : '${member.email?.split('@')[0] ?? ''}'; //(未承認)'; + return ListTile( + title: Text(displayName), + subtitle: isDummyEmail ? Text('Email未設定') : null, + onTap: () async { + final result = await Get.toNamed( + AppPages.MEMBER_DETAIL, + arguments: {'mode': 'edit', 'member': member, 'teamId': controller.selectedTeam.value?.id}, + ); + if (result == true) { + await controller.fetchTeamMembers(controller.selectedTeam.value!.id); + } + }, + ); + }, + ), + SizedBox(height: 16), + ElevatedButton( + child: Text('新しいメンバーを追加'), + onPressed: () async { + await Get.toNamed( + AppPages.MEMBER_DETAIL, + arguments: {'mode': 'new', 'teamId': controller.selectedTeam.value?.id}, + ); + if (controller.selectedTeam.value != null) { + controller.fetchTeamMembers(controller.selectedTeam.value!.id); + } + }, + ), + ], + ], ), - ], - ), - ), + ) + ); + }), ); + } -}// lib/pages/team/member_detail_page.dart + + +} + + + + + + +// lib/pages/team/member_detail_page.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/team/member_controller.dart'; +import 'package:intl/intl.dart'; // この行を追加 +import 'package:flutter_localizations/flutter_localizations.dart'; // 追加 +import 'package:flutter/cupertino.dart'; + +class MemberDetailPage extends StatefulWidget { + @override + _MemberDetailPageState createState() => _MemberDetailPageState(); +} + +class _MemberDetailPageState extends State { + final MemberController controller = Get.find(); + late TextEditingController _firstNameController; + late TextEditingController _lastNameController; + late TextEditingController _emailController; + + + @override + void initState() { + super.initState(); + _initializeControllers(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + final mode = Get.arguments['mode']; + final member = Get.arguments['member']; + if (mode == 'edit' && member != null) { + controller.setSelectedMember(member); + } + }); + } + + void _initializeControllers() { + _firstNameController = TextEditingController(text: controller.firstname.value); + _lastNameController = TextEditingController(text: controller.lastname.value); + _emailController = TextEditingController(text: controller.email.value); + + controller.firstname.listen((value) { + if (_firstNameController.text != value) { + _firstNameController.value = TextEditingValue( + text: value, + selection: TextSelection.fromPosition(TextPosition(offset: value.length)), + ); + } + }); + + controller.lastname.listen((value) { + if (_lastNameController.text != value) { + _lastNameController.value = TextEditingValue( + text: value, + selection: TextSelection.fromPosition(TextPosition(offset: value.length)), + ); + } + }); + + controller.email.listen((value) { + if (_emailController.text != value) { + _emailController.value = TextEditingValue( + text: value, + selection: TextSelection.fromPosition(TextPosition(offset: value.length)), + ); + } + }); + } -class MemberDetailPage extends GetView { @override Widget build(BuildContext context) { - final mode = Get.arguments['mode']; - final member = Get.arguments['member']; + final mode = Get.arguments['mode'] as String; + //final member = Get.arguments['member']; + final teamId = Get.arguments['teamId'] as int; + /* + return MaterialApp( // MaterialApp をここに追加 + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + const Locale('ja', 'JP'), + ], + home:Scaffold( + */ return Scaffold( appBar: AppBar( title: Text(mode == 'new' ? 'メンバー追加' : 'メンバー詳細'), ), - body: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (mode == 'edit' && member.email != null) - Text('Email: ${member.email}'), - if (mode == 'new' || (mode == 'edit' && member.email == null)) - TextField( - decoration: InputDecoration(labelText: 'Email'), - onChanged: (value) => controller.updateEmail(value), + body: Obx(() { + if (controller.isLoading.value) { + return Center(child: CircularProgressIndicator()); + } + // TextEditingControllerをObxの中で作成 + final emailController = TextEditingController(text: controller.email.value); + // カーソル位置を保持 + emailController.selection = TextSelection.fromPosition( + TextPosition(offset: controller.email.value.length), + ); + + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (mode == 'new') + TextFormField( + decoration: InputDecoration(labelText: 'メールアドレス'), + //onChanged: (value) => controller.email.value = value, + onChanged: (value) { + controller.email.value = value; + // カーソル位置を更新 + emailController.selection = TextSelection.fromPosition( + TextPosition(offset: value.length), + ); + }, + controller: emailController, + //controller: TextEditingController(text: controller.email.value), + keyboardType: TextInputType.emailAddress, // メールアドレス用のキーボードを表示 + autocorrect: false, // 自動修正を無効化 + enableSuggestions: false, + ) + else if (controller.isDummyEmail) + Text('メールアドレス: (メアド無し)') + else + Text('メールアドレス: ${controller.email.value}'), + + if (controller.email.value.isEmpty || mode == 'edit') ...[ + TextFormField( + decoration: InputDecoration(labelText: '姓'), + onChanged: (value) => controller.lastname.value = value, + controller: TextEditingController(text: controller.lastname.value), + ), + TextFormField( + decoration: InputDecoration(labelText: '名'), + onChanged: (value) => controller.firstname.value = value, + controller: TextEditingController(text: controller.firstname.value), + ), + // 生年月日 + if (controller.isDummyEmail || !controller.isOver18()) + ListTile( + title: Text('生年月日'), + subtitle: Text(controller.dateOfBirth.value != null + ? '${DateFormat('yyyy年MM月dd日').format(controller.dateOfBirth.value!)} (${controller.getAgeAndGrade()})' + : '未設定'), + onTap: () async { + final date = await showDatePicker( + context: context, + initialDate: controller.dateOfBirth.value ?? DateTime.now(), + firstDate: DateTime(1900), + lastDate: DateTime.now(), + //locale: const Locale('ja', 'JP'), + ); + if (date != null) controller.dateOfBirth.value = date; + }, + ) + else + Text('18歳以上'), + + SwitchListTile( + title: Text('性別'), + subtitle: Text(controller.female.value ? '女性' : '男性'), + value: controller.female.value, + onChanged: (value) => controller.female.value = value, + ), + ], + ]), + ), ), - TextField( - decoration: InputDecoration(labelText: '姓'), - onChanged: (value) => controller.updateLastname(value), - ), - TextField( - decoration: InputDecoration(labelText: '名'), - onChanged: (value) => controller.updateFirstname(value), - ), - // 誕生日選択ウィジェットを追加 - if (mode == 'edit') - Text('ステータス: ${controller.getMemberStatus()}'), - if (mode == 'edit' && controller.getMemberStatus() == '招待中') - ElevatedButton( - child: Text('招待メールを再送信'), - onPressed: () => controller.resendInvitation(), + Padding( + padding: EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + child: Text('削除'), + onPressed: () async { + final confirmed = await Get.dialog( + AlertDialog( + title: Text('確認'), + content: Text('このメンバーを削除してもよろしいですか?'), + actions: [ + TextButton( + child: Text('キャンセル'), + onPressed: () => Get.back(result: false), + ), + TextButton( + child: Text('削除'), + onPressed: () => Get.back(result: true), + ), + ], + ), + ); + if (confirmed == true) { + await controller.deleteMember(); + Get.back(result: true); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + ), + if (!controller.isDummyEmail && !controller.isApproved) + ElevatedButton( + child: Text('招待再送信'), + onPressed: () => controller.resendInvitation(), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + ), + ), + ElevatedButton( + child: Text('保存・招待'), + onPressed: () async { + await controller.saveMember(); + Get.back(result: true); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + ), + ], + ), ), - if (mode == 'edit') - ElevatedButton( - child: Text('メンバーから除外'), - onPressed: () => controller.deleteMember(), - ), - ], - ), - ), + ], + ); + }), ); } + + @override + void dispose() { + _firstNameController.dispose(); + _lastNameController.dispose(); + _emailController.dispose(); + super.dispose(); + } }// lib/controllers/member_controller.dart import 'package:get/get.dart'; import 'package:rogapp/model/user.dart'; +import 'package:rogapp/pages/team/team_controller.dart'; import 'package:rogapp/services/api_service.dart'; class MemberController extends GetxController { late final ApiService _apiService; - final int teamId = 0; + final selectedMember = Rx(null); + int teamId = 0; final member = Rx(null); final email = ''.obs; final firstname = ''.obs; final lastname = ''.obs; + final female = false.obs; final dateOfBirth = Rx(null); + final isLoading = false.obs; // isLoadingプロパティを追加 + final isActive = false.obs; + + //MemberController(this._apiService); @override - void onInit() async{ + void onInit() { super.onInit(); - await Get.putAsync(() => ApiService().init()); _apiService = Get.find(); - if (Get.arguments != null && Get.arguments['member'] != null) { - member.value = Get.arguments['member']; - _initializeMemberData(); + ever(member, (_) => _initializeMemberData()); + loadInitialData(); + } + + bool get isDummyEmail => email.value.startsWith('dummy_'); + bool get isApproved => !email.value.startsWith('dummy_') && member.value?.isActive == true; + + Future loadInitialData() async { + try { + isLoading.value = true; + if (Get.arguments != null) { + if (Get.arguments['member'] != null) { + member.value = Get.arguments['member']; + } + if (Get.arguments['teamId'] != null) { + teamId = Get.arguments['teamId']; + } + } + // 他の必要な初期データの取得をここで行う + } catch (e) { + print('Error loading initial data: $e'); + } finally { + isLoading.value = false; } } @@ -8446,19 +9446,82 @@ class MemberController extends GetxController { } } - Future createMember(int teamId) async { - try { - final newMember = await _apiService.createTeamMember( - teamId, - email.value, - firstname.value, - lastname.value, - dateOfBirth.value, - ); - member.value = newMember; - } catch (e) { - print('Error creating member: $e'); + + void setSelectedMember(User member) { + this.member.value = member; + email.value = member.email ?? ''; + firstname.value = member.firstname ?? ''; + lastname.value = member.lastname ?? ''; + dateOfBirth.value = member.dateOfBirth; + female.value = member.female ?? false; + isActive.value = member.isActive ?? false; + } + + bool _validateInputs() { + if (email.value.isNotEmpty && !isDummyEmail) { + return true; // Emailのみの場合は有効 } + if (firstname.value.isEmpty || lastname.value.isEmpty || dateOfBirth.value == null || female.value == null) { + Get.snackbar('エラー', 'Emailが空の場合、姓名と生年月日及び性別は必須です', snackPosition: SnackPosition.BOTTOM); + return false; + } + return true; + } + + void updateFirstName(String value) { + firstname.value = value; + } + + void updateLastName(String value) { + lastname.value = value; + } + + Future saveMember() async { + if (!_validateInputs()) return false; + + try { + isLoading.value = true; + User updatedMember; + if (member.value == null || member.value!.id == null) { + // 新規メンバー作成 + updatedMember = await _apiService.createTeamMember( + teamId, + isDummyEmail ? null : email.value, // dummy_メールの場合はnullを送信 + firstname.value, + lastname.value, + dateOfBirth.value, + female.value, + ); + } else { + // 既存メンバー更新 + updatedMember = await _apiService.updateTeamMember( + teamId, + member.value!.id!, + firstname.value, + lastname.value, + dateOfBirth.value, + female.value, + ); + } + member.value = updatedMember; + Get.snackbar('成功', 'メンバーが保存されました', snackPosition: SnackPosition.BOTTOM); + return true; + } catch (e) { + print('Error saving member: $e'); + Get.snackbar('エラー', 'メンバーの保存に失敗しました: ${e.toString()}', snackPosition: SnackPosition.BOTTOM); + return false; + } finally { + isLoading.value = false; + } + } + + + String getDisplayName() { + if (!isActive.value && !isDummyEmail) { + final displayName = email.value.split('@')[0]; + return '$displayName(未承認)'; + } + return '${lastname.value} ${firstname.value}'.trim(); } Future updateMember() async { @@ -8471,6 +9534,7 @@ class MemberController extends GetxController { firstname.value, lastname.value, dateOfBirth.value, + female.value, ); member.value = updatedMember; } catch (e) { @@ -8478,6 +9542,26 @@ class MemberController extends GetxController { } } + Future deleteMember() async { + if (member.value == null || member.value!.id == null) { + Get.snackbar('エラー', 'メンバー情報が不正です', snackPosition: SnackPosition.BOTTOM); + return; + } + + try { + isLoading.value = true; + await _apiService.deleteTeamMember(teamId, member.value!.id!); + Get.snackbar('成功', 'メンバーが削除されました', snackPosition: SnackPosition.BOTTOM); + member.value = null; + } catch (e) { + print('Error deleting member: $e'); + Get.snackbar('エラー', 'メンバーの削除に失敗しました: ${e.toString()}', snackPosition: SnackPosition.BOTTOM); + } finally { + isLoading.value = false; + } + } + +/* Future deleteMember() async { if (member.value == null) return; int? memberId = member.value?.id; @@ -8490,7 +9574,14 @@ class MemberController extends GetxController { } } + */ + Future resendInvitation() async { + if (isDummyEmail) { + Get.snackbar('エラー', 'ダミーメールアドレスには招待メールを送信できません', snackPosition: SnackPosition.BOTTOM); + return; + } + if (member.value == null || member.value!.email == null) return; int? memberId = member.value?.id; try { @@ -8513,6 +9604,54 @@ class MemberController extends GetxController { if (member.value!.isActive) return '承認済'; return '招待中'; } + + int calculateAge() { + if (dateOfBirth.value == null) return 0; + final today = DateTime.now(); + int age = today.year - dateOfBirth.value!.year; + if (today.month < dateOfBirth.value!.month || + (today.month == dateOfBirth.value!.month && today.day < dateOfBirth.value!.day)) { + age--; + } + return age; + } + + String calculateGrade() { + if (dateOfBirth.value == null) return '不明'; + + final today = DateTime.now(); + final birthDate = dateOfBirth.value!; + + // 今年の4月1日 + final thisYearSchoolStart = DateTime(today.year, 4, 1); + + // 生まれた年の翌年の4月1日(学齢期の始まり) + final schoolStartDate = DateTime(birthDate.year + 1, 4, 1); + + // 学齢期の開始からの年数 + int yearsFromSchoolStart = today.year - schoolStartDate.year; + + // 今年の4月1日より前なら1年引く + if (today.isBefore(thisYearSchoolStart)) { + yearsFromSchoolStart--; + } + + if (yearsFromSchoolStart < 0) return '未就学'; + if (yearsFromSchoolStart < 6) return '小${yearsFromSchoolStart + 1}'; + if (yearsFromSchoolStart < 9) return '中${yearsFromSchoolStart - 5}'; + if (yearsFromSchoolStart < 12) return '高${yearsFromSchoolStart - 8}'; + return '成人'; + } + + String getAgeAndGrade() { + final age = calculateAge(); + final grade = calculateGrade(); + return '$age歳/$grade'; + } + + bool isOver18() { + return calculateAge() >= 18; + } }import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/index/index_controller.dart'; @@ -8942,8 +10081,40 @@ import 'package:rogapp/utils/location_controller.dart'; // IndexPageクラスは、GetViewを継承したStatelessWidgetです。このクラスは、アプリのメインページを表すウィジェットです。 // -class IndexPage extends GetView { - IndexPage({Key? key}) : super(key: key); +import 'package:rogapp/widgets/helper_dialog.dart'; + +class IndexPage extends StatefulWidget { + @override + _IndexPageState createState() => _IndexPageState(); +} + +class _IndexPageState extends State { + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + //checkEntryAndShowHelper(); + }); + } + + /* + void checkEntryAndShowHelper() async { + final hasEntry = await checkIfUserHasEntry(); // この関数は実装する必要があります + if (!hasEntry) { + showHelperDialog( + context, + 'イベントに参加するには、チーム登録・メンバー登録及びエントリーが必要になります。', + 'entry_helper', + showDoNotShowAgain: true, + ); + } + } + + */ + +// class IndexPage extends GetView { +// IndexPage({Key? key}) : super(key: key); // IndexControllerとDestinationControllerのインスタンスを取得しています。 // @@ -9173,7 +10344,9 @@ import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:latlong2/latlong.dart'; import 'package:rogapp/model/destination.dart'; +import 'package:rogapp/model/entry.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; +import 'package:rogapp/pages/team/team_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/services/auth_service.dart'; import 'package:rogapp/services/location_service.dart'; @@ -9182,9 +10355,12 @@ import 'package:rogapp/widgets/debug_widget.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:rogapp/services/api_service.dart'; +import 'package:rogapp/model/user.dart'; -import '../../main.dart'; +import 'package:rogapp/main.dart'; + +import 'package:rogapp/widgets/helper_dialog.dart'; class IndexController extends GetxController with WidgetsBindingObserver { List locations = [].obs; @@ -9222,6 +10398,9 @@ class IndexController extends GetxController with WidgetsBindingObserver { String? userToken; + //late final ApiService _apiService; + final ApiService _apiService = Get.find(); + // mode = 0 is map mode, mode = 1 list mode var mode = 0.obs; @@ -9242,6 +10421,16 @@ class IndexController extends GetxController with WidgetsBindingObserver { final Connectivity _connectivity = Connectivity(); late StreamSubscription _connectivitySubscription; + final Rx lastUserUpdateTime = DateTime.now().obs; + + /* + void updateUserInfo(Map newUserInfo) { + currentUser.clear(); + currentUser.add(newUserInfo); + lastUserUpdateTime.value = DateTime.now(); + } + */ + void toggleMode() { if (mode.value == 0) { mode += 1; @@ -9365,6 +10554,7 @@ class IndexController extends GetxController with WidgetsBindingObserver { if (currentUser.isNotEmpty) { // 既にログインしている場合 await Get.putAsync(() => ApiService().init()); + //await Get.putAsync(() => ApiService().init()); // 必要に応じて追加の初期化処理 } } @@ -9501,6 +10691,12 @@ class IndexController extends GetxController with WidgetsBindingObserver { // ログイン成功後、api_serviceを初期化 await Get.putAsync(() => ApiService().init()); + // ユーザー情報の完全性をチェック + if (await checkUserInfoComplete()) { + Get.offAllNamed(AppPages.INDEX); + } else { + Get.offAllNamed(AppPages.USER_DETAILS_EDIT); + } } else { logManager.addOperationLog("User failed login : ${email} , ${password}."); @@ -9520,6 +10716,14 @@ class IndexController extends GetxController with WidgetsBindingObserver { }); } + Future checkUserInfoComplete() async { + final user = await ApiService.to.getCurrentUser(); + return user.firstname.isNotEmpty && + user.lastname.isNotEmpty && + user.dateOfBirth != null && + user.female != null; + } + // 要検討:エラーハンドリングが行われていますが、エラーメッセージをローカライズすることを検討してください。 // void changePassword( @@ -9655,7 +10859,7 @@ class IndexController extends GetxController with WidgetsBindingObserver { saveToDevice(currentUser[0]["token"]); } isLoading.value = false; - loadLocationsBound(); + loadLocationsBound( currentUser[0]["user"]["event_code"]); if (currentUser.isNotEmpty) { rogMode.value = 0; restoreGame(); @@ -9740,7 +10944,7 @@ class IndexController extends GetxController with WidgetsBindingObserver { // 要検討:Future.delayedを使用して非同期処理を待たずに先に進むようにしていますが、 // これによってメモリリークが発生する可能性があります。非同期処理の結果を適切に処理することを検討してください。 // - void loadLocationsBound() async { + void loadLocationsBound(String eventCode) async { if (isCustomAreaSelected.value == true) { return; } @@ -9772,12 +10976,13 @@ class IndexController extends GetxController with WidgetsBindingObserver { currentBound.clear(); currentBound.add(bounds); - isLoading.value = true; // ローディング状態をtrueに設定 + //isLoading.value = true; // ローディング状態をtrueに設定 //print("bounds --- (${bounds.southWest.latitude},${bounds.southWest.longitude}),(${bounds.northWest.latitude},${bounds.northWest.longitude}),(${bounds.northEast.latitude},${bounds.northEast.longitude}),(${bounds.southEast.latitude},${bounds.southEast.longitude})"); // 要検討:APIからのレスポンスがnullの場合のエラーハンドリングが不十分です。適切なエラーメッセージを表示するなどの処理を追加してください。 try { + final eventCode = currentUser[0]["user"]["event_code"]; final value = await LocationService.loadLocationsBound( bounds.southWest.latitude, bounds.southWest.longitude, @@ -9787,7 +10992,8 @@ class IndexController extends GetxController with WidgetsBindingObserver { bounds.northEast.longitude, bounds.southEast.latitude, bounds.southEast.longitude, - cat + cat, + eventCode ); /* if (value == null) { @@ -9891,6 +11097,32 @@ class IndexController extends GetxController with WidgetsBindingObserver { } return null; } + + + void reloadMap( String eventCode ) { + // マップをリロードするロジックを実装 + // 例: 現在の位置情報を再取得し、マップを更新する + loadLocationsBound( eventCode ); + } + + Future checkEntryData() async { + // エントリーデータの有無をチェックするロジック + final teamController = TeamController(); + bool hasEntryData = teamController.checkIfUserHasEntryData(); + if (!hasEntryData) { + await showHelperDialog( + 'イベントに参加するには、チーム登録・メンバー登録及びエントリーが必要になります。', + 'entry_check' + ); + // ドロワーを表示するロジック + Get.toNamed('/drawer'); + } + } + + void updateCurrentUser(User updatedUser) { + currentUser[0]['user'] = updatedUser.toJson(); + update(); + } } import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -10208,18 +11440,36 @@ class PermissionController { import 'package:get/get.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; +import 'package:rogapp/widgets/helper_dialog.dart'; // 要検討:ログインボタンとサインアップボタンの配色を見直すことを検討してください。現在の配色では、ボタンの役割がわかりにくい可能性があります。 // エラーメッセージをローカライズすることを検討してください。 // ログイン処理中にエラーが発生した場合のエラーハンドリングを追加することをお勧めします。 // -class LoginPage extends StatelessWidget { +class LoginPage extends StatefulWidget { + @override + _LoginPageState createState() => _LoginPageState(); +} + +class _LoginPageState extends State { +//class LoginPage extends StatelessWidget { final IndexController indexController = Get.find(); TextEditingController emailController = TextEditingController(); TextEditingController passwordController = TextEditingController(); - LoginPage({Key? key}) : super(key: key); + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + showHelperDialog( + '参加するにはユーザー登録が必要です。サインアップからユーザー登録してください。', + 'login_page' + ); + }); + } + + //LoginPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -10641,6 +11891,10 @@ import 'package:rogapp/pages/entry/entry_list_page.dart'; import 'package:rogapp/pages/entry/entry_detail_page.dart'; import 'package:rogapp/pages/entry/entry_binding.dart'; +import 'package:rogapp/pages/entry/event_entries_page.dart'; +import 'package:rogapp/pages/entry/event_entries_binding.dart'; +import 'package:rogapp/pages/register/user_detail_page.dart'; + part 'app_routes.dart'; class AppPages { @@ -10674,6 +11928,9 @@ class AppPages { static const MEMBER_DETAIL = Routes.MEMBER_DETAIL; static const ENTRY_LIST = Routes.ENTRY_LIST; static const ENTRY_DETAIL = Routes.ENTRY_DETAIL; + static const EVENT_ENTRY = Routes.EVENT_ENTRIES; + static const USER_DETAILS_EDIT = Routes.USER_DETAILS_EDIT; + static final routes = [ GetPage( @@ -10770,7 +12027,15 @@ class AppPages { page: () => EntryDetailPage(), binding: EntryBinding(), ), - + GetPage( + name: Routes.EVENT_ENTRIES, + page: () => EventEntriesPage(), + binding: EventEntriesBinding(), + ), + GetPage( + name: Routes.USER_DETAILS_EDIT, + page: () => UserDetailsEditPage(), + ), ]; } @@ -10810,6 +12075,10 @@ abstract class Routes { static const MEMBER_DETAIL = '/member-detail'; static const ENTRY_LIST = '/entry-list'; static const ENTRY_DETAIL = '/entry-detail'; + + static const EVENT_ENTRIES = '/event-entries'; + static const USER_DETAILS_EDIT = '/user-details-edit'; + } import 'dart:convert'; import 'package:http/http.dart' as http; @@ -10973,59 +12242,93 @@ class LocationService { double lon3, double lat4, double lon4, - String cat) async { + String cat, + String event_code) async { //print("-------- in location for bound -------------"); final IndexController indexController = Get.find(); - String url = ""; - String serverUrl = ConstValues.currentServer(); - if (cat.isNotEmpty) { - if (indexController.currentUser.isNotEmpty) { - bool rog = indexController.currentUser[0]['user']['is_rogaining']; - String r = rog == true ? 'True' : 'False'; - var grp = indexController.currentUser[0]['user']['event_code']; - url = - '$serverUrl/api/inbound?rog=$r&grp=$grp&ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4&cat=$cat'; - } else { - url = - '$serverUrl/api/inbound?ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4&cat=$cat'; - } - } else { - if (indexController.currentUser.isNotEmpty) { - bool rog = indexController.currentUser[0]['user']['is_rogaining']; - String r = rog == true ? 'True' : 'False'; - var grp = indexController.currentUser[0]['user']['event_code']; - //print("-------- requested user group $grp -------------"); - url = - '$serverUrl/api/inbound?rog=$r&grp=$grp&ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4'; - } else { - url = - '$serverUrl/api/inbound?ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4'; - } - } - //print('++++++++$url'); - final response = await http.get( - Uri.parse(url), - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - }, - ); + final updateTime = indexController.lastUserUpdateTime.value; - if (response.statusCode == 500) { - return null; //featuresFromGeoJson(utf8.decode(response.bodyBytes)); - } + // ユーザー情報の更新を最大5秒間待つ - if (response.statusCode == 200) { - DestinationController destinationController = - Get.find(); - GeoJSONFeatureCollection cc = - GeoJSONFeatureCollection.fromJSON(utf8.decode(response.bodyBytes)); - if (cc.features.isEmpty) { + try { + /* + // ユーザー情報の更新を最大5秒間待つ + final newUpdateTime = await indexController.lastUserUpdateTime.stream + .firstWhere( + (time) => time.isAfter(updateTime), + orElse: () => updateTime, + ).timeout(Duration(seconds: 5)); + + if (newUpdateTime == updateTime) { + print('ユーザー情報の更新がタイムアウトしました'); + // タイムアウト時の処理(例:エラー表示やリトライ) return null; - } else { - //print("---- feature got from server is ${cc.collection[0].properties} ------"); - return cc; } + */ + + /* + await indexController.lastUserUpdateTime.stream.firstWhere( + (time) => time.isAfter(updateTime), + orElse: () => updateTime, + ).timeout(Duration(seconds: 2), onTimeout: () => updateTime); + */ + + String url = ""; + String serverUrl = ConstValues.currentServer(); + if (cat.isNotEmpty) { + if (indexController.currentUser.isNotEmpty) { + bool rog = indexController.currentUser[0]['user']['is_rogaining']; + String r = rog == true ? 'True' : 'False'; + var grp = event_code; //indexController.currentUser[0]['user']['event_code']; + print("Group=$grp"); + url = + '$serverUrl/api/inbound?rog=$r&grp=$grp&ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4&cat=$cat'; + } else { + url = + '$serverUrl/api/inbound?ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4&cat=$cat'; + } + } else { + if (indexController.currentUser.isNotEmpty) { + bool rog = indexController.currentUser[0]['user']['is_rogaining']; + String r = rog == true ? 'True' : 'False'; + var grp = indexController.currentUser[0]['user']['event_code']; + print("-------- requested user group $grp -------------"); + url = + '$serverUrl/api/inbound?rog=$r&grp=$grp&ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4'; + } else { + url = + '$serverUrl/api/inbound?ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4'; + } + print('++++++++$url'); + final response = await http.get( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + ); + + if (response.statusCode == 500) { + return null; //featuresFromGeoJson(utf8.decode(response.bodyBytes)); + } + + if (response.statusCode == 200) { + DestinationController destinationController = + Get.find(); + + GeoJSONFeatureCollection cc = + GeoJSONFeatureCollection.fromJSON(utf8.decode(response.bodyBytes)); + if (cc.features.isEmpty) { + return null; + } else { + //print("---- feature got from server is ${cc.collection[0].properties} ------"); + return cc; + } + } + } + } catch(e) { + print("Error: $e"); } + return null; } @@ -11905,6 +13208,9 @@ import 'package:get/get.dart'; import 'package:flutter/material.dart'; import '../utils/const.dart'; +//import 'package:rogapp/services/team_service.dart'; +//import 'package:rogapp/services/member_service.dart'; + class AuthService { Future userLogin(String email, String password) async { @@ -12031,6 +13337,22 @@ class AuthService { // ユーザー登録 // + /* + Future registerUser(String email, String password, bool isFemale) async { + final user = await register(email, password); + if (user != null) { + final _teamController = TeamController(); + _teamController.createTeam(String teamName, int categoryId) ; + final teamService = TeamService(); + final memberService = MemberService(); + + final team = await teamService.createSoloTeam(user.id, isFemale); + await memberService.addMember(team.id, user.id); + } + } + + */ + static Future> register( String email, String password) async { Map cats = {}; @@ -12068,6 +13390,7 @@ class AuthService { return cats; } + static Future?> userDetails(int userid) async { List cats = []; String serverUrl = ConstValues.currentServer(); @@ -12293,6 +13616,8 @@ class DeviceInfoService { import 'package:get/get.dart'; import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:flutter/foundation.dart'; + import 'package:rogapp/model/entry.dart'; import 'package:rogapp/model/event.dart'; import 'package:rogapp/model/team.dart'; @@ -12300,27 +13625,43 @@ import 'package:rogapp/model/category.dart'; import 'package:rogapp/model/user.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import '../utils/const.dart'; +import 'package:intl/intl.dart'; + class ApiService extends GetxService{ static ApiService get to => Get.find(); String serverUrl = ''; String baseUrl = ''; - String token = 'your-auth-token'; // これが使用されている。 + String token = 'your-auth-token'; - Future init() async { + Future init() async { try { // ここで必要な初期化処理を行う serverUrl = ConstValues.currentServer(); baseUrl = '$serverUrl/api'; //await Future.delayed(Duration(seconds: 2)); // 仮の遅延(実際の初期化処理に置き換えてください) print('ApiService initialized successfully'); + return this; } catch(e) { print('Error in ApiService initialization: $e'); rethrow; // エラーを再スローして、呼び出し元で処理できるようにする + //return this; } } + /* + このメソッドは以下のように動作します: + + まず、渡された type パラメータに基づいて、どのクラスのフィールドを扱っているかを判断します。 + 次に、クラス内で fieldName に対応する期待される型を返します。 + クラスや フィールド名が予期されていないものである場合、'Unknown' または 'Unknown Type' を返します。 + + このメソッドを ApiService クラスに追加することで、_printDataComparison メソッドは各フィールドの期待される型を正確に表示できるようになります。 + さらに、このメソッドを使用することで、API レスポンスのデータ型が期待と異なる場合に簡単に検出できるようになります。例えば、Category クラスの duration フィールドが整数型(秒数)で期待されているのに対し、API が文字列を返した場合、すぐに問題を特定できます。 + 注意点として、API のレスポンス形式が変更された場合や、新しいフィールドが追加された場合は、このメソッドも更新する必要があります。そのため、API の変更とクライアントサイドのコードの同期を保つことが重要です。 + */ + String getToken() { // IndexControllerの初期化を待つ @@ -12339,33 +13680,68 @@ class ApiService extends GetxService{ init(); getToken(); - final response = await http.get( - Uri.parse('$baseUrl/teams/'), - headers: {'Authorization': 'Token $token'}, - ); + try { + final response = await http.get( + Uri.parse('$baseUrl/teams/'), + headers: {'Authorization': 'Token $token',"Content-Type": "application/json; charset=UTF-8"}, + ); - if (response.statusCode == 200) { - List teamsJson = json.decode(response.body); - return teamsJson.map((json) => Team.fromJson(json)).toList(); - } else { - throw Exception('Failed to load teams'); + if (response.statusCode == 200) { + // UTF-8でデコード + final decodedResponse = utf8.decode(response.bodyBytes); + //print('User Response body: $decodedResponse'); + List teamsJson = json.decode(decodedResponse); + + List teams = []; + for (var teamJson in teamsJson) { + //print('\nTeam Data:'); + //_printDataComparison(teamJson, Team); + teams.add(Team.fromJson(teamJson)); + } + return teams; + } else { + throw Exception('Failed to load teams. Status code: ${response.statusCode}'); + } + } catch (e, stackTrace) { + print('Error in getTeams: $e'); + print('Stack trace: $stackTrace'); + rethrow; } } - Future> getCategories() async { + + + Future> getCategories() async { init(); getToken(); - final response = await http.get( - Uri.parse('$baseUrl/categories/'), - headers: {'Authorization': 'Token $token'}, - ); + try { + final response = await http.get( + Uri.parse('$baseUrl/categories/'), + headers: {'Authorization': 'Token $token',"Content-Type": "application/json; charset=UTF-8"}, + ); - if (response.statusCode == 200) { - List categoriesJson = json.decode(response.body); - return categoriesJson.map((json) => Category.fromJson(json)).toList(); - } else { - throw Exception('Failed to load categories'); + if (response.statusCode == 200) { + final decodedResponse = utf8.decode(response.bodyBytes); + print('User Response body: $decodedResponse'); + List categoriesJson = json.decode(decodedResponse); + + List categories = []; + for (var categoryJson in categoriesJson) { + //print('\nCategory Data:'); + //_printDataComparison(categoryJson, NewCategory); + categories.add(NewCategory.fromJson(categoryJson)); + } + + return categories; + } else { + throw Exception( + 'Failed to load categories. Status code: ${response.statusCode}'); + } + }catch(e, stackTrace){ + print('Error in getCategories: $e'); + print('Stack trace: $stackTrace'); + rethrow; } } @@ -12373,18 +13749,90 @@ class ApiService extends GetxService{ init(); getToken(); - final response = await http.get( - Uri.parse('$baseUrl/user/'), - headers: {'Authorization': 'Token $token'}, - ); + try { + final response = await http.get( + Uri.parse('$baseUrl/user/'), + headers: {'Authorization': 'Token $token',"Content-Type": "application/json; charset=UTF-8"}, + ); - if (response.statusCode == 200) { - return User.fromJson(json.decode(response.body)); - } else { - throw Exception('Failed to get current user'); + if (response.statusCode == 200) { + final decodedResponse = utf8.decode(response.bodyBytes); + //print('User Response body: $decodedResponse'); + final jsonData = json.decode(decodedResponse); + + //print('\nUser Data Comparison:'); + //_printDataComparison(jsonData, User); + + return User.fromJson(jsonData); + } else { + throw Exception('Failed to get current user. Status code: ${response.statusCode}'); + } + } catch (e, stackTrace) { + print('Error in getCurrentUser: $e'); + print('Stack trace: $stackTrace'); + rethrow; } } + void _printDataComparison(Map data, Type expectedType) { + print('Field\t\t| Expected Type\t| Actual Type\t| Actual Value'); + print('------------------------------------------------------------'); + data.forEach((key, value) { + String expectedFieldType = _getExpectedFieldType(expectedType, key); + _printComparison(key, expectedFieldType, value); + }); + } + + String _getExpectedFieldType(Type type, String fieldName) { + // This method should return the expected type for each field based on the class definition + // You might need to implement this based on your class structures + + switch (type) { + case NewCategory: + switch (fieldName) { + case 'id': return 'int'; + case 'category_name': return 'String'; + case 'category_number': return 'int'; + case 'duration': return 'int (seconds)'; + case 'num_of_member': return 'int'; + case 'family': return 'bool'; + case 'female': return 'bool'; + default: return 'Unknown'; + } + case Team: + switch (fieldName) { + case 'id': return 'int'; + case 'zekken_number': return 'String'; + case 'team_name': return 'String'; + case 'category': return 'NewCategory (Object)'; + case 'owner': return 'User (Object)'; + default: return 'Unknown'; + } + case User: + switch (fieldName) { + case 'id': return 'int'; + case 'email': return 'String'; + case 'firstname': return 'String'; + case 'lastname': return 'String'; + case 'date_of_birth': return 'String (ISO8601)'; + case 'female': return 'bool'; + case 'is_active': return 'bool'; + default: return 'Unknown'; + } + default: + return 'Unknown Type'; + } + } + + void _printComparison(String fieldName, String expectedType, dynamic actualValue) { + String actualType = actualValue?.runtimeType.toString() ?? 'null'; + String displayValue = actualValue.toString(); + if (displayValue.length > 50) { + displayValue = '${displayValue.substring(0, 47)}...'; + } + print('$fieldName\t\t| $expectedType\t\t| $actualType\t\t| $displayValue'); + } + Future createTeam(String teamName, int categoryId) async { init(); getToken(); @@ -12393,7 +13841,7 @@ class ApiService extends GetxService{ Uri.parse('$baseUrl/teams/'), headers: { 'Authorization': 'Token $token', - 'Content-Type': 'application/json', + "Content-Type": "application/json; charset=UTF-8", }, body: json.encode({ 'team_name': teamName, @@ -12402,7 +13850,8 @@ class ApiService extends GetxService{ ); if (response.statusCode == 201) { - return Team.fromJson(json.decode(response.body)); + final decodedResponse = utf8.decode(response.bodyBytes); + return Team.fromJson(json.decode(decodedResponse)); } else { throw Exception('Failed to create team'); } @@ -12416,7 +13865,7 @@ class ApiService extends GetxService{ Uri.parse('$baseUrl/teams/$teamId/'), headers: { 'Authorization': 'Token $token', - 'Content-Type': 'application/json', + 'Content-Type': 'application/json; charset=UTF-8', }, body: json.encode({ 'team_name': teamName, @@ -12425,7 +13874,9 @@ class ApiService extends GetxService{ ); if (response.statusCode == 200) { - return Team.fromJson(json.decode(response.body)); + final decodedResponse = utf8.decode(response.bodyBytes); + + return Team.fromJson(json.decode(decodedResponse)); } else { throw Exception('Failed to update team'); } @@ -12437,10 +13888,14 @@ class ApiService extends GetxService{ final response = await http.delete( Uri.parse('$baseUrl/teams/$teamId/'), - headers: {'Authorization': 'Token $token'}, + headers: {'Authorization': 'Token $token','Content-Type': 'application/json; charset=UTF-8'}, ); - if (response.statusCode != 204) { + if( response.statusCode == 400) { + final decodedResponse = utf8.decode(response.bodyBytes); + print('User Response body: $decodedResponse'); + throw Exception('まだメンバーが残っているので、チームを削除できません。'); + }else if (response.statusCode != 204) { throw Exception('Failed to delete team'); } } @@ -12451,61 +13906,86 @@ class ApiService extends GetxService{ final response = await http.get( Uri.parse('$baseUrl/teams/$teamId/members/'), - headers: {'Authorization': 'Token $token'}, + headers: {'Authorization': 'Token $token','Content-Type': 'application/json; charset=UTF-8'}, ); if (response.statusCode == 200) { - List membersJson = json.decode(response.body); + final decodedResponse = utf8.decode(response.bodyBytes); + print('User Response body: $decodedResponse'); + List membersJson = json.decode(decodedResponse); + return membersJson.map((json) => User.fromJson(json)).toList(); } else { throw Exception('Failed to load team members'); } } - Future createTeamMember(int teamId, String email, String firstname, String lastname, DateTime? dateOfBirth) async { + Future createTeamMember(int teamId, String? email, String? firstname, String? lastname, DateTime? dateOfBirth,bool? female) async { init(); getToken(); + // emailが値を持っている場合の処理 + if (email != null && email.isNotEmpty) { + firstname ??= "dummy"; + lastname ??= "dummy"; + dateOfBirth ??= DateTime.now(); + female ??= false; + } + + String? formattedDateOfBirth; + if (dateOfBirth != null) { + formattedDateOfBirth = DateFormat('yyyy-MM-dd').format(dateOfBirth); + } + final response = await http.post( Uri.parse('$baseUrl/teams/$teamId/members/'), headers: { 'Authorization': 'Token $token', - 'Content-Type': 'application/json', + 'Content-Type': 'application/json; charset=UTF-8', }, body: json.encode({ 'email': email, 'firstname': firstname, 'lastname': lastname, - 'date_of_birth': dateOfBirth?.toIso8601String(), + 'date_of_birth': formattedDateOfBirth, + 'female': female, }), ); if (response.statusCode == 201) { - return User.fromJson(json.decode(response.body)); + final decodedResponse = utf8.decode(response.bodyBytes); + return User.fromJson(json.decode(decodedResponse)); } else { throw Exception('Failed to create team member'); } } - Future updateTeamMember(int teamId,int memberId, String firstname, String lastname, DateTime? dateOfBirth) async { + Future updateTeamMember(int teamId,int? memberId, String firstname, String lastname, DateTime? dateOfBirth,bool? female) async { init(); getToken(); + String? formattedDateOfBirth; + if (dateOfBirth != null) { + formattedDateOfBirth = DateFormat('yyyy-MM-dd').format(dateOfBirth); + } + final response = await http.put( Uri.parse('$baseUrl/teams/$teamId/members/$memberId/'), headers: { 'Authorization': 'Token $token', - 'Content-Type': 'application/json', + 'Content-Type': 'application/json; charset=UTF-8', }, body: json.encode({ 'firstname': firstname, 'lastname': lastname, - 'date_of_birth': dateOfBirth?.toIso8601String(), + 'date_of_birth': formattedDateOfBirth, + 'female': female, }), ); if (response.statusCode == 200) { - return User.fromJson(json.decode(response.body)); + final decodedResponse = utf8.decode(response.bodyBytes); + return User.fromJson(json.decode(decodedResponse)); } else { throw Exception('Failed to update team member'); } @@ -12525,13 +14005,25 @@ class ApiService extends GetxService{ } } + Future deleteAllTeamMembers(int teamId) async { + final response = await http.delete( + Uri.parse('$baseUrl/teams/$teamId/members/destroy_all/?confirm=true'), + headers: {'Authorization': 'Token $token'}, + ); + + if (response.statusCode != 200) { + throw Exception('Failed to delete team members'); + } + } + Future resendMemberInvitation(int memberId) async { init(); getToken(); final response = await http.post( Uri.parse('$baseUrl/members/$memberId/resend-invitation/'), - headers: {'Authorization': 'Token $token'}, + headers: {'Authorization': 'Token $token', 'Content-Type': 'application/json; charset=UTF-8', + }, ); if (response.statusCode != 200) { @@ -12544,12 +14036,14 @@ class ApiService extends GetxService{ getToken(); final response = await http.get( - Uri.parse('$baseUrl/entries/'), - headers: {'Authorization': 'Token $token'}, + Uri.parse('$baseUrl/entry/'), + headers: {'Authorization': 'Token $token', 'Content-Type': 'application/json; charset=UTF-8', + }, ); if (response.statusCode == 200) { - List entriesJson = json.decode(response.body); + final decodedResponse = utf8.decode(response.bodyBytes); + List entriesJson = json.decode(decodedResponse); return entriesJson.map((json) => Entry.fromJson(json)).toList(); } else { throw Exception('Failed to load entries'); @@ -12561,12 +14055,16 @@ class ApiService extends GetxService{ getToken(); final response = await http.get( - Uri.parse('$baseUrl/new-events/'), - headers: {'Authorization': 'Token $token'}, + Uri.parse('$baseUrl/new-events/',), + headers: {'Authorization': 'Token $token', 'Content-Type': 'application/json; charset=UTF-8', + }, ); if (response.statusCode == 200) { - List eventsJson = json.decode(response.body); + final decodedResponse = utf8.decode(response.bodyBytes); + print('Response body: $decodedResponse'); + List eventsJson = json.decode(decodedResponse); + return eventsJson.map((json) => Event.fromJson(json)).toList(); } else { throw Exception('Failed to load events'); @@ -12577,46 +14075,98 @@ class ApiService extends GetxService{ init(); getToken(); + String? formattedDate; + if (date != null) { + formattedDate = DateFormat('yyyy-MM-dd').format(date); + } + final response = await http.post( Uri.parse('$baseUrl/entry/'), headers: { 'Authorization': 'Token $token', - 'Content-Type': 'application/json', + 'Content-Type': 'application/json; charset=UTF-8', }, body: json.encode({ 'team': teamId, 'event': eventId, 'category': categoryId, - 'date': date.toIso8601String(), + 'date': formattedDate, }), ); if (response.statusCode == 201) { - return Entry.fromJson(json.decode(response.body)); + final decodedResponse = utf8.decode(response.bodyBytes); + + return Entry.fromJson(json.decode(decodedResponse)); } else { throw Exception('Failed to create entry'); } } - Future updateEntry(int entryId, int eventId, int categoryId, DateTime date) async { + Future updateUserInfo(int userId, Entry entry) async { init(); getToken(); + final entryId = entry.id; + + DateTime? date = entry.date; + String? formattedDate; + if (date != null) { + formattedDate = DateFormat('yyyy-MM-dd').format(date); + } + + final response = await http.put( + Uri.parse('$baseUrl/userinfo/$userId/'), + headers: { + 'Authorization': 'Token $token', + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: json.encode({ + 'zekken_number': entry.team.zekkenNumber, + 'event_code': entry.event.eventName, + 'group': entry.team.category.categoryName, + 'team_name': entry.team.teamName, + 'date': formattedDate, + }), + ); + if (response.statusCode == 200) { + final decodedResponse = utf8.decode(response.bodyBytes); + final updatedUserInfo = json.decode(decodedResponse); + //Get.find().updateUserInfo(updatedUserInfo); + + } else { + throw Exception('Failed to update entry'); + } + } + + + Future updateEntry(int entryId, int teamId, int eventId, int categoryId, DateTime date) async { + init(); + getToken(); + + String? formattedDate; + if (date != null) { + formattedDate = DateFormat('yyyy-MM-dd').format(date); + } + final response = await http.put( Uri.parse('$baseUrl/entry/$entryId/'), headers: { 'Authorization': 'Token $token', - 'Content-Type': 'application/json', + 'Content-Type': 'application/json; charset=UTF-8', }, body: json.encode({ + 'team': teamId, 'event': eventId, 'category': categoryId, - 'date': date.toIso8601String(), + 'date': formattedDate, }), ); if (response.statusCode == 200) { - return Entry.fromJson(json.decode(response.body)); + final decodedResponse = utf8.decode(response.bodyBytes); + + return Entry.fromJson(json.decode(decodedResponse)); } else { throw Exception('Failed to update entry'); } @@ -12635,6 +14185,43 @@ class ApiService extends GetxService{ throw Exception('Failed to delete entry'); } } + + static Future updateUserDetail(User user, String token) async { + String serverUrl = ConstValues.currentServer(); + int? userid = user.id; + String url = '$serverUrl/api/userdetail/$userid/'; + + try { + String? formattedDate; + if (user.dateOfBirth != null) { + formattedDate = DateFormat('yyyy-MM-dd').format(user.dateOfBirth!); + } + final http.Response response = await http.put( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + 'Authorization': 'Token $token' + }, + body: jsonEncode({ + 'firstname': user.firstname, + 'lastname': user.lastname, + 'date_of_birth': formattedDate, + 'female': user.female, + }), + ); + + if (response.statusCode == 200) { + return true; + } else { + print('Update failed with status code: ${response.statusCode}'); + return false; + } + } catch (e) { + print('Error in updateUserDetail: $e'); + return false; + } + } + }// import 'package:geojson/geojson.dart'; // import 'package:http/http.dart' as http; @@ -15595,3 +17182,76 @@ class _DebugWidgetState extends State { ); } } +// lib/widgets/helper_dialog.dart +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class HelperDialog extends StatefulWidget { + final String message; + final String screenKey; + + const HelperDialog({Key? key, required this.message, required this.screenKey}) : super(key: key); + + @override + _HelperDialogState createState() => _HelperDialogState(); +} + +class _HelperDialogState extends State { + bool _doNotShowAgain = false; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Row( + children: [ + Icon(Icons.help_outline, color: Colors.blue), + SizedBox(width: 10), + Text('ヘルプ'), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.message), + SizedBox(height: 20), + Row( + children: [ + Checkbox( + value: _doNotShowAgain, + onChanged: (value) { + setState(() { + _doNotShowAgain = value!; + }); + }, + ), + Text('この画面を二度と表示しない'), + ], + ), + ], + ), + actions: [ + TextButton( + child: Text('OK'), + onPressed: () async { + if (_doNotShowAgain) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('helper_${widget.screenKey}', false); + } + Get.back(); + }, + ), + ], + ); + } +} + +// ヘルパー画面を表示する関数 +Future showHelperDialog(String message, String screenKey) async { + final prefs = await SharedPreferences.getInstance(); + final showHelper = prefs.getBool('helper_$screenKey') ?? true; + if (showHelper) { + Get.dialog(HelperDialog(message: message, screenKey: screenKey)); + } +} \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a1b8ae5..67b408d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -11,9 +11,11 @@ PODS: - Flutter - flutter_keyboard_visibility (0.0.1): - Flutter - - FMDB (2.7.10): - - FMDB/standard (= 2.7.10) - - FMDB/standard (2.7.10) + - FMDB (2.7.12): + - FMDB/standard (= 2.7.12) + - FMDB/Core (2.7.12) + - FMDB/standard (2.7.12): + - FMDB/Core - geolocator_apple (1.2.0): - Flutter - image_gallery_saver (2.0.2): @@ -35,7 +37,7 @@ PODS: - qr_code_scanner (0.2.0): - Flutter - MTBBarcodeScanner - - ReachabilitySwift (5.2.1) + - ReachabilitySwift (5.2.3) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -121,7 +123,7 @@ SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_compass: cbbd285cea1584c7ac9c4e0c3e1f17cbea55e855 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 - FMDB: eae540775bf7d0c87a5af926ae37af69effe5a19 + FMDB: 728731dd336af3936ce00f91d9d8495f5718a0e6 geolocator_apple: 9157311f654584b9bb72686c55fc02a97b73f461 image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 @@ -132,7 +134,7 @@ SPEC CHECKSUMS: permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 pointer_interceptor_ios: 9280618c0b2eeb80081a343924aa8ad756c21375 qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e - ReachabilitySwift: 5ae15e16814b5f9ef568963fb2c87aeb49158c66 + ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979 shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 sqflite: 50a33e1d72bd59ee092a519a35d107502757ebed url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index eb5869c..35c588b 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -398,11 +398,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 480; + CURRENT_PROJECT_VERSION = 486; DEVELOPMENT_TEAM = UMNEWT25JR; ENABLE_BITCODE = NO; - FLUTTER_BUILD_NAME = 4.8.0; - FLUTTER_BUILD_NUMBER = 480; + FLUTTER_BUILD_NAME = 4.8.6; + FLUTTER_BUILD_NUMBER = 486; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "岐阜ナビ"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; @@ -411,7 +411,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.8.0; + MARKETING_VERSION = 4.8.6; PRODUCT_BUNDLE_IDENTIFIER = com.dvox.gifunavi; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -539,11 +539,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 480; + CURRENT_PROJECT_VERSION = 486; DEVELOPMENT_TEAM = UMNEWT25JR; ENABLE_BITCODE = NO; - FLUTTER_BUILD_NAME = 4.8.0; - FLUTTER_BUILD_NUMBER = 480; + FLUTTER_BUILD_NAME = 4.8.6; + FLUTTER_BUILD_NUMBER = 486; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "岐阜ナビ"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; @@ -552,7 +552,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.8.0; + MARKETING_VERSION = 4.8.6; PRODUCT_BUNDLE_IDENTIFIER = com.dvox.gifunavi; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -571,11 +571,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 480; + CURRENT_PROJECT_VERSION = 486; DEVELOPMENT_TEAM = UMNEWT25JR; ENABLE_BITCODE = NO; - FLUTTER_BUILD_NAME = 4.8.0; - FLUTTER_BUILD_NUMBER = 480; + FLUTTER_BUILD_NAME = 4.8.6; + FLUTTER_BUILD_NUMBER = 486; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "岐阜ナビ"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; @@ -584,7 +584,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.8.0; + MARKETING_VERSION = 4.8.6; PRODUCT_BUNDLE_IDENTIFIER = com.dvox.gifunavi; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/lib/pages/camera/camera_page.dart b/lib/pages/camera/camera_page.dart index 2cd9ae7..f209b0f 100644 --- a/lib/pages/camera/camera_page.dart +++ b/lib/pages/camera/camera_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; // この行を追加または確認 import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -10,6 +11,9 @@ import 'package:rogapp/services/external_service.dart'; import 'package:rogapp/utils/const.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart'; +import 'package:http/http.dart' as http; // この行を追加 + + // 関数 getTagText は、特定の条件に基づいて文字列から特定の部分を抽出し、返却するためのものです。 // 関数は2つのパラメータを受け取り、条件分岐を通じて結果を返します。 // @@ -613,235 +617,108 @@ class StartRogaining extends StatelessWidget { // 完了ボタンをタップすると、購入ポイントの処理が行われます。 // 購入なしボタンをタップすると、購入ポイントがキャンセルされます。 // -class BuyPointCamera extends StatelessWidget { - BuyPointCamera({Key? key, required this.destination}) : super(key: key); +class BuyPointCamera extends StatefulWidget { + final Destination destination; - Destination destination; - - DestinationController destinationController = - Get.find(); + const BuyPointCamera({Key? key, required this.destination}) : super(key: key); @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - title: Text( - "${destination.sub_loc_id} : ${destination.name}", - ), - ), - body: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Center( - child: Obx( - () => Container( - width: MediaQuery.of(context).size.width, - height: 370, - decoration: BoxDecoration( - image: DecorationImage( - // 要修正:getReceiptImage関数の戻り値がnullの場合のエラーハンドリングが不十分です。適切なデフォルト画像を表示するなどの処理を追加してください。 - // - image: getReceiptImage(), fit: BoxFit.cover)), - ), - ), - ), - ), - - Padding( - padding: const EdgeInsets.all(8.0), - child: Text(getTagText(true, destination.tags)), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Wrap( - spacing: 16.0, - runSpacing: 8.0, - children: [ - Obx(() => ElevatedButton( - onPressed: () { - destinationController.openCamera(context, destination); - }, - style: ElevatedButton.styleFrom( - shape: const CircleBorder(), - padding: const EdgeInsets.all(20), - backgroundColor: destinationController.photos.isEmpty - ? Colors.red - : Colors.grey[300], - ), - child: destinationController.photos.isEmpty - ? const Text("撮影", - style: TextStyle(color: Colors.white)) - : const Text("再撮影", - style: TextStyle(color: Colors.black)), - )), - ElevatedButton( - onPressed: () async { - await destinationController.cancelBuyPoint(destination); - Navigator.of(Get.context!).pop(); - destinationController.rogainingCounted.value = true; - destinationController.skipGps = false; - destinationController.isPhotoShoot.value = false; - }, - child: const Text("買い物なし")), - Obx(() => destinationController.photos.isNotEmpty - ? ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red), - onPressed: () async { - await destinationController.makeBuyPoint( - destination, - destinationController.photos[0].path); - Get.back(); - destinationController.rogainingCounted.value = true; - destinationController.skipGps = false; - destinationController.isPhotoShoot.value = false; - Get.snackbar("お買い物加点を行いました", - "${destination.sub_loc_id} : ${destination.name}", - backgroundColor: Colors.green, - colorText: Colors.white); - }, - child: const Text("完了", - style: TextStyle(color: Colors.white))) - : Container()) - ], - ), - ), - ], - ), - ), - ); - } + _BuyPointCameraState createState() => _BuyPointCameraState(); } +class _BuyPointCameraState extends State { + final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); + QRViewController? controller; + bool isQRScanned = false; -/* -class BuyPointCamera extends StatelessWidget { - BuyPointCamera({Key? key, required this.destination}) : super(key: key); - - Destination destination; - - DestinationController destinationController = - Get.find(); + final DestinationController destinationController = Get.find(); @override Widget build(BuildContext context) { - //print("in camera purchase 1 ${destinationController.isInRog.value}"); - return Scaffold( appBar: AppBar( automaticallyImplyLeading: false, - title: Text( - "${destination.sub_loc_id} : ${destination.name}", - ), + title: Text("${widget.destination.sub_loc_id} : ${widget.destination.name}"), ), body: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Center( - child: Obx( - () => Container( - width: MediaQuery.of(context).size.width, - height: 370, - decoration: BoxDecoration( - image: DecorationImage( - // 要修正:getReceiptImage関数の戻り値がnullの場合のエラーハンドリングが不十分です。適切なデフォルト画像を表示するなどの処理を追加してください。 - // - image: getReceiptImage(), fit: BoxFit.cover)), - ), - ), + Expanded( + flex: 4, + child: QRView( + key: qrKey, + onQRViewCreated: _onQRViewCreated, ), ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text(getTagText(true, destination.tags)), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - mainAxisSize: MainAxisSize.min, - children: [ - Obx(() => Row( - mainAxisSize: MainAxisSize.min, - children: [ - ElevatedButton( - onPressed: () { - // print( - // "in camera purchase 2 ${destinationController.isInRog.value}"); - destinationController.openCamera( - context, destination); - }, - child: destinationController.photos.isNotEmpty - ? const Text("再撮影") - : const Text("撮影")), - const SizedBox( - width: 10, - ), - ElevatedButton( - onPressed: () async { - await destinationController - .cancelBuyPoint(destination); - Navigator.of(Get.context!).pop(); - //Get.back(); - destinationController.rogainingCounted.value = true; - destinationController.skipGps = false; - destinationController.isPhotoShoot.value = false; - }, - child: const Text("買い物なし")) - ], - )), - Obx(() => destinationController.photos.isNotEmpty - ? Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // ElevatedButton( - // style: ElevatedButton.styleFrom( - // backgroundColor: Colors.red), - // onPressed: () async {}, - // child: const Text("買物なし")), - // const SizedBox( - // width: 10, - // ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red), - onPressed: () async { - // print( - // "in camera purchase 3 ${destinationController.isInRog.value}"); - await destinationController.makeBuyPoint( - destination, - destinationController.photos[0].path); - Get.back(); - // print( - // "in camera purchase 4 ${destinationController.isInRog.value}"); - destinationController.rogainingCounted.value = - true; - destinationController.skipGps = false; - destinationController.isPhotoShoot.value = false; - Get.snackbar("お買い物加点を行いました", - "${destination.sub_loc_id} : ${destination.name}", - backgroundColor: Colors.green, - colorText: Colors.white - ); - }, - child: const Text("完了")) - ], - ) - : Container()) - ], + Expanded( + flex: 1, + child: Center( + child: Text('QRコードをスキャンしてください'), + ), ), ], ), ); } + + void _onQRViewCreated(QRViewController controller) { + this.controller = controller; + controller.scannedDataStream.listen((scanData) { + if (!isQRScanned && scanData.code != null && scanData.code!.startsWith('https://rogaining.sumasen.net/api/activate_buy_point/')) { + isQRScanned = true; + _activateBuyPoint(scanData.code!); + } + }); + } + + void _activateBuyPoint(String qrCode) async { + final IndexController indexController = Get.find(); + + final userId = indexController.currentUser[0]["user"]["id"]; + final token = indexController.currentUser[0]["token"]; + final teamName = indexController.currentUser[0]["user"]['team_name']; + final eventCode = indexController.currentUser[0]["user"]["event_code"]; + //final cpNumber = destinationController.currentDestinationFeature[0].cp; + final cpNumber = widget.destination.cp; + + try { + final response = await http.post( + Uri.parse('https://rogaining.sumasen.net/api/activate_buy_point/'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Token $token', + }, + body: jsonEncode({ + 'user_id': userId, + 'team_name': teamName, + 'event_code': eventCode, + 'cp_number': cpNumber, + 'qr_code': qrCode, + }), + ); + + if (response.statusCode == 200) { + Get.snackbar('成功', 'お買い物ポイントが有効化されました'); + Navigator.of(context).pop(); + } else { + Get.snackbar('エラー', 'お買い物ポイントの有効化に失敗しました'); + } + } catch (e) { + Get.snackbar('エラー', 'ネットワークエラーが発生しました'); + } finally { + isQRScanned = false; + } + } + + @override + void dispose() { + controller?.dispose(); + super.dispose(); + } } -*/ + + class QRCodeScannerPage extends StatefulWidget { diff --git a/lib/pages/destination/destination_controller.dart b/lib/pages/destination/destination_controller.dart index 85a0ca5..e2a25d7 100644 --- a/lib/pages/destination/destination_controller.dart +++ b/lib/pages/destination/destination_controller.dart @@ -1028,7 +1028,7 @@ class DestinationController extends GetxController { print("An error occurred: $e"); // await checkForCheckin(); } finally { - await Future.delayed(const Duration(seconds: 5)); // 一定時間待機してから再帰呼び出し + await Future.delayed(const Duration(seconds: 1)); // 一定時間待機してから再帰呼び出し //print("--- End of checkForCheckin function, calling recursively ---"); unawaited( checkForCheckin() ); } diff --git a/lib/pages/drawer/drawer_page.dart b/lib/pages/drawer/drawer_page.dart index bdacdb9..8ae212a 100644 --- a/lib/pages/drawer/drawer_page.dart +++ b/lib/pages/drawer/drawer_page.dart @@ -104,7 +104,14 @@ class DrawerPage extends StatelessWidget { Get.toNamed(AppPages.EVENT_ENTRY); }, ), - + ListTile( + leading: const Icon(Icons.person), + title: Text("個人情報の修正"), + onTap: () { + Get.back(); // Close the drawer + Get.toNamed(AppPages.USER_DETAILS_EDIT); + }, + ), Obx(() => indexController.currentUser.isEmpty ? ListTile( diff --git a/lib/pages/entry/entry_controller.dart b/lib/pages/entry/entry_controller.dart index 6d9631d..7ef94f8 100644 --- a/lib/pages/entry/entry_controller.dart +++ b/lib/pages/entry/entry_controller.dart @@ -69,6 +69,7 @@ class EntryController extends GetxController { void initializeEditMode(Entry entry) { + currentEntry.value = entry; selectedEvent.value = entry.event; selectedTeam.value = entry.team; selectedCategory.value = entry.category; @@ -169,10 +170,14 @@ class EntryController extends GetxController { } Future updateEntry() async { - if (currentEntry.value == null) return; + if (currentEntry.value == null) { + Get.snackbar('Error', 'No entry selected for update'); + return; + } try { final updatedEntry = await _apiService.updateEntry( currentEntry.value!.id, + currentEntry.value!.team.id, selectedEvent.value!.id, selectedCategory.value!.id, selectedDate.value!, @@ -189,7 +194,10 @@ class EntryController extends GetxController { } Future deleteEntry() async { - if (currentEntry.value == null) return; + if (currentEntry.value == null) { + Get.snackbar('Error', 'No entry selected for deletion'); + return; + } try { await _apiService.deleteEntry(currentEntry.value!.id); entries.removeWhere((entry) => entry.id == currentEntry.value!.id); diff --git a/lib/pages/entry/entry_detail_page.dart b/lib/pages/entry/entry_detail_page.dart index 84a9900..7f736c0 100644 --- a/lib/pages/entry/entry_detail_page.dart +++ b/lib/pages/entry/entry_detail_page.dart @@ -90,21 +90,40 @@ class EntryDetailPage extends GetView { }, ), SizedBox(height: 32), - ElevatedButton( - child: Text(mode == 'new' ? 'エントリーを作成' : 'エントリーを更新'), - onPressed: () { - if (mode == 'new') { - controller.createEntry(); - } else { - controller.updateEntry(); - } - }, - ), - if (mode == 'edit' && controller.isOwner()) + if (mode == 'new') ElevatedButton( - child: Text('エントリーを削除'), - onPressed: () => controller.deleteEntry(), - style: ElevatedButton.styleFrom(backgroundColor: Colors.red), + child: Text('エントリーを作成'), + onPressed: () => controller.createEntry(), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + minimumSize: Size(double.infinity, 50), + ), + ) + else + Row( + children: [ + Expanded( + child: ElevatedButton( + child: Text('エントリーを削除'), + onPressed: () => controller.deleteEntry(), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + minimumSize: Size(0, 50), + ), + ), + ), + SizedBox(width: 16), + Expanded( + child: ElevatedButton( + child: Text('エントリーを更新'), + onPressed: () => controller.updateEntry(), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.lightBlue, + minimumSize: Size(0, 50), + ), + ), + ), + ], ), ], ), diff --git a/lib/pages/entry/entry_list_page.dart b/lib/pages/entry/entry_list_page.dart index 0101910..9da59ba 100644 --- a/lib/pages/entry/entry_list_page.dart +++ b/lib/pages/entry/entry_list_page.dart @@ -18,18 +18,30 @@ class EntryListPage extends GetView { ), ], ), - body: Obx(() => ListView.builder( - itemCount: controller.entries.length, - itemBuilder: (context, index) { - final entry = controller.entries[index]; - return ListTile( - title: Text(entry.event?.eventName ?? 'イベント未設定'), - subtitle: Text('${entry.team?.teamName ?? 'チーム未設定'} - ${entry.category?.categoryName ?? 'カテゴリ未設定'}'), - trailing: Text(entry.date?.toString().substring(0, 10) ?? '日付未設定'), - onTap: () => Get.toNamed(AppPages.ENTRY_DETAIL, arguments: {'mode': 'edit', 'entry': entry}), - ); - }, - )), + body: Obx((){ + // エントリーを日付昇順にソート + final sortedEntries = controller.entries.toList() + ..sort((a, b) => (a.date ?? DateTime(0)).compareTo(b.date ?? DateTime(0))); + + return ListView.builder( + itemCount: sortedEntries.length, + itemBuilder: (context, index) { + final entry = sortedEntries[index]; + return ListTile( + title: Text(entry.event?.eventName ?? 'イベント未設定'), + subtitle: Text( + '${entry.team?.teamName ?? 'チーム未設定'} - ${entry.category + ?.categoryName ?? 'カテゴリ未設定'}'), + trailing: Text( + entry.date?.toString().substring(0, 10) ?? '日付未設定'), + onTap: () => + Get.toNamed(AppPages.ENTRY_DETAIL, + arguments: {'mode': 'edit', 'entry': entry}), + ); + }, + ); + }), ); + } } \ No newline at end of file diff --git a/lib/pages/index/index_controller.dart b/lib/pages/index/index_controller.dart index d9c4b62..30d6f24 100644 --- a/lib/pages/index/index_controller.dart +++ b/lib/pages/index/index_controller.dart @@ -12,6 +12,7 @@ import 'package:latlong2/latlong.dart'; import 'package:rogapp/model/destination.dart'; import 'package:rogapp/model/entry.dart'; import 'package:rogapp/pages/destination/destination_controller.dart'; +import 'package:rogapp/pages/team/team_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/services/auth_service.dart'; import 'package:rogapp/services/location_service.dart'; @@ -20,9 +21,12 @@ import 'package:rogapp/widgets/debug_widget.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:rogapp/services/api_service.dart'; +import 'package:rogapp/model/user.dart'; -import '../../main.dart'; +import 'package:rogapp/main.dart'; + +import 'package:rogapp/widgets/helper_dialog.dart'; class IndexController extends GetxController with WidgetsBindingObserver { List locations = [].obs; @@ -353,6 +357,12 @@ class IndexController extends GetxController with WidgetsBindingObserver { // ログイン成功後、api_serviceを初期化 await Get.putAsync(() => ApiService().init()); + // ユーザー情報の完全性をチェック + if (await checkUserInfoComplete()) { + Get.offAllNamed(AppPages.INDEX); + } else { + Get.offAllNamed(AppPages.USER_DETAILS_EDIT); + } } else { logManager.addOperationLog("User failed login : ${email} , ${password}."); @@ -372,6 +382,14 @@ class IndexController extends GetxController with WidgetsBindingObserver { }); } + Future checkUserInfoComplete() async { + final user = await ApiService.to.getCurrentUser(); + return user.firstname.isNotEmpty && + user.lastname.isNotEmpty && + user.dateOfBirth != null && + user.female != null; + } + // 要検討:エラーハンドリングが行われていますが、エラーメッセージをローカライズすることを検討してください。 // void changePassword( @@ -507,7 +525,7 @@ class IndexController extends GetxController with WidgetsBindingObserver { saveToDevice(currentUser[0]["token"]); } isLoading.value = false; - loadLocationsBound( currentUser[0]["user"]["even_code"]); + loadLocationsBound( currentUser[0]["user"]["event_code"]); if (currentUser.isNotEmpty) { rogMode.value = 0; restoreGame(); @@ -752,4 +770,23 @@ class IndexController extends GetxController with WidgetsBindingObserver { // 例: 現在の位置情報を再取得し、マップを更新する loadLocationsBound( eventCode ); } + + Future checkEntryData() async { + // エントリーデータの有無をチェックするロジック + final teamController = TeamController(); + bool hasEntryData = teamController.checkIfUserHasEntryData(); + if (!hasEntryData) { + await showHelperDialog( + 'イベントに参加するには、チーム登録・メンバー登録及びエントリーが必要になります。', + 'entry_check' + ); + // ドロワーを表示するロジック + Get.toNamed('/drawer'); + } + } + + void updateCurrentUser(User updatedUser) { + currentUser[0]['user'] = updatedUser.toJson(); + update(); + } } diff --git a/lib/pages/index/index_page.dart b/lib/pages/index/index_page.dart index 3945201..8f0afab 100644 --- a/lib/pages/index/index_page.dart +++ b/lib/pages/index/index_page.dart @@ -23,8 +23,40 @@ import 'package:rogapp/utils/location_controller.dart'; // IndexPageクラスは、GetViewを継承したStatelessWidgetです。このクラスは、アプリのメインページを表すウィジェットです。 // -class IndexPage extends GetView { - IndexPage({Key? key}) : super(key: key); +import 'package:rogapp/widgets/helper_dialog.dart'; + +class IndexPage extends StatefulWidget { + @override + _IndexPageState createState() => _IndexPageState(); +} + +class _IndexPageState extends State { + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + //checkEntryAndShowHelper(); + }); + } + + /* + void checkEntryAndShowHelper() async { + final hasEntry = await checkIfUserHasEntry(); // この関数は実装する必要があります + if (!hasEntry) { + showHelperDialog( + context, + 'イベントに参加するには、チーム登録・メンバー登録及びエントリーが必要になります。', + 'entry_helper', + showDoNotShowAgain: true, + ); + } + } + + */ + +// class IndexPage extends GetView { +// IndexPage({Key? key}) : super(key: key); // IndexControllerとDestinationControllerのインスタンスを取得しています。 // diff --git a/lib/pages/login/login_page.dart b/lib/pages/login/login_page.dart index d4e8774..29743fe 100644 --- a/lib/pages/login/login_page.dart +++ b/lib/pages/login/login_page.dart @@ -2,18 +2,36 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; +import 'package:rogapp/widgets/helper_dialog.dart'; // 要検討:ログインボタンとサインアップボタンの配色を見直すことを検討してください。現在の配色では、ボタンの役割がわかりにくい可能性があります。 // エラーメッセージをローカライズすることを検討してください。 // ログイン処理中にエラーが発生した場合のエラーハンドリングを追加することをお勧めします。 // -class LoginPage extends StatelessWidget { +class LoginPage extends StatefulWidget { + @override + _LoginPageState createState() => _LoginPageState(); +} + +class _LoginPageState extends State { +//class LoginPage extends StatelessWidget { final IndexController indexController = Get.find(); TextEditingController emailController = TextEditingController(); TextEditingController passwordController = TextEditingController(); - LoginPage({Key? key}) : super(key: key); + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + showHelperDialog( + '参加するにはユーザー登録が必要です。サインアップからユーザー登録してください。', + 'login_page' + ); + }); + } + + //LoginPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/pages/register/register_page.dart b/lib/pages/register/register_page.dart index e070c53..646f296 100644 --- a/lib/pages/register/register_page.dart +++ b/lib/pages/register/register_page.dart @@ -2,15 +2,30 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/routes/app_pages.dart'; +import 'package:rogapp/widgets/helper_dialog.dart'; -class RegisterPage extends StatelessWidget { +class RegisterPage extends StatefulWidget { + @override + _RegisterPageState createState() => _RegisterPageState(); +} + +class _RegisterPageState extends State { final IndexController indexController = Get.find(); - TextEditingController emailController = TextEditingController(); - TextEditingController passwordController = TextEditingController(); - TextEditingController confirmPasswordController = TextEditingController(); + final TextEditingController emailController = TextEditingController(); + final TextEditingController passwordController = TextEditingController(); + final TextEditingController confirmPasswordController = TextEditingController(); - RegisterPage({Key? key}) : super(key: key); + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + showHelperDialog( + '登録メールにアクティベーションメールが送信されます。メールにあるリンクをタップすると正式登録になります。', + 'register_page' + ); + }); + } @override Widget build(BuildContext context) { @@ -21,150 +36,53 @@ class RegisterPage extends StatelessWidget { elevation: 0, backgroundColor: Colors.white, leading: IconButton( - onPressed: () { - Navigator.pop(context); - }, - icon: const Icon( - Icons.arrow_back_ios, - size: 20, - color: Colors.black, - )), + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.arrow_back_ios, size: 20, color: Colors.black), + ), ), body: SafeArea( child: SingleChildScrollView( - child: SizedBox( + child: Container( height: MediaQuery.of(context).size.height, width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 40), child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Column( - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Text( - "sign_up".tr, // "サインアップ" - style: const TextStyle( - fontSize: 30, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox( - height: 20, - ), - Text( - "create_account".tr, // アカウントを無料で作成します。 - style: TextStyle( - fontSize: 15, - color: Colors.grey[700], - ), - ), - const SizedBox( - height: 30, - ) - ], - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 40), - child: Column( - children: [ - makeInput(label: "email".tr, controller: emailController), // メールアドレス - makeInput( - label: "password".tr, - controller: passwordController, - obsureText: true), // パスワード - makeInput( - label: "confirm_password".tr, - controller: confirmPasswordController, - obsureText: true) // パスワード再確認 - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 40), - child: Container( - padding: const EdgeInsets.only(top: 3, left: 3), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(40), - border: const Border( - bottom: BorderSide(color: Colors.black), - top: BorderSide(color: Colors.black), - right: BorderSide(color: Colors.black), - left: BorderSide(color: Colors.black))), - child: MaterialButton( - minWidth: double.infinity, - height: 60, - onPressed: () { - if (passwordController.text != - confirmPasswordController.text) { - Get.snackbar( - "no_match".tr, - "password_does_not_match".tr, - backgroundColor: Colors.red, - colorText: Colors.white, - icon: const Icon(Icons.assistant_photo_outlined, - size: 40.0, color: Colors.blue), - snackPosition: SnackPosition.TOP, - duration: const Duration(seconds: 3), - // backgroundColor: Colors.yellow, - //icon:Image(image:AssetImage("assets/images/dora.png")) - ); - } - if (emailController.text.isEmpty || - passwordController.text.isEmpty) { - Get.snackbar( - "no_values".tr, - "email_and_password_required".tr, - backgroundColor: Colors.red, - colorText: Colors.white, - icon: const Icon(Icons.assistant_photo_outlined, - size: 40.0, color: Colors.blue), - snackPosition: SnackPosition.TOP, - duration: const Duration(seconds: 3), - //backgroundColor: Colors.yellow, - //icon:Image(image:AssetImage("assets/images/dora.png")) - ); - return; - } - indexController.isLoading.value = true; - indexController.register(emailController.text, - passwordController.text, context); - }, - color: Colors.redAccent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(40)), - child: Text( - "sign_up".tr, - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16, - ), - ), - ), - ), - ), - const SizedBox( - height: 20, - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible(child: Text("already_have_account".tr)), - TextButton( - onPressed: () { - Get.toNamed(AppPages.LOGIN); - }, - child: Text( - "login".tr, - style: TextStyle( - fontWeight: FontWeight.w600, fontSize: 18), - ), - ), - ], - ) - ], + Text( + "sign_up".tr, + style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold), ), + const SizedBox(height: 20), + Text( + "create_account".tr, + style: TextStyle(fontSize: 15, color: Colors.grey[700]), + ), + const SizedBox(height: 30), + makeInput(label: "email".tr, controller: emailController), + makeInput(label: "password".tr, controller: passwordController, obsureText: true), + makeInput(label: "confirm_password".tr, controller: confirmPasswordController, obsureText: true), + const SizedBox(height: 20), + ElevatedButton( + onPressed: _handleRegister, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.redAccent, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(40)), + minimumSize: const Size(double.infinity, 60), + ), + child: Text("sign_up".tr, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16)), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible(child: Text("already_have_account".tr)), + TextButton( + onPressed: () => Get.toNamed(AppPages.LOGIN), + child: Text("login".tr, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 18)), + ), + ], + ) ], ), ), @@ -172,40 +90,63 @@ class RegisterPage extends StatelessWidget { ), ); } + + void _handleRegister() { + if (passwordController.text != confirmPasswordController.text) { + _showErrorSnackbar("no_match".tr, "password_does_not_match".tr); + return; + } + if (emailController.text.isEmpty || passwordController.text.isEmpty) { + _showErrorSnackbar("no_values".tr, "email_and_password_required".tr); + return; + } + + indexController.isLoading.value = true; + try { + indexController.register( + emailController.text, + passwordController.text, + context + ); + // 登録が成功したと仮定し、ログインページに遷移 + Get.offNamed(AppPages.LOGIN); + } catch (error) { + _showErrorSnackbar("registration_error".tr, error.toString()); + } finally { + indexController.isLoading.value = false; + } + } + + void _showErrorSnackbar(String title, String message) { + Get.snackbar( + title, + message, + backgroundColor: Colors.red, + colorText: Colors.white, + icon: const Icon(Icons.error_outline, size: 40.0, color: Colors.white), + snackPosition: SnackPosition.TOP, + duration: const Duration(seconds: 3), + ); + } } -Widget makeInput( - {label, required TextEditingController controller, obsureText = false}) { +Widget makeInput({required String label, required TextEditingController controller, bool obsureText = false}) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - label, - style: const TextStyle( - fontSize: 15, fontWeight: FontWeight.w400, color: Colors.black87), - ), - const SizedBox( - height: 5, - ), + Text(label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w400, color: Colors.black87)), + const SizedBox(height: 5), TextField( controller: controller, obscureText: obsureText, decoration: InputDecoration( - contentPadding: - const EdgeInsets.symmetric(vertical: 0, horizontal: 10), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: (Colors.grey[400])!, - ), - ), - border: OutlineInputBorder( - borderSide: BorderSide(color: (Colors.grey[400])!), - ), + contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10), + enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey[400]!)), + border: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey[400]!)), ), ), - const SizedBox( - height: 30, - ) + const SizedBox(height: 20), ], ); } + diff --git a/lib/pages/register/user_detail_page.dart b/lib/pages/register/user_detail_page.dart new file mode 100644 index 0000000..c21a016 --- /dev/null +++ b/lib/pages/register/user_detail_page.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:rogapp/model/user.dart'; +import 'package:rogapp/routes/app_pages.dart'; +import 'package:rogapp/services/api_service.dart'; +import 'package:rogapp/pages/index/index_controller.dart'; + +class UserDetailsEditPage extends StatefulWidget { + @override + _UserDetailsEditPageState createState() => _UserDetailsEditPageState(); +} + +class _UserDetailsEditPageState extends State { + final _formKey = GlobalKey(); + final IndexController indexController = Get.find(); + late User _user; + final TextEditingController _firstnameController = TextEditingController(); + final TextEditingController _lastnameController = TextEditingController(); + final TextEditingController _dateOfBirthController = TextEditingController(); + late bool _female; + + @override + void initState() { + super.initState(); + _user = User.fromJson(indexController.currentUser[0]['user']); + _firstnameController.text = _user.firstname; + _lastnameController.text = _user.lastname; + _dateOfBirthController.text = _user.dateOfBirth != null + ? '${_user.dateOfBirth!.year}/${_user.dateOfBirth!.month.toString().padLeft(2, '0')}/${_user.dateOfBirth!.day.toString().padLeft(2, '0')}' + : ''; + _female = _user.female; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('個人情報の修正'), + automaticallyImplyLeading: false, + ), + body: Form( + key: _formKey, + child: ListView( + padding: EdgeInsets.all(16.0), + children: [ + TextFormField( + controller: _lastnameController, + decoration: InputDecoration( + labelText: '姓', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '姓を入力してください'; + } + return null; + }, + ), + + SizedBox(height: 16), + TextFormField( + controller: _firstnameController, + decoration: InputDecoration( + labelText: '名', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '名を入力してください'; + } + return null; + }, + ), + SizedBox(height: 16), + TextFormField( + controller: _dateOfBirthController, + decoration: InputDecoration( + labelText: '生年月日 (YYYY/MM/DD)', + border: OutlineInputBorder(), + hintText: 'YYYY/MM/DD', + ), + keyboardType: TextInputType.datetime, + validator: (value) { + if (value == null || value.isEmpty) { + return '生年月日を入力してください'; + } + if (!RegExp(r'^\d{4}/\d{2}/\d{2}$').hasMatch(value)) { + return '正しい形式で入力してください (YYYY/MM/DD)'; + } + final date = DateTime.tryParse(value.replaceAll('/', '-')); + if (date == null) { + return '有効な日付を入力してください'; + } + if (date.isAfter(DateTime.now())) { + return '未来の日付は入力できません'; + } + return null; + }, + ), + SizedBox(height: 16), + SwitchListTile( + title: Text('性別'), + subtitle: Text(_female ? '女性' : '男性'), + value: _female, + onChanged: (bool value) { + setState(() { + _female = value; + }); + }, + ), + SizedBox(height: 16), + TextFormField( + initialValue: _user.email, + decoration: InputDecoration( + labelText: 'メールアドレス', + border: OutlineInputBorder(), + ), + enabled: false, + ), + SizedBox(height: 16), + SwitchListTile( + title: Text('アクティブ状態'), + value: _user.isActive, + onChanged: null, + ), + SizedBox(height: 32), + ElevatedButton( + child: Text('更新'), + onPressed: _updateUserDetails, + ), + ], + ), + ), + ); + } + + void _updateUserDetails() async { + if (_formKey.currentState!.validate()) { + final dateOfBirth = DateTime.tryParse(_dateOfBirthController.text.replaceAll('/', '-')); + if (dateOfBirth == null || dateOfBirth.isAfter(DateTime.now())) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('生年月日が無効です', style: TextStyle(color: Colors.red))), + ); + return; + } + + User updatedUser = User( + id: _user.id, + email: _user.email, + firstname: _firstnameController.text, + lastname: _lastnameController.text, + dateOfBirth: dateOfBirth, + female: _female, + isActive: _user.isActive, + ); + + try { + bool success = await ApiService.updateUserDetail(updatedUser, indexController.currentUser[0]['token']); + if (success) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('個人情報が更新されました')), + ); + indexController.updateCurrentUser(updatedUser); + Get.offAllNamed(AppPages.INDEX); + //Get.back(); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('更新に失敗しました', style: TextStyle(color: Colors.red))), + ); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('エラーが発生しました: $e', style: TextStyle(color: Colors.red))), + ); + } + } + } +} + + + + diff --git a/lib/pages/team/member_detail_page.dart b/lib/pages/team/member_detail_page.dart index 722b7f9..d8d2e9a 100644 --- a/lib/pages/team/member_detail_page.dart +++ b/lib/pages/team/member_detail_page.dart @@ -85,121 +85,153 @@ class _MemberDetailPageState extends State { home:Scaffold( */ return Scaffold( - appBar: AppBar( - title: Text(mode == 'new' ? 'メンバー追加' : 'メンバー詳細'), - actions: [ - IconButton( - icon: Icon(Icons.save), - onPressed: () async { - await controller.saveMember(); - Get.back(result: true); - }, - ), - ], - ), - body: Obx(() { - if (controller.isLoading.value) { - return Center(child: CircularProgressIndicator()); - } - - return SingleChildScrollView( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (mode == 'new') - TextField( - decoration: InputDecoration(labelText: 'メールアドレス'), - onChanged: (value) => controller.email.value = value, - controller: TextEditingController(text: controller.email.value), - ) - else if (controller.isDummyEmail) - Text('メールアドレス: (メアド無し)') - else - Text('メールアドレス: ${controller.email.value}'), - - if (controller.email.value.isEmpty || mode == 'edit') ...[ - TextField( - decoration: InputDecoration(labelText: '姓'), - onChanged: (value) => controller.lastname.value = value, - controller: TextEditingController(text: controller.lastname.value), - ), - TextField( - decoration: InputDecoration(labelText: '名'), - onChanged: (value) => controller.firstname.value = value, - controller: TextEditingController(text: controller.firstname.value), - ), - // 生年月日 - if (controller.isDummyEmail || !controller.isOver18()) - ListTile( - title: Text('生年月日'), - subtitle: Text(controller.dateOfBirth.value != null - ? '${DateFormat('yyyy年MM月dd日').format(controller.dateOfBirth.value!)} (${controller.getAgeAndGrade()})' - : '未設定'), - onTap: () async { - final date = await showDatePicker( - context: context, - initialDate: controller.dateOfBirth.value ?? DateTime.now(), - firstDate: DateTime(1900), - lastDate: DateTime.now(), - //locale: const Locale('ja', 'JP'), - ); - if (date != null) controller.dateOfBirth.value = date; - }, - ) - else - Text('18歳以上'), - - SwitchListTile( - title: Text('性別'), - subtitle: Text(controller.female.value ? '女性' : '男性'), - value: controller.female.value, - onChanged: (value) => controller.female.value = value, - ), - ], - // 招待メール再送信ボタン(通常のEmailで未承認の場合のみ) - if (!controller.isDummyEmail && !controller.isApproved) - ElevatedButton( - child: Text('招待メールを再送信'), - onPressed: () => controller.resendInvitation(), - ), - - // メンバー削除ボタン - ElevatedButton( - child: Text('メンバーから削除'), - onPressed: () async { - final confirmed = await Get.dialog( - AlertDialog( - title: Text('確認'), - content: Text('このメンバーを削除してもよろしいですか?'), - actions: [ - TextButton( - child: Text('キャンセル'), - onPressed: () => Get.back(result: false), - ), - TextButton( - child: Text('削除'), - onPressed: () => Get.back(result: true), - ), - ], - ), - ); - if (confirmed == true) { - await controller.deleteMember(); - Get.back(result: true); - } - }, - style: ElevatedButton.styleFrom(backgroundColor: Colors.red), - ), - ], - ), - ); - - - - }), + appBar: AppBar( + title: Text(mode == 'new' ? 'メンバー追加' : 'メンバー詳細'), + ), + body: Obx(() { + if (controller.isLoading.value) { + return Center(child: CircularProgressIndicator()); + } + // TextEditingControllerをObxの中で作成 + final emailController = TextEditingController(text: controller.email.value); + // カーソル位置を保持 + emailController.selection = TextSelection.fromPosition( + TextPosition(offset: controller.email.value.length), ); + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (mode == 'new') + TextFormField( + decoration: InputDecoration(labelText: 'メールアドレス'), + //onChanged: (value) => controller.email.value = value, + onChanged: (value) { + controller.email.value = value; + // カーソル位置を更新 + emailController.selection = TextSelection.fromPosition( + TextPosition(offset: value.length), + ); + }, + controller: emailController, + //controller: TextEditingController(text: controller.email.value), + keyboardType: TextInputType.emailAddress, // メールアドレス用のキーボードを表示 + autocorrect: false, // 自動修正を無効化 + enableSuggestions: false, + ) + else if (controller.isDummyEmail) + Text('メールアドレス: (メアド無し)') + else + Text('メールアドレス: ${controller.email.value}'), + + if (controller.email.value.isEmpty || mode == 'edit') ...[ + TextFormField( + decoration: InputDecoration(labelText: '姓'), + onChanged: (value) => controller.lastname.value = value, + controller: TextEditingController(text: controller.lastname.value), + ), + TextFormField( + decoration: InputDecoration(labelText: '名'), + onChanged: (value) => controller.firstname.value = value, + controller: TextEditingController(text: controller.firstname.value), + ), + // 生年月日 + if (controller.isDummyEmail || !controller.isOver18()) + ListTile( + title: Text('生年月日'), + subtitle: Text(controller.dateOfBirth.value != null + ? '${DateFormat('yyyy年MM月dd日').format(controller.dateOfBirth.value!)} (${controller.getAgeAndGrade()})' + : '未設定'), + onTap: () async { + final date = await showDatePicker( + context: context, + initialDate: controller.dateOfBirth.value ?? DateTime.now(), + firstDate: DateTime(1900), + lastDate: DateTime.now(), + //locale: const Locale('ja', 'JP'), + ); + if (date != null) controller.dateOfBirth.value = date; + }, + ) + else + Text('18歳以上'), + + SwitchListTile( + title: Text('性別'), + subtitle: Text(controller.female.value ? '女性' : '男性'), + value: controller.female.value, + onChanged: (value) => controller.female.value = value, + ), + ], + ]), + ), + ), + Padding( + padding: EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + child: Text('削除'), + onPressed: () async { + final confirmed = await Get.dialog( + AlertDialog( + title: Text('確認'), + content: Text('このメンバーを削除してもよろしいですか?'), + actions: [ + TextButton( + child: Text('キャンセル'), + onPressed: () => Get.back(result: false), + ), + TextButton( + child: Text('削除'), + onPressed: () => Get.back(result: true), + ), + ], + ), + ); + if (confirmed == true) { + await controller.deleteMember(); + Get.back(result: true); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + ), + if (!controller.isDummyEmail && !controller.isApproved) + ElevatedButton( + child: Text('招待再送信'), + onPressed: () => controller.resendInvitation(), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + ), + ), + ElevatedButton( + child: Text('保存・招待'), + onPressed: () async { + await controller.saveMember(); + Get.back(result: true); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + ), + ], + ), + ), + ], + ); + }), + ); } @override diff --git a/lib/pages/team/team_controller.dart b/lib/pages/team/team_controller.dart index 91ef000..bed9898 100644 --- a/lib/pages/team/team_controller.dart +++ b/lib/pages/team/team_controller.dart @@ -1,5 +1,6 @@ // lib/controllers/team_controller.dart import 'dart:convert'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:rogapp/model/team.dart'; import 'package:rogapp/model/category.dart'; @@ -27,6 +28,7 @@ class TeamController extends GetxController { super.onInit(); try { _apiService = Get.find(); + isLoading.value = true; await fetchCategories(); await Future.wait([ @@ -67,8 +69,13 @@ class TeamController extends GetxController { void resetForm() { selectedTeam.value = null; teamName.value = ''; - selectedCategory.value = categories.isNotEmpty ? categories.first : null; - teamMembers.clear(); + if (categories.isNotEmpty) { + selectedCategory.value = categories.first; + } else { + selectedCategory.value = null; + // カテゴリが空の場合、エラーメッセージをセット + error.value = 'カテゴリデータが取得できませんでした。'; + } teamMembers.clear(); } void cleanupForNavigation() { @@ -93,6 +100,14 @@ class TeamController extends GetxController { } } + bool checkIfUserHasEntryData(){ + if (teams.isEmpty) { + return false; + }else { + return true; + } + } + Future fetchCategories() async { try { final fetchedCategories = await _apiService.getCategories(); @@ -115,6 +130,17 @@ class TeamController extends GetxController { Future createTeam(String teamName, int categoryId) async { final newTeam = await _apiService.createTeam(teamName, categoryId); + + // 自分自身をメンバーとして自動登録 + await _apiService.createTeamMember( + newTeam.id, + currentUser.value?.email, + currentUser.value!.firstname, + currentUser.value!.lastname, + currentUser.value?.dateOfBirth, + currentUser.value?.female, + ); + return newTeam; } @@ -125,11 +151,51 @@ class TeamController extends GetxController { } Future deleteTeam(int teamId) async { - try { - await _apiService.deleteTeam(teamId); - teams.removeWhere((team) => team.id == teamId); - } catch (e) { - print('Error deleting team: $e'); + bool confirmDelete = await Get.dialog( + AlertDialog( + title: Text('チーム削除の確認'), + content: Text('このチームとそのすべてのメンバーを削除しますか?この操作は取り消せません。'), + actions: [ + TextButton( + child: Text('キャンセル'), + onPressed: () => Get.back(result: false), + ), + TextButton( + child: Text('削除'), + onPressed: () => Get.back(result: true), + ), + ], + ), + ) ?? false; + + if (confirmDelete) { + try { + // まず、チームのメンバーを全て削除 + await _apiService.deleteAllTeamMembers(teamId); + + // その後、チームを削除 + await _apiService.deleteTeam(teamId); + + // ローカルのチームリストを更新 + teams.removeWhere((team) => team.id == teamId); + + /* + Get.snackbar( + '成功', + 'チームとそのメンバーが削除されました', + backgroundColor: Colors.green, + colorText: Colors.white, + snackPosition: SnackPosition.BOTTOM, + ); + */ + + // チームリスト画面に戻る + Get.back(); + + } catch (e) { + print('Error deleting team and members: $e'); + Get.snackbar('エラー', 'チームとメンバーの削除に失敗しました'); + } } } @@ -237,4 +303,30 @@ class TeamController extends GetxController { } } + List getFilteredCategories() { + //List teamMembers = getCurrentTeamMembers(); + return categories.where((category) { + return isCategoryValid(category, teamMembers); + }).toList(); + } + + bool isCategoryValid(NewCategory category, List teamMembers) { + int maleCount = teamMembers.where((member) => !member.female).length; + int femaleCount = teamMembers.where((member) => member.female).length; + int totalCount = teamMembers.length; + + bool isValidGender = category.female ? (femaleCount == totalCount) : true; + bool isValidMemberCount = totalCount == category.numOfMember; + bool isValidFamily = category.family ? areAllMembersFamily(teamMembers) : true; + + return isValidGender && isValidMemberCount && isValidFamily; + } + + bool areAllMembersFamily(List teamMembers) { + // 家族かどうかを判断するロジック(例: 同じ姓を持つメンバーが2人以上いる場合は家族とみなす) + Set familyNames = teamMembers.map((member) => member.lastname).toSet(); + return familyNames.length < teamMembers.length; + } + + } \ No newline at end of file diff --git a/lib/pages/team/team_detail_page.dart b/lib/pages/team/team_detail_page.dart index efbc320..4ce558d 100644 --- a/lib/pages/team/team_detail_page.dart +++ b/lib/pages/team/team_detail_page.dart @@ -126,15 +126,26 @@ class _TeamDetailPageState extends State { ), SizedBox(height: 16), - DropdownButtonFormField( - decoration: InputDecoration(labelText: 'カテゴリ'), - value: controller.selectedCategory.value, - items: controller.categories.map((category) => DropdownMenuItem( - value: category, - child: Text(category.categoryName), - )).toList(), - onChanged: (value) => controller.updateCategory(value), - ), + Obx(() { + if (controller.categories.isEmpty) { + return Text('カテゴリデータを読み込めませんでした。', style: TextStyle(color: Colors.red)); + } + return DropdownButtonFormField( + decoration: InputDecoration(labelText: 'カテゴリ'), + value: controller.selectedCategory.value, + items: controller.categories.map((category) => DropdownMenuItem( + value: category, + child: Text(category.categoryName), + )).toList(), + /* + items: controller.getFilteredCategories().map((category) => DropdownMenuItem( + value: category, + child: Text(category.categoryName), + )).toList(), + */ + onChanged: (value) => controller.updateCategory(value), + ); + }), if (mode.value == 'edit') Padding( padding: EdgeInsets.symmetric(vertical: 16), @@ -160,7 +171,7 @@ class _TeamDetailPageState extends State { ? '${member.lastname} ${member.firstname}' : member.isActive ? '${member.lastname} ${member.firstname}' - : '${member.email?.split('@')[0] ?? ''}(未承認)'; + : '${member.email?.split('@')[0] ?? ''}'; //(未承認)'; return ListTile( title: Text(displayName), subtitle: isDummyEmail ? Text('Email未設定') : null, diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 0dd1711..e2873fc 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -39,7 +39,7 @@ import 'package:rogapp/pages/entry/entry_binding.dart'; import 'package:rogapp/pages/entry/event_entries_page.dart'; import 'package:rogapp/pages/entry/event_entries_binding.dart'; - +import 'package:rogapp/pages/register/user_detail_page.dart'; part 'app_routes.dart'; @@ -75,6 +75,8 @@ class AppPages { static const ENTRY_LIST = Routes.ENTRY_LIST; static const ENTRY_DETAIL = Routes.ENTRY_DETAIL; static const EVENT_ENTRY = Routes.EVENT_ENTRIES; + static const USER_DETAILS_EDIT = Routes.USER_DETAILS_EDIT; + static final routes = [ GetPage( @@ -176,7 +178,10 @@ class AppPages { page: () => EventEntriesPage(), binding: EventEntriesBinding(), ), - + GetPage( + name: Routes.USER_DETAILS_EDIT, + page: () => UserDetailsEditPage(), + ), ]; } diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index d2eece8..9c3bb03 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -36,4 +36,6 @@ abstract class Routes { static const ENTRY_DETAIL = '/entry-detail'; static const EVENT_ENTRIES = '/event-entries'; + static const USER_DETAILS_EDIT = '/user-details-edit'; + } diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index ca2ccd7..c0724dc 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -75,13 +75,13 @@ class ApiService extends GetxService{ if (response.statusCode == 200) { // UTF-8でデコード final decodedResponse = utf8.decode(response.bodyBytes); - print('User Response body: $decodedResponse'); + //print('User Response body: $decodedResponse'); List teamsJson = json.decode(decodedResponse); List teams = []; for (var teamJson in teamsJson) { - print('\nTeam Data:'); - _printDataComparison(teamJson, Team); + //print('\nTeam Data:'); + //_printDataComparison(teamJson, Team); teams.add(Team.fromJson(teamJson)); } return teams; @@ -114,8 +114,8 @@ class ApiService extends GetxService{ List categories = []; for (var categoryJson in categoriesJson) { - print('\nCategory Data:'); - _printDataComparison(categoryJson, NewCategory); + //print('\nCategory Data:'); + //_printDataComparison(categoryJson, NewCategory); categories.add(NewCategory.fromJson(categoryJson)); } @@ -143,11 +143,11 @@ class ApiService extends GetxService{ if (response.statusCode == 200) { final decodedResponse = utf8.decode(response.bodyBytes); - print('User Response body: $decodedResponse'); + //print('User Response body: $decodedResponse'); final jsonData = json.decode(decodedResponse); - print('\nUser Data Comparison:'); - _printDataComparison(jsonData, User); + //print('\nUser Data Comparison:'); + //_printDataComparison(jsonData, User); return User.fromJson(jsonData); } else { @@ -277,7 +277,11 @@ class ApiService extends GetxService{ headers: {'Authorization': 'Token $token','Content-Type': 'application/json; charset=UTF-8'}, ); - if (response.statusCode != 204) { + if( response.statusCode == 400) { + final decodedResponse = utf8.decode(response.bodyBytes); + print('User Response body: $decodedResponse'); + throw Exception('まだメンバーが残っているので、チームを削除できません。'); + }else if (response.statusCode != 204) { throw Exception('Failed to delete team'); } } @@ -302,10 +306,18 @@ class ApiService extends GetxService{ } } - Future createTeamMember(int teamId, String? email, String firstname, String lastname, DateTime? dateOfBirth,bool? female) async { + Future createTeamMember(int teamId, String? email, String? firstname, String? lastname, DateTime? dateOfBirth,bool? female) async { init(); getToken(); + // emailが値を持っている場合の処理 + if (email != null && email.isNotEmpty) { + firstname ??= "dummy"; + lastname ??= "dummy"; + dateOfBirth ??= DateTime.now(); + female ??= false; + } + String? formattedDateOfBirth; if (dateOfBirth != null) { formattedDateOfBirth = DateFormat('yyyy-MM-dd').format(dateOfBirth); @@ -379,6 +391,17 @@ class ApiService extends GetxService{ } } + Future deleteAllTeamMembers(int teamId) async { + final response = await http.delete( + Uri.parse('$baseUrl/teams/$teamId/members/destroy_all/?confirm=true'), + headers: {'Authorization': 'Token $token'}, + ); + + if (response.statusCode != 200) { + throw Exception('Failed to delete team members'); + } + } + Future resendMemberInvitation(int memberId) async { init(); getToken(); @@ -503,7 +526,7 @@ class ApiService extends GetxService{ } - Future updateEntry(int entryId, int eventId, int categoryId, DateTime date) async { + Future updateEntry(int entryId, int teamId, int eventId, int categoryId, DateTime date) async { init(); getToken(); @@ -519,6 +542,7 @@ class ApiService extends GetxService{ 'Content-Type': 'application/json; charset=UTF-8', }, body: json.encode({ + 'team': teamId, 'event': eventId, 'category': categoryId, 'date': formattedDate, @@ -547,4 +571,41 @@ class ApiService extends GetxService{ throw Exception('Failed to delete entry'); } } + + static Future updateUserDetail(User user, String token) async { + String serverUrl = ConstValues.currentServer(); + int? userid = user.id; + String url = '$serverUrl/api/userdetail/$userid/'; + + try { + String? formattedDate; + if (user.dateOfBirth != null) { + formattedDate = DateFormat('yyyy-MM-dd').format(user.dateOfBirth!); + } + final http.Response response = await http.put( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + 'Authorization': 'Token $token' + }, + body: jsonEncode({ + 'firstname': user.firstname, + 'lastname': user.lastname, + 'date_of_birth': formattedDate, + 'female': user.female, + }), + ); + + if (response.statusCode == 200) { + return true; + } else { + print('Update failed with status code: ${response.statusCode}'); + return false; + } + } catch (e) { + print('Error in updateUserDetail: $e'); + return false; + } + } + } \ No newline at end of file diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 4bb3640..1186cb5 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -5,6 +5,9 @@ import 'package:get/get.dart'; import 'package:flutter/material.dart'; import '../utils/const.dart'; +//import 'package:rogapp/services/team_service.dart'; +//import 'package:rogapp/services/member_service.dart'; + class AuthService { Future userLogin(String email, String password) async { @@ -131,6 +134,22 @@ class AuthService { // ユーザー登録 // + /* + Future registerUser(String email, String password, bool isFemale) async { + final user = await register(email, password); + if (user != null) { + final _teamController = TeamController(); + _teamController.createTeam(String teamName, int categoryId) ; + final teamService = TeamService(); + final memberService = MemberService(); + + final team = await teamService.createSoloTeam(user.id, isFemale); + await memberService.addMember(team.id, user.id); + } + } + + */ + static Future> register( String email, String password) async { Map cats = {}; @@ -168,6 +187,7 @@ class AuthService { return cats; } + static Future?> userDetails(int userid) async { List cats = []; String serverUrl = ConstValues.currentServer(); diff --git a/lib/utils/const.dart b/lib/utils/const.dart index 9df3aa6..be8d7ff 100644 --- a/lib/utils/const.dart +++ b/lib/utils/const.dart @@ -4,6 +4,8 @@ class ConstValues{ static const container_svr = "http://container.intranet.sumasen.net:8100"; static const server_uri = "https://rogaining.intranet.sumasen.net"; + //static const container_svr = "http://container.sumasen.net:8100"; + //static const server_uri = "https://rogaining.sumasen.net"; static const dev_server = "http://localhost:8100"; static const dev_ip_server = "http://192.168.8.100:8100"; static const dev_home_ip_server = "http://172.20.10.9:8100"; diff --git a/lib/utils/string_values.dart b/lib/utils/string_values.dart index 5953e65..f7ca32b 100644 --- a/lib/utils/string_values.dart +++ b/lib/utils/string_values.dart @@ -311,7 +311,7 @@ class StringValues extends Translations{ 'already_have_account': 'すでにアカウントをお持ちですか?', 'sign_up': 'サインアップ', 'create_account': 'アカウントを無料で作成します', - 'confirm_password': 'パスワードを認証する', + 'confirm_password': '確認用パスワード', 'cancel_checkin': 'チェックイン取消', 'go_here': 'ルート表示', 'cancel_route':'ルート消去', diff --git a/lib/widgets/helper_dialog.dart b/lib/widgets/helper_dialog.dart new file mode 100644 index 0000000..799308d --- /dev/null +++ b/lib/widgets/helper_dialog.dart @@ -0,0 +1,73 @@ +// lib/widgets/helper_dialog.dart +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class HelperDialog extends StatefulWidget { + final String message; + final String screenKey; + + const HelperDialog({Key? key, required this.message, required this.screenKey}) : super(key: key); + + @override + _HelperDialogState createState() => _HelperDialogState(); +} + +class _HelperDialogState extends State { + bool _doNotShowAgain = false; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Row( + children: [ + Icon(Icons.help_outline, color: Colors.blue), + SizedBox(width: 10), + Text('ヘルプ'), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.message), + SizedBox(height: 20), + Row( + children: [ + Checkbox( + value: _doNotShowAgain, + onChanged: (value) { + setState(() { + _doNotShowAgain = value!; + }); + }, + ), + Text('この画面を二度と表示しない'), + ], + ), + ], + ), + actions: [ + TextButton( + child: Text('OK'), + onPressed: () async { + if (_doNotShowAgain) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('helper_${widget.screenKey}', false); + } + Get.back(); + }, + ), + ], + ); + } +} + +// ヘルパー画面を表示する関数 +Future showHelperDialog(String message, String screenKey) async { + final prefs = await SharedPreferences.getInstance(); + final showHelper = prefs.getBool('helper_$screenKey') ?? true; + if (showHelper) { + Get.dialog(HelperDialog(message: message, screenKey: screenKey)); + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 542339b..5be1174 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 4.8.2+483 +version: 4.8.5+485 environment: sdk: ">=3.2.0 <4.0.0"