ほぼ完成:QRcodeトライ

This commit is contained in:
2024-08-01 20:08:12 +09:00
parent 3d7a5ae0c1
commit ee007795b9
25 changed files with 3214 additions and 1121 deletions

File diff suppressed because it is too large Load Diff

View File

@ -11,9 +11,11 @@ PODS:
- Flutter
- flutter_keyboard_visibility (0.0.1):
- Flutter
- FMDB (2.7.10):
- FMDB/standard (= 2.7.10)
- FMDB/standard (2.7.10)
- FMDB (2.7.12):
- FMDB/standard (= 2.7.12)
- FMDB/Core (2.7.12)
- FMDB/standard (2.7.12):
- FMDB/Core
- geolocator_apple (1.2.0):
- Flutter
- image_gallery_saver (2.0.2):
@ -35,7 +37,7 @@ PODS:
- qr_code_scanner (0.2.0):
- Flutter
- MTBBarcodeScanner
- ReachabilitySwift (5.2.1)
- ReachabilitySwift (5.2.3)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
@ -121,7 +123,7 @@ SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_compass: cbbd285cea1584c7ac9c4e0c3e1f17cbea55e855
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
FMDB: eae540775bf7d0c87a5af926ae37af69effe5a19
FMDB: 728731dd336af3936ce00f91d9d8495f5718a0e6
geolocator_apple: 9157311f654584b9bb72686c55fc02a97b73f461
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
@ -132,7 +134,7 @@ SPEC CHECKSUMS:
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
pointer_interceptor_ios: 9280618c0b2eeb80081a343924aa8ad756c21375
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
ReachabilitySwift: 5ae15e16814b5f9ef568963fb2c87aeb49158c66
ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
sqflite: 50a33e1d72bd59ee092a519a35d107502757ebed
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812

