大幅変更&環境バージョンアップ
This commit is contained in:
11
lib/pages/team/member_binding.dart
Normal file
11
lib/pages/team/member_binding.dart
Normal file
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gifunavi/pages/team/member_controller.dart';
|
||||
import 'package:gifunavi/services/api_service.dart';
|
||||
|
||||
class MemberBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<ApiService>(() => ApiService());
|
||||
Get.lazyPut<MemberController>(() => MemberController());
|
||||
}
|
||||
}
|
||||
270
lib/pages/team/member_controller.dart
Normal file
270
lib/pages/team/member_controller.dart
Normal file
@ -0,0 +1,270 @@
|
||||
// lib/controllers/member_controller.dart
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gifunavi/model/user.dart';
|
||||
import 'package:gifunavi/services/api_service.dart';
|
||||
import 'package:gifunavi/pages/team/team_controller.dart';
|
||||
|
||||
class MemberController extends GetxController {
|
||||
late final ApiService _apiService;
|
||||
late final TeamController _teamController;
|
||||
|
||||
final selectedMember = Rx<User?>(null);
|
||||
int teamId = 0;
|
||||
final member = Rx<User?>(null);
|
||||
final email = ''.obs;
|
||||
final firstname = ''.obs;
|
||||
final lastname = ''.obs;
|
||||
final female = false.obs;
|
||||
final dateOfBirth = Rx<DateTime?>(null);
|
||||
final isLoading = false.obs; // isLoadingプロパティを追加
|
||||
final isActive = false.obs;
|
||||
|
||||
//MemberController(this._apiService);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_apiService = Get.find<ApiService>();
|
||||
_teamController = Get.find<TeamController>();
|
||||
ever(member, (_) => _initializeMemberData());
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
bool get isDummyEmail => email.value.startsWith('dummy_');
|
||||
bool get isApproved => !email.value.startsWith('dummy_') && member.value?.isActive == true;
|
||||
|
||||
Future<void> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void _initializeMemberData() {
|
||||
if (member.value != null) {
|
||||
email.value = member.value!.email ?? '';
|
||||
firstname.value = member.value!.firstname ?? '';
|
||||
lastname.value = member.value!.lastname ?? '';
|
||||
dateOfBirth.value = member.value!.dateOfBirth;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
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<bool> 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;
|
||||
await _teamController.updateTeamComposition();
|
||||
|
||||
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<bool> updateMember() async {
|
||||
if (member.value == null) return false;
|
||||
int? memberId = member.value?.id;
|
||||
try {
|
||||
final updatedMember = await _apiService.updateTeamMember(
|
||||
teamId,
|
||||
memberId,
|
||||
firstname.value,
|
||||
lastname.value,
|
||||
dateOfBirth.value,
|
||||
female.value,
|
||||
);
|
||||
member.value = updatedMember;
|
||||
return true;
|
||||
} catch (e) {
|
||||
print('Error updating member: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> deleteMember() async {
|
||||
if (member.value == null || member.value!.id == null) {
|
||||
Get.snackbar('エラー', 'メンバー情報が不正です', snackPosition: SnackPosition.BOTTOM);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
await _apiService.deleteTeamMember(teamId, member.value!.id!);
|
||||
|
||||
await _teamController.updateTeamComposition();
|
||||
|
||||
Get.snackbar('成功', 'メンバーが削除されました', snackPosition: SnackPosition.BOTTOM);
|
||||
member.value = null;
|
||||
|
||||
return true;
|
||||
|
||||
} catch (e) {
|
||||
print('Error deleting member: $e');
|
||||
Get.snackbar('エラー', 'メンバーの削除に失敗しました: ${e.toString()}', snackPosition: SnackPosition.BOTTOM);
|
||||
isLoading.value = false;
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> 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 {
|
||||
await _apiService.resendMemberInvitation(memberId!);
|
||||
Get.snackbar('Success', 'Invitation resent successfully');
|
||||
} catch (e) {
|
||||
print('Error resending invitation: $e');
|
||||
Get.snackbar('Error', 'Failed to resend invitation');
|
||||
}
|
||||
}
|
||||
|
||||
void updateEmail(String value) => email.value = value;
|
||||
void updateFirstname(String value) => firstname.value = value;
|
||||
void updateLastname(String value) => lastname.value = value;
|
||||
void updateDateOfBirth(DateTime value) => dateOfBirth.value = value;
|
||||
|
||||
String getMemberStatus() {
|
||||
if (member.value == null) return '';
|
||||
if (member.value!.email == null) return '未登録';
|
||||
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 < 7) return '未就学';
|
||||
if (yearsFromSchoolStart < 13) return '小${yearsFromSchoolStart - 6}';
|
||||
if (yearsFromSchoolStart < 16) return '中${yearsFromSchoolStart - 12}';
|
||||
if (yearsFromSchoolStart < 19) return '高${yearsFromSchoolStart - 15}';
|
||||
return '成人';
|
||||
}
|
||||
|
||||
String getAgeAndGrade() {
|
||||
final age = calculateAge();
|
||||
final grade = calculateGrade();
|
||||
return '$age歳/$grade';
|
||||
}
|
||||
|
||||
bool isOver18() {
|
||||
return calculateAge() >= 18;
|
||||
}
|
||||
}
|
||||
325
lib/pages/team/member_detail_page.dart
Normal file
325
lib/pages/team/member_detail_page.dart
Normal file
@ -0,0 +1,325 @@
|
||||
// lib/pages/team/member_detail_page.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gifunavi/pages/team/member_controller.dart';
|
||||
import 'package:gifunavi/pages/team/team_controller.dart';
|
||||
import 'package:intl/intl.dart'; // この行を追加
|
||||
import 'package:gifunavi/widgets/custom_date_picker.dart';
|
||||
// 追加
|
||||
|
||||
import 'package:gifunavi/routes/app_pages.dart';
|
||||
import 'package:gifunavi/model/user.dart';
|
||||
|
||||
class MemberDetailPage extends StatefulWidget {
|
||||
const MemberDetailPage({super.key});
|
||||
|
||||
@override
|
||||
_MemberDetailPageState createState() => _MemberDetailPageState();
|
||||
}
|
||||
|
||||
class _MemberDetailPageState extends State<MemberDetailPage> {
|
||||
final MemberController controller = Get.find<MemberController>();
|
||||
final TeamController teamController = Get.find<TeamController>();
|
||||
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)),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _handleSaveAndNavigateBack_old() async {
|
||||
bool success = await controller.saveMember();
|
||||
if (success) {
|
||||
Get.until((route) => Get.currentRoute == AppPages.TEAM_DETAIL);
|
||||
// スナックバーが表示されるのを待つ
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
// 現在のスナックバーを安全に閉じる
|
||||
if (Get.isSnackbarOpen) {
|
||||
await Get.closeCurrentSnackbar();
|
||||
}
|
||||
|
||||
// リストページに戻る
|
||||
//Get.until((route) => Get.currentRoute == '/team');
|
||||
// または、リストページの具体的なルート名がある場合は以下を使用
|
||||
// Get.offNamed('/team');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleSaveAndNavigateBack() async {
|
||||
|
||||
if (!controller.validateInputs()) return;
|
||||
|
||||
try {
|
||||
await controller.saveMember();
|
||||
await teamController.updateTeamComposition();
|
||||
|
||||
Get.until((route) => Get.currentRoute == AppPages.TEAM_DETAIL);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (Get.isSnackbarOpen) {
|
||||
await Get.closeCurrentSnackbar();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error saving member: $e');
|
||||
Get.snackbar('エラー', 'メンバーの保存に失敗しました: ${e.toString()}', snackPosition: SnackPosition.BOTTOM);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> _handleDeleteAndNavigateBack() async {
|
||||
final confirmed = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: const Text('確認'),
|
||||
content: const Text('このメンバーを削除してもよろしいですか?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('キャンセル'),
|
||||
onPressed: () => Get.back(result: false),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('削除'),
|
||||
onPressed: () => Get.back(result: true),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) {
|
||||
try {
|
||||
if (controller.member.value != null && controller.member.value!.id != null) {
|
||||
await teamController.removeMember(controller.member.value!.id!);
|
||||
|
||||
// スナックバーの処理を安全に行う
|
||||
await _safelyCloseSnackbar();
|
||||
|
||||
// 画面遷移
|
||||
Get.until((route) => Get.currentRoute == AppPages.TEAM_DETAIL);
|
||||
|
||||
} else {
|
||||
Get.snackbar('エラー', 'メンバー情報が不正です', snackPosition: SnackPosition.BOTTOM);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error deleting member: $e');
|
||||
Get.snackbar('エラー', 'メンバーの削除に失敗しました: ${e.toString()}', snackPosition: SnackPosition.BOTTOM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _safelyCloseSnackbar() async {
|
||||
if (Get.isSnackbarOpen) {
|
||||
try {
|
||||
await Get.closeCurrentSnackbar();
|
||||
} catch (e) {
|
||||
print('Error closing snackbar: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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' ? 'メンバー追加' : 'メンバー詳細'),
|
||||
),
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const 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: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (mode == 'new')
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
onChanged: (value) => controller.updateEmail(value),
|
||||
decoration: const InputDecoration(labelText: 'メールアドレス'),
|
||||
keyboardType: TextInputType.emailAddress, // メールアドレス用のキーボードを表示
|
||||
autocorrect: false, // 自動修正を無効化
|
||||
enableSuggestions: false,
|
||||
)
|
||||
else if (controller.isDummyEmail)
|
||||
const Text('メールアドレス: (メアド無し)')
|
||||
else
|
||||
Text('メールアドレス: ${controller.email.value}'),
|
||||
|
||||
if (controller.email.value.isEmpty || controller.isDummyEmail || mode == 'edit') ...[
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: '姓'),
|
||||
controller: _lastNameController,
|
||||
onChanged: (value) => controller.updateLastName(value),
|
||||
|
||||
//controller: TextEditingController(text: controller.lastname.value),
|
||||
),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: '名'),
|
||||
controller: _firstNameController,
|
||||
//onChanged: (value) => controller.firstname.value = value,
|
||||
onChanged: (value) => controller.updateFirstName(value),
|
||||
//controller: TextEditingController(text: controller.firstname.value),
|
||||
),
|
||||
// 生年月日
|
||||
if (controller.isDummyEmail || !controller.isOver18())
|
||||
ListTile(
|
||||
title: const Text('生年月日'),
|
||||
subtitle: Text(controller.dateOfBirth.value != null
|
||||
? '${DateFormat('yyyy年MM月dd日').format(controller.dateOfBirth.value!)} (${controller.getAgeAndGrade()})'
|
||||
: '未設定'),
|
||||
onTap: () async {
|
||||
final date = await showDialog<DateTime>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CustomDatePicker(
|
||||
initialDate: controller.dateOfBirth.value ?? DateTime.now(),
|
||||
firstDate: DateTime(1920),
|
||||
lastDate: DateTime.now(),
|
||||
currentDateText: controller.dateOfBirth.value != null
|
||||
? DateFormat('yyyy/MM/dd').format(controller.dateOfBirth.value!)
|
||||
: '',
|
||||
);
|
||||
},
|
||||
);
|
||||
if (date != null) {
|
||||
controller.dateOfBirth.value = date;
|
||||
}
|
||||
},
|
||||
)
|
||||
else
|
||||
const Text('18歳以上'),
|
||||
|
||||
SwitchListTile(
|
||||
title: const Text('性別'),
|
||||
subtitle: Text(controller.female.value ? '女性' : '男性'),
|
||||
value: controller.female.value,
|
||||
onChanged: (value) => controller.female.value = value,
|
||||
),
|
||||
],
|
||||
]),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: _handleDeleteAndNavigateBack,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('削除'),
|
||||
),
|
||||
if (!controller.isDummyEmail && !controller.isApproved && mode == 'edit')
|
||||
ElevatedButton(
|
||||
onPressed: () => controller.resendInvitation(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
),
|
||||
child: const Text('招待再送信'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _handleSaveAndNavigateBack,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: Text(controller.isDummyEmail ? '保存' :
|
||||
(mode == 'new' && !controller.isDummyEmail) ? '保存・招待' :
|
||||
'保存'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_firstNameController.dispose();
|
||||
_lastNameController.dispose();
|
||||
_emailController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
11
lib/pages/team/team_binding.dart
Normal file
11
lib/pages/team/team_binding.dart
Normal file
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gifunavi/pages/team/team_controller.dart';
|
||||
import 'package:gifunavi/services/api_service.dart';
|
||||
|
||||
class TeamBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<ApiService>(() => ApiService());
|
||||
Get.lazyPut<TeamController>(() => TeamController());
|
||||
}
|
||||
}
|
||||
692
lib/pages/team/team_controller.dart
Normal file
692
lib/pages/team/team_controller.dart
Normal file
@ -0,0 +1,692 @@
|
||||
// lib/controllers/team_controller.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gifunavi/model/team.dart';
|
||||
import 'package:gifunavi/model/category.dart';
|
||||
import 'package:gifunavi/model/user.dart';
|
||||
import 'package:gifunavi/model/entry.dart';
|
||||
|
||||
import 'package:gifunavi/services/api_service.dart';
|
||||
import 'package:gifunavi/widgets/category_change_dialog.dart';
|
||||
import 'package:gifunavi/routes/app_pages.dart';
|
||||
import 'package:gifunavi/pages/entry/entry_controller.dart';
|
||||
|
||||
import 'package:gifunavi/model/event.dart';
|
||||
|
||||
|
||||
class TeamController extends GetxController {
|
||||
late final ApiService _apiService;
|
||||
late final EntryController _entryController;
|
||||
|
||||
final teams = <Team>[].obs;
|
||||
final categories = <NewCategory>[].obs;
|
||||
final RxList<User> teamMembers = <User>[].obs;
|
||||
final teamEntries = <Entry>[].obs;
|
||||
|
||||
final selectedCategory = Rx<NewCategory?>(null);
|
||||
final selectedTeam = Rx<Team?>(null);
|
||||
final currentUser = Rx<User?>(null);
|
||||
|
||||
final teamName = ''.obs;
|
||||
final isLoading = false.obs;
|
||||
final error = RxString('');
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
try {
|
||||
_apiService = Get.find<ApiService>();
|
||||
|
||||
if (!Get.isRegistered<EntryController>()) {
|
||||
Get.put(EntryController());
|
||||
}
|
||||
_entryController = Get.find<EntryController>();
|
||||
|
||||
await loadInitialData();
|
||||
} catch (e) {
|
||||
error.value = e.toString();
|
||||
print('Error in TeamController onInit: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadInitialData() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
await Future.wait([
|
||||
fetchCategories(),
|
||||
fetchTeams(),
|
||||
getCurrentUser(),
|
||||
]);
|
||||
if (categories.isNotEmpty && selectedCategory.value == null) {
|
||||
selectedCategory.value = categories.first;
|
||||
}
|
||||
} catch (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<void> 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;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchCategories() async {
|
||||
try {
|
||||
final fetchedCategories = await _apiService.getCategories();
|
||||
categories.assignAll(fetchedCategories);
|
||||
print("Fetched categories: ${categories.length}"); // デバッグ用
|
||||
|
||||
} catch (e) {
|
||||
print('Error fetching categories: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getCurrentUser() async {
|
||||
try {
|
||||
final user = await _apiService.getCurrentUser();
|
||||
currentUser.value = user;
|
||||
} catch (e) {
|
||||
print('Error getting current user: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Team> 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<Team> updateTeam(int teamId, String teamName, int categoryId) async {
|
||||
// APIサービスを使用してチームを更新
|
||||
final updatedTeam = await _apiService.updateTeam(teamId, teamName, categoryId);
|
||||
return updatedTeam;
|
||||
}
|
||||
|
||||
Future<void> deleteTeam(int teamId) async {
|
||||
bool confirmDelete = await Get.dialog(
|
||||
AlertDialog(
|
||||
title: const Text('チーム削除の確認'),
|
||||
content: const Text('このチームとそのすべてのメンバーを削除しますか?この操作は取り消せません。'),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('キャンセル'),
|
||||
onPressed: () => Get.back(result: false),
|
||||
),
|
||||
TextButton(
|
||||
child: const 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<void> fetchTeamMembers(int teamId) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
final members = await _apiService.getTeamMembers(teamId);
|
||||
teamMembers.assignAll(members);
|
||||
teamMembers.refresh(); // 明示的に更新を通知
|
||||
} catch (e) {
|
||||
error.value = 'メンバーの取得に失敗しました: $e';
|
||||
print('Error fetching team members: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> 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<void> 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<void> 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<void> 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<void> deleteSelectedTeam() async {
|
||||
if (selectedTeam.value != null) {
|
||||
await deleteTeam(selectedTeam.value!.id);
|
||||
selectedTeam.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
List<NewCategory> getFilteredCategories_old() {
|
||||
//List<User> teamMembers = getCurrentTeamMembers();
|
||||
return categories.where((category) {
|
||||
return isCategoryValid(category, teamMembers);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
List<NewCategory> getFilteredCategories() {
|
||||
if (teamMembers.isEmpty && currentUser.value != null) {
|
||||
// ソロの場合
|
||||
String baseCategory = currentUser.value!.female ? 'ソロ女子' : 'ソロ男子';
|
||||
return categories.where((c) => c.categoryName.startsWith(baseCategory)).toList();
|
||||
} else {
|
||||
bool hasElementaryOrYounger = teamMembers.any(isElementarySchoolOrYounger);
|
||||
String baseCategory = hasElementaryOrYounger ? 'ファミリー' : '一般';
|
||||
return categories.where((c) => c.categoryName.startsWith(baseCategory)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
bool isElementarySchoolOrYounger(User user) {
|
||||
final now = DateTime.now();
|
||||
final age = now.year - user.dateOfBirth!.year;
|
||||
return age <= 12;
|
||||
}
|
||||
|
||||
bool isCategoryValid(NewCategory category, List<User> 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<User> teamMembers) {
|
||||
// 家族かどうかを判断するロジック(例: 同じ姓を持つメンバーが2人以上いる場合は家族とみなす)
|
||||
Set<String> familyNames = teamMembers.map((member) => member.lastname).toSet();
|
||||
return familyNames.length < teamMembers.length;
|
||||
}
|
||||
|
||||
|
||||
Future<void> updateTeamComposition() async {
|
||||
NewCategory? oldCategory = selectedCategory.value;
|
||||
String? oldTime = oldCategory?.time;
|
||||
|
||||
// メンバーリストの最新状態を取得
|
||||
await fetchTeamMembers(selectedTeam.value!.id);
|
||||
|
||||
List<NewCategory> eligibleCategories = [];
|
||||
if (teamMembers.isEmpty || teamMembers.length == 1) {
|
||||
if (currentUser.value != null) {
|
||||
String baseCategory = currentUser.value!.female ? 'ソロ女子' : 'ソロ男子';
|
||||
eligibleCategories = categories.where((c) => c.baseCategory == baseCategory).toList();
|
||||
}
|
||||
} else {
|
||||
bool hasElementaryOrYounger = teamMembers.any(isElementarySchoolOrYounger);
|
||||
String baseCategory = hasElementaryOrYounger ? 'ファミリー' : '一般';
|
||||
eligibleCategories = categories.where((c) => c.baseCategory == baseCategory).toList();
|
||||
}
|
||||
|
||||
// 同じ時間のカテゴリを優先的に選択
|
||||
NewCategory? newCategory = eligibleCategories.firstWhereOrNull((c) => c.time == oldTime);
|
||||
|
||||
if (newCategory == null && eligibleCategories.isNotEmpty) {
|
||||
// 同じ時間のカテゴリがない場合、最初のカテゴリを選択
|
||||
newCategory = eligibleCategories.first;
|
||||
|
||||
// 警告メッセージを表示
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
title: Text('カテゴリ変更の通知'),
|
||||
content: Text('旧カテゴリの時間($oldTime)と一致するカテゴリがないため、${newCategory.time ?? "時間指定なし"}を選択しました。'),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('OK'),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (newCategory != null && oldCategory != newCategory) {
|
||||
selectedCategory.value = newCategory;
|
||||
|
||||
// チームのカテゴリを更新
|
||||
if (selectedTeam.value != null) {
|
||||
try {
|
||||
final updatedTeam = await updateTeam(selectedTeam.value!.id, selectedTeam.value!.teamName, newCategory.id);
|
||||
selectedTeam.value = updatedTeam;
|
||||
|
||||
// チームリストも更新
|
||||
final index = teams.indexWhere((team) => team.id == updatedTeam.id);
|
||||
if (index != -1) {
|
||||
teams[index] = updatedTeam;
|
||||
}
|
||||
|
||||
// エントリーの締め切りチェック
|
||||
bool hasClosedEntries = await checkForClosedEntries(selectedTeam.value!.id);
|
||||
if (hasClosedEntries) {
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
title: Text('警告'),
|
||||
content: Text('締め切りを過ぎたエントリーがあります。カテゴリ変更が必要な場合は事務局にお問い合わせください。'),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('OK'),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await checkAndHandleCategoryChange(oldCategory!, newCategory);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error updating team category: $e');
|
||||
Get.snackbar('エラー', 'チームのカテゴリ更新に失敗しました');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> checkForClosedEntries(int teamId) async {
|
||||
try {
|
||||
final entries = await _apiService.getEntries();
|
||||
return entries.any((entry) => !isEntryEditable(entry.event));
|
||||
} catch (e) {
|
||||
print('Error checking for closed entries: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool isEntryEditable(Event event) {
|
||||
return DateTime.now().isBefore(event.deadlineDateTime);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Future<void> updateTeamComposition_old() async {
|
||||
NewCategory? oldCategory = selectedCategory.value;
|
||||
String? oldTime = oldCategory?.time;
|
||||
|
||||
// メンバーリストの最新状態を取得
|
||||
await fetchTeamMembers(selectedTeam.value!.id);
|
||||
|
||||
List<NewCategory> eligibleCategories = [];
|
||||
if (teamMembers.isEmpty) {
|
||||
if (currentUser.value != null) {
|
||||
String baseCategory = currentUser.value!.female ? 'ソロ女子' : 'ソロ男子';
|
||||
eligibleCategories = categories.where((c) => c.baseCategory == baseCategory).toList();
|
||||
}
|
||||
} else {
|
||||
bool hasElementaryOrYounger = teamMembers.any(isElementarySchoolOrYounger);
|
||||
String baseCategory = hasElementaryOrYounger ? 'ファミリー' : '一般';
|
||||
eligibleCategories = categories.where((c) => c.baseCategory == baseCategory).toList();
|
||||
}
|
||||
|
||||
// 同じ時間のカテゴリを優先的に選択
|
||||
NewCategory? newCategory = eligibleCategories.firstWhereOrNull((c) => c.time == oldTime);
|
||||
|
||||
if (newCategory == null && eligibleCategories.isNotEmpty) {
|
||||
// 同じ時間のカテゴリがない場合、最初のカテゴリを選択
|
||||
newCategory = eligibleCategories.first;
|
||||
|
||||
// 警告メッセージを表示
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
title: Text('カテゴリ変更の通知'),
|
||||
content: Text('旧カテゴリの時間($oldTime)と一致するカテゴリがないため、${newCategory.time ?? "時間指定なし"}を選択しました。'),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('OK'),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (newCategory != null && oldCategory != newCategory) {
|
||||
selectedCategory.value = newCategory;
|
||||
|
||||
// チームのカテゴリを更新
|
||||
if (selectedTeam.value != null) {
|
||||
try {
|
||||
final updatedTeam = await updateTeam(selectedTeam.value!.id, selectedTeam.value!.teamName, newCategory.id);
|
||||
selectedTeam.value = updatedTeam;
|
||||
|
||||
// チームリストも更新
|
||||
final index = teams.indexWhere((team) => team.id == updatedTeam.id);
|
||||
if (index != -1) {
|
||||
teams[index] = updatedTeam;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error updating team category: $e');
|
||||
Get.snackbar('エラー', 'チームのカテゴリ更新に失敗しました');
|
||||
}
|
||||
}
|
||||
|
||||
await checkAndHandleCategoryChange(oldCategory!, newCategory);
|
||||
}
|
||||
}
|
||||
|
||||
// メンバーの追加後に呼び出すメソッド
|
||||
Future<void> addMember(User newMember) async {
|
||||
try {
|
||||
await _apiService.createTeamMember(selectedTeam.value!.id, newMember.email, newMember.firstname, newMember.lastname, newMember.dateOfBirth, newMember.female);
|
||||
await updateTeamComposition();
|
||||
} catch (e) {
|
||||
print('Error adding member: $e');
|
||||
Get.snackbar('エラー', 'メンバーの追加に失敗しました');
|
||||
}
|
||||
}
|
||||
|
||||
// メンバーの削除後に呼び出すメソッド
|
||||
Future<void> removeMember(int memberId) async {
|
||||
try {
|
||||
await _apiService.deleteTeamMember(selectedTeam.value!.id, memberId);
|
||||
|
||||
// selectedTeamからメンバーを削除
|
||||
if (selectedTeam.value != null) {
|
||||
selectedTeam.value!.members.removeWhere((member) => member.id == memberId);
|
||||
selectedTeam.refresh();
|
||||
}
|
||||
|
||||
// メンバー削除後にチーム構成を更新
|
||||
await updateTeamComposition();
|
||||
|
||||
// teamMembersを更新
|
||||
await fetchTeamMembers(selectedTeam.value!.id);
|
||||
} catch (e) {
|
||||
print('Error removing member: $e');
|
||||
Get.snackbar('エラー', 'メンバーの削除に失敗しました');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> checkAndHandleCategoryChange(NewCategory oldCategory, NewCategory newCategory) async {
|
||||
try {
|
||||
if (selectedTeam.value == null) {
|
||||
print('No team selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// エントリーの存在を確認
|
||||
bool hasEntries = await checkIfTeamHasEntries(selectedTeam.value!.id);
|
||||
|
||||
if (hasEntries) {
|
||||
bool shouldCreateNewTeam = await Get.dialog<bool>(
|
||||
CategoryChangeDialog(
|
||||
oldCategory: oldCategory.categoryName,
|
||||
newCategory: newCategory.categoryName,
|
||||
),
|
||||
) ?? false;
|
||||
|
||||
if (shouldCreateNewTeam) {
|
||||
await createNewTeamWithCurrentMembers();
|
||||
} else {
|
||||
await updateEntriesWithNewCategory();
|
||||
}
|
||||
}else{
|
||||
// エントリーが存在しない場合は、カテゴリの更新のみを行う
|
||||
await updateTeamCategory(newCategory);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error in checkAndHandleCategoryChange: $e');
|
||||
Get.snackbar('エラー', 'カテゴリ変更の処理中にエラーが発生しました');
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> checkIfTeamHasEntries(int teamId) async {
|
||||
try {
|
||||
final entries = await _apiService.getTeamEntries(teamId);
|
||||
return entries.isNotEmpty;
|
||||
} catch (e) {
|
||||
print('Error checking if team has entries: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateTeamCategory(NewCategory newCategory) async {
|
||||
try {
|
||||
if (selectedTeam.value != null) {
|
||||
final updatedTeam = await updateTeam(selectedTeam.value!.id, selectedTeam.value!.teamName, newCategory.id);
|
||||
selectedTeam.value = updatedTeam;
|
||||
|
||||
// チームリストも更新
|
||||
final index = teams.indexWhere((team) => team.id == updatedTeam.id);
|
||||
if (index != -1) {
|
||||
teams[index] = updatedTeam;
|
||||
}
|
||||
|
||||
Get.snackbar('成功', 'チームのカテゴリを更新しました');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error updating team category: $e');
|
||||
Get.snackbar('エラー', 'チームのカテゴリ更新に失敗しました');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchTeamEntries(int teamId) async {
|
||||
try {
|
||||
final fetchedEntries = await _apiService.getEntries();
|
||||
teamEntries.assignAll(fetchedEntries);
|
||||
} catch (e) {
|
||||
print('Error fetching team entries: $e');
|
||||
teamEntries.clear(); // エラーが発生した場合、エントリーリストをクリア
|
||||
//Get.snackbar('エラー', 'チームのエントリー取得に失敗しました');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> createNewTeamWithCurrentMembers() async {
|
||||
String newTeamName = '${selectedTeam.value!.teamName}_${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
try {
|
||||
Team newTeam = await _apiService.createTeam(newTeamName, selectedCategory.value!.id);
|
||||
|
||||
for (var member in teamMembers) {
|
||||
await _apiService.createTeamMember(
|
||||
newTeam.id,
|
||||
member.email,
|
||||
member.firstname,
|
||||
member.lastname,
|
||||
member.dateOfBirth,
|
||||
member.female,
|
||||
);
|
||||
}
|
||||
|
||||
Get.offNamed(AppPages.TEAM_DETAIL, arguments: {'mode': 'edit', 'team': newTeam});
|
||||
} catch (e) {
|
||||
print('Error creating new team: $e');
|
||||
Get.snackbar('エラー', '新しいチームの作成に失敗しました');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateEntriesWithNewCategory() async {
|
||||
try {
|
||||
for (var entry in teamEntries) {
|
||||
//await _apiService.updateEntry(entry.id, selectedCategory.value!.id);
|
||||
final updatedEntry = await _apiService.updateEntry(
|
||||
entry.id,
|
||||
entry.team.id,
|
||||
entry.event.id,
|
||||
selectedCategory.value!.id,
|
||||
entry.date!,
|
||||
entry.zekkenNumber,
|
||||
);
|
||||
}
|
||||
Get.snackbar('成功', 'エントリーのカテゴリを更新しました');
|
||||
} catch (e) {
|
||||
print('Error updating entries: $e');
|
||||
Get.snackbar('エラー', 'エントリーの更新に失敗しました');
|
||||
}
|
||||
}
|
||||
|
||||
void updateCurrentUserGender(bool isFemale) {
|
||||
if (currentUser.value != null) {
|
||||
currentUser.value!.female = isFemale;
|
||||
updateTeamComposition();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
263
lib/pages/team/team_detail_page.dart
Normal file
263
lib/pages/team/team_detail_page.dart
Normal file
@ -0,0 +1,263 @@
|
||||
// lib/pages/team/team_detail_page.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gifunavi/pages/team/team_controller.dart';
|
||||
import 'package:gifunavi/routes/app_pages.dart';
|
||||
import 'package:gifunavi/model/team.dart';
|
||||
import 'package:gifunavi/model/category.dart';
|
||||
|
||||
class TeamDetailPage extends StatefulWidget {
|
||||
const TeamDetailPage({super.key});
|
||||
|
||||
@override
|
||||
_TeamDetailPageState createState() => _TeamDetailPageState();
|
||||
}
|
||||
|
||||
class _TeamDetailPageState extends State<TeamDetailPage> {
|
||||
late TeamController controller;
|
||||
late TextEditingController _teamNameController = TextEditingController();
|
||||
final RxString mode = ''.obs;
|
||||
final Rx<Team?> team = Rx<Team?>(null);
|
||||
Worker? _teamNameWorker;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = Get.find<TeamController>();
|
||||
_teamNameController = TextEditingController();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_initializeData();
|
||||
});
|
||||
}
|
||||
|
||||
void _initializeData() {
|
||||
final args = Get.arguments;
|
||||
if (args != null && args is Map<String, dynamic>) {
|
||||
mode.value = args['mode'] as String? ?? '';
|
||||
team.value = args['team'] as Team?;
|
||||
|
||||
if (mode.value == 'edit' && team.value != null) {
|
||||
controller.setSelectedTeam(team.value!);
|
||||
controller.fetchTeamMembers(team.value!.id); // メンバーを取得
|
||||
} 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(
|
||||
Get.arguments['mode'] == 'new' ? '新規チーム作成' : 'チーム詳細'),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
controller.cleanupForNavigation();
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const 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: const Icon(Icons.delete),
|
||||
onPressed: () async {
|
||||
try {
|
||||
await controller.deleteSelectedTeam();
|
||||
Get.back();
|
||||
} catch (e) {
|
||||
Get.snackbar('エラー', e.toString(),
|
||||
snackPosition: SnackPosition.BOTTOM);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}),
|
||||
],
|
||||
),
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final filteredCategories = controller.getFilteredCategories();
|
||||
final categoriesToDisplay = filteredCategories.isEmpty
|
||||
? controller.categories
|
||||
: filteredCategories;
|
||||
|
||||
// 選択されているカテゴリが表示リストに含まれていない場合、最初の項目を選択する
|
||||
if (controller.selectedCategory.value == null ||
|
||||
!categoriesToDisplay.contains(controller.selectedCategory.value)) {
|
||||
controller.updateCategory(categoriesToDisplay.isNotEmpty
|
||||
? categoriesToDisplay.first
|
||||
: null);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Obx(() =>
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: 'チーム名'),
|
||||
controller: TextEditingController(text: controller
|
||||
.selectedTeam.value?.teamName ?? ''),
|
||||
onChanged: (value) => controller.updateTeamName(value),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (categoriesToDisplay.isEmpty)
|
||||
const Text('カテゴリデータを読み込めませんでした。',
|
||||
style: TextStyle(color: Colors.red))
|
||||
else
|
||||
Obx(() =>
|
||||
DropdownButtonFormField<NewCategory>(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'カテゴリ'),
|
||||
value: controller.selectedCategory.value,
|
||||
items: categoriesToDisplay.map((category) =>
|
||||
DropdownMenuItem(
|
||||
value: category,
|
||||
child: Text(category.categoryName),
|
||||
)).toList(),
|
||||
onChanged: (value) => controller.updateCategory(value),
|
||||
),
|
||||
),
|
||||
|
||||
if (filteredCategories.isEmpty &&
|
||||
controller.categories.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'注意: チーム構成に適さないカテゴリーも表示されています。',
|
||||
style: TextStyle(color: Colors.orange[700], fontSize: 12),
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Text('所有者: ${controller.currentUser.value?.email ??
|
||||
"未設定"}'),
|
||||
),
|
||||
|
||||
if (mode.value == 'edit') ...[
|
||||
const SizedBox(height: 24),
|
||||
const Text('メンバーリスト', style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
Obx(() {
|
||||
final teamMembers = controller.teamMembers;
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: teamMembers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final member = teamMembers[index];
|
||||
|
||||
final isDummyEmail = member.email?.startsWith(
|
||||
'dummy_') ?? false;
|
||||
final isCurrentUser = member.email ==
|
||||
controller.currentUser.value?.email;
|
||||
final displayName = isDummyEmail
|
||||
? '${member.lastname} ${member.firstname}'
|
||||
: member.isActive
|
||||
? '${member.lastname} ${member.firstname}'
|
||||
: member.email?.split('@')[0] ?? ''; //(未承認)';
|
||||
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'$displayName${isCurrentUser ? ' (自分)' : ''}'),
|
||||
subtitle: isDummyEmail
|
||||
? const Text('Email未設定')
|
||||
: null,
|
||||
|
||||
onTap: isCurrentUser ? null : () async {
|
||||
final result = await Get.toNamed(
|
||||
AppPages.MEMBER_DETAIL,
|
||||
arguments: {
|
||||
'mode': 'edit',
|
||||
'member': member,
|
||||
'teamId': controller.selectedTeam.value?.id
|
||||
},
|
||||
);
|
||||
|
||||
await controller.fetchTeamMembers(
|
||||
controller.selectedTeam.value!.id);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
child: const Text('新しいメンバーを追加'),
|
||||
onPressed: () async {
|
||||
final result = await Get.toNamed(
|
||||
AppPages.MEMBER_DETAIL,
|
||||
arguments: {
|
||||
'mode': 'new',
|
||||
'teamId': controller.selectedTeam.value?.id
|
||||
},
|
||||
);
|
||||
if (result == true && controller.selectedTeam.value != null) {
|
||||
await controller.fetchTeamMembers(controller.selectedTeam.value!.id);
|
||||
controller.teamMembers.refresh(); // 明示的に更新を通知
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
57
lib/pages/team/team_list_page.dart
Normal file
57
lib/pages/team/team_list_page.dart
Normal file
@ -0,0 +1,57 @@
|
||||
// lib/pages/team/team_list_page.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gifunavi/pages/team/team_controller.dart';
|
||||
import 'package:gifunavi/routes/app_pages.dart';
|
||||
|
||||
class TeamListPage extends GetWidget<TeamController> {
|
||||
const TeamListPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('チーム管理'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () => Get.toNamed(AppPages.TEAM_DETAIL, arguments: {'mode': 'new'}),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (controller.error.value.isNotEmpty) {
|
||||
return Center(child: Text(controller.error.value));
|
||||
} else if (controller.teams.isEmpty) {
|
||||
return const 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} '),
|
||||
onTap: () async {
|
||||
await Get.toNamed(
|
||||
AppPages.TEAM_DETAIL,
|
||||
arguments: {'mode': 'edit', 'team': team},
|
||||
);
|
||||
controller.fetchTeams();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user