ほぼ完成:QRcodeトライ
This commit is contained in:
@ -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;
|
||||
|
||||
DestinationController destinationController =
|
||||
Get.find<DestinationController>();
|
||||
const BuyPointCamera({Key? key, required this.destination}) : super(key: key);
|
||||
|
||||
@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())
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
_BuyPointCameraState createState() => _BuyPointCameraState();
|
||||
}
|
||||
|
||||
|
||||
|
||||
class _BuyPointCameraState extends State<BuyPointCamera> {
|
||||
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
|
||||
QRViewController? controller;
|
||||
bool isQRScanned = false;
|
||||
|
||||
/*
|
||||
class BuyPointCamera extends StatelessWidget {
|
||||
BuyPointCamera({Key? key, required this.destination}) : super(key: key);
|
||||
|
||||
Destination destination;
|
||||
|
||||
DestinationController destinationController =
|
||||
Get.find<DestinationController>();
|
||||
final 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),
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: 370,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
// 要修正:getReceiptImage関数の戻り値がnullの場合のエラーハンドリングが不十分です。適切なデフォルト画像を表示するなどの処理を追加してください。
|
||||
//
|
||||
image: getReceiptImage(), fit: BoxFit.cover)),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: QRView(
|
||||
key: qrKey,
|
||||
onQRViewCreated: _onQRViewCreated,
|
||||
),
|
||||
),
|
||||
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())
|
||||
],
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Center(
|
||||
child: Text('QRコードをスキャンしてください'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
|
||||
@ -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() );
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -90,21 +90,40 @@ class EntryDetailPage extends GetView<EntryController> {
|
||||
},
|
||||
),
|
||||
SizedBox(height: 32),
|
||||
ElevatedButton(
|
||||
child: Text(mode == 'new' ? 'エントリーを作成' : 'エントリーを更新'),
|
||||
onPressed: () {
|
||||
if (mode == 'new') {
|
||||
controller.createEntry();
|
||||
} else {
|
||||
controller.updateEntry();
|
||||
}
|
||||
},
|
||||
),
|
||||
if (mode == 'edit' && controller.isOwner())
|
||||
if (mode == 'new')
|
||||
ElevatedButton(
|
||||
child: Text('エントリーを削除'),
|
||||
onPressed: () => controller.deleteEntry(),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
child: Text('エントリーを作成'),
|
||||
onPressed: () => controller.createEntry(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
minimumSize: Size(double.infinity, 50),
|
||||
),
|
||||
)
|
||||
else
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
child: Text('エントリーを削除'),
|
||||
onPressed: () => controller.deleteEntry(),
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -18,18 +18,30 @@ class EntryListPage extends GetView<EntryController> {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Obx(() => ListView.builder(
|
||||
itemCount: controller.entries.length,
|
||||
itemBuilder: (context, index) {
|
||||
final entry = controller.entries[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}),
|
||||
);
|
||||
},
|
||||
)),
|
||||
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 = 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}),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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のインスタンスを取得しています。
|
||||
//
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,150 +36,53 @@ 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,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
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(
|
||||
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(
|
||||
"sign_up".tr,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
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),
|
||||
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),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(child: Text("already_have_account".tr)),
|
||||
TextButton(
|
||||
onPressed: () => Get.toNamed(AppPages.LOGIN),
|
||||
child: Text("login".tr, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 18)),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -172,40 +90,63 @@ class RegisterPage extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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])!,
|
||||
),
|
||||
),
|
||||
border: 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]!)),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 30,
|
||||
)
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
182
lib/pages/register/user_detail_page.dart
Normal file
182
lib/pages/register/user_detail_page.dart
Normal 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))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -85,121 +85,153 @@ class _MemberDetailPageState extends State<MemberDetailPage> {
|
||||
home:Scaffold(
|
||||
*/
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(mode == 'new' ? 'メンバー追加' : 'メンバー詳細'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.save),
|
||||
onPressed: () async {
|
||||
await controller.saveMember();
|
||||
Get.back(result: true);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (mode == 'new')
|
||||
TextField(
|
||||
decoration: InputDecoration(labelText: 'メールアドレス'),
|
||||
onChanged: (value) => controller.email.value = value,
|
||||
controller: TextEditingController(text: controller.email.value),
|
||||
)
|
||||
else if (controller.isDummyEmail)
|
||||
Text('メールアドレス: (メアド無し)')
|
||||
else
|
||||
Text('メールアドレス: ${controller.email.value}'),
|
||||
|
||||
if (controller.email.value.isEmpty || mode == 'edit') ...[
|
||||
TextField(
|
||||
decoration: InputDecoration(labelText: '姓'),
|
||||
onChanged: (value) => controller.lastname.value = value,
|
||||
controller: TextEditingController(text: controller.lastname.value),
|
||||
),
|
||||
TextField(
|
||||
decoration: InputDecoration(labelText: '名'),
|
||||
onChanged: (value) => controller.firstname.value = value,
|
||||
controller: TextEditingController(text: controller.firstname.value),
|
||||
),
|
||||
// 生年月日
|
||||
if (controller.isDummyEmail || !controller.isOver18())
|
||||
ListTile(
|
||||
title: Text('生年月日'),
|
||||
subtitle: Text(controller.dateOfBirth.value != null
|
||||
? '${DateFormat('yyyy年MM月dd日').format(controller.dateOfBirth.value!)} (${controller.getAgeAndGrade()})'
|
||||
: '未設定'),
|
||||
onTap: () async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: controller.dateOfBirth.value ?? DateTime.now(),
|
||||
firstDate: DateTime(1900),
|
||||
lastDate: DateTime.now(),
|
||||
//locale: const Locale('ja', 'JP'),
|
||||
);
|
||||
if (date != null) controller.dateOfBirth.value = date;
|
||||
},
|
||||
)
|
||||
else
|
||||
Text('18歳以上'),
|
||||
|
||||
SwitchListTile(
|
||||
title: Text('性別'),
|
||||
subtitle: Text(controller.female.value ? '女性' : '男性'),
|
||||
value: controller.female.value,
|
||||
onChanged: (value) => controller.female.value = value,
|
||||
),
|
||||
],
|
||||
// 招待メール再送信ボタン(通常のEmailで未承認の場合のみ)
|
||||
if (!controller.isDummyEmail && !controller.isApproved)
|
||||
ElevatedButton(
|
||||
child: Text('招待メールを再送信'),
|
||||
onPressed: () => controller.resendInvitation(),
|
||||
),
|
||||
|
||||
// メンバー削除ボタン
|
||||
ElevatedButton(
|
||||
child: Text('メンバーから削除'),
|
||||
onPressed: () async {
|
||||
final confirmed = await Get.dialog<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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
|
||||
}),
|
||||
appBar: AppBar(
|
||||
title: Text(mode == 'new' ? 'メンバー追加' : 'メンバー詳細'),
|
||||
),
|
||||
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 Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (mode == 'new')
|
||||
TextFormField(
|
||||
decoration: InputDecoration(labelText: 'メールアドレス'),
|
||||
//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('メールアドレス: (メアド無し)')
|
||||
else
|
||||
Text('メールアドレス: ${controller.email.value}'),
|
||||
|
||||
if (controller.email.value.isEmpty || mode == 'edit') ...[
|
||||
TextFormField(
|
||||
decoration: InputDecoration(labelText: '姓'),
|
||||
onChanged: (value) => controller.lastname.value = value,
|
||||
controller: TextEditingController(text: controller.lastname.value),
|
||||
),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(labelText: '名'),
|
||||
onChanged: (value) => controller.firstname.value = value,
|
||||
controller: TextEditingController(text: controller.firstname.value),
|
||||
),
|
||||
// 生年月日
|
||||
if (controller.isDummyEmail || !controller.isOver18())
|
||||
ListTile(
|
||||
title: Text('生年月日'),
|
||||
subtitle: Text(controller.dateOfBirth.value != null
|
||||
? '${DateFormat('yyyy年MM月dd日').format(controller.dateOfBirth.value!)} (${controller.getAgeAndGrade()})'
|
||||
: '未設定'),
|
||||
onTap: () async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: controller.dateOfBirth.value ?? DateTime.now(),
|
||||
firstDate: DateTime(1900),
|
||||
lastDate: DateTime.now(),
|
||||
//locale: const Locale('ja', 'JP'),
|
||||
);
|
||||
if (date != null) controller.dateOfBirth.value = date;
|
||||
},
|
||||
)
|
||||
else
|
||||
Text('18歳以上'),
|
||||
|
||||
SwitchListTile(
|
||||
title: Text('性別'),
|
||||
subtitle: Text(controller.female.value ? '女性' : '男性'),
|
||||
value: controller.female.value,
|
||||
onChanged: (value) => controller.female.value = value,
|
||||
),
|
||||
],
|
||||
]),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
child: Text('削除'),
|
||||
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,
|
||||
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
|
||||
|
||||
@ -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 {
|
||||
try {
|
||||
await _apiService.deleteTeam(teamId);
|
||||
teams.removeWhere((team) => team.id == teamId);
|
||||
} catch (e) {
|
||||
print('Error deleting team: $e');
|
||||
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 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -126,15 +126,26 @@ class _TeamDetailPageState extends State<TeamDetailPage> {
|
||||
),
|
||||
|
||||
SizedBox(height: 16),
|
||||
DropdownButtonFormField<NewCategory>(
|
||||
decoration: InputDecoration(labelText: 'カテゴリ'),
|
||||
value: controller.selectedCategory.value,
|
||||
items: controller.categories.map((category) => DropdownMenuItem(
|
||||
value: category,
|
||||
child: Text(category.categoryName),
|
||||
)).toList(),
|
||||
onChanged: (value) => controller.updateCategory(value),
|
||||
),
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user