Team,Member, Entryの登録まで完了

This commit is contained in:
2024-07-29 08:41:28 +09:00
parent 7f8adeea01
commit 9c98d3ed53
10 changed files with 803 additions and 260 deletions

View File

@ -9,7 +9,7 @@ class Entry {
final Team team; final Team team;
final Event event; final Event event;
final NewCategory category; final NewCategory category;
final DateTime date; final DateTime? date;
final String owner; final String owner;
Entry({ Entry({
@ -27,8 +27,10 @@ class Entry {
team: Team.fromJson(json['team']), team: Team.fromJson(json['team']),
event: Event.fromJson(json['event']), event: Event.fromJson(json['event']),
category: NewCategory.fromJson(json['category']), category: NewCategory.fromJson(json['category']),
date: DateTime.parse(json['date']), date: json['date'] != null
owner: json['owner'], ? DateTime.tryParse(json['date'])
: null,
owner: json['owner'] is Map ? json['owner']['name'] ?? '' : json['owner'] ?? '',
); );
} }
@ -38,7 +40,7 @@ class Entry {
'team': team.toJson(), 'team': team.toJson(),
'event': event.toJson(), 'event': event.toJson(),
'category': category.toJson(), 'category': category.toJson(),
'date': date.toIso8601String(), 'date': date?.toIso8601String(),
'owner': owner, 'owner': owner,
}; };
} }

View File

