diff --git a/lib/model/entry.dart b/lib/model/entry.dart index e190bdd..8fa81a4 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -9,7 +9,7 @@ class Entry { final Team team; final Event event; final NewCategory category; - final DateTime date; + final DateTime? date; final String owner; Entry({ @@ -27,8 +27,10 @@ class Entry { team: Team.fromJson(json['team']), event: Event.fromJson(json['event']), category: NewCategory.fromJson(json['category']), - date: DateTime.parse(json['date']), - owner: json['owner'], + date: json['date'] != null + ? DateTime.tryParse(json['date']) + : null, + owner: json['owner'] is Map ? json['owner']['name'] ?? '' : json['owner'] ?? '', ); } @@ -38,7 +40,7 @@ class Entry { 'team': team.toJson(), 'event': event.toJson(), 'category': category.toJson(), - 'date': date.toIso8601String(), + 'date': date?.toIso8601String(), 'owner': owner, }; } diff --git a/lib/pages/entry/entry_controller.dart b/lib/pages/entry/entry_controller.dart index be856ad..6d9631d 100644 --- a/lib/pages/entry/entry_controller.dart +++ b/lib/pages/entry/entry_controller.dart @@ -1,6 +1,7 @@ // 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'; @@ -8,7 +9,7 @@ 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; @@ -21,14 +22,27 @@ class EntryController extends GetxController { 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(), @@ -37,14 +51,53 @@ 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) { + 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; @@ -60,6 +113,7 @@ class EntryController extends GetxController { entries.assignAll(fetchedEntries); } catch (e) { print('Error fetching entries: $e'); + Get.snackbar('Error', 'Failed to fetch entries'); } } @@ -69,6 +123,7 @@ class EntryController extends GetxController { events.assignAll(fetchedEvents); } catch (e) { print('Error fetching events: $e'); + Get.snackbar('Error', 'Failed to fetch events'); } } @@ -78,6 +133,7 @@ class EntryController extends GetxController { teams.assignAll(fetchedTeams); } catch (e) { print('Error fetching teams: $e'); + Get.snackbar('Error', 'Failed to fetch team'); } } @@ -87,6 +143,7 @@ class EntryController extends GetxController { categories.assignAll(fetchedCategories); } catch (e) { print('Error fetching categories: $e'); + Get.snackbar('Error', 'Failed to fetch categories'); } } @@ -143,10 +200,6 @@ class EntryController extends GetxController { } } - void updateEvent(Event? value) => selectedEvent.value = value; - void updateTeam(Team? value) => selectedTeam.value = value; - void updateCategory(NewCategory? 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 diff --git a/lib/pages/entry/entry_detail_page.dart b/lib/pages/entry/entry_detail_page.dart index 85230de..84a9900 100644 --- a/lib/pages/entry/entry_detail_page.dart +++ b/lib/pages/entry/entry_detail_page.dart @@ -3,6 +3,10 @@ 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 @@ -11,52 +15,121 @@ 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), + ElevatedButton( + child: Text(mode == 'new' ? 'エントリーを作成' : 'エントリーを更新'), + onPressed: () { + if (mode == 'new') { + controller.createEntry(); + } else { + controller.updateEntry(); + } + }, + ), + if (mode == 'edit' && controller.isOwner()) + ElevatedButton( + child: Text('エントリーを削除'), + onPressed: () => controller.deleteEntry(), + style: ElevatedButton.styleFrom(backgroundColor: Colors.red), + ), + ], ), - 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, ); } } \ No newline at end of file diff --git a/lib/pages/team/member_controller.dart b/lib/pages/team/member_controller.dart index ef252c5..a0aa29d 100644 --- a/lib/pages/team/member_controller.dart +++ b/lib/pages/team/member_controller.dart @@ -2,31 +2,52 @@ 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 selectedMember = Rx(null); - final int teamId = 0; + 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 = true.obs; // isLoadingプロパティを追加 + 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(); - await loadInitialData(); + ever(member, (_) => _initializeMemberData()); + loadInitialData(); + } - if (Get.arguments != null && Get.arguments['member'] != null) { - member.value = Get.arguments['member']; - _initializeMemberData(); + 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; } } @@ -40,23 +61,26 @@ class MemberController extends GetxController { } } - Future loadInitialData() async { - try { - isLoading.value = true; - // 必要な初期データの取得をここで行う - // 例: await fetchTeamMembers(); - } catch (e) { - print('Error loading initial data: $e'); - } finally { - isLoading.value = false; - } - } - void setSelectedMember(User member) { - selectedMember.value = member; - firstname.value = member.firstname; - lastname.value = member.lastname; + 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) { @@ -67,32 +91,52 @@ class MemberController extends GetxController { lastname.value = value; } - Future saveMember() async { + Future saveMember() async { + if (!_validateInputs()) return false; + try { isLoading.value = true; - // メンバー保存のロジックをここに実装 - // 例: await _apiService.updateMember(selectedMember.value!.id, firstName.value, lastName.value); + 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; } } - 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'); + + String getDisplayName() { + if (!isActive.value && !isDummyEmail) { + final displayName = email.value.split('@')[0]; + return '$displayName(未承認)'; } + return '${lastname.value} ${firstname.value}'.trim(); } Future updateMember() async { @@ -105,6 +149,7 @@ class MemberController extends GetxController { firstname.value, lastname.value, dateOfBirth.value, + female.value, ); member.value = updatedMember; } catch (e) { @@ -112,6 +157,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; @@ -124,7 +189,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 { @@ -147,4 +219,52 @@ 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; + } } \ No newline at end of file diff --git a/lib/pages/team/member_detail_page.dart b/lib/pages/team/member_detail_page.dart index df303b7..722b7f9 100644 --- a/lib/pages/team/member_detail_page.dart +++ b/lib/pages/team/member_detail_page.dart @@ -3,92 +3,210 @@ 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; -class MemberDetailPage extends GetView { - final TextEditingController _firstNameController = TextEditingController(); - final TextEditingController _lastNameController = TextEditingController(); @override - Widget build(BuildContext context) { - final mode = Get.arguments['mode']; - final member = Get.arguments['member']; + 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); } }); + } - return Scaffold( - appBar: AppBar( - title: Text(mode == 'new' ? 'メンバー追加' : 'メンバー詳細'), - actions: [ - IconButton( - icon: Icon(Icons.save), - onPressed: () async { - await controller.saveMember(); - Get.back(); - }, - ), - ], - ), - body: Obx(() - { - if (controller.isLoading.value) { - return Center(child: CircularProgressIndicator()); + 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)), + ); } + }); - _firstNameController.value = _firstNameController.value.copyWith( - text: controller.firstname.value, - selection: TextSelection.collapsed( - offset: controller.firstname.value.length), - ); - _lastNameController.value = _lastNameController.value.copyWith( - text: controller.lastname.value, - selection: TextSelection.collapsed( - offset: controller.lastname.value.length), - ); + controller.lastname.listen((value) { + if (_lastNameController.text != value) { + _lastNameController.value = TextEditingValue( + text: value, + selection: TextSelection.fromPosition(TextPosition(offset: value.length)), + ); + } + }); - return 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), - ), - TextField( - decoration: InputDecoration(labelText: '姓'), - controller: _lastNameController, - onChanged: (value) => controller.updateLastName(value), - ), - TextField( - decoration: InputDecoration(labelText: '名'), - controller: _firstNameController, - onChanged: (value) => controller.updateFirstName(value), - ), + controller.email.listen((value) { + if (_emailController.text != value) { + _emailController.value = TextEditingValue( + text: value, + selection: TextSelection.fromPosition(TextPosition(offset: value.length)), + ); + } + }); + } - // 誕生日選択ウィジェットを追加 - if (mode == 'edit') - Text('ステータス: ${controller.getMemberStatus()}'), - if (mode == 'edit' && controller.getMemberStatus() == '招待中') - ElevatedButton( - child: Text('招待メールを再送信'), - onPressed: () => controller.resendInvitation(), + @override + Widget build(BuildContext context) { + 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' ? 'メンバー追加' : 'メンバー詳細'), + actions: [ + IconButton( + icon: Icon(Icons.save), + onPressed: () async { + await controller.saveMember(); + Get.back(result: true); + }, ), - if (mode == 'edit') - ElevatedButton( - child: Text('メンバーから除外'), - onPressed: () => controller.deleteMember(), + ], + ), + 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), + ), + ], ), - ], - ), - ); - }) - ); + ); + + + + }), + ); + + } + + @override + void dispose() { + _firstNameController.dispose(); + _lastNameController.dispose(); + _emailController.dispose(); + super.dispose(); } } \ No newline at end of file diff --git a/lib/pages/team/team_controller.dart b/lib/pages/team/team_controller.dart index 528b9a5..91ef000 100644 --- a/lib/pages/team/team_controller.dart +++ b/lib/pages/team/team_controller.dart @@ -71,6 +71,15 @@ class TeamController extends GetxController { 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; @@ -126,25 +135,75 @@ class TeamController extends GetxController { 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; } } + 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; + } + } + + 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 = categories.firstWhere( - (category) => category.id == value.id, - orElse: () => value, - ); + 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 { @@ -161,18 +220,10 @@ class TeamController extends GetxController { // サーバーから最新のデータを再取得 await fetchTeams(); - - // 選択中のチームを更新 - if (selectedTeam.value != null) { - selectedTeam.value = teams.firstWhere((t) => t.id == selectedTeam.value!.id); - teamName.value = selectedTeam.value!.teamName; - selectedCategory.value = selectedTeam.value!.category; - } - update(); // UIを強制的に更新 } catch (e) { error.value = 'チームの保存に失敗しました: $e'; - print("Team save error: $e"); + } finally { isLoading.value = false; } diff --git a/lib/pages/team/team_detail_page.dart b/lib/pages/team/team_detail_page.dart index f63de66..efbc320 100644 --- a/lib/pages/team/team_detail_page.dart +++ b/lib/pages/team/team_detail_page.dart @@ -7,21 +7,76 @@ import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/model/team.dart'; import 'package:rogapp/model/category.dart'; -class TeamDetailPage extends GetView { +class TeamDetailPage extends StatefulWidget { @override - Widget build(BuildContext context) { - final mode = Get.arguments['mode'] as String; - final team = Get.arguments['team'] as Team?; + _TeamDetailPageState createState() => _TeamDetailPageState(); +} - if (mode == 'edit' && team != null) { - controller.setSelectedTeam(team); +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(); + } + + @override + Widget build(BuildContext context) { 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: [ IconButton( icon: Icon(Icons.save), @@ -34,56 +89,63 @@ class TeamDetailPage extends GetView { } }, ), - if (mode == 'edit') - IconButton( - icon: Icon(Icons.delete), - onPressed: () async { - try { - await controller.deleteSelectedTeam(); - 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: GetBuilder( - builder: (controller) { - if (controller.isLoading.value) { - return Center(child: CircularProgressIndicator()); - } + body: Obx(() { + if (controller.isLoading.value) { + return Center(child: CircularProgressIndicator()); + } - return SingleChildScrollView( + return SingleChildScrollView( child: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextFormField( - initialValue: controller.teamName.value, - decoration: InputDecoration(labelText: 'チーム名'), - onChanged: (value) => controller.updateTeamName(value), - ), - 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), - ), - if (mode == 'edit') - Padding( - padding: EdgeInsets.symmetric(vertical: 16), - child: Text('ゼッケン番号: ${controller.selectedTeam.value?.zekkenNumber ?? ""}'), - ), + 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), + 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.value == 'edit') Padding( padding: EdgeInsets.symmetric(vertical: 16), - child: Text('所有者: ${controller.currentUser.value?.email ?? "未設定"}'), + 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), @@ -93,34 +155,55 @@ class TeamDetailPage extends GetView { 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('${member.lastname}, ${member.firstname}'), - onTap: () => Get.toNamed( - AppPages.MEMBER_DETAIL, - arguments: {'mode': 'edit', 'member': member, 'teamId': controller.selectedTeam.value?.id}, - ), + 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: () => Get.toNamed( - AppPages.MEMBER_DETAIL, - arguments: {'mode': 'new', 'teamId': controller.selectedTeam.value?.id}, - ), + 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); + } + }, ), ], - ), + ], ), - ); - }, - ), + ) + ); + }), ); + } + + } + diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index f86bc24..f1bd943 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -11,6 +11,8 @@ 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{ @@ -19,16 +21,18 @@ class ApiService extends GetxService{ String baseUrl = ''; 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; } } @@ -232,7 +236,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'); } @@ -255,7 +260,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'); } @@ -295,10 +302,15 @@ class ApiService extends GetxService{ } } - 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(); + String? formattedDateOfBirth; + if (dateOfBirth != null) { + formattedDateOfBirth = DateFormat('yyyy-MM-dd').format(dateOfBirth); + } + final response = await http.post( Uri.parse('$baseUrl/teams/$teamId/members/'), headers: { @@ -309,21 +321,28 @@ class ApiService extends GetxService{ '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: { @@ -333,12 +352,14 @@ class ApiService extends GetxService{ 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'); } @@ -378,13 +399,14 @@ class ApiService extends GetxService{ getToken(); final response = await http.get( - Uri.parse('$baseUrl/entries/'), + 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'); @@ -416,6 +438,11 @@ 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: { @@ -426,12 +453,14 @@ class ApiService extends GetxService{ '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'); } @@ -441,6 +470,11 @@ class ApiService extends GetxService{ 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: { @@ -450,12 +484,14 @@ class ApiService extends GetxService{ body: json.encode({ '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'); } diff --git a/pubspec.lock b/pubspec.lock index d25644a..82bdaaa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -374,6 +374,11 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_map: dependency: "direct main" description: @@ -660,10 +665,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" isar: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c1a9057..542339b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -81,7 +83,7 @@ dependencies: circular_menu: ^2.0.1 camera: ^0.10.0+3 camera_camera: ^3.0.0 - intl: ^0.18.1 + intl: ^0.19.0 #^0.18.1 modal_bottom_sheet: ^3.0.0-pre connectivity_plus: ^5.0.2 flutter_map_tile_caching: ^9.0.0-dev.5