View File

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 60;
objects = {
/* Begin PBXBuildFile section */
@ -398,11 +398,11 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 480;
CURRENT_PROJECT_VERSION = 486;
DEVELOPMENT_TEAM = UMNEWT25JR;
ENABLE_BITCODE = NO;
FLUTTER_BUILD_NAME = 4.8.0;
FLUTTER_BUILD_NUMBER = 480;
FLUTTER_BUILD_NAME = 4.8.6;
FLUTTER_BUILD_NUMBER = 486;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "岐阜ナビ";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness";
@ -411,7 +411,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.8.0;
MARKETING_VERSION = 4.8.6;
PRODUCT_BUNDLE_IDENTIFIER = com.dvox.gifunavi;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -539,11 +539,11 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 480;
CURRENT_PROJECT_VERSION = 486;
DEVELOPMENT_TEAM = UMNEWT25JR;
ENABLE_BITCODE = NO;
FLUTTER_BUILD_NAME = 4.8.0;
FLUTTER_BUILD_NUMBER = 480;
FLUTTER_BUILD_NAME = 4.8.6;
FLUTTER_BUILD_NUMBER = 486;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "岐阜ナビ";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness";
@ -552,7 +552,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.8.0;
MARKETING_VERSION = 4.8.6;
PRODUCT_BUNDLE_IDENTIFIER = com.dvox.gifunavi;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -571,11 +571,11 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 480;
CURRENT_PROJECT_VERSION = 486;
DEVELOPMENT_TEAM = UMNEWT25JR;
ENABLE_BITCODE = NO;
FLUTTER_BUILD_NAME = 4.8.0;
FLUTTER_BUILD_NUMBER = 480;
FLUTTER_BUILD_NAME = 4.8.6;
FLUTTER_BUILD_NUMBER = 486;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "岐阜ナビ";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness";
@ -584,7 +584,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.8.0;
MARKETING_VERSION = 4.8.6;
PRODUCT_BUNDLE_IDENTIFIER = com.dvox.gifunavi;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert'; // この行を追加または確認
import 'package:flutter/material.dart';
import 'package:get/get.dart';
@ -10,6 +11,9 @@ import 'package:rogapp/services/external_service.dart';
import 'package:rogapp/utils/const.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
import 'package:http/http.dart' as http; // この行を追加
// 関数 getTagText は、特定の条件に基づいて文字列から特定の部分を抽出し、返却するためのものです。
// 関数は2つのパラメータを受け取り、条件分岐を通じて結果を返します。
//
@ -613,235 +617,108 @@ class StartRogaining extends StatelessWidget {
// 完了ボタンをタップすると、購入ポイントの処理が行われます。
// 購入なしボタンをタップすると、購入ポイントがキャンセルされます。
//
class BuyPointCamera extends StatelessWidget {
BuyPointCamera({Key? key, required this.destination}) : super(key: key);
class BuyPointCamera extends StatefulWidget {
final Destination destination;
Destination destination;
const BuyPointCamera({Key? key, required this.destination}) : super(key: key);
DestinationController destinationController =
Get.find<DestinationController>();
@override
_BuyPointCameraState createState() => _BuyPointCameraState();
}
class _BuyPointCameraState extends State<BuyPointCamera> {
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
QRViewController? controller;
bool isQRScanned = false;
final DestinationController destinationController = Get.find<DestinationController>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(
"${destination.sub_loc_id} : ${destination.name}",
),
),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Obx(
() => Container(
width: MediaQuery.of(context).size.width,
height: 370,
decoration: BoxDecoration(
image: DecorationImage(
// 要修正getReceiptImage関数の戻り値がnullの場合のエラーハンドリングが不十分です。適切なデフォルト画像を表示するなどの処理を追加してください。
//
image: getReceiptImage(), fit: BoxFit.cover)),
),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(getTagText(true, destination.tags)),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Wrap(
spacing: 16.0,
runSpacing: 8.0,
children: [
Obx(() => ElevatedButton(
onPressed: () {
destinationController.openCamera(context, destination);
},
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
padding: const EdgeInsets.all(20),
backgroundColor: destinationController.photos.isEmpty
? Colors.red
: Colors.grey[300],
),
child: destinationController.photos.isEmpty
? const Text("撮影",
style: TextStyle(color: Colors.white))
: const Text("再撮影",
style: TextStyle(color: Colors.black)),
)),
ElevatedButton(
onPressed: () async {
await destinationController.cancelBuyPoint(destination);
Navigator.of(Get.context!).pop();
destinationController.rogainingCounted.value = true;
destinationController.skipGps = false;
destinationController.isPhotoShoot.value = false;
},
child: const Text("買い物なし")),
Obx(() => destinationController.photos.isNotEmpty
? ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red),
onPressed: () async {
await destinationController.makeBuyPoint(
destination,
destinationController.photos[0].path);
Get.back();
destinationController.rogainingCounted.value = true;
destinationController.skipGps = false;
destinationController.isPhotoShoot.value = false;
Get.snackbar("お買い物加点を行いました",
"${destination.sub_loc_id} : ${destination.name}",
backgroundColor: Colors.green,
colorText: Colors.white);
},
child: const Text("完了",
style: TextStyle(color: Colors.white)))
: Container())
],
),
),
],
),
),
);
}
}
/*
class BuyPointCamera extends StatelessWidget {
BuyPointCamera({Key? key, required this.destination}) : super(key: key);
Destination destination;
DestinationController destinationController =
Get.find<DestinationController>();
@override
Widget build(BuildContext context) {
//print("in camera purchase 1 ${destinationController.isInRog.value}");
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(
"${destination.sub_loc_id} : ${destination.name}",
),
title: Text("${widget.destination.sub_loc_id} : ${widget.destination.name}"),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
Expanded(
flex: 4,
child: QRView(
key: qrKey,
onQRViewCreated: _onQRViewCreated,
),
),
Expanded(
flex: 1,
child: Center(
child: Obx(
() => Container(
width: MediaQuery.of(context).size.width,
height: 370,
decoration: BoxDecoration(
image: DecorationImage(
// 要修正getReceiptImage関数の戻り値がnullの場合のエラーハンドリングが不十分です。適切なデフォルト画像を表示するなどの処理を追加してください。
//
image: getReceiptImage(), fit: BoxFit.cover)),
child: Text('QRコードをスキャンしてください'),
),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(getTagText(true, destination.tags)),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: () {
// print(
// "in camera purchase 2 ${destinationController.isInRog.value}");
destinationController.openCamera(
context, destination);
},
child: destinationController.photos.isNotEmpty
? const Text("再撮影")
: const Text("撮影")),
const SizedBox(
width: 10,
),
ElevatedButton(
onPressed: () async {
await destinationController
.cancelBuyPoint(destination);
Navigator.of(Get.context!).pop();
//Get.back();
destinationController.rogainingCounted.value = true;
destinationController.skipGps = false;
destinationController.isPhotoShoot.value = false;
},
child: const Text("買い物なし"))
],
)),
Obx(() => destinationController.photos.isNotEmpty
? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// ElevatedButton(
// style: ElevatedButton.styleFrom(
// backgroundColor: Colors.red),
// onPressed: () async {},
// child: const Text("買物なし")),
// const SizedBox(
// width: 10,
// ),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red),
onPressed: () async {
// print(
// "in camera purchase 3 ${destinationController.isInRog.value}");
await destinationController.makeBuyPoint(
destination,
destinationController.photos[0].path);
Get.back();
// print(
// "in camera purchase 4 ${destinationController.isInRog.value}");
destinationController.rogainingCounted.value =
true;
destinationController.skipGps = false;
destinationController.isPhotoShoot.value = false;
Get.snackbar("お買い物加点を行いました",
"${destination.sub_loc_id} : ${destination.name}",
backgroundColor: Colors.green,
colorText: Colors.white
);
},
child: const Text("完了"))
],
)
: Container())
],
),
],
),
);
}
void _onQRViewCreated(QRViewController controller) {
this.controller = controller;
controller.scannedDataStream.listen((scanData) {
if (!isQRScanned && scanData.code != null && scanData.code!.startsWith('https://rogaining.sumasen.net/api/activate_buy_point/')) {
isQRScanned = true;
_activateBuyPoint(scanData.code!);
}
*/
});
}
void _activateBuyPoint(String qrCode) async {
final IndexController indexController = Get.find<IndexController>();
final userId = indexController.currentUser[0]["user"]["id"];
final token = indexController.currentUser[0]["token"];
final teamName = indexController.currentUser[0]["user"]['team_name'];
final eventCode = indexController.currentUser[0]["user"]["event_code"];
//final cpNumber = destinationController.currentDestinationFeature[0].cp;
final cpNumber = widget.destination.cp;
try {
final response = await http.post(
Uri.parse('https://rogaining.sumasen.net/api/activate_buy_point/'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Token $token',
},
body: jsonEncode({
'user_id': userId,
'team_name': teamName,
'event_code': eventCode,
'cp_number': cpNumber,
'qr_code': qrCode,
}),
);
if (response.statusCode == 200) {
Get.snackbar('成功', 'お買い物ポイントが有効化されました');
Navigator.of(context).pop();
} else {
Get.snackbar('エラー', 'お買い物ポイントの有効化に失敗しました');
}
} catch (e) {
Get.snackbar('エラー', 'ネットワークエラーが発生しました');
} finally {
isQRScanned = false;
}
}
@override
void dispose() {
controller?.dispose();
super.dispose();
}
}
class QRCodeScannerPage extends StatefulWidget {

View File

@ -1028,7 +1028,7 @@ class DestinationController extends GetxController {
print("An error occurred: $e");
// await checkForCheckin();
} finally {
await Future.delayed(const Duration(seconds: 5)); // 一定時間待機してから再帰呼び出し
await Future.delayed(const Duration(seconds: 1)); // 一定時間待機してから再帰呼び出し
//print("--- End of checkForCheckin function, calling recursively ---");
unawaited( checkForCheckin() );
}

View File

@ -104,7 +104,14 @@ class DrawerPage extends StatelessWidget {
Get.toNamed(AppPages.EVENT_ENTRY);
},
),
ListTile(
leading: const Icon(Icons.person),
title: Text("個人情報の修正"),
onTap: () {
Get.back(); // Close the drawer
Get.toNamed(AppPages.USER_DETAILS_EDIT);
},
),
Obx(() => indexController.currentUser.isEmpty
? ListTile(

View File

@ -69,6 +69,7 @@ class EntryController extends GetxController {
void initializeEditMode(Entry entry) {
currentEntry.value = entry;
selectedEvent.value = entry.event;
selectedTeam.value = entry.team;
selectedCategory.value = entry.category;
@ -169,10 +170,14 @@ class EntryController extends GetxController {
}
Future<void> updateEntry() async {
if (currentEntry.value == null) return;
if (currentEntry.value == null) {
Get.snackbar('Error', 'No entry selected for update');
return;
}
try {
final updatedEntry = await _apiService.updateEntry(
currentEntry.value!.id,
currentEntry.value!.team.id,
selectedEvent.value!.id,
selectedCategory.value!.id,
selectedDate.value!,
@ -189,7 +194,10 @@ class EntryController extends GetxController {
}
Future<void> deleteEntry() async {
if (currentEntry.value == null) return;
if (currentEntry.value == null) {
Get.snackbar('Error', 'No entry selected for deletion');
return;
}
try {
await _apiService.deleteEntry(currentEntry.value!.id);
entries.removeWhere((entry) => entry.id == currentEntry.value!.id);

View File

@ -90,21 +90,40 @@ class EntryDetailPage extends GetView<EntryController> {
},
),
SizedBox(height: 32),
if (mode == 'new')
ElevatedButton(
child: Text(mode == 'new' ? 'エントリーを作成' : 'エントリーを更新'),
onPressed: () {
if (mode == 'new') {
controller.createEntry();
} else {
controller.updateEntry();
}
},
child: Text('エントリーを作成'),
onPressed: () => controller.createEntry(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
minimumSize: Size(double.infinity, 50),
),
if (mode == 'edit' && controller.isOwner())
ElevatedButton(
)
else
Row(
children: [
Expanded(
child: ElevatedButton(
child: Text('エントリーを削除'),
onPressed: () => controller.deleteEntry(),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
minimumSize: Size(0, 50),
),
),
),
SizedBox(width: 16),
Expanded(
child: ElevatedButton(
child: Text('エントリーを更新'),
onPressed: () => controller.updateEntry(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.lightBlue,
minimumSize: Size(0, 50),
),
),
),
],
),
],
),

View File

@ -18,18 +18,30 @@ class EntryListPage extends GetView<EntryController> {
),
],
),
body: Obx(() => ListView.builder(
itemCount: controller.entries.length,
body: Obx((){
// エントリーを日付昇順にソート
final sortedEntries = controller.entries.toList()
..sort((a, b) => (a.date ?? DateTime(0)).compareTo(b.date ?? DateTime(0)));
return ListView.builder(
itemCount: sortedEntries.length,
itemBuilder: (context, index) {
final entry = controller.entries[index];
final entry = sortedEntries[index];
return ListTile(
title: Text(entry.event?.eventName ?? 'イベント未設定'),
subtitle: Text('${entry.team?.teamName ?? 'チーム未設定'} - ${entry.category?.categoryName ?? 'カテゴリ未設定'}'),
trailing: Text(entry.date?.toString().substring(0, 10) ?? '日付未設定'),
onTap: () => Get.toNamed(AppPages.ENTRY_DETAIL, arguments: {'mode': 'edit', 'entry': entry}),
subtitle: Text(
'${entry.team?.teamName ?? 'チーム未設定'} - ${entry.category
?.categoryName ?? 'カテゴリ未設定'}'),
trailing: Text(
entry.date?.toString().substring(0, 10) ?? '日付未設定'),
onTap: () =>
Get.toNamed(AppPages.ENTRY_DETAIL,
arguments: {'mode': 'edit', 'entry': entry}),
);
},
)),
);
}),
);
}
}

View File

@ -12,6 +12,7 @@ import 'package:latlong2/latlong.dart';
import 'package:rogapp/model/destination.dart';
import 'package:rogapp/model/entry.dart';
import 'package:rogapp/pages/destination/destination_controller.dart';
import 'package:rogapp/pages/team/team_controller.dart';
import 'package:rogapp/routes/app_pages.dart';
import 'package:rogapp/services/auth_service.dart';
import 'package:rogapp/services/location_service.dart';
@ -20,9 +21,12 @@ import 'package:rogapp/widgets/debug_widget.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:rogapp/services/api_service.dart';
import 'package:rogapp/model/user.dart';
import '../../main.dart';
import 'package:rogapp/main.dart';
import 'package:rogapp/widgets/helper_dialog.dart';
class IndexController extends GetxController with WidgetsBindingObserver {
List<GeoJSONFeatureCollection> locations = <GeoJSONFeatureCollection>[].obs;
@ -353,6 +357,12 @@ class IndexController extends GetxController with WidgetsBindingObserver {
// ログイン成功後、api_serviceを初期化
await Get.putAsync(() => ApiService().init());
// ユーザー情報の完全性をチェック
if (await checkUserInfoComplete()) {
Get.offAllNamed(AppPages.INDEX);
} else {
Get.offAllNamed(AppPages.USER_DETAILS_EDIT);
}
} else {
logManager.addOperationLog("User failed login : ${email} , ${password}.");
@ -372,6 +382,14 @@ class IndexController extends GetxController with WidgetsBindingObserver {
});
}
Future<bool> checkUserInfoComplete() async {
final user = await ApiService.to.getCurrentUser();
return user.firstname.isNotEmpty &&
user.lastname.isNotEmpty &&
user.dateOfBirth != null &&
user.female != null;
}
// 要検討:エラーハンドリングが行われていますが、エラーメッセージをローカライズすることを検討してください。
//
void changePassword(
@ -507,7 +525,7 @@ class IndexController extends GetxController with WidgetsBindingObserver {
saveToDevice(currentUser[0]["token"]);
}
isLoading.value = false;
loadLocationsBound( currentUser[0]["user"]["even_code"]);
loadLocationsBound( currentUser[0]["user"]["event_code"]);
if (currentUser.isNotEmpty) {
rogMode.value = 0;
restoreGame();
@ -752,4 +770,23 @@ class IndexController extends GetxController with WidgetsBindingObserver {
// 例: 現在の位置情報を再取得し、マップを更新する
loadLocationsBound( eventCode );
}
Future<void> checkEntryData() async {
// エントリーデータの有無をチェックするロジック
final teamController = TeamController();
bool hasEntryData = teamController.checkIfUserHasEntryData();
if (!hasEntryData) {
await showHelperDialog(
'イベントに参加するには、チーム登録・メンバー登録及びエントリーが必要になります。',
'entry_check'
);
// ドロワーを表示するロジック
Get.toNamed('/drawer');
}
}
void updateCurrentUser(User updatedUser) {
currentUser[0]['user'] = updatedUser.toJson();
update();
}
}

View File

@ -23,8 +23,40 @@ import 'package:rogapp/utils/location_controller.dart';
// IndexPageクラスは、GetView<IndexController>を継承したStatelessWidgetです。このクラスは、アプリのメインページを表すウィジェットです。
//
class IndexPage extends GetView<IndexController> {
IndexPage({Key? key}) : super(key: key);
import 'package:rogapp/widgets/helper_dialog.dart';
class IndexPage extends StatefulWidget {
@override
_IndexPageState createState() => _IndexPageState();
}
class _IndexPageState extends State<IndexPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
//checkEntryAndShowHelper();
});
}
/*
void checkEntryAndShowHelper() async {
final hasEntry = await checkIfUserHasEntry(); // この関数は実装する必要があります
if (!hasEntry) {
showHelperDialog(
context,
'イベントに参加するには、チーム登録・メンバー登録及びエントリーが必要になります。',
'entry_helper',
showDoNotShowAgain: true,
);
}
}
*/
// class IndexPage extends GetView<IndexController> {
// IndexPage({Key? key}) : super(key: key);
// IndexControllerとDestinationControllerのインスタンスを取得しています。
//