@ -1,6 +1,7 @@
// lib/entry/entry_controller.dart // lib/entry/entry_controller.dart
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:rogapp/model/entry.dart'; import 'package:rogapp/model/entry.dart';
import 'package:rogapp/model/event.dart'; import 'package:rogapp/model/event.dart';
import 'package:rogapp/model/team.dart'; import 'package:rogapp/model/team.dart';
@ -8,7 +9,7 @@ import 'package:rogapp/model/category.dart';
import 'package:rogapp/services/api_service.dart'; import 'package:rogapp/services/api_service.dart';
class EntryController extends GetxController { class EntryController extends GetxController {
late final ApiService _apiService; late ApiService _apiService;
final entries = <Entry>[].obs; final entries = <Entry>[].obs;
final events = <Event>[].obs; final events = <Event>[].obs;
@ -21,14 +22,27 @@ class EntryController extends GetxController {
final selectedDate = Rx<DateTime?>(null); final selectedDate = Rx<DateTime?>(null);
final currentEntry = Rx<Entry?>(null); final currentEntry = Rx<Entry?>(null);
final isLoading = true.obs;
@override @override
void onInit() async{ void onInit() async {
super.onInit(); super.onInit();
await Get.putAsync(() => ApiService().init()); await initializeApiService();
_apiService = Get.find<ApiService>(); await loadInitialData();
}
Future<void> initializeApiService() async {
try { try {
_apiService = await Get.putAsync(() => ApiService().init());
} catch (e) {
print('Error initializing ApiService: $e');
Get.snackbar('Error', 'Failed to initialize API service');
}
}
Future<void> loadInitialData() async {
try {
isLoading.value = true;
await Future.wait([ await Future.wait([
fetchEntries(), fetchEntries(),
fetchEvents(), fetchEvents(),
@ -37,14 +51,53 @@ class EntryController extends GetxController {
]); ]);
if (Get.arguments != null && Get.arguments['entry'] != null) { if (Get.arguments != null && Get.arguments['entry'] != null) {
currentEntry.value = Get.arguments['entry']; currentEntry.value = Get.arguments['entry'];
_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'); 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() { void _initializeEntryData() {
if (currentEntry.value != null) { if (currentEntry.value != null) {
selectedEvent.value = currentEntry.value!.event; selectedEvent.value = currentEntry.value!.event;
@ -60,6 +113,7 @@ class EntryController extends GetxController {
entries.assignAll(fetchedEntries); entries.assignAll(fetchedEntries);
} catch (e) { } catch (e) {
print('Error fetching entries: $e'); print('Error fetching entries: $e');
Get.snackbar('Error', 'Failed to fetch entries');
} }
} }
@ -69,6 +123,7 @@ class EntryController extends GetxController {
events.assignAll(fetchedEvents); events.assignAll(fetchedEvents);
} catch (e) { } catch (e) {
print('Error fetching events: $e'); print('Error fetching events: $e');
Get.snackbar('Error', 'Failed to fetch events');
} }
} }
@ -78,6 +133,7 @@ class EntryController extends GetxController {
teams.assignAll(fetchedTeams); teams.assignAll(fetchedTeams);
} catch (e) { } catch (e) {
print('Error fetching teams: $e'); print('Error fetching teams: $e');
Get.snackbar('Error', 'Failed to fetch team');
} }
} }
@ -87,6 +143,7 @@ class EntryController extends GetxController {
categories.assignAll(fetchedCategories); categories.assignAll(fetchedCategories);
} catch (e) { } catch (e) {
print('Error fetching categories: $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() { bool isOwner() {
// Implement logic to check if the current user is the owner of the entry // Implement logic to check if the current user is the owner of the entry

View File

@ -3,6 +3,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:rogapp/pages/entry/entry_controller.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<EntryController> { class EntryDetailPage extends GetView<EntryController> {
@override @override
@ -11,52 +15,121 @@ class EntryDetailPage extends GetView<EntryController> {
final mode = Get.arguments['mode'] as String? ?? 'new'; final mode = Get.arguments['mode'] as String? ?? 'new';
final entry = Get.arguments['entry']; final entry = Get.arguments['entry'];
if (mode == 'edit' && entry != null) {
controller.initializeEditMode(entry);
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(mode == 'new' ? 'エントリー登録' : 'エントリー詳細'), title: Text(mode == 'new' ? 'エントリー登録' : 'エントリー詳細'),
), ),
body: Padding( body: Obx(() {
if (controller.isLoading.value) {
return Center(child: CircularProgressIndicator());
}
return Padding(
padding: EdgeInsets.all(16.0), padding: EdgeInsets.all(16.0),
child: Obx(() => Column( child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
DropdownButtonFormField( _buildDropdown<Event>(
decoration: InputDecoration(labelText: 'イベント'), label: 'イベント',
value: controller.selectedEvent.value, items: controller.events,
items: controller.events.map((event) => DropdownMenuItem( selectedId: controller.selectedEvent.value?.id,
value: event, onChanged: (eventId) => controller.updateEvent(
child: Text(event.eventName), controller.events.firstWhere((e) => e.id == eventId)
)).toList(),
onChanged: (value) => controller.updateEvent(value),
), ),
DropdownButtonFormField( getDisplayName: (event) => event.eventName,
decoration: InputDecoration(labelText: 'チーム'), getId: (event) => event.id,
value: controller.selectedTeam.value,
items: controller.teams.map((team) => DropdownMenuItem(
value: team,
child: Text(team.teamName),
)).toList(),
onChanged: (value) => controller.updateTeam(value),
), ),
DropdownButtonFormField( SizedBox(height: 16),
decoration: InputDecoration(labelText: 'カテゴリ'), _buildDropdown<Team>(
value: controller.selectedCategory.value, label: 'チーム',
items: controller.categories.map((category) => DropdownMenuItem( items: controller.teams,
value: category, selectedId: controller.selectedTeam.value?.id,
child: Text(category.categoryName), onChanged: (teamId) => controller.updateTeam(
)).toList(), controller.teams.firstWhere((t) => t.id == teamId)
onChanged: (value) => controller.updateCategory(value), ),
getDisplayName: (team) => team.teamName,
getId: (team) => team.id,
),
SizedBox(height: 16),
_buildDropdown<NewCategory>(
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()) if (mode == 'edit' && controller.isOwner())
ElevatedButton( ElevatedButton(
child: Text('エントリーを削除'), child: Text('エントリーを削除'),
onPressed: () => controller.deleteEntry(), onPressed: () => controller.deleteEntry(),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
), ),
], ],
), ),
), ),
) );
}),
);
}
Widget _buildDropdown<T>({
required String label,
required List<T> items,
required int? selectedId,
required void Function(int?) onChanged,
required String Function(T) getDisplayName,
required int Function(T) getId,
}) {
return DropdownButtonFormField<int>(
decoration: InputDecoration(labelText: label),
value: selectedId,
items: items.map((item) => DropdownMenuItem<int>(
value: getId(item),
child: Text(getDisplayName(item)),
)).toList(),
onChanged: onChanged,
); );
} }
} }

