diff --git a/lib/model/category.dart b/lib/model/category.dart index 1cf623d..ad58c63 100644 --- a/lib/model/category.dart +++ b/lib/model/category.dart @@ -19,6 +19,16 @@ class NewCategory { required this.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, diff --git a/lib/model/team.dart b/lib/model/team.dart index 2785f2e..53024a4 100644 --- a/lib/model/team.dart +++ b/lib/model/team.dart @@ -1,5 +1,6 @@ // lib/models/team.dart +import 'dart:convert'; import 'category.dart'; import 'user.dart'; @@ -10,6 +11,7 @@ class Team { final NewCategory category; final User owner; + Team({ required this.id, required this.zekkenNumber, diff --git a/lib/pages/team/member_controller.dart b/lib/pages/team/member_controller.dart index fea2b2d..ef252c5 100644 --- a/lib/pages/team/member_controller.dart +++ b/lib/pages/team/member_controller.dart @@ -7,18 +7,23 @@ import 'package:rogapp/services/api_service.dart'; class MemberController extends GetxController { late final ApiService _apiService; + final selectedMember = Rx(null); final int teamId = 0; final member = Rx(null); final email = ''.obs; final firstname = ''.obs; final lastname = ''.obs; final dateOfBirth = Rx(null); + final isLoading = true.obs; // isLoadingプロパティを追加 + @override void onInit() async{ super.onInit(); await Get.putAsync(() => ApiService().init()); _apiService = Get.find(); + await loadInitialData(); + if (Get.arguments != null && Get.arguments['member'] != null) { member.value = Get.arguments['member']; _initializeMemberData(); @@ -35,6 +40,46 @@ 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; + } + + void updateFirstName(String value) { + firstname.value = value; + } + + void updateLastName(String value) { + lastname.value = value; + } + + Future saveMember() async { + try { + isLoading.value = true; + // メンバー保存のロジックをここに実装 + // 例: await _apiService.updateMember(selectedMember.value!.id, firstName.value, lastName.value); + } catch (e) { + print('Error saving member: $e'); + // エラーハンドリング(例:ユーザーへの通知) + } finally { + isLoading.value = false; + } + } + Future createMember(int teamId) async { try { final newMember = await _apiService.createTeamMember( diff --git a/lib/pages/team/member_detail_page.dart b/lib/pages/team/member_detail_page.dart index 0d0b9e9..df303b7 100644 --- a/lib/pages/team/member_detail_page.dart +++ b/lib/pages/team/member_detail_page.dart @@ -5,16 +5,51 @@ import 'package:get/get.dart'; import 'package:rogapp/pages/team/member_controller.dart'; 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']; + WidgetsBinding.instance.addPostFrameCallback((_) { + 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: Padding( + body: Obx(() + { + if (controller.isLoading.value) { + return Center(child: CircularProgressIndicator()); + } + + _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), + ); + + return Padding( padding: EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -28,12 +63,15 @@ class MemberDetailPage extends GetView { ), TextField( decoration: InputDecoration(labelText: '姓'), - onChanged: (value) => controller.updateLastname(value), + controller: _lastNameController, + onChanged: (value) => controller.updateLastName(value), ), TextField( decoration: InputDecoration(labelText: '名'), - onChanged: (value) => controller.updateFirstname(value), + controller: _firstNameController, + onChanged: (value) => controller.updateFirstName(value), ), + // 誕生日選択ウィジェットを追加 if (mode == 'edit') Text('ステータス: ${controller.getMemberStatus()}'), @@ -49,7 +87,8 @@ class MemberDetailPage extends GetView { ), ], ), - ), + ); + }) ); } } \ No newline at end of file diff --git a/lib/pages/team/team_controller.dart b/lib/pages/team/team_controller.dart index 094f48e..528b9a5 100644 --- a/lib/pages/team/team_controller.dart +++ b/lib/pages/team/team_controller.dart @@ -1,5 +1,5 @@ // lib/controllers/team_controller.dart - +import 'dart:convert'; import 'package:get/get.dart'; import 'package:rogapp/model/team.dart'; import 'package:rogapp/model/category.dart'; @@ -9,44 +9,78 @@ import 'package:rogapp/services/api_service.dart'; class TeamController extends GetxController { late final ApiService _apiService; - final teams = [].obs; final categories = [].obs; final teamMembers = [].obs; + 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(); - 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 = ''; + selectedCategory.value = categories.isNotEmpty ? categories.first : null; + teamMembers.clear(); + } + 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; } } @@ -54,6 +88,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'); } @@ -68,25 +104,15 @@ 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); + 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 { @@ -108,10 +134,56 @@ class TeamController extends GetxController { } void updateTeamName(String value) { - // Update local state + teamName.value = value; } void updateCategory(NewCategory? value) { - selectedCategory.value = 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(); + + // 選択中のチームを更新 + 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; + } + } + + + Future deleteSelectedTeam() async { + if (selectedTeam.value != null) { + await deleteTeam(selectedTeam.value!.id); + selectedTeam.value = null; + } + } + } \ 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 e9d1af7..f63de66 100644 --- a/lib/pages/team/team_detail_page.dart +++ b/lib/pages/team/team_detail_page.dart @@ -4,66 +4,123 @@ 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 GetView { @override Widget build(BuildContext context) { - final mode = Get.arguments['mode']; - final team = Get.arguments['team']; + final mode = Get.arguments['mode'] as String; + final team = Get.arguments['team'] as Team?; + + if (mode == 'edit' && team != null) { + controller.setSelectedTeam(team); + } else { + controller.resetForm(); + } return Scaffold( appBar: AppBar( title: Text(mode == 'new' ? '新規チーム作成' : 'チーム詳細'), actions: [ + IconButton( + icon: Icon(Icons.save), + onPressed: () async { + try { + await controller.saveTeam(); + Get.back(); + } catch (e) { + Get.snackbar('エラー', e.toString(), snackPosition: SnackPosition.BOTTOM); + } + }, + ), if (mode == 'edit') IconButton( - icon: Icon(Icons.add), - onPressed: () => Get.toNamed(AppPages.MEMBER_DETAIL, arguments: {'mode': 'new', 'teamId': team.id}), + icon: Icon(Icons.delete), + onPressed: () async { + try { + await controller.deleteSelectedTeam(); + Get.back(); + } catch (e) { + Get.snackbar('エラー', e.toString(), snackPosition: SnackPosition.BOTTOM); + } + }, ), ], ), - body: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextField( - decoration: InputDecoration(labelText: 'チーム名'), - onChanged: (value) => controller.updateTeamName(value), + body: GetBuilder( + builder: (controller) { + if (controller.isLoading.value) { + return Center(child: CircularProgressIndicator()); + } + + 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( + padding: EdgeInsets.symmetric(vertical: 16), + child: Text('所有者: ${controller.currentUser.value?.email ?? "未設定"}'), + ), + 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]; + return ListTile( + title: Text('${member.lastname}, ${member.firstname}'), + onTap: () => Get.toNamed( + AppPages.MEMBER_DETAIL, + arguments: {'mode': 'edit', 'member': member, 'teamId': controller.selectedTeam.value?.id}, + ), + ); + }, + ), + SizedBox(height: 16), + ElevatedButton( + child: Text('新しいメンバーを追加'), + onPressed: () => Get.toNamed( + AppPages.MEMBER_DETAIL, + arguments: {'mode': 'new', 'teamId': controller.selectedTeam.value?.id}, + ), + ), + ], + ), ), - 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}), - ); - }, - )), - ), - ], - ), + ); + }, ), ); } -} \ No newline at end of file +} + + + + + diff --git a/lib/pages/team/team_list_page.dart b/lib/pages/team/team_list_page.dart index 217e885..2f240f7 100644 --- a/lib/pages/team/team_list_page.dart +++ b/lib/pages/team/team_list_page.dart @@ -6,7 +6,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( @@ -19,36 +19,38 @@ class TeamListPage extends GetView { ), ], ), - body: FutureBuilder( - future: Get.putAsync(() => ApiService().init()), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return Obx(() { - if (controller.isLoading.value) { - return Center(child: CircularProgressIndicator()); - } else if (controller.error.value.isNotEmpty) { - return Center(child: Text('エラー: ${controller.error.value}')); - } else { - return 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('メンバー: (メンバー数表示予定)'), - onTap: () => Get.toNamed(AppPages.TEAM_DETAIL, arguments: {'mode': 'edit', 'team': team}), + 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}'), + onTap: () async { + await Get.toNamed( + AppPages.TEAM_DETAIL, + arguments: {'mode': 'edit', 'team': team}, ); + controller.fetchTeams(); }, ); - } - }); - - } else { - return Center(child: CircularProgressIndicator()); - } - }, - ), + }, + ), + ); + } + }), ); } -} \ No newline at end of file +} + + diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index 199c0ca..f86bc24 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -17,7 +17,7 @@ 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 { try { @@ -65,7 +65,7 @@ class ApiService extends GetxService{ try { final response = await http.get( Uri.parse('$baseUrl/teams/'), - headers: {'Authorization': 'Token $token'}, + headers: {'Authorization': 'Token $token',"Content-Type": "application/json; charset=UTF-8"}, ); if (response.statusCode == 200) { @@ -100,7 +100,7 @@ class ApiService extends GetxService{ try { final response = await http.get( Uri.parse('$baseUrl/categories/'), - headers: {'Authorization': 'Token $token'}, + headers: {'Authorization': 'Token $token',"Content-Type": "application/json; charset=UTF-8"}, ); if (response.statusCode == 200) { @@ -134,7 +134,7 @@ class ApiService extends GetxService{ try { final response = await http.get( Uri.parse('$baseUrl/user/'), - headers: {'Authorization': 'Token $token'}, + headers: {'Authorization': 'Token $token',"Content-Type": "application/json; charset=UTF-8"}, ); if (response.statusCode == 200) { @@ -223,7 +223,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, @@ -246,7 +246,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, @@ -267,7 +267,7 @@ 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) { @@ -281,7 +281,7 @@ 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) { @@ -303,7 +303,7 @@ class ApiService extends GetxService{ 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, @@ -328,7 +328,7 @@ class ApiService extends GetxService{ 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, @@ -364,7 +364,8 @@ class ApiService extends GetxService{ 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) { @@ -378,7 +379,8 @@ class ApiService extends GetxService{ final response = await http.get( Uri.parse('$baseUrl/entries/'), - headers: {'Authorization': 'Token $token'}, + headers: {'Authorization': 'Token $token', 'Content-Type': 'application/json; charset=UTF-8', + }, ); if (response.statusCode == 200) { @@ -394,8 +396,9 @@ 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) { @@ -417,7 +420,7 @@ class ApiService extends GetxService{ Uri.parse('$baseUrl/entry/'), headers: { 'Authorization': 'Token $token', - 'Content-Type': 'application/json', + 'Content-Type': 'application/json; charset=UTF-8', }, body: json.encode({ 'team': teamId, @@ -442,7 +445,7 @@ class ApiService extends GetxService{ Uri.parse('$baseUrl/entry/$entryId/'), headers: { 'Authorization': 'Token $token', - 'Content-Type': 'application/json', + 'Content-Type': 'application/json; charset=UTF-8', }, body: json.encode({ 'event': eventId,