View File

@ -2,18 +2,36 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rogapp/pages/index/index_controller.dart';
import 'package:rogapp/routes/app_pages.dart';
import 'package:rogapp/widgets/helper_dialog.dart';
// 要検討:ログインボタンとサインアップボタンの配色を見直すことを検討してください。現在の配色では、ボタンの役割がわかりにくい可能性があります。
// エラーメッセージをローカライズすることを検討してください。
// ログイン処理中にエラーが発生した場合のエラーハンドリングを追加することをお勧めします。
//
class LoginPage extends StatelessWidget {
class LoginPage extends StatefulWidget {
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
//class LoginPage extends StatelessWidget {
final IndexController indexController = Get.find<IndexController>();
TextEditingController emailController = TextEditingController();
TextEditingController passwordController = TextEditingController();
LoginPage({Key? key}) : super(key: key);
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
showHelperDialog(
'参加するにはユーザー登録が必要です。サインアップからユーザー登録してください。',
'login_page'
);
});
}
//LoginPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {

View File

@ -2,15 +2,30 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rogapp/pages/index/index_controller.dart';
import 'package:rogapp/routes/app_pages.dart';
import 'package:rogapp/widgets/helper_dialog.dart';
class RegisterPage extends StatelessWidget {
class RegisterPage extends StatefulWidget {
@override
_RegisterPageState createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
final IndexController indexController = Get.find<IndexController>();
TextEditingController emailController = TextEditingController();
TextEditingController passwordController = TextEditingController();
TextEditingController confirmPasswordController = TextEditingController();
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
final TextEditingController confirmPasswordController = TextEditingController();
RegisterPage({Key? key}) : super(key: key);
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
showHelperDialog(
'登録メールにアクティベーションメールが送信されます。メールにあるリンクをタップすると正式登録になります。',
'register_page'
);
});
}
@override
Widget build(BuildContext context) {
@ -21,191 +36,117 @@ class RegisterPage extends StatelessWidget {
elevation: 0,
backgroundColor: Colors.white,
leading: IconButton(
onPressed: () {
Navigator.pop(context);
},
icon: const Icon(
Icons.arrow_back_ios,
size: 20,
color: Colors.black,
)),
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.arrow_back_ios, size: 20, color: Colors.black),
),
),
body: SafeArea(
child: SingleChildScrollView(
child: SizedBox(
child: Container(
height: MediaQuery.of(context).size.height,
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
"sign_up".tr, // "サインアップ"
style: const TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
),
),
const SizedBox(
height: 20,
),
Text(
"create_account".tr, // アカウントを無料で作成します。
style: TextStyle(
fontSize: 15,
color: Colors.grey[700],
),
),
const SizedBox(
height: 30,
)
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
makeInput(label: "email".tr, controller: emailController), // メールアドレス
makeInput(
label: "password".tr,
controller: passwordController,
obsureText: true), // パスワード
makeInput(
label: "confirm_password".tr,
controller: confirmPasswordController,
obsureText: true) // パスワード再確認
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Container(
padding: const EdgeInsets.only(top: 3, left: 3),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40),
border: const Border(
bottom: BorderSide(color: Colors.black),
top: BorderSide(color: Colors.black),
right: BorderSide(color: Colors.black),
left: BorderSide(color: Colors.black))),
child: MaterialButton(
minWidth: double.infinity,
height: 60,
onPressed: () {
if (passwordController.text !=
confirmPasswordController.text) {
Get.snackbar(
"no_match".tr,
"password_does_not_match".tr,
backgroundColor: Colors.red,
colorText: Colors.white,
icon: const Icon(Icons.assistant_photo_outlined,
size: 40.0, color: Colors.blue),
snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 3),
// backgroundColor: Colors.yellow,
//icon:Image(image:AssetImage("assets/images/dora.png"))
);
}
if (emailController.text.isEmpty ||
passwordController.text.isEmpty) {
Get.snackbar(
"no_values".tr,
"email_and_password_required".tr,
backgroundColor: Colors.red,
colorText: Colors.white,
icon: const Icon(Icons.assistant_photo_outlined,
size: 40.0, color: Colors.blue),
snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 3),
//backgroundColor: Colors.yellow,
//icon:Image(image:AssetImage("assets/images/dora.png"))
);
return;
}
indexController.isLoading.value = true;
indexController.register(emailController.text,
passwordController.text, context);
},
color: Colors.redAccent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40)),
child: Text(
Text(
"sign_up".tr,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
Text(
"create_account".tr,
style: TextStyle(fontSize: 15, color: Colors.grey[700]),
),
const SizedBox(height: 30),
makeInput(label: "email".tr, controller: emailController),
makeInput(label: "password".tr, controller: passwordController, obsureText: true),
makeInput(label: "confirm_password".tr, controller: confirmPasswordController, obsureText: true),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _handleRegister,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(40)),
minimumSize: const Size(double.infinity, 60),
),
child: Text("sign_up".tr, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16)),
),
),
const SizedBox(
height: 20,
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(child: Text("already_have_account".tr)),
TextButton(
onPressed: () {
Get.toNamed(AppPages.LOGIN);
},
child: Text(
"login".tr,
style: TextStyle(
fontWeight: FontWeight.w600, fontSize: 18),
),
onPressed: () => Get.toNamed(AppPages.LOGIN),
child: Text("login".tr, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 18)),
),
],
)
],
),
],
),
),
),
),
);
}
void _handleRegister() {
if (passwordController.text != confirmPasswordController.text) {
_showErrorSnackbar("no_match".tr, "password_does_not_match".tr);
return;
}
if (emailController.text.isEmpty || passwordController.text.isEmpty) {
_showErrorSnackbar("no_values".tr, "email_and_password_required".tr);
return;
}
indexController.isLoading.value = true;
try {
indexController.register(
emailController.text,
passwordController.text,
context
);
// 登録が成功したと仮定し、ログインページに遷移
Get.offNamed(AppPages.LOGIN);
} catch (error) {
_showErrorSnackbar("registration_error".tr, error.toString());
} finally {
indexController.isLoading.value = false;
}
}
void _showErrorSnackbar(String title, String message) {
Get.snackbar(
title,
message,
backgroundColor: Colors.red,
colorText: Colors.white,
icon: const Icon(Icons.error_outline, size: 40.0, color: Colors.white),
snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 3),
);
}
}
Widget makeInput(
{label, required TextEditingController controller, obsureText = false}) {
Widget makeInput({required String label, required TextEditingController controller, bool obsureText = false}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w400, color: Colors.black87),
),
const SizedBox(
height: 5,
),
Text(label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w400, color: Colors.black87)),
const SizedBox(height: 5),
TextField(
controller: controller,
obscureText: obsureText,
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 0, horizontal: 10),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: (Colors.grey[400])!,
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10),
enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey[400]!)),
border: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey[400]!)),
),
),
border: OutlineInputBorder(
borderSide: BorderSide(color: (Colors.grey[400])!),
),
),
),
const SizedBox(
height: 30,
)
const SizedBox(height: 20),
],
);
}