View File

@ -2,31 +2,52 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:rogapp/model/user.dart'; import 'package:rogapp/model/user.dart';
import 'package:rogapp/pages/team/team_controller.dart';
import 'package:rogapp/services/api_service.dart'; import 'package:rogapp/services/api_service.dart';
class MemberController extends GetxController { class MemberController extends GetxController {
late final ApiService _apiService; late final ApiService _apiService;
final selectedMember = Rx<User?>(null); final selectedMember = Rx<User?>(null);
final int teamId = 0; int teamId = 0;
final member = Rx<User?>(null); final member = Rx<User?>(null);
final email = ''.obs; final email = ''.obs;
final firstname = ''.obs; final firstname = ''.obs;
final lastname = ''.obs; final lastname = ''.obs;
final female = false.obs;
final dateOfBirth = Rx<DateTime?>(null); final dateOfBirth = Rx<DateTime?>(null);
final isLoading = true.obs; // isLoadingプロパティを追加 final isLoading = false.obs; // isLoadingプロパティを追加
final isActive = false.obs;
//MemberController(this._apiService);
@override @override
void onInit() async{ void onInit() {
super.onInit(); super.onInit();
await Get.putAsync(() => ApiService().init());
_apiService = Get.find<ApiService>(); _apiService = Get.find<ApiService>();
await loadInitialData(); ever(member, (_) => _initializeMemberData());
loadInitialData();
}
if (Get.arguments != null && Get.arguments['member'] != null) { 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']; member.value = Get.arguments['member'];
_initializeMemberData(); }
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<void> loadInitialData() async {
try {
isLoading.value = true;
// 必要な初期データの取得をここで行う
// 例: await fetchTeamMembers();
} catch (e) {
print('Error loading initial data: $e');
} finally {
isLoading.value = false;
}
}
void setSelectedMember(User member) { void setSelectedMember(User member) {
selectedMember.value = member; this.member.value = member;
firstname.value = member.firstname; email.value = member.email ?? '';
lastname.value = member.lastname; 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) { void updateFirstName(String value) {
@ -67,32 +91,52 @@ class MemberController extends GetxController {
lastname.value = value; lastname.value = value;
} }
Future<void> saveMember() async { Future<bool> saveMember() async {
if (!_validateInputs()) return false;
try { try {
isLoading.value = true; isLoading.value = true;
// メンバー保存のロジックをここに実装 User updatedMember;
// 例: await _apiService.updateMember(selectedMember.value!.id, firstName.value, lastName.value); 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) { } catch (e) {
print('Error saving member: $e'); print('Error saving member: $e');
// エラーハンドリング(例:ユーザーへの通知) Get.snackbar('エラー', 'メンバーの保存に失敗しました: ${e.toString()}', snackPosition: SnackPosition.BOTTOM);
return false;
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
} }
Future<void> createMember(int teamId) async {
try { String getDisplayName() {
final newMember = await _apiService.createTeamMember( if (!isActive.value && !isDummyEmail) {
teamId, final displayName = email.value.split('@')[0];
email.value, return '$displayName(未承認)';
firstname.value,
lastname.value,
dateOfBirth.value,
);
member.value = newMember;
} catch (e) {
print('Error creating member: $e');
} }
return '${lastname.value} ${firstname.value}'.trim();
} }
Future<void> updateMember() async { Future<void> updateMember() async {
@ -105,6 +149,7 @@ class MemberController extends GetxController {
firstname.value, firstname.value,
lastname.value, lastname.value,
dateOfBirth.value, dateOfBirth.value,
female.value,
); );
member.value = updatedMember; member.value = updatedMember;
} catch (e) { } catch (e) {
@ -112,6 +157,26 @@ class MemberController extends GetxController {
} }
} }
Future<void> 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<void> deleteMember() async { Future<void> deleteMember() async {
if (member.value == null) return; if (member.value == null) return;
int? memberId = member.value?.id; int? memberId = member.value?.id;
@ -124,7 +189,14 @@ class MemberController extends GetxController {
} }
} }
*/
Future<void> resendInvitation() async { Future<void> resendInvitation() async {
if (isDummyEmail) {
Get.snackbar('エラー', 'ダミーメールアドレスには招待メールを送信できません', snackPosition: SnackPosition.BOTTOM);
return;
}
if (member.value == null || member.value!.email == null) return; if (member.value == null || member.value!.email == null) return;
int? memberId = member.value?.id; int? memberId = member.value?.id;
try { try {
@ -147,4 +219,52 @@ class MemberController extends GetxController {
if (member.value!.isActive) return '承認済'; if (member.value!.isActive) return '承認済';
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;
}
} }

View File

@ -3,22 +3,87 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:rogapp/pages/team/member_controller.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<MemberDetailPage> {
final MemberController controller = Get.find<MemberController>();
late TextEditingController _firstNameController;
late TextEditingController _lastNameController;
late TextEditingController _emailController;
class MemberDetailPage extends GetView<MemberController> {
final TextEditingController _firstNameController = TextEditingController();
final TextEditingController _lastNameController = TextEditingController();
@override @override
Widget build(BuildContext context) { void initState() {
final mode = Get.arguments['mode']; super.initState();
final member = Get.arguments['member']; _initializeControllers();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final mode = Get.arguments['mode'];
final member = Get.arguments['member'];
if (mode == 'edit' && member != null) { if (mode == 'edit' && member != null) {
controller.setSelectedMember(member); 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)),
);
}
});
}
@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( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(mode == 'new' ? 'メンバー追加' : 'メンバー詳細'), title: Text(mode == 'new' ? 'メンバー追加' : 'メンバー詳細'),
@ -27,68 +92,121 @@ class MemberDetailPage extends GetView<MemberController> {
icon: Icon(Icons.save), icon: Icon(Icons.save),
onPressed: () async { onPressed: () async {
await controller.saveMember(); await controller.saveMember();
Get.back(); Get.back(result: true);
}, },
), ),
], ],
), ),
body: Obx(() body: Obx(() {
{
if (controller.isLoading.value) { if (controller.isLoading.value) {
return Center(child: CircularProgressIndicator()); return Center(child: CircularProgressIndicator());
} }
_firstNameController.value = _firstNameController.value.copyWith( return SingleChildScrollView(
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), padding: EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (mode == 'edit' && member.email != null) if (mode == 'new')
Text('Email: ${member.email}'),
if (mode == 'new' || (mode == 'edit' && member.email == null))
TextField( TextField(
decoration: InputDecoration(labelText: 'Email'), decoration: InputDecoration(labelText: 'メールアドレス'),
onChanged: (value) => controller.updateEmail(value), 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( TextField(
decoration: InputDecoration(labelText: ''), decoration: InputDecoration(labelText: ''),
controller: _lastNameController, onChanged: (value) => controller.lastname.value = value,
onChanged: (value) => controller.updateLastName(value), controller: TextEditingController(text: controller.lastname.value),
), ),
TextField( TextField(
decoration: InputDecoration(labelText: ''), decoration: 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: 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(
if (mode == 'edit') title: Text('性別'),
Text('ステータス: ${controller.getMemberStatus()}'), subtitle: Text(controller.female.value ? '女性' : '男性'),
if (mode == 'edit' && controller.getMemberStatus() == '招待中') value: controller.female.value,
onChanged: (value) => controller.female.value = value,
),
],
// 招待メール再送信ボタン通常のEmailで未承認の場合のみ
if (!controller.isDummyEmail && !controller.isApproved)
ElevatedButton( ElevatedButton(
child: Text('招待メールを再送信'), child: Text('招待メールを再送信'),
onPressed: () => controller.resendInvitation(), onPressed: () => controller.resendInvitation(),
), ),
if (mode == 'edit')
// メンバー削除ボタン
ElevatedButton( ElevatedButton(
child: Text('メンバーから除'), child: Text('メンバーから'),
onPressed: () => controller.deleteMember(), onPressed: () async {
final confirmed = await Get.dialog<bool>(
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();
} }
} }

View File

@ -71,6 +71,15 @@ class TeamController extends GetxController {
teamMembers.clear(); teamMembers.clear();
} }
void cleanupForNavigation() {
selectedTeam.value = null;
teamName.value = '';
selectedCategory.value = categories.isNotEmpty ? categories.first : null;
teamMembers.clear();
//teamMembersはクリアしない
// 必要に応じて他のクリーンアップ処理を追加
}
Future<void> fetchTeams() async { Future<void> fetchTeams() async {
try { try {
isLoading.value = true; isLoading.value = true;
@ -126,25 +135,75 @@ class TeamController extends GetxController {
Future<void> fetchTeamMembers(int teamId) async { Future<void> fetchTeamMembers(int teamId) async {
try { try {
isLoading.value = true;
final members = await _apiService.getTeamMembers(teamId); final members = await _apiService.getTeamMembers(teamId);
teamMembers.assignAll(members); teamMembers.assignAll(members);
} catch (e) { } catch (e) {
error.value = 'メンバーの取得に失敗しました: $e';
print('Error fetching team members: $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) { void updateTeamName(String value) {
teamName.value = value; teamName.value = value;
} }
void updateCategory(NewCategory? value) { void updateCategory(NewCategory? value) {
if (value != null) { if (value != null) {
selectedCategory.value = categories.firstWhere( selectedCategory.value = value;
(category) => category.id == value.id,
orElse: () => value,
);
} }
} }
//void updateCategory(NewCategory? value) {
// if (value != null) {
// selectedCategory.value = categories.firstWhere(
// (category) => category.id == value.id,
// orElse: () => value,
// );
// }
//}
Future<void> saveTeam() async { Future<void> saveTeam() async {
try { try {
@ -161,18 +220,10 @@ class TeamController extends GetxController {
// サーバーから最新のデータを再取得 // サーバーから最新のデータを再取得
await fetchTeams(); 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を強制的に更新 update(); // UIを強制的に更新
} catch (e) { } catch (e) {
error.value = 'チームの保存に失敗しました: $e'; error.value = 'チームの保存に失敗しました: $e';
print("Team save error: $e");
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }

View File

@ -7,21 +7,76 @@ import 'package:rogapp/routes/app_pages.dart';
import 'package:rogapp/model/team.dart'; import 'package:rogapp/model/team.dart';
import 'package:rogapp/model/category.dart'; import 'package:rogapp/model/category.dart';
class TeamDetailPage extends GetView<TeamController> { class TeamDetailPage extends StatefulWidget {
@override @override
Widget build(BuildContext context) { _TeamDetailPageState createState() => _TeamDetailPageState();
final mode = Get.arguments['mode'] as String; }
final team = Get.arguments['team'] as Team?;
if (mode == 'edit' && team != null) { class _TeamDetailPageState extends State<TeamDetailPage> {
controller.setSelectedTeam(team); 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!);
} else { } else {
controller.resetForm(); 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( return Scaffold(
appBar: AppBar( 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: [ actions: [
IconButton( IconButton(
icon: Icon(Icons.save), icon: Icon(Icons.save),
@ -34,8 +89,9 @@ class TeamDetailPage extends GetView<TeamController> {
} }
}, },
), ),
if (mode == 'edit') Obx(() {
IconButton( if (mode.value == 'edit') {
return IconButton(
icon: Icon(Icons.delete), icon: Icon(Icons.delete),
onPressed: () async { onPressed: () async {
try { try {
@ -45,11 +101,14 @@ class TeamDetailPage extends GetView<TeamController> {
Get.snackbar('エラー', e.toString(), snackPosition: SnackPosition.BOTTOM); Get.snackbar('エラー', e.toString(), snackPosition: SnackPosition.BOTTOM);
} }
}, },
), );
} else {
return SizedBox.shrink();
}
}),
], ],
), ),
body: GetBuilder<TeamController>( body: Obx(() {
builder: (controller) {
if (controller.isLoading.value) { if (controller.isLoading.value) {
return Center(child: CircularProgressIndicator()); return Center(child: CircularProgressIndicator());
} }
@ -61,10 +120,11 @@ class TeamDetailPage extends GetView<TeamController> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
TextFormField( TextFormField(
initialValue: controller.teamName.value,
decoration: InputDecoration(labelText: 'チーム名'), decoration: InputDecoration(labelText: 'チーム名'),
controller: _teamNameController,
onChanged: (value) => controller.updateTeamName(value), onChanged: (value) => controller.updateTeamName(value),
), ),
SizedBox(height: 16), SizedBox(height: 16),
DropdownButtonFormField<NewCategory>( DropdownButtonFormField<NewCategory>(
decoration: InputDecoration(labelText: 'カテゴリ'), decoration: InputDecoration(labelText: 'カテゴリ'),
@ -75,7 +135,7 @@ class TeamDetailPage extends GetView<TeamController> {
)).toList(), )).toList(),
onChanged: (value) => controller.updateCategory(value), onChanged: (value) => controller.updateCategory(value),
), ),
if (mode == 'edit') if (mode.value == 'edit')
Padding( Padding(
padding: EdgeInsets.symmetric(vertical: 16), padding: EdgeInsets.symmetric(vertical: 16),
child: Text('ゼッケン番号: ${controller.selectedTeam.value?.zekkenNumber ?? ""}'), child: Text('ゼッケン番号: ${controller.selectedTeam.value?.zekkenNumber ?? ""}'),
@ -84,6 +144,8 @@ class TeamDetailPage extends GetView<TeamController> {
padding: EdgeInsets.symmetric(vertical: 16), padding: EdgeInsets.symmetric(vertical: 16),
child: Text('所有者: ${controller.currentUser.value?.email ?? "未設定"}'), child: Text('所有者: ${controller.currentUser.value?.email ?? "未設定"}'),
), ),
if (mode.value == 'edit') ...[
SizedBox(height: 24), SizedBox(height: 24),
Text('メンバーリスト', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), Text('メンバーリスト', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 8), SizedBox(height: 8),
@ -93,34 +155,55 @@ class TeamDetailPage extends GetView<TeamController> {
itemCount: controller.teamMembers.length, itemCount: controller.teamMembers.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final member = controller.teamMembers[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( return ListTile(
title: Text('${member.lastname}, ${member.firstname}'), title: Text(displayName),
onTap: () => Get.toNamed( subtitle: isDummyEmail ? Text('Email未設定') : null,
onTap: () async {
final result = await Get.toNamed(
AppPages.MEMBER_DETAIL, AppPages.MEMBER_DETAIL,
arguments: {'mode': 'edit', 'member': member, 'teamId': controller.selectedTeam.value?.id}, arguments: {'mode': 'edit', 'member': member, 'teamId': controller.selectedTeam.value?.id},
), );
if (result == true) {
await controller.fetchTeamMembers(controller.selectedTeam.value!.id);
}
},
); );
}, },
), ),
SizedBox(height: 16), SizedBox(height: 16),
ElevatedButton( ElevatedButton(
child: Text('新しいメンバーを追加'), child: Text('新しいメンバーを追加'),
onPressed: () => Get.toNamed( onPressed: () async {
await Get.toNamed(
AppPages.MEMBER_DETAIL, AppPages.MEMBER_DETAIL,
arguments: {'mode': 'new', 'teamId': controller.selectedTeam.value?.id}, arguments: {'mode': 'new', 'teamId': controller.selectedTeam.value?.id},
),
),
],
),
),
); );
if (controller.selectedTeam.value != null) {
controller.fetchTeamMembers(controller.selectedTeam.value!.id);
}
}, },
), ),
],
],
),
)
); );
}),
);
} }
} }

View File

@ -11,6 +11,8 @@ import 'package:rogapp/model/category.dart';
import 'package:rogapp/model/user.dart'; import 'package:rogapp/model/user.dart';
import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart';
import '../utils/const.dart'; import '../utils/const.dart';
import 'package:intl/intl.dart';
class ApiService extends GetxService{ class ApiService extends GetxService{
@ -19,16 +21,18 @@ class ApiService extends GetxService{
String baseUrl = ''; String baseUrl = '';
String token = 'your-auth-token'; String token = 'your-auth-token';
Future<void> init() async { Future<ApiService> init() async {
try { try {
// ここで必要な初期化処理を行う // ここで必要な初期化処理を行う
serverUrl = ConstValues.currentServer(); serverUrl = ConstValues.currentServer();
baseUrl = '$serverUrl/api'; baseUrl = '$serverUrl/api';
//await Future.delayed(Duration(seconds: 2)); // 仮の遅延(実際の初期化処理に置き換えてください) //await Future.delayed(Duration(seconds: 2)); // 仮の遅延(実際の初期化処理に置き換えてください)
print('ApiService initialized successfully'); print('ApiService initialized successfully');
return this;
} catch(e) { } catch(e) {
print('Error in ApiService initialization: $e'); print('Error in ApiService initialization: $e');
rethrow; // エラーを再スローして、呼び出し元で処理できるようにする rethrow; // エラーを再スローして、呼び出し元で処理できるようにする
//return this;
} }
} }
@ -232,7 +236,8 @@ class ApiService extends GetxService{
); );
if (response.statusCode == 201) { if (response.statusCode == 201) {
return Team.fromJson(json.decode(response.body)); final decodedResponse = utf8.decode(response.bodyBytes);
return Team.fromJson(json.decode(decodedResponse));
} else { } else {
throw Exception('Failed to create team'); throw Exception('Failed to create team');
} }
@ -255,7 +260,9 @@ class ApiService extends GetxService{
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
return Team.fromJson(json.decode(response.body)); final decodedResponse = utf8.decode(response.bodyBytes);
return Team.fromJson(json.decode(decodedResponse));
} else { } else {
throw Exception('Failed to update team'); throw Exception('Failed to update team');
} }
@ -295,10 +302,15 @@ class ApiService extends GetxService{
} }
} }
Future<User> createTeamMember(int teamId, String email, String firstname, String lastname, DateTime? dateOfBirth) async { Future<User> createTeamMember(int teamId, String? email, String firstname, String lastname, DateTime? dateOfBirth,bool? female) async {
init(); init();
getToken(); getToken();
String? formattedDateOfBirth;
if (dateOfBirth != null) {
formattedDateOfBirth = DateFormat('yyyy-MM-dd').format(dateOfBirth);
}
final response = await http.post( final response = await http.post(
Uri.parse('$baseUrl/teams/$teamId/members/'), Uri.parse('$baseUrl/teams/$teamId/members/'),
headers: { headers: {
@ -309,21 +321,28 @@ class ApiService extends GetxService{
'email': email, 'email': email,
'firstname': firstname, 'firstname': firstname,
'lastname': lastname, 'lastname': lastname,
'date_of_birth': dateOfBirth?.toIso8601String(), 'date_of_birth': formattedDateOfBirth,
'female': female,
}), }),
); );
if (response.statusCode == 201) { if (response.statusCode == 201) {
return User.fromJson(json.decode(response.body)); final decodedResponse = utf8.decode(response.bodyBytes);
return User.fromJson(json.decode(decodedResponse));
} else { } else {
throw Exception('Failed to create team member'); throw Exception('Failed to create team member');
} }
} }
Future<User> updateTeamMember(int teamId,int memberId, String firstname, String lastname, DateTime? dateOfBirth) async { Future<User> updateTeamMember(int teamId,int? memberId, String firstname, String lastname, DateTime? dateOfBirth,bool? female) async {
init(); init();
getToken(); getToken();
String? formattedDateOfBirth;
if (dateOfBirth != null) {
formattedDateOfBirth = DateFormat('yyyy-MM-dd').format(dateOfBirth);
}
final response = await http.put( final response = await http.put(
Uri.parse('$baseUrl/teams/$teamId/members/$memberId/'), Uri.parse('$baseUrl/teams/$teamId/members/$memberId/'),
headers: { headers: {
@ -333,12 +352,14 @@ class ApiService extends GetxService{
body: json.encode({ body: json.encode({
'firstname': firstname, 'firstname': firstname,
'lastname': lastname, 'lastname': lastname,
'date_of_birth': dateOfBirth?.toIso8601String(), 'date_of_birth': formattedDateOfBirth,
'female': female,
}), }),
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
return User.fromJson(json.decode(response.body)); final decodedResponse = utf8.decode(response.bodyBytes);
return User.fromJson(json.decode(decodedResponse));
} else { } else {
throw Exception('Failed to update team member'); throw Exception('Failed to update team member');
} }
@ -378,13 +399,14 @@ class ApiService extends GetxService{
getToken(); getToken();
final response = await http.get( final response = await http.get(
Uri.parse('$baseUrl/entries/'), Uri.parse('$baseUrl/entry/'),
headers: {'Authorization': 'Token $token', 'Content-Type': 'application/json; charset=UTF-8', headers: {'Authorization': 'Token $token', 'Content-Type': 'application/json; charset=UTF-8',
}, },
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
List<dynamic> entriesJson = json.decode(response.body); final decodedResponse = utf8.decode(response.bodyBytes);
List<dynamic> entriesJson = json.decode(decodedResponse);
return entriesJson.map((json) => Entry.fromJson(json)).toList(); return entriesJson.map((json) => Entry.fromJson(json)).toList();
} else { } else {
throw Exception('Failed to load entries'); throw Exception('Failed to load entries');
@ -416,6 +438,11 @@ class ApiService extends GetxService{
init(); init();
getToken(); getToken();
String? formattedDate;
if (date != null) {
formattedDate = DateFormat('yyyy-MM-dd').format(date);
}
final response = await http.post( final response = await http.post(
Uri.parse('$baseUrl/entry/'), Uri.parse('$baseUrl/entry/'),
headers: { headers: {
@ -426,12 +453,14 @@ class ApiService extends GetxService{
'team': teamId, 'team': teamId,
'event': eventId, 'event': eventId,
'category': categoryId, 'category': categoryId,
'date': date.toIso8601String(), 'date': formattedDate,
}), }),
); );
if (response.statusCode == 201) { if (response.statusCode == 201) {
return Entry.fromJson(json.decode(response.body)); final decodedResponse = utf8.decode(response.bodyBytes);
return Entry.fromJson(json.decode(decodedResponse));
} else { } else {
throw Exception('Failed to create entry'); throw Exception('Failed to create entry');
} }
@ -441,6 +470,11 @@ class ApiService extends GetxService{
init(); init();
getToken(); getToken();
String? formattedDate;
if (date != null) {
formattedDate = DateFormat('yyyy-MM-dd').format(date);
}
final response = await http.put( final response = await http.put(
Uri.parse('$baseUrl/entry/$entryId/'), Uri.parse('$baseUrl/entry/$entryId/'),
headers: { headers: {
@ -450,12 +484,14 @@ class ApiService extends GetxService{
body: json.encode({ body: json.encode({
'event': eventId, 'event': eventId,
'category': categoryId, 'category': categoryId,
'date': date.toIso8601String(), 'date': formattedDate,
}), }),
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
return Entry.fromJson(json.decode(response.body)); final decodedResponse = utf8.decode(response.bodyBytes);
return Entry.fromJson(json.decode(decodedResponse));
} else { } else {
throw Exception('Failed to update entry'); throw Exception('Failed to update entry');
} }

View File

@ -374,6 +374,11 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_map: flutter_map:
dependency: "direct main" dependency: "direct main"
description: description:
@ -660,10 +665,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: intl name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.18.1" version: "0.19.0"
isar: isar:
dependency: transitive dependency: transitive
description: description:

View File

@ -29,6 +29,8 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations:
sdk: flutter
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
@ -81,7 +83,7 @@ dependencies:
circular_menu: ^2.0.1 circular_menu: ^2.0.1
camera: ^0.10.0+3 camera: ^0.10.0+3
camera_camera: ^3.0.0 camera_camera: ^3.0.0
intl: ^0.18.1 intl: ^0.19.0 #^0.18.1
modal_bottom_sheet: ^3.0.0-pre modal_bottom_sheet: ^3.0.0-pre
connectivity_plus: ^5.0.2 connectivity_plus: ^5.0.2
flutter_map_tile_caching: ^9.0.0-dev.5 flutter_map_tile_caching: ^9.0.0-dev.5