View File

@ -0,0 +1,182 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rogapp/model/user.dart';
import 'package:rogapp/routes/app_pages.dart';
import 'package:rogapp/services/api_service.dart';
import 'package:rogapp/pages/index/index_controller.dart';
class UserDetailsEditPage extends StatefulWidget {
@override
_UserDetailsEditPageState createState() => _UserDetailsEditPageState();
}
class _UserDetailsEditPageState extends State<UserDetailsEditPage> {
final _formKey = GlobalKey<FormState>();
final IndexController indexController = Get.find<IndexController>();
late User _user;
final TextEditingController _firstnameController = TextEditingController();
final TextEditingController _lastnameController = TextEditingController();
final TextEditingController _dateOfBirthController = TextEditingController();
late bool _female;
@override
void initState() {
super.initState();
_user = User.fromJson(indexController.currentUser[0]['user']);
_firstnameController.text = _user.firstname;
_lastnameController.text = _user.lastname;
_dateOfBirthController.text = _user.dateOfBirth != null
? '${_user.dateOfBirth!.year}/${_user.dateOfBirth!.month.toString().padLeft(2, '0')}/${_user.dateOfBirth!.day.toString().padLeft(2, '0')}'
: '';
_female = _user.female;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('個人情報の修正'),
automaticallyImplyLeading: false,
),
body: Form(
key: _formKey,
child: ListView(
padding: EdgeInsets.all(16.0),
children: [
TextFormField(
controller: _lastnameController,
decoration: InputDecoration(
labelText: '',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '姓を入力してください';
}
return null;
},
),
SizedBox(height: 16),
TextFormField(
controller: _firstnameController,
decoration: InputDecoration(
labelText: '',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '名を入力してください';
}
return null;
},
),
SizedBox(height: 16),
TextFormField(
controller: _dateOfBirthController,
decoration: InputDecoration(
labelText: '生年月日 (YYYY/MM/DD)',
border: OutlineInputBorder(),
hintText: 'YYYY/MM/DD',
),
keyboardType: TextInputType.datetime,
validator: (value) {
if (value == null || value.isEmpty) {
return '生年月日を入力してください';
}
if (!RegExp(r'^\d{4}/\d{2}/\d{2}$').hasMatch(value)) {
return '正しい形式で入力してください (YYYY/MM/DD)';
}
final date = DateTime.tryParse(value.replaceAll('/', '-'));
if (date == null) {
return '有効な日付を入力してください';
}
if (date.isAfter(DateTime.now())) {
return '未来の日付は入力できません';
}
return null;
},
),
SizedBox(height: 16),
SwitchListTile(
title: Text('性別'),
subtitle: Text(_female ? '女性' : '男性'),
value: _female,
onChanged: (bool value) {
setState(() {
_female = value;
});
},
),
SizedBox(height: 16),
TextFormField(
initialValue: _user.email,
decoration: InputDecoration(
labelText: 'メールアドレス',
border: OutlineInputBorder(),
),
enabled: false,
),
SizedBox(height: 16),
SwitchListTile(
title: Text('アクティブ状態'),
value: _user.isActive,
onChanged: null,
),
SizedBox(height: 32),
ElevatedButton(
child: Text('更新'),
onPressed: _updateUserDetails,
),
],
),
),
);
}
void _updateUserDetails() async {
if (_formKey.currentState!.validate()) {
final dateOfBirth = DateTime.tryParse(_dateOfBirthController.text.replaceAll('/', '-'));
if (dateOfBirth == null || dateOfBirth.isAfter(DateTime.now())) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('生年月日が無効です', style: TextStyle(color: Colors.red))),
);
return;
}
User updatedUser = User(
id: _user.id,
email: _user.email,
firstname: _firstnameController.text,
lastname: _lastnameController.text,
dateOfBirth: dateOfBirth,
female: _female,
isActive: _user.isActive,
);
try {
bool success = await ApiService.updateUserDetail(updatedUser, indexController.currentUser[0]['token']);
if (success) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('個人情報が更新されました')),
);
indexController.updateCurrentUser(updatedUser);
Get.offAllNamed(AppPages.INDEX);
//Get.back();
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('更新に失敗しました', style: TextStyle(color: Colors.red))),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('エラーが発生しました: $e', style: TextStyle(color: Colors.red))),
);
}
}
}
}

View File

@ -87,31 +87,42 @@ class _MemberDetailPageState extends State<MemberDetailPage> {
return Scaffold(
appBar: AppBar(
title: Text(mode == 'new' ? 'メンバー追加' : 'メンバー詳細'),
actions: [
IconButton(
icon: Icon(Icons.save),
onPressed: () async {
await controller.saveMember();
Get.back(result: true);
},
),
],
),
body: Obx(() {
if (controller.isLoading.value) {
return Center(child: CircularProgressIndicator());
}
// TextEditingControllerをObxの中で作成
final emailController = TextEditingController(text: controller.email.value);
// カーソル位置を保持
emailController.selection = TextSelection.fromPosition(
TextPosition(offset: controller.email.value.length),
);
return SingleChildScrollView(
return Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (mode == 'new')
TextField(
TextFormField(
decoration: InputDecoration(labelText: 'メールアドレス'),
onChanged: (value) => controller.email.value = value,
controller: TextEditingController(text: controller.email.value),
//onChanged: (value) => controller.email.value = value,
onChanged: (value) {
controller.email.value = value;
// カーソル位置を更新
emailController.selection = TextSelection.fromPosition(
TextPosition(offset: value.length),
);
},
controller: emailController,
//controller: TextEditingController(text: controller.email.value),
keyboardType: TextInputType.emailAddress, // メールアドレス用のキーボードを表示
autocorrect: false, // 自動修正を無効化
enableSuggestions: false,
)
else if (controller.isDummyEmail)
Text('メールアドレス: (メアド無し)')
@ -119,12 +130,12 @@ class _MemberDetailPageState extends State<MemberDetailPage> {
Text('メールアドレス: ${controller.email.value}'),
if (controller.email.value.isEmpty || mode == 'edit') ...[
TextField(
TextFormField(
decoration: InputDecoration(labelText: ''),
onChanged: (value) => controller.lastname.value = value,
controller: TextEditingController(text: controller.lastname.value),
),
TextField(
TextFormField(
decoration: InputDecoration(labelText: ''),
onChanged: (value) => controller.firstname.value = value,
controller: TextEditingController(text: controller.firstname.value),
@ -157,16 +168,16 @@ class _MemberDetailPageState extends State<MemberDetailPage> {
onChanged: (value) => controller.female.value = value,
),
],
// 招待メール再送信ボタン通常のEmailで未承認の場合のみ
if (!controller.isDummyEmail && !controller.isApproved)
ElevatedButton(
child: Text('招待メールを再送信'),
onPressed: () => controller.resendInvitation(),
]),
),
// メンバー削除ボタン
),
Padding(
padding: EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
child: Text('メンバーから削除'),
child: Text('削除'),
onPressed: () async {
final confirmed = await Get.dialog<bool>(
AlertDialog(
@ -189,17 +200,38 @@ class _MemberDetailPageState extends State<MemberDetailPage> {
Get.back(result: true);
}
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
),
if (!controller.isDummyEmail && !controller.isApproved)
ElevatedButton(
child: Text('招待再送信'),
onPressed: () => controller.resendInvitation(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
),
),
ElevatedButton(
child: Text('保存・招待'),
onPressed: () async {
await controller.saveMember();
Get.back(result: true);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
),
],
),
),
],
);
}),
);
}
@override

View File

@ -1,5 +1,6 @@
// lib/controllers/team_controller.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rogapp/model/team.dart';
import 'package:rogapp/model/category.dart';
@ -27,6 +28,7 @@ class TeamController extends GetxController {
super.onInit();
try {
_apiService = Get.find<ApiService>();
isLoading.value = true;
await fetchCategories();
await Future.wait([
@ -67,8 +69,13 @@ class TeamController extends GetxController {
void resetForm() {
selectedTeam.value = null;
teamName.value = '';
selectedCategory.value = categories.isNotEmpty ? categories.first : null;
teamMembers.clear();
if (categories.isNotEmpty) {
selectedCategory.value = categories.first;
} else {
selectedCategory.value = null;
// カテゴリが空の場合、エラーメッセージをセット
error.value = 'カテゴリデータが取得できませんでした。';
} teamMembers.clear();
}
void cleanupForNavigation() {
@ -93,6 +100,14 @@ class TeamController extends GetxController {
}
}
bool checkIfUserHasEntryData(){
if (teams.isEmpty) {
return false;
}else {
return true;
}
}
Future<void> fetchCategories() async {
try {
final fetchedCategories = await _apiService.getCategories();
@ -115,6 +130,17 @@ class TeamController extends GetxController {
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;
}
@ -125,11 +151,51 @@ class TeamController extends GetxController {
}
Future<void> deleteTeam(int teamId) async {
bool confirmDelete = await Get.dialog(
AlertDialog(
title: Text('チーム削除の確認'),
content: Text('このチームとそのすべてのメンバーを削除しますか?この操作は取り消せません。'),
actions: [
TextButton(
child: Text('キャンセル'),
onPressed: () => Get.back(result: false),
),
TextButton(
child: Text('削除'),
onPressed: () => Get.back(result: true),
),
],
),
) ?? false;
if (confirmDelete) {
try {
// まず、チームのメンバーを全て削除
await _apiService.deleteAllTeamMembers(teamId);
// その後、チームを削除
await _apiService.deleteTeam(teamId);
// ローカルのチームリストを更新
teams.removeWhere((team) => team.id == teamId);
/*
Get.snackbar(
'成功',
'チームとそのメンバーが削除されました',
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
*/
// チームリスト画面に戻る
Get.back();
} catch (e) {
print('Error deleting team: $e');
print('Error deleting team and members: $e');
Get.snackbar('エラー', 'チームとメンバーの削除に失敗しました');
}
}
}
@ -237,4 +303,30 @@ class TeamController extends GetxController {
}
}
List<NewCategory> getFilteredCategories() {
//List<User> teamMembers = getCurrentTeamMembers();
return categories.where((category) {
return isCategoryValid(category, teamMembers);
}).toList();
}
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;
}
}

View File

@ -126,15 +126,26 @@ class _TeamDetailPageState extends State<TeamDetailPage> {
),
SizedBox(height: 16),
DropdownButtonFormField<NewCategory>(
Obx(() {
if (controller.categories.isEmpty) {
return Text('カテゴリデータを読み込めませんでした。', style: TextStyle(color: Colors.red));
}
return DropdownButtonFormField<NewCategory>(
decoration: InputDecoration(labelText: 'カテゴリ'),
value: controller.selectedCategory.value,
items: controller.categories.map((category) => DropdownMenuItem(
value: category,
child: Text(category.categoryName),
)).toList(),
/*
items: controller.getFilteredCategories().map((category) => DropdownMenuItem(
value: category,
child: Text(category.categoryName),
)).toList(),
*/
onChanged: (value) => controller.updateCategory(value),
),
);
}),
if (mode.value == 'edit')
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
@ -160,7 +171,7 @@ class _TeamDetailPageState extends State<TeamDetailPage> {
? '${member.lastname} ${member.firstname}'
: member.isActive
? '${member.lastname} ${member.firstname}'
: '${member.email?.split('@')[0] ?? ''}(未承認)';
: '${member.email?.split('@')[0] ?? ''}'; //(未承認)';
return ListTile(
title: Text(displayName),
subtitle: isDummyEmail ? Text('Email未設定') : null,

View File

@ -39,7 +39,7 @@ import 'package:rogapp/pages/entry/entry_binding.dart';
import 'package:rogapp/pages/entry/event_entries_page.dart';
import 'package:rogapp/pages/entry/event_entries_binding.dart';
import 'package:rogapp/pages/register/user_detail_page.dart';
part 'app_routes.dart';
@ -75,6 +75,8 @@ class AppPages {
static const ENTRY_LIST = Routes.ENTRY_LIST;
static const ENTRY_DETAIL = Routes.ENTRY_DETAIL;
static const EVENT_ENTRY = Routes.EVENT_ENTRIES;
static const USER_DETAILS_EDIT = Routes.USER_DETAILS_EDIT;
static final routes = [
GetPage(
@ -176,7 +178,10 @@ class AppPages {
page: () => EventEntriesPage(),
binding: EventEntriesBinding(),
),
GetPage(
name: Routes.USER_DETAILS_EDIT,
page: () => UserDetailsEditPage(),
),
];
}

View File

@ -36,4 +36,6 @@ abstract class Routes {
static const ENTRY_DETAIL = '/entry-detail';
static const EVENT_ENTRIES = '/event-entries';
static const USER_DETAILS_EDIT = '/user-details-edit';
}

View File

@ -75,13 +75,13 @@ class ApiService extends GetxService{
if (response.statusCode == 200) {
// UTF-8でデコード
final decodedResponse = utf8.decode(response.bodyBytes);
print('User Response body: $decodedResponse');
//print('User Response body: $decodedResponse');
List<dynamic> teamsJson = json.decode(decodedResponse);
List<Team> teams = [];
for (var teamJson in teamsJson) {
print('\nTeam Data:');
_printDataComparison(teamJson, Team);
//print('\nTeam Data:');
//_printDataComparison(teamJson, Team);
teams.add(Team.fromJson(teamJson));
}
return teams;
@ -114,8 +114,8 @@ class ApiService extends GetxService{
List<NewCategory> categories = [];
for (var categoryJson in categoriesJson) {
print('\nCategory Data:');
_printDataComparison(categoryJson, NewCategory);
//print('\nCategory Data:');
//_printDataComparison(categoryJson, NewCategory);
categories.add(NewCategory.fromJson(categoryJson));
}
@ -143,11 +143,11 @@ class ApiService extends GetxService{
if (response.statusCode == 200) {
final decodedResponse = utf8.decode(response.bodyBytes);
print('User Response body: $decodedResponse');
//print('User Response body: $decodedResponse');
final jsonData = json.decode(decodedResponse);
print('\nUser Data Comparison:');
_printDataComparison(jsonData, User);
//print('\nUser Data Comparison:');
//_printDataComparison(jsonData, User);
return User.fromJson(jsonData);
} else {
@ -277,7 +277,11 @@ class ApiService extends GetxService{
headers: {'Authorization': 'Token $token','Content-Type': 'application/json; charset=UTF-8'},
);
if (response.statusCode != 204) {
if( response.statusCode == 400) {
final decodedResponse = utf8.decode(response.bodyBytes);
print('User Response body: $decodedResponse');
throw Exception('まだメンバーが残っているので、チームを削除できません。');
}else if (response.statusCode != 204) {
throw Exception('Failed to delete team');
}
}
@ -302,10 +306,18 @@ class ApiService extends GetxService{
}
}
Future<User> createTeamMember(int teamId, String? email, String firstname, String lastname, DateTime? dateOfBirth,bool? female) async {
Future<User> createTeamMember(int teamId, String? email, String? firstname, String? lastname, DateTime? dateOfBirth,bool? female) async {
init();
getToken();
// emailが値を持っている場合の処理
if (email != null && email.isNotEmpty) {
firstname ??= "dummy";
lastname ??= "dummy";
dateOfBirth ??= DateTime.now();
female ??= false;
}
String? formattedDateOfBirth;
if (dateOfBirth != null) {
formattedDateOfBirth = DateFormat('yyyy-MM-dd').format(dateOfBirth);
@ -379,6 +391,17 @@ class ApiService extends GetxService{
}
}
Future<void> deleteAllTeamMembers(int teamId) async {
final response = await http.delete(
Uri.parse('$baseUrl/teams/$teamId/members/destroy_all/?confirm=true'),
headers: {'Authorization': 'Token $token'},
);
if (response.statusCode != 200) {
throw Exception('Failed to delete team members');
}
}
Future<void> resendMemberInvitation(int memberId) async {
init();
getToken();
@ -503,7 +526,7 @@ class ApiService extends GetxService{
}
Future<Entry> updateEntry(int entryId, int eventId, int categoryId, DateTime date) async {
Future<Entry> updateEntry(int entryId, int teamId, int eventId, int categoryId, DateTime date) async {
init();
getToken();
@ -519,6 +542,7 @@ class ApiService extends GetxService{
'Content-Type': 'application/json; charset=UTF-8',
},
body: json.encode({
'team': teamId,
'event': eventId,
'category': categoryId,
'date': formattedDate,
@ -547,4 +571,41 @@ class ApiService extends GetxService{
throw Exception('Failed to delete entry');
}
}
static Future<bool> updateUserDetail(User user, String token) async {
String serverUrl = ConstValues.currentServer();
int? userid = user.id;
String url = '$serverUrl/api/userdetail/$userid/';
try {
String? formattedDate;
if (user.dateOfBirth != null) {
formattedDate = DateFormat('yyyy-MM-dd').format(user.dateOfBirth!);
}
final http.Response response = await http.put(
Uri.parse(url),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Token $token'
},
body: jsonEncode({
'firstname': user.firstname,
'lastname': user.lastname,
'date_of_birth': formattedDate,
'female': user.female,
}),
);
if (response.statusCode == 200) {
return true;
} else {
print('Update failed with status code: ${response.statusCode}');
return false;
}
} catch (e) {
print('Error in updateUserDetail: $e');
return false;
}
}
}

View File

@ -5,6 +5,9 @@ import 'package:get/get.dart';
import 'package:flutter/material.dart';
import '../utils/const.dart';
//import 'package:rogapp/services/team_service.dart';
//import 'package:rogapp/services/member_service.dart';
class AuthService {
Future<AuthUser?> userLogin(String email, String password) async {
@ -131,6 +134,22 @@ class AuthService {
// ユーザー登録
//
/*
Future<void> registerUser(String email, String password, bool isFemale) async {
final user = await register(email, password);
if (user != null) {
final _teamController = TeamController();
_teamController.createTeam(String teamName, int categoryId) ;
final teamService = TeamService();
final memberService = MemberService();
final team = await teamService.createSoloTeam(user.id, isFemale);
await memberService.addMember(team.id, user.id);
}
}
*/
static Future<Map<String, dynamic>> register(
String email, String password) async {
Map<String, dynamic> cats = {};
@ -168,6 +187,7 @@ class AuthService {
return cats;
}
static Future<List<dynamic>?> userDetails(int userid) async {
List<dynamic> cats = [];
String serverUrl = ConstValues.currentServer();

View File

@ -4,6 +4,8 @@
class ConstValues{
static const container_svr = "http://container.intranet.sumasen.net:8100";
static const server_uri = "https://rogaining.intranet.sumasen.net";
//static const container_svr = "http://container.sumasen.net:8100";
//static const server_uri = "https://rogaining.sumasen.net";
static const dev_server = "http://localhost:8100";
static const dev_ip_server = "http://192.168.8.100:8100";
static const dev_home_ip_server = "http://172.20.10.9:8100";

View File

@ -311,7 +311,7 @@ class StringValues extends Translations{
'already_have_account': 'すでにアカウントをお持ちですか?',
'sign_up': 'サインアップ',
'create_account': 'アカウントを無料で作成します',
'confirm_password': 'パスワードを認証する',
'confirm_password': '確認用パスワード',
'cancel_checkin': 'チェックイン取消',
'go_here': 'ルート表示',
'cancel_route':'ルート消去',

View File

@ -0,0 +1,73 @@
// lib/widgets/helper_dialog.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
class HelperDialog extends StatefulWidget {
final String message;
final String screenKey;
const HelperDialog({Key? key, required this.message, required this.screenKey}) : super(key: key);
@override
_HelperDialogState createState() => _HelperDialogState();
}
class _HelperDialogState extends State<HelperDialog> {
bool _doNotShowAgain = false;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Row(
children: [
Icon(Icons.help_outline, color: Colors.blue),
SizedBox(width: 10),
Text('ヘルプ'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.message),
SizedBox(height: 20),
Row(
children: [
Checkbox(
value: _doNotShowAgain,
onChanged: (value) {
setState(() {
_doNotShowAgain = value!;
});
},
),
Text('この画面を二度と表示しない'),
],
),
],
),
actions: [
TextButton(
child: Text('OK'),
onPressed: () async {
if (_doNotShowAgain) {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('helper_${widget.screenKey}', false);
}
Get.back();
},
),
],
);
}
}
// ヘルパー画面を表示する関数
Future<void> showHelperDialog(String message, String screenKey) async {
final prefs = await SharedPreferences.getInstance();
final showHelper = prefs.getBool('helper_$screenKey') ?? true;
if (showHelper) {
Get.dialog(HelperDialog(message: message, screenKey: screenKey));
}
}

View File

@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 4.8.2+483
version: 4.8.5+485
environment:
sdk: ">=3.2.0 <4.0.0"