12 Commits

55 changed files with 5799 additions and 867 deletions

View File

@ -1,5 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.dvox.gifunavi"> package="com.dvox.gifunavi">
<uses-sdk android:minSdkVersion="23" android:targetSdkVersion="34" />
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

View File

@ -64,6 +64,7 @@ class LocationService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Log.d("LocationService", "Android: onCreate.")
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
gpsDatabaseHelper = GpsDatabaseHelper.getInstance(applicationContext) gpsDatabaseHelper = GpsDatabaseHelper.getInstance(applicationContext)
@ -71,6 +72,8 @@ class LocationService : Service() {
// 位置情報の権限チェックとGPS有効化の確認を行う // 位置情報の権限チェックとGPS有効化の確認を行う
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED && if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.FOREGROUND_SERVICE_LOCATION) == PackageManager.PERMISSION_GRANTED) { ContextCompat.checkSelfPermission(this, Manifest.permission.FOREGROUND_SERVICE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
Log.d("LocationService", "Android: onCreate : 位置情報の権限チェックとGPS有効化の確認")
val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
val locationRequest = LocationRequest.create().apply { val locationRequest = LocationRequest.create().apply {

View File

@ -52,12 +52,14 @@ class MainActivity: FlutterActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Log.d("MainActivity", "Android: onCreate.") Log.d("MainActivity", "Android: onCreate.")
// 位置情報の権限をリクエストする // 位置情報の権限をリクエストする==> main() の前にコールされるので除外 2024-7-19
/*
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), PERMISSION_REQUEST_CODE) ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), PERMISSION_REQUEST_CODE)
} else { } else {
// startLocationService() // アプリ起動時にLocationServiceを開始する ==> main.dartで制御する。 // startLocationService() // アプリ起動時にLocationServiceを開始する ==> main.dartで制御する。
} }
*/
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

View File

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

View File

@ -398,11 +398,11 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 480; CURRENT_PROJECT_VERSION = 497;
DEVELOPMENT_TEAM = UMNEWT25JR; DEVELOPMENT_TEAM = UMNEWT25JR;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FLUTTER_BUILD_NAME = 4.8.0; FLUTTER_BUILD_NAME = 4.8.17;
FLUTTER_BUILD_NUMBER = 480; FLUTTER_BUILD_NUMBER = 497;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "岐阜ナビ"; INFOPLIST_KEY_CFBundleDisplayName = "岐阜ナビ";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness";
@ -411,7 +411,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 4.8.0; MARKETING_VERSION = 4.8.17;
PRODUCT_BUNDLE_IDENTIFIER = com.dvox.gifunavi; PRODUCT_BUNDLE_IDENTIFIER = com.dvox.gifunavi;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -539,11 +539,11 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 480; CURRENT_PROJECT_VERSION = 497;
DEVELOPMENT_TEAM = UMNEWT25JR; DEVELOPMENT_TEAM = UMNEWT25JR;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FLUTTER_BUILD_NAME = 4.8.0; FLUTTER_BUILD_NAME = 4.8.17;
FLUTTER_BUILD_NUMBER = 480; FLUTTER_BUILD_NUMBER = 497;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "岐阜ナビ"; INFOPLIST_KEY_CFBundleDisplayName = "岐阜ナビ";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness";
@ -552,7 +552,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 4.8.0; MARKETING_VERSION = 4.8.17;
PRODUCT_BUNDLE_IDENTIFIER = com.dvox.gifunavi; PRODUCT_BUNDLE_IDENTIFIER = com.dvox.gifunavi;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -571,11 +571,11 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 480; CURRENT_PROJECT_VERSION = 497;
DEVELOPMENT_TEAM = UMNEWT25JR; DEVELOPMENT_TEAM = UMNEWT25JR;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FLUTTER_BUILD_NAME = 4.8.0; FLUTTER_BUILD_NAME = 4.8.17;
FLUTTER_BUILD_NUMBER = 480; FLUTTER_BUILD_NUMBER = 497;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "岐阜ナビ"; INFOPLIST_KEY_CFBundleDisplayName = "岐阜ナビ";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness";
@ -584,7 +584,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 4.8.0; MARKETING_VERSION = 4.8.17;
PRODUCT_BUNDLE_IDENTIFIER = com.dvox.gifunavi; PRODUCT_BUNDLE_IDENTIFIER = com.dvox.gifunavi;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -3,7 +3,7 @@ import 'dart:io';
//import 'dart:convert'; //import 'dart:convert';
//import 'dart:developer'; //import 'dart:developer';
import 'package:rogapp/model/gps_data.dart'; import 'package:rogapp/model/gps_data.dart';
import 'package:rogapp/pages/home/home_page.dart'; //import 'package:rogapp/pages/home/home_page.dart';
import 'package:rogapp/utils/database_gps.dart'; import 'package:rogapp/utils/database_gps.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart';
@ -11,6 +11,7 @@ import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
//import 'package:vm_service/vm_service.dart'; //import 'package:vm_service/vm_service.dart';
//import 'package:dart_vm_info/dart_vm_info.dart'; //import 'package:dart_vm_info/dart_vm_info.dart';
import 'package:timezone/data/latest.dart' as tz;
import 'package:rogapp/pages/settings/settings_controller.dart'; import 'package:rogapp/pages/settings/settings_controller.dart';
@ -20,6 +21,7 @@ import 'package:rogapp/pages/index/index_controller.dart';
import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/routes/app_pages.dart';
import 'package:rogapp/utils/location_controller.dart'; import 'package:rogapp/utils/location_controller.dart';
import 'package:rogapp/utils/string_values.dart'; import 'package:rogapp/utils/string_values.dart';
import 'package:rogapp/widgets/debug_widget.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
// import 'package:is_lock_screen/is_lock_screen.dart'; // import 'package:is_lock_screen/is_lock_screen.dart';
@ -33,7 +35,7 @@ import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'pages/permission/permission.dart'; import 'pages/permission/permission.dart';
import 'package:rogapp/services/api_service.dart';
Map<String, dynamic> deviceInfo = {}; Map<String, dynamic> deviceInfo = {};
@ -99,6 +101,7 @@ void restoreGame() async {
pref.getBool("rogaining_counted") ?? false; pref.getBool("rogaining_counted") ?? false;
DestinationController.ready_for_goal = DestinationController.ready_for_goal =
pref.getBool("ready_for_goal") ?? false; pref.getBool("ready_for_goal") ?? false;
await Get.putAsync(() => ApiService().init());
} }
} }
@ -127,7 +130,7 @@ void main() async {
FlutterError.presentError(details); FlutterError.presentError(details);
Get.log('Flutter error: ${details.exception}'); Get.log('Flutter error: ${details.exception}');
Get.log('Stack trace: ${details.stack}'); Get.log('Stack trace: ${details.stack}');
ErrorService.reportError(details.exception, details.stack ?? StackTrace.current, deviceInfo); ErrorService.reportError(details.exception, details.stack ?? StackTrace.current, deviceInfo, LogManager().operationLogs);
}; };
//Get.put(LocationController()); //Get.put(LocationController());
@ -140,6 +143,7 @@ void main() async {
// startMemoryMonitoring(); // 2024-4-8 Akira: メモリ使用量のチェックを開始 See #2810 // startMemoryMonitoring(); // 2024-4-8 Akira: メモリ使用量のチェックを開始 See #2810
Get.put(SettingsController()); // これを追加 Get.put(SettingsController()); // これを追加
/* /*
runZonedGuarded(() { runZonedGuarded(() {
runApp(const ProviderScope(child: MyApp())); runApp(const ProviderScope(child: MyApp()));
@ -148,12 +152,42 @@ void main() async {
}); });
*/ */
runApp(const ProviderScope(child: MyApp())); try {
//runApp(HomePage()); // MyApp()からHomePage()に変更 tz.initializeTimeZones();
//runApp(const MyApp());
// ApiServiceを初期化
//await Get.putAsync(() => ApiService().init());
await initServices();
runApp(const ProviderScope(child: MyApp()));
//runApp(HomePage()); // MyApp()からHomePage()に変更
//runApp(const MyApp());
}catch(e, stackTrace){
print('Error during initialization: $e');
print('Stack trace: $stackTrace');
}
}
Future<void> initServices() async {
print('Starting services ...');
try {
await Get.putAsync(() => ApiService().init());
print('All services started...');
}catch(e){
print('Error initializing ApiService: $e');
}
try {
Get.put(SettingsController());
print('SettingsController initialized successfully');
} catch (e) {
print('Error initializing SettingsController: $e');
}
print('All services started...');
} }
/*
Future<void> requestLocationPermission() async { Future<void> requestLocationPermission() async {
try { try {
final status = await Permission.locationAlways.request(); final status = await Permission.locationAlways.request();
@ -167,7 +201,7 @@ Future<void> requestLocationPermission() async {
print('Error requesting location permission: $e'); print('Error requesting location permission: $e');
} }
} }
*/
// メモリ使用量の解説https://qiita.com/hukusuke1007/items/e4e987836412e9bc73b9 // メモリ使用量の解説https://qiita.com/hukusuke1007/items/e4e987836412e9bc73b9
@ -323,7 +357,7 @@ Future<void> addGPStoDB(double la, double ln) async {
is_checkin: 0, is_checkin: 0,
created_at: DateTime.now().millisecondsSinceEpoch); created_at: DateTime.now().millisecondsSinceEpoch);
var res = await db.insertGps(gps_data); var res = await db.insertGps(gps_data);
//debugPrint("バックグラウンドでのGPS保存"); debugPrint("バックグラウンドでのGPS保存");
} catch (err) { } catch (err) {
print("errr ready gps ${err}"); print("errr ready gps ${err}");
return; return;
@ -366,15 +400,49 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
} }
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
/* // ウィジェットが構築された後に権限をチェック
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) {
await PermissionController.checkAndRequestPermissions(); PermissionController.checkAndRequestPermissions();
}); });
*/
debugPrint("Start MyAppState..."); debugPrint("Start MyAppState...");
} }
/*
void showPermissionRequiredDialog() {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: Text('権限が必要です'),
content: Text('このアプリは機能するために位置情報の権限が必要です。設定で権限を許可してください。'),
actions: <Widget>[
TextButton(
child: Text('設定を開く'),
onPressed: () {
openAppSettings();
Navigator.of(context).pop();
},
),
TextButton(
child: Text('アプリを終了'),
onPressed: () {
// アプリを終了
Navigator.of(context).pop();
// よりクリーンな終了のために 'flutter_exit_app' のようなプラグインを使用することをお勧めします
// 今回は単にすべてのルートをポップします
Navigator.of(context).popUntil((route) => false);
},
),
],
);
},
);
}
*/
@override @override
void dispose() { void dispose() {

70
lib/model/category.dart Normal file
View File

@ -0,0 +1,70 @@
// lib/models/category.dart
class NewCategory {
final int id;
final String categoryName;
final int categoryNumber;
final Duration duration;
final int numOfMember;
final bool family;
final bool female;
NewCategory({
required this.id,
required this.categoryName,
required this.categoryNumber,
required this.duration,
required this.numOfMember,
required this.family,
required this.female,
});
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is NewCategory &&
runtimeType == other.runtimeType &&
id == other.id;
@override
int get hashCode => id.hashCode;
factory NewCategory.fromJson(Map<String, dynamic> json) {
return NewCategory(
id: json['id'] ?? 0,
categoryName: json['category_name'] ?? 'Unknown Category',
categoryNumber: json['category_number'] ?? 0,
duration: parseDuration(json['duration']),
numOfMember: json['num_of_member'] ?? 1,
family: json['family'] ?? false,
female: json['female'] ?? false,
);
}
static Duration parseDuration(String s) {
int hours = 0;
int minutes = 0;
int micros;
List<String> parts = s.split(':');
if (parts.length > 2) {
hours = int.parse(parts[parts.length - 3]);
}
if (parts.length > 1) {
minutes = int.parse(parts[parts.length - 2]);
}
micros = (double.parse(parts[parts.length - 1]) * 1000000).round();
return Duration(hours: hours, minutes: minutes, microseconds: micros);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'category_name': categoryName,
'category_number': categoryNumber,
'duration': duration.inSeconds,
'num_of_member': numOfMember,
'family': family,
'female': female,
};
}
}

51
lib/model/entry.dart Normal file
View File

@ -0,0 +1,51 @@
// lib/models/entry.dart
import 'event.dart';
import 'event.dart';
import 'team.dart';
import 'category.dart';
class Entry {
final int id;
final Team team;
final Event event;
final NewCategory category;
final DateTime? date;
final int zekkenNumber; // 新しく追加
final String owner;
Entry({
required this.id,
required this.team,
required this.event,
required this.category,
required this.date,
required this.zekkenNumber,
required this.owner,
});
factory Entry.fromJson(Map<String, dynamic> json) {
return Entry(
id: json['id'],
team: Team.fromJson(json['team']),
event: Event.fromJson(json['event']),
category: NewCategory.fromJson(json['category']),
date: json['date'] != null
? DateTime.tryParse(json['date'])
: null,
zekkenNumber: json['zekken_number'], // 新しく追加
owner: json['owner'] is Map ? json['owner']['name'] ?? '' : json['owner'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'team': team.toJson(),
'event': event.toJson(),
'category': category.toJson(),
'date': date?.toIso8601String(),
'zekken_number': zekkenNumber, // 新しく追加
'owner': owner,
};
}
}

33
lib/model/event.dart Normal file
View File

@ -0,0 +1,33 @@
// lib/models/event.dart
class Event {
final int id;
final String eventName;
final DateTime startDatetime;
final DateTime endDatetime;
Event({
required this.id,
required this.eventName,
required this.startDatetime,
required this.endDatetime,
});
factory Event.fromJson(Map<String, dynamic> json) {
return Event(
id: json['id'],
eventName: json['event_name'],
startDatetime: DateTime.parse(json['start_datetime']),
endDatetime: DateTime.parse(json['end_datetime']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'event_name': eventName,
'start_datetime': startDatetime.toIso8601String(),
'end_datetime': endDatetime.toIso8601String(),
};
}
}

46
lib/model/team.dart Normal file
View File

@ -0,0 +1,46 @@
// lib/models/team.dart
import 'dart:convert';
import 'category.dart';
import 'user.dart';
class Team {
final int id;
// final String zekkenNumber;
final String teamName;
final NewCategory category;
final User owner;
Team({
required this.id,
// required this.zekkenNumber,
required this.teamName,
required this.category,
required this.owner,
});
factory Team.fromJson(Map<String, dynamic> json) {
return Team(
id: json['id'] ?? 0,
//zekkenNumber: json['zekken_number'] ?? 'Unknown',
teamName: json['team_name'] ?? 'Unknown Team',
category: json['category'] != null
? NewCategory.fromJson(json['category'])
: NewCategory(id: 0, categoryName: 'Unknown', categoryNumber: 0, duration: Duration.zero, numOfMember: 1, family: false, female: false),
owner: json['owner'] != null
? User.fromJson(json['owner'])
: User(id: 0, email: 'unknown@example.com', firstname: 'Unknown', lastname: 'User', dateOfBirth: null, female: false, isActive: false),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
//'zekken_number': zekkenNumber,
'team_name': teamName,
'category': category.toJson(),
'owner': owner.toJson(),
};
}
}

47
lib/model/user.dart Normal file
View File

@ -0,0 +1,47 @@
// lib/models/user.dart
class User {
final int? id;
final String? email;
final String firstname;
final String lastname;
final DateTime? dateOfBirth;
final bool female;
final bool isActive;
User({
this.id,
this.email,
required this.firstname,
required this.lastname,
this.dateOfBirth,
required this.female,
required this.isActive,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
email: json['email'],
firstname: json['firstname'] ?? 'Unknown',
lastname: json['lastname'] ?? 'Unknown',
dateOfBirth: json['date_of_birth'] != null
? DateTime.tryParse(json['date_of_birth'])
: null,
female: json['female'] ?? false,
isActive: json['is_active'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'email': email,
'firstname': firstname,
'lastname': lastname,
'date_of_birth': dateOfBirth?.toIso8601String(),
'female': female,
'is_active': isActive,
};
}
}

View File

@ -1,8 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; // この行を追加または確認
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:rogapp/model/destination.dart'; import 'package:rogapp/model/destination.dart';
import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/destination/destination_controller.dart';
import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart';
@ -10,6 +14,9 @@ import 'package:rogapp/services/external_service.dart';
import 'package:rogapp/utils/const.dart'; import 'package:rogapp/utils/const.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart';
import 'package:http/http.dart' as http; // この行を追加
// 関数 getTagText は、特定の条件に基づいて文字列から特定の部分を抽出し、返却するためのものです。 // 関数 getTagText は、特定の条件に基づいて文字列から特定の部分を抽出し、返却するためのものです。
// 関数は2つのパラメータを受け取り、条件分岐を通じて結果を返します。 // 関数は2つのパラメータを受け取り、条件分岐を通じて結果を返します。
// //
@ -232,6 +239,7 @@ class CameraPage extends StatelessWidget {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
shape: const CircleBorder(), shape: const CircleBorder(),
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
foregroundColor: Colors.white,
backgroundColor: destinationController.photos.isEmpty backgroundColor: destinationController.photos.isEmpty
? Colors.red ? Colors.red
: Colors.grey[300], : Colors.grey[300],
@ -287,7 +295,10 @@ class CameraPage extends StatelessWidget {
? settingGoal.value == false ? settingGoal.value == false
? ElevatedButton( ? ElevatedButton(
style: style:
ElevatedButton.styleFrom(backgroundColor: Colors.red), ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: Colors.red
),
onPressed: () async { onPressed: () async {
// print( // print(
// "----- user isss ${indexController.currentUser[0]} -----"); // "----- user isss ${indexController.currentUser[0]} -----");
@ -332,7 +343,7 @@ class CameraPage extends StatelessWidget {
isgoal: true); isgoal: true);
} else { } else {
//print("---- status ${value['status']} ---- "); //print("---- status ${value['status']} ---- ");
Get.snackbar("目標が追加されていません", "please_try_again", Get.snackbar(value["detail"], "ERROR",
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white colorText: Colors.white
); );
@ -352,7 +363,7 @@ class CameraPage extends StatelessWidget {
], ],
); );
} else if (destinationController.isInRog.value && } else if ((destinationController.isInRog.value || (destination.buy_point != null && destination.buy_point! > 0)) &&
dbDest?.checkedin != null && dbDest?.checkedin != null &&
destination.cp != -1 && destination.cp != -1 &&
dbDest?.checkedin == true) { dbDest?.checkedin == true) {
@ -392,7 +403,7 @@ class CameraPage extends StatelessWidget {
], ],
); );
} else if (destinationController.isInRog.value && } else if ((destinationController.isInRog.value || (destination.buy_point != null && destination.buy_point! > 0)) &&
dbDest?.checkedin != null && dbDest?.checkedin != null &&
destination.cp != -1 && destination.cp != -1 &&
destination.use_qr_code == true && destination.use_qr_code == true &&
@ -448,6 +459,7 @@ class CameraPage extends StatelessWidget {
style: ElevatedButton.styleFrom(backgroundColor: Colors.red), style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () async { onPressed: () async {
// print( // print(
// "##### current destination ${indexController.currentDestinationFeature[0].sub_loc_id} #######"); // "##### current destination ${indexController.currentDestinationFeature[0].sub_loc_id} #######");
await destinationController.makeCheckin( await destinationController.makeCheckin(
indexController.currentDestinationFeature[0], indexController.currentDestinationFeature[0],
@ -489,10 +501,13 @@ class CameraPage extends StatelessWidget {
if (buyPointPhoto == true) { if (buyPointPhoto == true) {
// buyPointPhotoがtrueの場合は、BuyPointCameraウィジェットを返します。 // buyPointPhotoがtrueの場合は、BuyPointCameraウィジェットを返します。
//print("--- buy point camera ${destination.toString()}"); //print("--- buy point camera ${destination.toString()}");
return BuyPointCamera(destination: destination); //return BuyPointCamera(destination: destination);
return SwitchableBuyPointCamera(destination: destination);
//}else if(destination.use_qr_code){ //}else if(destination.use_qr_code){
// return QRCodeScannerPage(destination: destination); // return QRCodeScannerPage(destination: destination);
} else if (destinationController.isInRog.value) { } else if (destinationController.isInRog.value || (destination.buy_point != null && destination.buy_point! > 0)) {
// isInRogがtrueの場合は、カメラページのUIを構築します。 // isInRogがtrueの場合は、カメラページのUIを構築します。
// AppBarには、目的地の情報を表示します。 // AppBarには、目的地の情報を表示します。
// ボディには、目的地の画像、タグ、アクションボタンを表示します。 // ボディには、目的地の画像、タグ、アクションボタンを表示します。
@ -613,101 +628,83 @@ class StartRogaining extends StatelessWidget {
// 完了ボタンをタップすると、購入ポイントの処理が行われます。 // 完了ボタンをタップすると、購入ポイントの処理が行われます。
// 購入なしボタンをタップすると、購入ポイントがキャンセルされます。 // 購入なしボタンをタップすると、購入ポイントがキャンセルされます。
// //
class BuyPointCamera extends StatelessWidget { class SwitchableBuyPointCamera extends StatefulWidget {
BuyPointCamera({Key? key, required this.destination}) : super(key: key); final Destination destination;
Destination destination; const SwitchableBuyPointCamera({Key? key, required this.destination}) : super(key: key);
DestinationController destinationController = @override
Get.find<DestinationController>(); _SwitchableBuyPointCameraState createState() => _SwitchableBuyPointCameraState();
}
class _SwitchableBuyPointCameraState extends State<SwitchableBuyPointCamera> {
bool isQRMode = true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
final qrViewWidth = screenWidth * 2 / 3;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
title: Text( title: Text("${widget.destination.sub_loc_id} : ${widget.destination.name}"),
"${destination.sub_loc_id} : ${destination.name}",
),
), ),
body: SingleChildScrollView( body: SafeArea(
child: Column( child: Stack(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
Padding( if (isQRMode)
padding: const EdgeInsets.all(8.0), Column(
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: [ children: [
Obx(() => ElevatedButton( SizedBox(height: screenHeight * 0.1),
onPressed: () { Center(
destinationController.openCamera(context, destination); child: SizedBox(
}, width: qrViewWidth,
style: ElevatedButton.styleFrom( height: qrViewWidth,
shape: const CircleBorder(), child: BuyPointCamera_QR(destination: widget.destination),
padding: const EdgeInsets.all(20),
backgroundColor: destinationController.photos.isEmpty
? Colors.red
: Colors.grey[300],
), ),
child: destinationController.photos.isEmpty ),
? const Text("撮影", Expanded(
style: TextStyle(color: Colors.white)) child: Align(
: const Text("再撮影", alignment: Alignment.topCenter,
style: TextStyle(color: Colors.black)), child: Padding(
)), padding: const EdgeInsets.only(top: 16.0),
ElevatedButton( child: Text(
onPressed: () async { "岐阜ロゲQRコードにかざしてください。",
await destinationController.cancelBuyPoint(destination); style: TextStyle(fontSize: 16),
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())
], ],
)
else
Positioned.fill(
child: BuyPointCamera(destination: widget.destination),
),
Positioned(
right: 16,
bottom: 16,
child: Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.7),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(isQRMode ? "カメラへ" : "QRへ"),
Switch(
value: isQRMode,
onChanged: (value) {
setState(() {
isQRMode = value;
});
},
),
],
),
), ),
), ),
], ],
@ -718,130 +715,220 @@ class BuyPointCamera extends StatelessWidget {
} }
/*
class BuyPointCamera extends StatelessWidget { class BuyPointCamera extends StatelessWidget {
BuyPointCamera({Key? key, required this.destination}) : super(key: key); BuyPointCamera({Key? key, required this.destination}) : super(key: key);
Destination destination; Destination destination;
DestinationController destinationController = DestinationController destinationController =
Get.find<DestinationController>(); Get.find<DestinationController>();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
//print("in camera purchase 1 ${destinationController.isInRog.value}"); return SingleChildScrollView(
child: Column(
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(
"${destination.sub_loc_id} : ${destination.name}",
),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Center( child: Center(
child: Obx( child: Obx(
() => Container( () =>
width: MediaQuery.of(context).size.width, Container(
height: 370, width: MediaQuery
decoration: BoxDecoration( .of(context)
image: DecorationImage( .size
// 要修正getReceiptImage関数の戻り値がnullの場合のエラーハンドリングが不十分です。適切なデフォルト画像を表示するなどの処理を追加してください。 .width,
// height: 370,
image: getReceiptImage(), fit: BoxFit.cover)), decoration: BoxDecoration(
), image: DecorationImage(
// 要修正getReceiptImage関数の戻り値がnullの場合のエラーハンドリングが不十分です。適切なデフォルト画像を表示するなどの処理を追加してください。
//
image: getReceiptImage(), fit: BoxFit.cover)),
),
), ),
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text(getTagText(true, destination.tags)), child: Text(getTagText(true, destination.tags)),
), ),
Row( Padding(
mainAxisAlignment: MainAxisAlignment.spaceAround, padding: const EdgeInsets.symmetric(horizontal: 16.0),
mainAxisSize: MainAxisSize.min, child: Wrap(
children: [ spacing: 16.0,
Obx(() => Row( runSpacing: 8.0,
mainAxisSize: MainAxisSize.min, children: [
children: [ Obx(() =>
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
// print( destinationController.openCamera(context, destination);
// "in camera purchase 2 ${destinationController.isInRog.value}"); },
destinationController.openCamera( style: ElevatedButton.styleFrom(
context, destination); shape: const CircleBorder(),
}, padding: const EdgeInsets.all(20),
child: destinationController.photos.isNotEmpty backgroundColor: destinationController.photos.isEmpty
? const Text("再撮影") ? Colors.red
: const Text("撮影")), : Colors.grey[300],
const SizedBox(
width: 10,
), ),
ElevatedButton( child: destinationController.photos.isEmpty
onPressed: () async { ? const Text("撮影",
await destinationController style: TextStyle(color: Colors.white))
.cancelBuyPoint(destination); : const Text("再撮影",
Navigator.of(Get.context!).pop(); style: TextStyle(color: Colors.black)),
//Get.back(); )),
destinationController.rogainingCounted.value = true; ElevatedButton(
destinationController.skipGps = false; onPressed: () async {
destinationController.isPhotoShoot.value = false; await destinationController.cancelBuyPoint(destination);
}, Navigator.of(Get.context!).pop();
child: const Text("買い物なし")) destinationController.rogainingCounted.value = true;
], destinationController.skipGps = false;
)), destinationController.isPhotoShoot.value = false;
Obx(() => destinationController.photos.isNotEmpty },
? Row( child: const Text("買い物なし")),
mainAxisAlignment: MainAxisAlignment.spaceBetween, Obx(() =>
children: [ destinationController.photos.isNotEmpty
// ElevatedButton( ? ElevatedButton(
// style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
// backgroundColor: Colors.red), backgroundColor: Colors.red),
// onPressed: () async {}, onPressed: () async {
// child: const Text("買物なし")), await destinationController.makeBuyPoint(
// const SizedBox( destination,
// width: 10, destinationController.photos[0].path);
// ), Get.back();
ElevatedButton( destinationController.rogainingCounted.value = true;
style: ElevatedButton.styleFrom( destinationController.skipGps = false;
backgroundColor: Colors.red), destinationController.isPhotoShoot.value = false;
onPressed: () async { Get.snackbar("お買い物加点を行いました",
// print( "${destination.sub_loc_id} : ${destination.name}",
// "in camera purchase 3 ${destinationController.isInRog.value}"); backgroundColor: Colors.green,
await destinationController.makeBuyPoint( colorText: Colors.white);
destination, },
destinationController.photos[0].path); child: const Text("完了",
Get.back(); style: TextStyle(color: Colors.white)))
// print( : Container())
// "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())
],
), ),
], ],
), ),
); );
} }
} }
*/
class BuyPointCamera_QR extends StatefulWidget {
final Destination destination;
const BuyPointCamera_QR({Key? key, required this.destination}) : super(key: key);
@override
_BuyPointCamera_QRState createState() => _BuyPointCamera_QRState();
}
class _BuyPointCamera_QRState extends State<BuyPointCamera_QR> {
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
QRViewController? controller;
bool isQRScanned = false;
final DestinationController destinationController = Get.find<DestinationController>();
@override
Widget build(BuildContext context) {
return QRView(
key: qrKey,
onQRViewCreated: _onQRViewCreated,
);
}
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;
_processBuyPoint();
//_activateBuyPoint(scanData.code!);
}
});
}
Future<String> getImageFilePathFromAssets(String assetPath) async {
final byteData = await rootBundle.load(assetPath);
final buffer = byteData.buffer;
Directory tempDir = await getTemporaryDirectory();
String tempPath = tempDir.path;
var filePath = '$tempPath/temp_qr_receipt.png';
return (await File(filePath).writeAsBytes(
buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes)
)).path;
}
void _processBuyPoint() async {
// アセットの画像をテンポラリファイルにコピー
String predefinedImagePath = await getImageFilePathFromAssets('assets/images/QR_certificate.png');
try {
await destinationController.makeBuyPoint(widget.destination, predefinedImagePath);
Get.snackbar('成功', 'お買い物ポイントが有効化されました');
Navigator.of(context).pop();
} catch (e) {
Get.snackbar('エラー', 'お買い物ポイントの有効化に失敗しました');
} finally {
isQRScanned = false;
}
}
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 { class QRCodeScannerPage extends StatefulWidget {

View File

@ -1,10 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart';
import 'package:rogapp/widgets/debug_widget.dart';
class ChangePasswordPage extends StatelessWidget { class ChangePasswordPage extends StatelessWidget {
ChangePasswordPage({Key? key}) : super(key: key); ChangePasswordPage({Key? key}) : super(key: key);
LogManager logManager = LogManager();
IndexController indexController = Get.find<IndexController>(); IndexController indexController = Get.find<IndexController>();
TextEditingController oldPasswordController = TextEditingController(); TextEditingController oldPasswordController = TextEditingController();
@ -20,6 +23,7 @@ class ChangePasswordPage extends StatelessWidget {
backgroundColor: Colors.white, backgroundColor: Colors.white,
leading: IconButton( leading: IconButton(
onPressed: () { onPressed: () {
logManager.addOperationLog('User clicked cancel button on the drawer');
Navigator.pop(context); Navigator.pop(context);
}, },
icon: const Icon( icon: const Icon(
@ -89,6 +93,7 @@ class ChangePasswordPage extends StatelessWidget {
.text.isEmpty || .text.isEmpty ||
newPasswordController newPasswordController
.text.isEmpty) { .text.isEmpty) {
logManager.addOperationLog('User tried to login with blank old password ${oldPasswordController.text} or new password ${newPasswordController.text}.');
Get.snackbar( Get.snackbar(
"no_values".tr, "no_values".tr,
"values_required".tr, "values_required".tr,

View File

@ -16,6 +16,7 @@ import 'package:rogapp/model/gps_data.dart';
import 'package:rogapp/pages/camera/camera_page.dart'; import 'package:rogapp/pages/camera/camera_page.dart';
import 'package:rogapp/pages/camera/custom_camera_view.dart'; import 'package:rogapp/pages/camera/custom_camera_view.dart';
import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart';
import 'package:rogapp/pages/team/team_controller.dart';
import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/routes/app_pages.dart';
import 'package:rogapp/services/DatabaseService.dart'; import 'package:rogapp/services/DatabaseService.dart';
import 'package:rogapp/services/destination_service.dart'; import 'package:rogapp/services/destination_service.dart';
@ -44,6 +45,7 @@ import 'package:rogapp/pages/permission/permission.dart';
class DestinationController extends GetxController { class DestinationController extends GetxController {
late LocationSettings locationSettings; // 位置情報の設定を保持する変数です。 late LocationSettings locationSettings; // 位置情報の設定を保持する変数です。
//late TeamController teamController = TeamController();
//Timer? _GPStimer; // GPSタイマーを保持する変数です。 //Timer? _GPStimer; // GPSタイマーを保持する変数です。
var destinationCount = 0.obs; // 目的地の数を保持するReactive変数です。 var destinationCount = 0.obs; // 目的地の数を保持するReactive変数です。
@ -161,11 +163,11 @@ class DestinationController extends GetxController {
} }
} }
// //
//==== Akira .. GPS信号シミュレーション用 ======= ここまで //==== Akira .. GPS信号シミュレーション用 ======= ここまで
*/ */
// ルートをクリアする関数です。 // ルートをクリアする関数です。
void clearRoute() { void clearRoute() {
indexController.routePoints.clear(); indexController.routePoints.clear();
@ -429,7 +431,7 @@ class DestinationController extends GetxController {
debugPrint("* 目的地がない場合 ==> 検知半径=-1の場合"); debugPrint("* 目的地がない場合 ==> 検知半径=-1の場合");
// print("----- in location popup cp - ${d.cp}----"); // print("----- in location popup cp - ${d.cp}----");
if ((d.cp == -1 || d.cp==0 ) && DateTime.now().difference(lastGoalAt).inHours >= 24) { if ((d.cp == -1 || d.cp==0 ) && DateTime.now().difference(lastGoalAt).inHours >= 10) {
debugPrint("**1: 開始CPで、最後にゴールしてから時間経過していれば、"); debugPrint("**1: 開始CPで、最後にゴールしてから時間経過していれば、");
chekcs = 1; chekcs = 1;
@ -671,11 +673,11 @@ class DestinationController extends GetxController {
} else if (isInRog.value == false && } else if (isInRog.value == false &&
indexController.rogMode.value == 1 && indexController.rogMode.value == 1 &&
DateTime.now().difference(lastGoalAt).inHours >= 24) { DateTime.now().difference(lastGoalAt).inHours >= 10) {
//start //start
//print("---- in start -----"); //print("---- in start -----");
debugPrint("**5 スタートの場合で最後のゴールから24時間経過している場合"); debugPrint("**5 スタートの場合で最後のゴールから10時間経過している場合");
chekcs = 6; // start point chekcs = 6; // start point
@ -823,7 +825,7 @@ class DestinationController extends GetxController {
//print("is rog ---- ${is_in_rog.value} ----"); //print("is rog ---- ${is_in_rog.value} ----");
if (d.hidden_location != null && if (d.hidden_location != null &&
d.hidden_location == 0 && d.hidden_location == 0 &&
isInRog.value == true && (isInRog.value == true || (d.buy_point != null && d.buy_point! > 0)) &&
d.cp != -1 && d.cp != 0 && d.cp != -2) { d.cp != -1 && d.cp != 0 && d.cp != -2) {
chekcs = 3; chekcs = 3;
photos.clear(); photos.clear();
@ -1028,7 +1030,7 @@ class DestinationController extends GetxController {
print("An error occurred: $e"); print("An error occurred: $e");
// await checkForCheckin(); // await checkForCheckin();
} finally { } finally {
await Future.delayed(const Duration(seconds: 5)); // 一定時間待機してから再帰呼び出し await Future.delayed(const Duration(seconds: 1)); // 一定時間待機してから再帰呼び出し
//print("--- End of checkForCheckin function, calling recursively ---"); //print("--- End of checkForCheckin function, calling recursively ---");
unawaited( checkForCheckin() ); unawaited( checkForCheckin() );
} }
@ -1156,9 +1158,13 @@ class DestinationController extends GetxController {
//await _saveImageFromPath(imageurl); //await _saveImageFromPath(imageurl);
await _saveImageToGallery(imageurl); await _saveImageToGallery(imageurl);
if (indexController.currentUser.isNotEmpty) { if (indexController.currentUser.isNotEmpty) {
double cpNum = destination.cp!; double cpNum = destination.cp!;
//int teamId = indexController.teamId.value; // teamIdを使用
int userId = indexController.currentUser[0]["user"]["id"]; int userId = indexController.currentUser[0]["user"]["id"];
//print("--- Pressed -----"); //print("--- Pressed -----");
String team = indexController.currentUser[0]["user"]['team_name']; String team = indexController.currentUser[0]["user"]['team_name'];
@ -1222,6 +1228,11 @@ class DestinationController extends GetxController {
if (indexController.currentUser.isNotEmpty) { if (indexController.currentUser.isNotEmpty) {
double cpNum = destination.cp!; double cpNum = destination.cp!;
//int teamId = indexController.teamId.value; // teamIdを使用
//Team team0 = teamController.teams[0];
//print("team={team0}");
int userId = indexController.currentUser[0]["user"]["id"]; int userId = indexController.currentUser[0]["user"]["id"];
//print("--- Pressed -----"); //print("--- Pressed -----");
String team = indexController.currentUser[0]["user"]['team_name']; String team = indexController.currentUser[0]["user"]['team_name'];
@ -1238,7 +1249,7 @@ class DestinationController extends GetxController {
// print("------ checkin event $eventCode ------"); // print("------ checkin event $eventCode ------");
ExternalService() ExternalService()
.makeCheckpoint( .makeCheckpoint(
userId, userId, // teamIdを使用
token, token,
formattedDate, formattedDate,
team, team,
@ -1628,7 +1639,7 @@ class DestinationController extends GetxController {
// 地図のイベントリスナーを設定 // 地図のイベントリスナーを設定
indexController.mapController.mapEventStream.listen((MapEvent mapEvent) { indexController.mapController.mapEventStream.listen((MapEvent mapEvent) {
if (mapEvent is MapEventMoveEnd) { if (mapEvent is MapEventMoveEnd) {
indexController.loadLocationsBound(); indexController.loadLocationsBound(indexController.currentUser[0]["user"]["event_code"]);
} }
}); });
@ -1653,7 +1664,7 @@ class DestinationController extends GetxController {
); );
indexController.currentBound.clear(); indexController.currentBound.clear();
indexController.currentBound.add(bnds); indexController.currentBound.add(bnds);
indexController.loadLocationsBound(); indexController.loadLocationsBound(indexController.currentUser[0]["user"]["event_code"]);
centerMapToCurrentLocation(); centerMapToCurrentLocation();
} }
}); });
@ -1716,6 +1727,8 @@ class DestinationController extends GetxController {
//print('----- %%%%%%%%%%%%%%%%%%%%% ----- $val'); //print('----- %%%%%%%%%%%%%%%%%%%%% ----- $val');
Map<String, dynamic> res = {}; Map<String, dynamic> res = {};
if (val == "wifi" || val == "mobile") { if (val == "wifi" || val == "mobile") {
//int teamId = indexController.teamId.value; // teamIdを使用
String token = indexController.currentUser[0]["token"]; String token = indexController.currentUser[0]["token"];
DatabaseHelper db = DatabaseHelper.instance; DatabaseHelper db = DatabaseHelper.instance;
db.allRogianing().then((value) { db.allRogianing().then((value) {
@ -1725,7 +1738,7 @@ class DestinationController extends GetxController {
} else if (e.rog_action_type == 1) { } else if (e.rog_action_type == 1) {
var datetime = DateTime.fromMicrosecondsSinceEpoch(e.checkintime!); var datetime = DateTime.fromMicrosecondsSinceEpoch(e.checkintime!);
res = await ExternalService().makeCheckpoint( res = await ExternalService().makeCheckpoint(
e.user_id!, e.user_id!, // teamId???
token, token,
getFormatedTime(datetime), getFormatedTime(datetime),
e.team_name!, e.team_name!,
@ -1735,7 +1748,7 @@ class DestinationController extends GetxController {
} else if (e.rog_action_type == 2) { } else if (e.rog_action_type == 2) {
var datetime = DateTime.fromMicrosecondsSinceEpoch(e.checkintime!); var datetime = DateTime.fromMicrosecondsSinceEpoch(e.checkintime!);
res = await ExternalService().makeGoal( res = await ExternalService().makeGoal(
e.user_id!, e.user_id!, // // teamId???
token, token,
e.team_name!, e.team_name!,
e.image!, e.image!,

View File

@ -170,7 +170,7 @@ class DestinationMapPage extends StatelessWidget {
indexController.currentBound.clear(); indexController.currentBound.clear();
indexController.currentBound.add(bounds); indexController.currentBound.add(bounds);
if (indexController.currentUser.isEmpty) { if (indexController.currentUser.isEmpty) {
indexController.loadLocationsBound(); indexController.loadLocationsBound(indexController.currentUser[0]["user"]["event_code"]);
} }
} }
}); });

View File

@ -5,6 +5,7 @@ import 'package:rogapp/pages/index/index_controller.dart';
import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/routes/app_pages.dart';
import 'package:rogapp/services/auth_service.dart'; import 'package:rogapp/services/auth_service.dart';
import 'package:rogapp/utils/database_helper.dart'; import 'package:rogapp/utils/database_helper.dart';
import 'package:rogapp/widgets/debug_widget.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:rogapp/pages/WebView/WebView_page.dart'; import 'package:rogapp/pages/WebView/WebView_page.dart';
@ -16,6 +17,8 @@ class DrawerPage extends StatelessWidget {
final IndexController indexController = Get.find<IndexController>(); final IndexController indexController = Get.find<IndexController>();
LogManager logManager = LogManager();
// 要検討URLの起動に失敗した場合のエラーハンドリングが不十分です。適切なエラーメッセージを表示するなどの処理を追加してください。 // 要検討URLの起動に失敗した場合のエラーハンドリングが不十分です。適切なエラーメッセージを表示するなどの処理を追加してください。
// //
/* /*
@ -26,6 +29,7 @@ class DrawerPage extends StatelessWidget {
void _launchURL(BuildContext context,String urlString) async { void _launchURL(BuildContext context,String urlString) async {
try { try {
logManager.addOperationLog('User clicked ${urlString} on the drawer');
Uri url = Uri.parse(urlString); Uri url = Uri.parse(urlString);
if (await canLaunchUrl(url)) { if (await canLaunchUrl(url)) {
await launchUrl(url); await launchUrl(url);
@ -52,14 +56,8 @@ class DrawerPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return SafeArea(
child: Drawer( child: Drawer(
// Add a ListView to the drawer. This ensures the user can scroll
// through the options in the drawer if there isn't enough vertical
// space to fit everything.
child: Column( child: Column(
children: [ children: [
// 最初のアイテムは、ユーザーのログイン状態に応じて表示が変わります。
// ユーザーがログインしていない場合は、"drawer_title".trというテキストを表示します。
// ユーザーがログインしている場合は、ユーザーのメールアドレスを表示します。
Container( Container(
height: 100, height: 100,
color: Colors.amber, color: Colors.amber,
@ -81,9 +79,40 @@ class DrawerPage extends StatelessWidget {
), ),
)), )),
), ),
// 次に、IndexControllerのcurrentUserリストが空かどうかに応じて、ログインまたはログアウトのアイテムを表示します。
// currentUserリストが空の場合は、"login".trというテキストのログインアイテムを表示し、タップするとAppPages.LOGINにナビゲートします。 ListTile(
// currentUserリストが空でない場合は、"logout".trというテキストのログアウトアイテムを表示し、タップするとindexController.logout()を呼び出してログアウトし、AppPages.LOGINにナビゲートします。 leading: Icon(Icons.group),
title: Text('チーム管理'),
onTap: () {
Get.back();
Get.toNamed(AppPages.TEAM_LIST);
},
),
ListTile(
leading: Icon(Icons.event),
title: Text('エントリー管理'),
onTap: () {
Get.back();
Get.toNamed(AppPages.ENTRY_LIST);
},
),
ListTile(
leading: Icon(Icons.event),
title: Text('イベント参加'),
onTap: () {
Get.back(); // ドロワーを閉じる
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 Obx(() => indexController.currentUser.isEmpty
? ListTile( ? ListTile(
leading: const Icon(Icons.login), leading: const Icon(Icons.login),
@ -100,8 +129,6 @@ class DrawerPage extends StatelessWidget {
Get.toNamed(AppPages.LOGIN); Get.toNamed(AppPages.LOGIN);
}, },
)), )),
// パスワード変更のアイテムは、ユーザーがログインしている場合にのみ表示されます。
// "change_password".trというテキストを表示し、タップするとAppPages.CHANGE_PASSWORDにナビゲートします。
indexController.currentUser.isNotEmpty indexController.currentUser.isNotEmpty
? ListTile( ? ListTile(
leading: const Icon(Icons.password), leading: const Icon(Icons.password),
@ -114,8 +141,6 @@ class DrawerPage extends StatelessWidget {
width: 0, width: 0,
height: 0, height: 0,
), ),
// サインアップのアイテムは、ユーザーがログインしていない場合にのみ表示されます。
// "sign_up".trというテキストを表示し、タップするとAppPages.REGISTERにナビゲートします。
indexController.currentUser.isEmpty indexController.currentUser.isEmpty
? ListTile( ? ListTile(
leading: const Icon(Icons.person), leading: const Icon(Icons.person),
@ -128,20 +153,19 @@ class DrawerPage extends StatelessWidget {
width: 0, width: 0,
height: 0, height: 0,
), ),
// リセットのアイテムは、ユーザーがログインしている場合にのみ表示されます。
// タップすると、確認ダイアログを表示し、ユーザーがリセットを確認するとDestinationControllerのresetRogaining()メソッドを呼び出してゲームデータをリセットします。
indexController.currentUser.isNotEmpty indexController.currentUser.isNotEmpty
? ListTile( ? ListTile(
leading: const Icon(Icons.password), leading: const Icon(Icons.password),
title: const Text("リセット"), title: Text('reset_button'.tr),
onTap: () { onTap: () {
logManager.addOperationLog('User clicked RESET button on the drawer');
// 要検討:リセット操作の確認メッセージをローカライズすることを検討してください。 // 要検討:リセット操作の確認メッセージをローカライズすることを検討してください。
// //
Get.defaultDialog( Get.defaultDialog(
title: "リセットしますがよろしいですか?", title: "reset_title".tr,
middleText: "これにより、すべてのゲーム データが削除され、すべての状態が削除されます", middleText: "reset_message".tr,
textConfirm: "確認する", textConfirm: "confirm".tr,
textCancel: "キャンセルする", textCancel: "cancel".tr,
onCancel: () => Get.back(), onCancel: () => Get.back(),
onConfirm: () async { onConfirm: () async {
DestinationController destinationController = DestinationController destinationController =
@ -157,8 +181,8 @@ class DrawerPage extends StatelessWidget {
//destinationController.deleteDBDestinations(); //destinationController.deleteDBDestinations();
Get.back(); Get.back();
Get.snackbar( Get.snackbar(
"リセット完了", "reset_done".tr,
"すべてリセットされました。ロゲ開始から再開して下さい。", "reset_explain".tr,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
@ -171,20 +195,19 @@ class DrawerPage extends StatelessWidget {
width: 0, width: 0,
height: 0, height: 0,
), ),
// アカウント削除のアイテムは、ユーザーがログインしている場合にのみ表示されます。
// "delete_account".trというテキストを表示し、タップするとAuthService.deleteUser()を呼び出してアカウントを削除し、AppPages.TRAVELにナビゲートします。
indexController.currentUser.isNotEmpty indexController.currentUser.isNotEmpty
? ListTile( ? ListTile(
leading: const Icon(Icons.delete_forever), leading: const Icon(Icons.delete_forever),
title: Text("delete_account".tr), title: Text("delete_account".tr),
onTap: () { onTap: () {
Get.defaultDialog( Get.defaultDialog(
title: "アカウントを削除しますがよろしいですか?", title: "delete_account_title".tr,
middleText: "これにより、アカウント情報とすべてのゲーム データが削除され、すべての状態が削除されます", middleText: "delete_account_middle".tr,
textConfirm: "確認する", textConfirm: "confirm".tr,
textCancel: "キャンセルする", textCancel: "cancel".tr,
onCancel: () => Get.back(), onCancel: () => Get.back(),
onConfirm: () { onConfirm: () {
logManager.addOperationLog('User clicked Confirm button on the account delete dialog');
String token = indexController.currentUser[0]['token']; String token = indexController.currentUser[0]['token'];
AuthService.deleteUser(token).then((value) { AuthService.deleteUser(token).then((value) {
if (value.isNotEmpty) { if (value.isNotEmpty) {
@ -205,53 +228,6 @@ class DrawerPage extends StatelessWidget {
width: 0, width: 0,
height: 0, height: 0,
), ),
/*
// ユーザーデータ削除のアイテムは、ユーザーがログインしている場合にのみ表示されます。
// タップすると、AuthService.deleteUser()を呼び出してユーザーデータを削除します。
indexController.currentUser.isNotEmpty
? ListTile(
// 要検討:アカウント削除のリクエストが失敗した場合のエラーハンドリングを追加することをお勧めします。
//
leading: const Icon(Icons.delete_forever),
title: Text("ユーザーデータを削除する".tr),
onTap: () {
Get.defaultDialog(
title: "アカウントを削除しますがよろしいですか?",
middleText: "これにより、アカウント情報とすべてのゲーム データが削除され、すべての状態が削除されます",
textConfirm: "確認する",
textCancel: "キャンセルする",
onCancel: () => Get.back(),
onConfirm: () {
String token = indexController.currentUser[0]['token'];
AuthService.deleteUser(token).then((value) {
Get.snackbar("ユーザーデータを削除する",
"データを削除するためにユーザーの同意が設定されています アプリとサーバーでユーザーデータが削除されました");
});
});
},
)
: const SizedBox(
width: 0,
height: 0,
),
// ListTile(
// leading: const Icon(Icons.person),
// title: Text("profile".tr),
// onTap: (){},
// ),
// ListTile(
// leading: const Icon(Icons.route),
// title: Text("recommended_route".tr),
// onTap: (){},
// ),
// ListTile(
// leading: const Icon(Icons.favorite_rounded),
// title: Text("point_rank".tr),
// onTap: (){},
// ),
*/
// "rog_web".trというテキストのアイテムは、ユーザーがログインしている場合にのみ表示されます。
// タップすると、_launchURL()メソッドを呼び出して外部のウェブサイトを開きます。
indexController.currentUser.isNotEmpty indexController.currentUser.isNotEmpty
? ListTile( ? ListTile(
leading: const Icon(Icons.featured_video), leading: const Icon(Icons.featured_video),
@ -264,8 +240,7 @@ class DrawerPage extends StatelessWidget {
width: 0, width: 0,
height: 0, height: 0,
), ),
// "privacy".trというテキストのアイテムは、常に表示されます。
// タップすると、_launchURL()メソッドを呼び出してプライバシーポリシーのURLを開きます。
ListTile( ListTile(
leading: const Icon(Icons.privacy_tip), leading: const Icon(Icons.privacy_tip),
title: Text("privacy".tr), title: Text("privacy".tr),
@ -275,21 +250,23 @@ class DrawerPage extends StatelessWidget {
), ),
ListTile( ListTile(
leading: const Icon(Icons.settings), leading: const Icon(Icons.settings),
title: const Text('設定'), title: Text('open_settings'.tr),
onTap: () { onTap: () {
Get.back(); // ドロワーを閉じる Get.back(); // ドロワーを閉じる
Get.toNamed(Routes.SETTINGS); Get.toNamed(Routes.SETTINGS);
}, },
),
//ListTile(
// leading: const Icon(Icons.developer_mode),
// title: const Text('open_settings'),
// onTap: () {
// Get.back(); // ドロワーを閉じる
// Get.toNamed('/debug'); // デバッグ画面に遷移
// },
//),
),
ListTile(
leading: const Icon(Icons.developer_mode),
title: const Text('開発者メニュー'),
onTap: () {
Get.back(); // ドロワーを閉じる
Get.toNamed('/debug'); // デバッグ画面に遷移
},
),
// ListTile( // ListTile(
// leading: const Icon(Icons.router), // leading: const Icon(Icons.router),
// title: Text("my_route".tr), // title: Text("my_route".tr),

View File

@ -0,0 +1,12 @@
import 'package:get/get.dart';
import 'package:rogapp/pages/entry/entry_controller.dart';
import 'package:rogapp/pages/team/member_controller.dart';
import 'package:rogapp/services/api_service.dart';
class EntryBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<ApiService>(() => ApiService());
Get.lazyPut<EntryController>(() => EntryController());
}
}

View File

@ -0,0 +1,246 @@
// lib/entry/entry_controller.dart
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:rogapp/model/entry.dart';
import 'package:rogapp/model/event.dart';
import 'package:rogapp/model/team.dart';
import 'package:rogapp/model/category.dart';
import 'package:rogapp/services/api_service.dart';
import '../index/index_controller.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tz;
class EntryController extends GetxController {
late ApiService _apiService;
final entries = <Entry>[].obs;
final events = <Event>[].obs;
final teams = <Team>[].obs;
final categories = <NewCategory>[].obs;
final selectedEvent = Rx<Event?>(null);
final selectedTeam = Rx<Team?>(null);
final selectedCategory = Rx<NewCategory?>(null);
final selectedDate = Rx<DateTime?>(null);
final currentEntry = Rx<Entry?>(null);
final isLoading = true.obs;
@override
void onInit() async {
super.onInit();
await initializeApiService();
await loadInitialData();
}
Future<void> initializeApiService() async {
try {
_apiService = await Get.putAsync(() => ApiService().init());
} catch (e) {
print('Error initializing ApiService: $e');
Get.snackbar('Error', 'Failed to initialize API service');
}
}
Future<void> loadInitialData() async {
try {
isLoading.value = true;
await Future.wait([
fetchEntries(),
fetchEvents(),
fetchTeams(),
fetchCategories(),
]);
if (Get.arguments != null && Get.arguments['entry'] != null) {
currentEntry.value = Get.arguments['entry'];
initializeEditMode(currentEntry.value!);
} else {
// 新規作成モードの場合、最初のイベントを選択
if (events.isNotEmpty) {
selectedEvent.value = events.first;
selectedDate.value = events.first.startDatetime;
}
}
} catch(e) {
print('Error initializing data: $e');
Get.snackbar('Error', 'Failed to load initial data');
} finally {
isLoading.value = false;
}
}
void initializeEditMode(Entry entry) {
currentEntry.value = entry;
selectedEvent.value = entry.event;
selectedTeam.value = entry.team;
selectedCategory.value = entry.category;
selectedDate.value = entry.date;
}
void updateEvent(Event? value) {
selectedEvent.value = value;
if (value != null) {
// イベント変更時に日付を調整
if (selectedDate.value == null ||
selectedDate.value!.isBefore(value.startDatetime) ||
selectedDate.value!.isAfter(value.endDatetime)) {
selectedDate.value = value.startDatetime;
}
}
}
void updateTeam(Team? value) => selectedTeam.value = value;
void updateCategory(NewCategory? value) => selectedCategory.value = value;
//void updateDate(DateTime value) => selectedDate.value = value;
void updateDate(DateTime value) {
selectedDate.value = tz.TZDateTime.from(value, tz.getLocation('Asia/Tokyo'));
}
/*
void updateDate(DateTime value){
selectedDate.value = DateFormat('yyyy-MM-dd').format(value!) as DateTime?;
}
*/
void _initializeEntryData() {
if (currentEntry.value != null) {
selectedEvent.value = currentEntry.value!.event;
selectedTeam.value = currentEntry.value!.team;
selectedCategory.value = currentEntry.value!.category;
selectedDate.value = currentEntry.value!.date;
}
}
Future<void> fetchEntries() async {
try {
final fetchedEntries = await _apiService.getEntries();
entries.assignAll(fetchedEntries);
} catch (e) {
print('Error fetching entries: $e');
Get.snackbar('Error', 'Failed to fetch entries');
}
}
Future<void> fetchEvents() async {
try {
final fetchedEvents = await _apiService.getEvents();
events.assignAll(fetchedEvents);
} catch (e) {
print('Error fetching events: $e');
Get.snackbar('Error', 'Failed to fetch events');
}
}
Future<void> fetchTeams() async {
try {
final fetchedTeams = await _apiService.getTeams();
teams.assignAll(fetchedTeams);
} catch (e) {
print('Error fetching teams: $e');
Get.snackbar('Error', 'Failed to fetch team');
}
}
Future<void> fetchCategories() async {
try {
final fetchedCategories = await _apiService.getCategories();
categories.assignAll(fetchedCategories);
} catch (e) {
print('Error fetching categories: $e');
Get.snackbar('Error', 'Failed to fetch categories');
}
}
Future<void> createEntry() async {
if (selectedEvent.value == null || selectedTeam.value == null ||
selectedCategory.value == null || selectedDate.value == null) {
Get.snackbar('Error', 'Please fill all fields');
return;
}
try {
isLoading.value = true;
// Get zekken number
final updatedCategory = await _apiService.getZekkenNumber(selectedCategory.value!.id);
final zekkenNumber = updatedCategory.categoryNumber.toString();
final newEntry = await _apiService.createEntry(
selectedTeam.value!.id,
selectedEvent.value!.id,
selectedCategory.value!.id,
selectedDate.value!,
zekkenNumber,
);
entries.add(newEntry);
Get.back();
} catch (e) {
print('Error creating entry: $e');
Get.snackbar('Error', 'Failed to create entry');
} finally {
isLoading.value = false;
}
}
Future<void> updateEntryAndRefreshMap() async {
await updateEntry();
// エントリーが正常に更新された後、マップをリフレッシュ
final indexController = Get.find<IndexController>();
final eventCode = currentEntry.value?.event.eventName ?? '';
indexController.reloadMap(eventCode);
}
Future<void> updateEntry() async {
if (currentEntry.value == null) {
Get.snackbar('Error', 'No entry selected for update');
return;
}
try {
isLoading.value = true;
final updatedEntry = await _apiService.updateEntry(
currentEntry.value!.id,
currentEntry.value!.team.id,
selectedEvent.value!.id,
selectedCategory.value!.id,
selectedDate.value!,
currentEntry.value!.zekkenNumber,
);
final index = entries.indexWhere((entry) => entry.id == updatedEntry.id);
if (index != -1) {
entries[index] = updatedEntry;
}
Get.back();
} catch (e) {
print('Error updating entry: $e');
Get.snackbar('Error', 'Failed to update entry');
} finally {
isLoading.value = false;
}
}
Future<void> deleteEntry() async {
if (currentEntry.value == null) {
Get.snackbar('Error', 'No entry selected for deletion');
return;
}
try {
isLoading.value = true;
await _apiService.deleteEntry(currentEntry.value!.id);
entries.removeWhere((entry) => entry.id == currentEntry.value!.id);
Get.back();
} catch (e) {
print('Error deleting entry: $e');
Get.snackbar('Error', 'Failed to delete entry');
} finally {
isLoading.value = false;
}
}
bool isOwner() {
// Implement logic to check if the current user is the owner of the entry
return true; // Placeholder
}
}

View File

@ -0,0 +1,171 @@
// lib/pages/entry/entry_detail_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rogapp/pages/entry/entry_controller.dart';
import 'package:rogapp/model/event.dart';
import 'package:rogapp/model/category.dart';
import 'package:rogapp/model/team.dart';
import 'package:intl/intl.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tz;
class EntryDetailPage extends GetView<EntryController> {
@override
Widget build(BuildContext context) {
final Map<String, dynamic> arguments = Get.arguments ?? {};
final mode = Get.arguments['mode'] as String? ?? 'new';
final entry = Get.arguments['entry'];
if (mode == 'edit' && entry != null) {
controller.initializeEditMode(entry);
}
return Scaffold(
appBar: AppBar(
title: Text(mode == 'new' ? 'エントリー登録' : 'エントリー詳細'),
),
body: Obx(() {
if (controller.isLoading.value) {
return Center(child: CircularProgressIndicator());
}
return Padding(
padding: EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDropdown<Event>(
label: 'イベント',
items: controller.events,
selectedId: controller.selectedEvent.value?.id,
onChanged: (eventId) => controller.updateEvent(
controller.events.firstWhere((e) => e.id == eventId)
),
getDisplayName: (event) => event.eventName,
getId: (event) => event.id,
),
SizedBox(height: 16),
_buildDropdown<Team>(
label: 'チーム',
items: controller.teams,
selectedId: controller.selectedTeam.value?.id,
onChanged: (teamId) => controller.updateTeam(
controller.teams.firstWhere((t) => t.id == teamId)
),
getDisplayName: (team) => team.teamName,
getId: (team) => team.id,
),
SizedBox(height: 16),
_buildDropdown<NewCategory>(
label: 'カテゴリ',
items: controller.categories,
selectedId: controller.selectedCategory.value?.id,
onChanged: (categoryId) => controller.updateCategory(
controller.categories.firstWhere((c) => c.id == categoryId)
),
getDisplayName: (category) => category.categoryName,
getId: (category) => category.id,
),
SizedBox(height: 16),
ListTile(
title: Text('日付'),
subtitle: Text(
controller.selectedDate.value != null
? DateFormat('yyyy-MM-dd').format(tz.TZDateTime.from(controller.selectedDate.value!, tz.getLocation('Asia/Tokyo')))
: '日付を選択してください',
),
onTap: () async {
if (controller.selectedEvent.value == null) {
Get.snackbar('Error', 'Please select an event first');
return;
}
final tz.TZDateTime now = tz.TZDateTime.now(tz.getLocation('Asia/Tokyo'));
final tz.TZDateTime eventStart = tz.TZDateTime.from(controller.selectedEvent.value!.startDatetime, tz.getLocation('Asia/Tokyo'));
final tz.TZDateTime eventEnd = tz.TZDateTime.from(controller.selectedEvent.value!.endDatetime, tz.getLocation('Asia/Tokyo'));
final tz.TZDateTime initialDate = controller.selectedDate.value != null
? tz.TZDateTime.from(controller.selectedDate.value!, tz.getLocation('Asia/Tokyo'))
: (now.isAfter(eventStart) ? now : eventStart);
// 選択可能な最初の日付を設定(今日かイベント開始日のうち、より後の日付)
final tz.TZDateTime firstDate = now.isAfter(eventStart) ? now : eventStart;
final DateTime? picked = await showDatePicker(
context: context,
initialDate: initialDate.isAfter(firstDate) ? initialDate : firstDate,
firstDate: firstDate,
lastDate: eventEnd,
);
if (picked != null) {
controller.updateDate(tz.TZDateTime.from(picked, tz.getLocation('Asia/Tokyo')));
}
},
),
SizedBox(height: 32),
if (mode == 'new')
ElevatedButton(
child: Text('エントリーを作成'),
onPressed: () => controller.createEntry(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
minimumSize: Size(double.infinity, 50),
),
)
else
Row(
children: [
Expanded(
child: ElevatedButton(
child: Text('エントリーを削除'),
onPressed: () => controller.deleteEntry(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
minimumSize: Size(0, 50),
),
),
),
SizedBox(width: 16),
Expanded(
child: ElevatedButton(
child: Text('エントリーを更新'),
onPressed: () => controller.updateEntry(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.lightBlue,
foregroundColor: Colors.white,
minimumSize: Size(0, 50),
),
),
),
],
),
],
),
),
);
}),
);
}
Widget _buildDropdown<T>({
required String label,
required List<T> items,
required int? selectedId,
required void Function(int?) onChanged,
required String Function(T) getDisplayName,
required int Function(T) getId,
}) {
return DropdownButtonFormField<int>(
decoration: InputDecoration(labelText: label),
value: selectedId,
items: items.map((item) => DropdownMenuItem<int>(
value: getId(item),
child: Text(getDisplayName(item)),
)).toList(),
onChanged: onChanged,
);
}
}

View File

@ -0,0 +1,112 @@
// lib/pages/entry/entry_list_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:rogapp/pages/entry/entry_controller.dart';
import 'package:rogapp/routes/app_pages.dart';
import 'package:timezone/timezone.dart' as tz;
class EntryListPage extends GetView<EntryController> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('エントリー管理'),
actions: [
IconButton(
icon: Icon(Icons.add),
onPressed: () => Get.toNamed(AppPages.ENTRY_DETAIL, arguments: {'mode': 'new'}),
),
],
),
body: Obx(() {
if (controller.entries.isEmpty) {
return Center(
child: Text('表示するエントリーがありません。'),
);
}
return ListView.builder(
itemCount: controller.entries.length,
itemBuilder: (context, index) {
final entry = controller.entries[index];
return ListTile(
title: Row(
children: [
Expanded(
child: Text('${_formatDate(entry.date)}: ${entry.event.eventName}'),
),
Text(entry.team.teamName, style: TextStyle(fontWeight: FontWeight.bold)),
],
),
subtitle: Row(
children: [
Expanded(
child: Text('カテゴリー: ${entry.category.categoryName}'),
),
Text('ゼッケン: ${entry.zekkenNumber ?? "未設定"}'),
],
),
onTap: () =>
Get.toNamed(AppPages.ENTRY_DETAIL,
arguments: {'mode': 'edit', 'entry': entry}),
);
},
);
}),
);
}
String _formatDate(DateTime? date) {
if (date == null) {
return '日時未設定';
}
final jstDate = tz.TZDateTime.from(date, tz.getLocation('Asia/Tokyo'));
return DateFormat('yyyy-MM-dd').format(jstDate);
}
}
class EntryListPage_old extends GetView<EntryController> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('エントリー管理'),
actions: [
IconButton(
icon: Icon(Icons.add),
onPressed: () => Get.toNamed(AppPages.ENTRY_DETAIL, arguments: {'mode': 'new'}),
),
],
),
body: Obx((){
if (controller.isLoading.value) {
return Center(child: CircularProgressIndicator());
}
// エントリーを日付昇順にソート
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}),
);
},
);
}),
);
}
}

View File

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import 'package:rogapp/pages/entry/event_entries_controller.dart';
class EventEntriesBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<EventEntriesController>(() => EventEntriesController());
}
}

View File

@ -0,0 +1,129 @@
import 'package:get/get.dart';
import 'package:rogapp/model/entry.dart';
import 'package:rogapp/pages/index/index_controller.dart';
import 'package:rogapp/pages/destination/destination_controller.dart';
import 'package:rogapp/services/api_service.dart';
import 'package:flutter/material.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tz;
class EventEntriesController extends GetxController {
final ApiService _apiService = Get.find<ApiService>();
final IndexController _indexController = Get.find<IndexController>();
late final DestinationController _destinationController;
final entries = <Entry>[].obs;
final filteredEntries = <Entry>[].obs;
final showTodayEntries = true.obs;
static bool _timezoneInitialized = false;
@override
void onInit() {
super.onInit();
_initializeTimezone();
// DestinationControllerが登録されていない場合に備えて、lazyPutを使用
Get.lazyPut<DestinationController>(() => DestinationController(), fenix: true);
_destinationController = Get.find<DestinationController>();
fetchEntries();
}
void _initializeTimezone() {
if (!_timezoneInitialized) {
tz.initializeTimeZones();
_timezoneInitialized = true;
}
}
Future<void> fetchEntries() async {
try {
final fetchedEntries = await _apiService.getEntries();
entries.assignAll(fetchedEntries);
filterEntries();
} catch (e) {
print('Error fetching entries: $e');
// エラー処理を追加
}
}
void filterEntries() {
if (showTodayEntries.value) {
filterEntriesForToday();
} else {
filteredEntries.assignAll(entries);
}
}
void filterEntriesForToday() {
final now = tz.TZDateTime.now(tz.getLocation('Asia/Tokyo'));
filteredEntries.assignAll(entries.where((entry) {
final entryDate = tz.TZDateTime.from(entry.date!, tz.getLocation('Asia/Tokyo'));
return entryDate.year == now.year &&
entryDate.month == now.month &&
entryDate.day == now.day;
}));
}
void filterEntriesForToday_old() {
final now = DateTime.now();
filteredEntries.assignAll(entries.where((entry) =>
entry.date?.year == now.year &&
entry.date?.month == now.month &&
entry.date?.day == now.day
));
}
void toggleShowTodayEntries() {
showTodayEntries.toggle();
filterEntries();
}
void refreshMap() {
final tk = _indexController.currentUser[0]["token"];
if (tk != null) {
_destinationController.fixMapBound(tk);
}
}
Future<void> joinEvent(Entry entry) async {
//final now = DateTime.now();
final now = tz.TZDateTime.now(tz.getLocation('Asia/Tokyo'));
final entryDate = tz.TZDateTime.from(entry.date!, tz.getLocation('Asia/Tokyo'));
bool isToday = entryDate.year == now.year &&
entryDate.month == now.month &&
entryDate.day == now.day;
_indexController.setReferenceMode(!isToday);
_indexController.setSelectedEventName(entry.event.eventName);
final userid = _indexController.currentUser[0]["user"]["id"];
await _apiService.updateUserInfo(userid,entry);
_indexController.currentUser[0]["user"]["event_date"] = entryDate; // 追加2024-8-9
_indexController.currentUser[0]["user"]["event_code"] = entry.event.eventName;
_indexController.currentUser[0]["user"]["team_name"] = entry.team.teamName;
_indexController.currentUser[0]["user"]["group"] = entry.team.category.categoryName;
_indexController.currentUser[0]["user"]["zekken_number"] = entry.zekkenNumber;
Get.back(); // エントリー一覧ページを閉じる
//_indexController.isLoading.value = true;
_indexController.reloadMap(entry.event.eventName);
refreshMap();
if (isToday) {
Get.snackbar('成功', 'イベントに参加しました。',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white);
} else {
Get.snackbar('参照モード', '過去または未来のイベントを参照しています。ロゲの開始やチェックインはできません。',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange,
colorText: Colors.white);
}
}
}

View File

@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:rogapp/pages/entry/event_entries_controller.dart';
import 'package:timezone/timezone.dart' as tz;
class EventEntriesPage_old extends GetView<EventEntriesController> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('イベント参加')),
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.category.categoryName} - ${entry.date}'),
onTap: () => controller.joinEvent(entry),
);
},
)),
);
}
}
class EventEntriesPage extends GetView<EventEntriesController> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Obx(() => Text(controller.showTodayEntries.value ? 'イベント参加' : 'イベント参照')),
),
body: Column(
children: [
Padding(
padding: EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(() => Text(
controller.showTodayEntries.value ? '本日のエントリー' : 'すべてのエントリー',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
)),
Obx(() => Switch(
value: !controller.showTodayEntries.value,
onChanged: (value) {
controller.toggleShowTodayEntries();
},
activeColor: Colors.blue,
)),
],
),
),
Expanded(
child: Obx(() {
if (controller.filteredEntries.isEmpty) {
return Center(
child: Text('表示するエントリーがありません。'),
);
}
return ListView.builder(
itemCount: controller.filteredEntries.length,
itemBuilder: (context, index) {
final entry = controller.filteredEntries[index];
return ListTile(
title: Row(
children: [
Expanded(
child: Text('${_formatDate(entry.date)}: ${entry.event.eventName}'),
),
Text(entry.team.teamName, style: TextStyle(fontWeight: FontWeight.bold)),
],
),
subtitle: Row(
children: [
Expanded(
child: Text('カテゴリー: ${entry.category.categoryName}'),
),
Text('ゼッケン: ${entry.zekkenNumber ?? "未設定"}'),
],
),
onTap: () async {
await controller.joinEvent(entry);
},
);
},
);
}),
),
],
),
);
}
String _formatDate(DateTime? date) {
if (date == null) {
return '日時未設定';
}
final jstDate = tz.TZDateTime.from(date, tz.getLocation('Asia/Tokyo'));
return DateFormat('yyyy-MM-dd').format(jstDate);
}
}

View File

@ -10,15 +10,27 @@ import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:rogapp/model/destination.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/destination/destination_controller.dart';
import 'package:rogapp/pages/team/team_controller.dart';
import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/routes/app_pages.dart';
import 'package:rogapp/services/auth_service.dart'; import 'package:rogapp/services/auth_service.dart';
import 'package:rogapp/services/location_service.dart'; import 'package:rogapp/services/location_service.dart';
import 'package:rogapp/utils/database_helper.dart'; import 'package:rogapp/utils/database_helper.dart';
import 'package:rogapp/widgets/debug_widget.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../../main.dart'; import 'package:rogapp/services/api_service.dart';
import 'package:rogapp/model/user.dart';
import 'package:rogapp/model/rog.dart';
import 'package:rogapp/main.dart';
import 'package:rogapp/widgets/helper_dialog.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tz;
class IndexController extends GetxController with WidgetsBindingObserver { class IndexController extends GetxController with WidgetsBindingObserver {
List<GeoJSONFeatureCollection> locations = <GeoJSONFeatureCollection>[].obs; List<GeoJSONFeatureCollection> locations = <GeoJSONFeatureCollection>[].obs;
@ -52,8 +64,14 @@ class IndexController extends GetxController with WidgetsBindingObserver {
MapController mapController = MapController(); MapController mapController = MapController();
MapController rogMapController = MapController(); MapController rogMapController = MapController();
LogManager logManager = LogManager();
String? userToken; String? userToken;
//late final ApiService _apiService;
final ApiService _apiService = Get.find<ApiService>();
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
// mode = 0 is map mode, mode = 1 list mode // mode = 0 is map mode, mode = 1 list mode
var mode = 0.obs; var mode = 0.obs;
@ -69,11 +87,44 @@ class IndexController extends GetxController with WidgetsBindingObserver {
String areaDropdownValue = "-1"; String areaDropdownValue = "-1";
String cateogory = "-all-"; String cateogory = "-all-";
final selectedEventName = 'add_location'.tr.obs;
void setSelectedEventName(String eventName) {
selectedEventName.value = eventName;
}
ConnectivityResult connectionStatus = ConnectivityResult.none; ConnectivityResult connectionStatus = ConnectivityResult.none;
var connectionStatusName = "".obs; var connectionStatusName = "".obs;
final Connectivity _connectivity = Connectivity(); final Connectivity _connectivity = Connectivity();
late StreamSubscription<ConnectivityResult> _connectivitySubscription; late StreamSubscription<ConnectivityResult> _connectivitySubscription;
final Rx<DateTime> lastUserUpdateTime = DateTime.now().obs;
RxInt teamId = RxInt(-1); // チームIDを保存するための変数
//late TeamController teamController = TeamController();
/*
void updateUserInfo(Map<String, dynamic> newUserInfo) {
currentUser.clear();
currentUser.add(newUserInfo);
lastUserUpdateTime.value = DateTime.now();
}
*/
final isReferenceMode = false.obs;
void setReferenceMode(bool value) {
isReferenceMode.value = value;
}
bool canStartRoge() {
return !isReferenceMode.value;
}
bool canCheckin() {
return !isReferenceMode.value;
}
void toggleMode() { void toggleMode() {
if (mode.value == 0) { if (mode.value == 0) {
mode += 1; mode += 1;
@ -155,14 +206,15 @@ class IndexController extends GetxController with WidgetsBindingObserver {
return WillPopScope( return WillPopScope(
onWillPop: () async => false, onWillPop: () async => false,
child: AlertDialog( child: AlertDialog(
title: Text('位置情報の許可が必要です'), title: Text('location_permission_needed_title'.tr),
content: Text('設定>プライバシーとセキュリティ>位置情報サービス を開いて、岐阜ナビを探し、「位置情報の許可」を「常に」にして下さい。'), content: Text('location_permission_needed_main'.tr),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
logManager.addOperationLog("User tapped confirm button for location permission required.");
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text('OK'), child: Text('confirm'.tr),
), ),
], ],
), ),
@ -186,11 +238,23 @@ class IndexController extends GetxController with WidgetsBindingObserver {
WidgetsBinding.instance?.addObserver(this); WidgetsBinding.instance?.addObserver(this);
_startLocationService(); // アプリ起動時にLocationServiceを開始する _startLocationService(); // アプリ起動時にLocationServiceを開始する
initializeApiService();
print('IndexController onInit called'); // デバッグ用の出力を追加 print('IndexController onInit called'); // デバッグ用の出力を追加
tz.initializeTimeZones();
//teamController = Get.find<TeamController>();
} }
Future<void> initializeApiService() async {
if (currentUser.isNotEmpty) {
// 既にログインしている場合
await Get.putAsync(() => ApiService().init());
//await Get.putAsync(() => ApiService().init());
// 必要に応じて追加の初期化処理
}
}
/* /*
void checkPermission() void checkPermission()
@ -230,6 +294,7 @@ class IndexController extends GetxController with WidgetsBindingObserver {
void _startLocationService() async { void _startLocationService() async {
const platform = MethodChannel('location'); const platform = MethodChannel('location');
try { try {
logManager.addOperationLog("Called start location service.");
await platform.invokeMethod('startLocationService'); await platform.invokeMethod('startLocationService');
} on PlatformException catch (e) { } on PlatformException catch (e) {
print("Failed to start location service: '${e.message}'."); print("Failed to start location service: '${e.message}'.");
@ -239,6 +304,7 @@ class IndexController extends GetxController with WidgetsBindingObserver {
void _stopLocationService() async { void _stopLocationService() async {
const platform = MethodChannel('location'); const platform = MethodChannel('location');
try { try {
logManager.addOperationLog("Called stop location service.");
await platform.invokeMethod('stopLocationService'); await platform.invokeMethod('stopLocationService');
} on PlatformException catch (e) { } on PlatformException catch (e) {
print("Failed to stop location service: '${e.message}'."); print("Failed to stop location service: '${e.message}'.");
@ -301,24 +367,40 @@ class IndexController extends GetxController with WidgetsBindingObserver {
} }
} }
logManager.addOperationLog("Called boundsFromLatLngList (${x1!},${y1!})-(${x0!},${y0!}).");
return LatLngBounds(LatLng(x1!, y1!), LatLng(x0!, y0!)); return LatLngBounds(LatLng(x1!, y1!), LatLng(x0!, y0!));
} }
// 要検討:エラーハンドリングが行われていますが、エラーメッセージをローカライズすることを検討してください。 // 要検討:エラーハンドリングが行われていますが、エラーメッセージをローカライズすることを検討してください。
// //
void login(String email, String password, BuildContext context) { void login(String email, String password, BuildContext context) async{
AuthService.login(email, password).then((value) {
AuthService.login(email, password).then((value) async {
print("------- logged in user details ######## $value ###### --------"); print("------- logged in user details ######## $value ###### --------");
if (value.isNotEmpty) { if (value.isNotEmpty) {
logManager.addOperationLog("User logged in : ${value}.");
// Navigator.pop(context); // Navigator.pop(context);
print("--------- user details login ----- $value"); print("--------- user details login ----- $value");
//await Future.delayed(const Duration(milliseconds: 500)); // Added Akira:2024-4-6, #2800
changeUser(value); changeUser(value);
// ログイン成功後、api_serviceを初期化
await Get.putAsync(() => ApiService().init());
// ユーザー情報の完全性をチェック
if (await checkUserInfoComplete()) {
Get.offAllNamed(AppPages.INDEX);
} else {
Get.offAllNamed(AppPages.USER_DETAILS_EDIT);
}
} else { } else {
logManager.addOperationLog("User failed login : ${email} , ${password}.");
isLoading.value = false; isLoading.value = false;
Get.snackbar( Get.snackbar(
"ログイン失敗", "login_failed".tr,
"ログインIDかパスワードを確認して下さい。", "check_login_id_or_password".tr,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
icon: const Icon(Icons.error, size: 40.0, color: Colors.blue), icon: const Icon(Icons.error, size: 40.0, color: Colors.blue),
@ -331,6 +413,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( void changePassword(
@ -340,6 +430,7 @@ class IndexController extends GetxController with WidgetsBindingObserver {
AuthService.changePassword(oldpassword, newpassword, token).then((value) { AuthService.changePassword(oldpassword, newpassword, token).then((value) {
////print("------- change password ######## $value ###### --------"); ////print("------- change password ######## $value ###### --------");
if (value.isNotEmpty) { if (value.isNotEmpty) {
logManager.addOperationLog("User successed to change password : ${oldpassword} , ${newpassword}.");
isLoading.value = false; isLoading.value = false;
Navigator.pop(context); Navigator.pop(context);
if (rogMode.value == 1) { if (rogMode.value == 1) {
@ -348,6 +439,7 @@ class IndexController extends GetxController with WidgetsBindingObserver {
switchPage(AppPages.INDEX); switchPage(AppPages.INDEX);
} }
} else { } else {
logManager.addOperationLog("User failed to change password : ${oldpassword} , ${newpassword}.");
Get.snackbar( Get.snackbar(
'failed'.tr, 'failed'.tr,
'password_change_failed_please_try_again'.tr, 'password_change_failed_please_try_again'.tr,
@ -383,6 +475,7 @@ class IndexController extends GetxController with WidgetsBindingObserver {
*/ */
void logout() async { void logout() async {
logManager.addOperationLog("User logout : ${currentUser} .");
saveGameState(); saveGameState();
locations.clear(); locations.clear();
DatabaseHelper db = DatabaseHelper.instance; DatabaseHelper db = DatabaseHelper.instance;
@ -400,27 +493,44 @@ class IndexController extends GetxController with WidgetsBindingObserver {
// 要検討:エラーハンドリングが行われていますが、エラーメッセージをローカライズすることを検討してください。 // 要検討:エラーハンドリングが行われていますが、エラーメッセージをローカライズすることを検討してください。
// //
void register(String email, String password, BuildContext context) { void register(String email, String password, String password2, BuildContext context) {
AuthService.register(email, password).then((value) { AuthService.register(email, password,password2).then((value) {
if (value.isNotEmpty) { if (value is Map && value.containsKey("error")) {
currentUser.clear(); String err_message = value["error"];
currentUser.add(value); debugPrint("ユーザー登録失敗:${email}, ${password},${err_message}");
isLoading.value = false; logManager.addOperationLog("User failed to register new account : ${email} , ${password} ,${err_message}.");
Navigator.pop(context);
Get.toNamed(AppPages.INDEX);
} else {
isLoading.value = false; isLoading.value = false;
Get.snackbar( Get.snackbar(
'failed'.tr, 'user_registration_failed_please_try_again'.tr, // ユーザー登録に失敗しました。
'user_registration_failed_please_try_again'.tr, err_message,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
icon: const Icon(Icons.error, size: 40.0, color: Colors.blue), icon: const Icon(Icons.error, size: 40.0, color: Colors.blue),
snackPosition: SnackPosition.TOP, snackPosition: SnackPosition.TOP,
duration: const Duration(milliseconds: 800), duration: const Duration(seconds: 3),
//backgroundColor: Colors.yellow, //backgroundColor: Colors.yellow,
//icon:Image(image:AssetImage("assets/images/dora.png")) //icon:Image(image:AssetImage("assets/images/dora.png"))
); );
}else{
debugPrint("ユーザー登録成功:${email}, ${password}");
logManager.addOperationLog("User tried to register new account : ${email} , ${password} .");
currentUser.clear();
//currentUser.add(value);
isLoading.value = false;
// ユーザー登録成功メッセージを表示
Get.snackbar(
'success'.tr,
'user_registration_successful'.tr,
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 3),
);
//Navigator.pop(context);
Get.toNamed(AppPages.LOGIN);
} }
}); });
} }
@ -451,23 +561,98 @@ class IndexController extends GetxController with WidgetsBindingObserver {
} }
*/ */
void changeUser(Map<String, dynamic> value, {bool replace = true}) { void changeUser(Map<String, dynamic> value, {bool replace = true}) async{
currentUser.clear(); currentUser.clear();
currentUser.add(value); currentUser.add(value);
if (replace) { if (replace) {
saveToDevice(currentUser[0]["token"]); saveToDevice(currentUser[0]["token"]);
} }
isLoading.value = false; isLoading.value = false;
loadLocationsBound();
// ユーザーのイベント情報を取得
await fetchUserEventInfo();
loadLocationsBound( currentUser[0]["user"]["event_code"]);
if (currentUser.isNotEmpty) { if (currentUser.isNotEmpty) {
rogMode.value = 0; rogMode.value = 0;
restoreGame(); restoreGame();
// チームデータを取得
await fetchTeamData();
} else { } else {
rogMode.value = 1; rogMode.value = 1;
} }
Get.toNamed(AppPages.INDEX); Get.toNamed(AppPages.INDEX);
} }
Future<void> fetchUserEventInfo() async {
try {
final List<Entry> entries = await _apiService.getEntries();
if (entries.isNotEmpty) {
final Entry latestEntry = entries.last;
final tokyo = tz.getLocation('Asia/Tokyo');
final eventDate = latestEntry.date!.toUtc();
//final eventDate = tz.TZDateTime.from(utcDate, tokyo);
final eventDateOnly = tz.TZDateTime(tokyo, eventDate.year, eventDate.month, eventDate.day);
currentUser[0]['user']['event_date'] = eventDateOnly.toIso8601String().split('T')[0];
currentUser[0]['user']['event_code'] = latestEntry.event.eventName;
currentUser[0]['user']['team_name'] = latestEntry.team.teamName;
currentUser[0]['user']['group'] = latestEntry.team.category.categoryName;
currentUser[0]['user']['zekken_number'] = latestEntry.zekkenNumber;
// 最後のゴール日時を取得
final lastGoalTime = await getLastGoalTime();
currentUser[0]['user']['last_goal_time'] = lastGoalTime?.toIso8601String();
print('Updated user event info: ${currentUser[0]['user']}');
} else {
print('No entries found for the user');
_clearUserEventInfo();
}
} catch (e) {
print('Error fetching user event info: $e');
_clearUserEventInfo();
}
}
Future<DateTime?> getLastGoalTime() async {
try {
final userId = currentUser[0]['user']['id'];
return await _apiService.getLastGoalTime(userId);
} catch (e) {
print('Error getting last goal time: $e');
}
return null;
}
void _clearUserEventInfo() {
currentUser[0]['user']['event_date'] = null;
currentUser[0]['user']['event_code'] = null;
currentUser[0]['user']['team_name'] = null;
currentUser[0]['user']['group'] = null;
currentUser[0]['user']['zekken_number'] = null;
}
Future<void> fetchTeamData() async {
try {
Get.put(TeamController());
// \"TeamController\" not found. You need to call \"Get.put(TeamController())\" or \"Get.lazyPut(()=>TeamController())\"
final teamController = Get.find<TeamController>();
await teamController.fetchTeams();
if (teamController.teams.isNotEmpty) {
teamId.value = teamController.teams.first.id;
}
} catch (e) {
print("Error fetching team data: $e");
}
}
loadUserDetailsForToken(String token) async { loadUserDetailsForToken(String token) async {
AuthService.userForToken(token).then((value) { AuthService.userForToken(token).then((value) {
print("----token val-- $value ------"); print("----token val-- $value ------");
@ -543,7 +728,7 @@ class IndexController extends GetxController with WidgetsBindingObserver {
// 要検討Future.delayedを使用して非同期処理を待たずに先に進むようにしていますが、 // 要検討Future.delayedを使用して非同期処理を待たずに先に進むようにしていますが、
// これによってメモリリークが発生する可能性があります。非同期処理の結果を適切に処理することを検討してください。 // これによってメモリリークが発生する可能性があります。非同期処理の結果を適切に処理することを検討してください。
// //
void loadLocationsBound() async { void loadLocationsBound(String eventCode) async {
if (isCustomAreaSelected.value == true) { if (isCustomAreaSelected.value == true) {
return; return;
} }
@ -575,12 +760,13 @@ class IndexController extends GetxController with WidgetsBindingObserver {
currentBound.clear(); currentBound.clear();
currentBound.add(bounds); currentBound.add(bounds);
isLoading.value = true; // ローディング状態をtrueに設定 //isLoading.value = true; // ローディング状態をtrueに設定
//print("bounds --- (${bounds.southWest.latitude},${bounds.southWest.longitude}),(${bounds.northWest.latitude},${bounds.northWest.longitude}),(${bounds.northEast.latitude},${bounds.northEast.longitude}),(${bounds.southEast.latitude},${bounds.southEast.longitude})"); //print("bounds --- (${bounds.southWest.latitude},${bounds.southWest.longitude}),(${bounds.northWest.latitude},${bounds.northWest.longitude}),(${bounds.northEast.latitude},${bounds.northEast.longitude}),(${bounds.southEast.latitude},${bounds.southEast.longitude})");
// 要検討APIからのレスポンスがnullの場合のエラーハンドリングが不十分です。適切なエラーメッセージを表示するなどの処理を追加してください。 // 要検討APIからのレスポンスがnullの場合のエラーハンドリングが不十分です。適切なエラーメッセージを表示するなどの処理を追加してください。
try { try {
final eventCode = currentUser[0]["user"]["event_code"];
final value = await LocationService.loadLocationsBound( final value = await LocationService.loadLocationsBound(
bounds.southWest.latitude, bounds.southWest.latitude,
bounds.southWest.longitude, bounds.southWest.longitude,
@ -590,7 +776,8 @@ class IndexController extends GetxController with WidgetsBindingObserver {
bounds.northEast.longitude, bounds.northEast.longitude,
bounds.southEast.latitude, bounds.southEast.latitude,
bounds.southEast.longitude, bounds.southEast.longitude,
cat cat,
eventCode
); );
/* /*
if (value == null) { if (value == null) {
@ -694,4 +881,30 @@ class IndexController extends GetxController with WidgetsBindingObserver {
} }
return null; return null;
} }
void reloadMap( String eventCode ) {
// マップをリロードするロジックを実装
// 例: 現在の位置情報を再取得し、マップを更新する
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,66 @@ import 'package:rogapp/utils/location_controller.dart';
// IndexPageクラスは、GetView<IndexController>を継承したStatelessWidgetです。このクラスは、アプリのメインページを表すウィジェットです。 // IndexPageクラスは、GetView<IndexController>を継承したStatelessWidgetです。このクラスは、アプリのメインページを表すウィジェットです。
// //
class IndexPage extends GetView<IndexController> { import 'package:rogapp/widgets/helper_dialog.dart';
IndexPage({Key? key}) : super(key: key);
class IndexPage extends StatefulWidget {
@override
_IndexPageState createState() => _IndexPageState();
}
class _IndexPageState extends State<IndexPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
//checkLoginAndShowDialog();
});
}
void checkLoginAndShowDialog() {
if (indexController.currentUser.isEmpty) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: Text('ログインが必要です'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('1) ログインされていません。ロゲに参加するにはログインが必要です。'),
SizedBox(height: 10),
Text('2) ログイン後、個人情報入力、チーム登録、エントリー登録を行なってください。'),
SizedBox(height: 10),
Text('3) エントリー登録は場所と日にちごとに行なってください。'),
],
),
actions: [
TextButton(
child: Text('キャンセル'),
onPressed: () {
Navigator.of(context).pop();
},
),
ElevatedButton(
child: Text('ログイン'),
onPressed: () {
Navigator.of(context).pop();
Get.toNamed(AppPages.LOGIN);
},
),
],
);
},
);
}
}
// class IndexPage extends GetView<IndexController> {
// IndexPage({Key? key}) : super(key: key);
// IndexControllerとDestinationControllerのインスタンスを取得しています。 // IndexControllerとDestinationControllerのインスタンスを取得しています。
// //
@ -46,7 +104,8 @@ class IndexPage extends GetView<IndexController> {
// //
drawer: DrawerPage(), drawer: DrawerPage(),
appBar: AppBar( appBar: AppBar(
title: Text("add_location".tr), title: Obx(() => Text(indexController.selectedEventName.value)),
//title: Text("add_location".tr),
actions: [ actions: [
// IconButton( // IconButton(
// onPressed: () { // onPressed: () {

View File

@ -2,18 +2,107 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart';
import 'package:rogapp/routes/app_pages.dart'; import 'package:rogapp/routes/app_pages.dart';
import 'package:rogapp/widgets/helper_dialog.dart';
import 'package:rogapp/services/api_service.dart';
import 'package:package_info_plus/package_info_plus.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>(); final IndexController indexController = Get.find<IndexController>();
final ApiService apiService = Get.find<ApiService>();
TextEditingController emailController = TextEditingController(); TextEditingController emailController = TextEditingController();
TextEditingController passwordController = TextEditingController(); TextEditingController passwordController = TextEditingController();
bool _obscureText = true;
String _version = ''; // バージョン情報を保持する変数
LoginPage({Key? key}) : super(key: key); @override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
showHelperDialog(
'参加するにはユーザー登録が必要です。サインアップからユーザー登録してください。',
'login_page'
);
});
_getVersionInfo(); // バージョン情報を取得
}
// バージョン情報を取得するメソッド
Future<void> _getVersionInfo() async {
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
setState(() {
_version = packageInfo.version;
});
}
void _showResetPasswordDialog() {
TextEditingController resetEmailController = TextEditingController();
Get.dialog(
AlertDialog(
title: Text('パスワードのリセット'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('パスワードをリセットするメールアドレスを入力してください。'),
SizedBox(height: 10),
TextField(
controller: resetEmailController,
decoration: InputDecoration(
labelText: 'メールアドレス',
border: OutlineInputBorder(),
),
),
],
),
actions: [
TextButton(
child: Text('キャンセル'),
onPressed: () => Get.back(),
),
ElevatedButton(
child: Text('リセット'),
onPressed: () async {
if (resetEmailController.text.isNotEmpty) {
bool success = await apiService.resetPassword(resetEmailController.text);
Get.back();
if (success) {
Get.dialog(
AlertDialog(
title: Text('パスワードリセット'),
content: Text('パスワードリセットメールを送信しました。メールのリンクからパスワードを設定してください。'),
actions: [
TextButton(
child: Text('OK'),
onPressed: () => Get.back(),
),
],
),
);
} else {
Get.snackbar('エラー', 'パスワードリセットに失敗しました。もう一度お試しください。',
snackPosition: SnackPosition.BOTTOM);
}
}
},
),
],
),
);
}
//LoginPage({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -25,226 +114,251 @@ class LoginPage extends StatelessWidget {
backgroundColor: Colors.white, backgroundColor: Colors.white,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
), ),
body: indexController.currentUser.isEmpty body: GestureDetector(
? SizedBox( onTap: () => FocusScope.of(context).unfocus(),
width: double.infinity, child: indexController.currentUser.isEmpty
? SizedBox(
child: Column( width: double.infinity,
mainAxisAlignment: MainAxisAlignment.start, child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Column(
children: [ children: [
Column( Column(
children: [ children: [
Column( Container(
children: [ height: MediaQuery.of(context).size.height / 6,
Container( decoration: const BoxDecoration(
height: MediaQuery.of(context).size.height / 6, image: DecorationImage(
decoration: const BoxDecoration( image: AssetImage(
image: DecorationImage( 'assets/images/login_image.jpg'))),
image: AssetImage(
'assets/images/login_image.jpg'))),
),
const SizedBox(
height: 5,
),
],
), ),
Padding( const SizedBox(
padding: const EdgeInsets.symmetric(horizontal: 40), height: 5,
child: Column( ),
children: [ // バージョン情報を表示
makeInput( Text(
label: "email".tr, controller: emailController), 'Version: $_version',
makeInput( style: TextStyle(
label: "password".tr, fontSize: 12,
controller: passwordController, color: Colors.grey[600],
obsureText: true),
],
), ),
), ),
Padding( ],
padding: const EdgeInsets.symmetric(horizontal: 40), ),
child: Container( Padding(
padding: const EdgeInsets.only(top: 3, left: 3), padding: const EdgeInsets.symmetric(horizontal: 40),
decoration: BoxDecoration( child: Column(
borderRadius: BorderRadius.circular(40), children: [
), makeInput(
child: Obx( label: "email".tr, controller: emailController),
(() => indexController.isLoading.value == true makePasswordInput(
? MaterialButton( label: "password".tr,
minWidth: double.infinity, controller: passwordController,
height: 60, obscureText: _obscureText,
onPressed: () {}, onToggleVisibility: () {
color: Colors.grey[400], setState(() {
shape: RoundedRectangleBorder( _obscureText = !_obscureText;
borderRadius: });
BorderRadius.circular(40)), }),
child: const CircularProgressIndicator(), ],
) ),
: Column( ),
children: [ Padding(
MaterialButton( padding: const EdgeInsets.symmetric(horizontal: 40),
minWidth: double.infinity, child: Container(
height: 40, padding: const EdgeInsets.only(top: 3, left: 3),
onPressed: () async { decoration: BoxDecoration(
if (emailController.text.isEmpty || borderRadius: BorderRadius.circular(40),
passwordController ),
.text.isEmpty) { child: Obx(
Get.snackbar( (() => indexController.isLoading.value == true
"no_values".tr, ? MaterialButton(
"email_and_password_required" minWidth: double.infinity,
.tr, height: 60,
backgroundColor: Colors.red, onPressed: () {},
colorText: Colors.white, color: Colors.grey[400],
icon: const Icon( shape: RoundedRectangleBorder(
Icons borderRadius:
.assistant_photo_outlined, BorderRadius.circular(40)),
size: 40.0, child: const CircularProgressIndicator(),
color: Colors.blue), )
snackPosition: : Column(
SnackPosition.TOP, children: [
duration: const Duration( MaterialButton(
seconds: 3), minWidth: double.infinity,
// backgroundColor: Colors.yellow, height: 40,
//icon:Image(image:AssetImage("assets/images/dora.png")) onPressed: () async {
); if (emailController.text.isEmpty ||
return; passwordController
} .text.isEmpty) {
indexController.isLoading.value = Get.snackbar(
true; "no_values".tr,
indexController.login( "email_and_password_required"
emailController.text, .tr,
passwordController.text, backgroundColor: Colors.red,
context); colorText: Colors.white,
}, icon: const Icon(
color: Colors.indigoAccent[400], Icons
shape: RoundedRectangleBorder( .assistant_photo_outlined,
borderRadius: size: 40.0,
BorderRadius.circular(40)), color: Colors.blue),
child: Text( snackPosition:
"login".tr, SnackPosition.TOP,
style: TextStyle( duration: const Duration(
fontWeight: FontWeight.w600, seconds: 3),
fontSize: 16, );
color: Colors.white70), return;
), }
), indexController.isLoading.value =
const SizedBox( true;
height: 5.0, indexController.login(
), emailController.text,
MaterialButton( passwordController.text,
minWidth: double.infinity, context);
height: 36, },
onPressed: () { color: Colors.indigoAccent[400],
Get.toNamed(AppPages.REGISTER); shape: RoundedRectangleBorder(
}, borderRadius:
color: Colors.redAccent, BorderRadius.circular(40)),
shape: RoundedRectangleBorder( child: Text(
borderRadius: "login".tr,
BorderRadius.circular(40)), style: const TextStyle(
child: Text( fontWeight: FontWeight.w600,
"sign_up".tr, fontSize: 16,
style: const TextStyle( color: Colors.white70),
fontWeight: FontWeight.w600, ),
fontSize: 16, ),
color: Colors.white70), const SizedBox(
), height: 5.0,
), ),
const SizedBox( MaterialButton(
height: 2.0, minWidth: double.infinity,
), height: 36,
MaterialButton( onPressed: () {
minWidth: double.infinity, Get.toNamed(AppPages.REGISTER);
height: 36, },
onPressed: () { color: Colors.redAccent,
Get.back(); shape: RoundedRectangleBorder(
}, borderRadius:
color: Colors.grey, BorderRadius.circular(40)),
shape: RoundedRectangleBorder( child: Text(
borderRadius: "sign_up".tr,
BorderRadius.circular(40)), style: const TextStyle(
child: Text( fontWeight: FontWeight.w600,
"cancel".tr, fontSize: 16,
style: const TextStyle( color: Colors.white70),
fontWeight: FontWeight.w600, ),
fontSize: 16, ),
color: Colors.white70), ],
),
),
],
)),
),
)), )),
const SizedBox( ),
height: 3, )),
const SizedBox(
height: 3,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: _showResetPasswordDialog,
child: Text(
"forgot_password".tr,
style: TextStyle(color: Colors.blue),
),
), ),
Row( ],
mainAxisAlignment: MainAxisAlignment.center, ),
children: [ Row(
Flexible( mainAxisAlignment: MainAxisAlignment.center,
child: Padding( children: [
padding: const EdgeInsets.all(8.0), Flexible(
child: Text( child: Padding(
"rogaining_user_need_tosign_up".tr, padding: const EdgeInsets.all(8.0),
style: const TextStyle( child: Text(
overflow: TextOverflow.ellipsis, "app_developed_by_gifu_dx".tr,
fontSize: 10.0 style: const TextStyle(
), overflow: TextOverflow.ellipsis,
), fontSize: 10.0),
),
),
),
],
),
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text(
"※第8回と第9回は、岐阜県の令和年度「清流の国ぎふ」SDGs推進ネットワーク連携促進補助金を受けています",
style: TextStyle(
fontSize: 10.0,
), ),
), ),
], ),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"app_developed_by_gifu_dx".tr,
style: const TextStyle(
overflow: TextOverflow.ellipsis,
fontSize: 10.0),
),
),
),
],
),
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text(
"※第8回と第9回は、岐阜県の令和年度「清流の国ぎふ」SDGs推進ネットワーク連携促進補助金を受けています",
style: TextStyle(
//overflow: TextOverflow.ellipsis,
fontSize:
10.0, // Consider adjusting the font size if the text is too small.
// Removed overflow: TextOverflow.ellipsis to allow text wrapping.
),
),
),
),
],
), ),
], ],
), ),
], ],
), ),
],
) ),
: TextButton( )
onPressed: () { : TextButton(
indexController.currentUser.clear(); onPressed: () {
}, indexController.logout();
child: const Text("Already Logged in, Click to logout"), Get.offAllNamed(AppPages.LOGIN);
), },
child: const Text("Already Logged in, Click to logout"),
),
),
); );
} }
} }
Widget makePasswordInput({
required String label,
required TextEditingController controller,
required bool obscureText,
required VoidCallback onToggleVisibility,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w400, color: Colors.black87),
),
const SizedBox(height: 5),
TextField(
controller: controller,
obscureText: obscureText,
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]!),
),
suffixIcon: IconButton(
icon: Icon(
obscureText ? Icons.visibility : Icons.visibility_off,
color: Colors.grey,
),
onPressed: onToggleVisibility,
),
),
),
const SizedBox(height: 30.0)
],
);
}
Widget makeInput( Widget makeInput(
{label, required TextEditingController controller, obsureText = false}) { {label, required TextEditingController controller, obsureText = false}) {
return Column( return Column(

View File

@ -0,0 +1,381 @@
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';
import 'package:rogapp/services/api_service.dart';
// 要検討:ログインボタンとサインアップボタンの配色を見直すことを検討してください。現在の配色では、ボタンの役割がわかりにくい可能性があります。
// エラーメッセージをローカライズすることを検討してください。
// ログイン処理中にエラーが発生した場合のエラーハンドリングを追加することをお勧めします。
//
class LoginPage extends StatefulWidget {
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
//class LoginPage extends StatelessWidget {
final IndexController indexController = Get.find<IndexController>();
final ApiService apiService = Get.find<ApiService>();
TextEditingController emailController = TextEditingController();
TextEditingController passwordController = TextEditingController();
bool _obscureText = true;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
showHelperDialog(
'参加するにはユーザー登録が必要です。サインアップからユーザー登録してください。',
'login_page'
);
});
}
void _showResetPasswordDialog() {
TextEditingController resetEmailController = TextEditingController();
Get.dialog(
AlertDialog(
title: Text('パスワードのリセット'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('パスワードをリセットするメールアドレスを入力してください。'),
SizedBox(height: 10),
TextField(
controller: resetEmailController,
decoration: InputDecoration(
labelText: 'メールアドレス',
border: OutlineInputBorder(),
),
),
],
),
actions: [
TextButton(
child: Text('キャンセル'),
onPressed: () => Get.back(),
),
ElevatedButton(
child: Text('リセット'),
onPressed: () async {
if (resetEmailController.text.isNotEmpty) {
bool success = await apiService.resetPassword(resetEmailController.text);
Get.back();
if (success) {
Get.dialog(
AlertDialog(
title: Text('パスワードリセット'),
content: Text('パスワードリセットメールを送信しました。メールのリンクからパスワードを設定してください。'),
actions: [
TextButton(
child: Text('OK'),
onPressed: () => Get.back(),
),
],
),
);
} else {
Get.snackbar('エラー', 'パスワードリセットに失敗しました。もう一度お試しください。',
snackPosition: SnackPosition.BOTTOM);
}
}
},
),
],
),
);
}
//LoginPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: Colors.white,
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.white,
automaticallyImplyLeading: false,
),
body: indexController.currentUser.isEmpty
? SizedBox(
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Column(
children: [
Column(
children: [
Container(
height: MediaQuery.of(context).size.height / 6,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(
'assets/images/login_image.jpg'))),
),
const SizedBox(
height: 5,
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
children: [
makeInput(
label: "email".tr, controller: emailController),
makePasswordInput(
label: "password".tr,
controller: passwordController,
obscureText: _obscureText,
onToggleVisibility: () {
setState(() {
_obscureText = !_obscureText;
});
}),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Container(
padding: const EdgeInsets.only(top: 3, left: 3),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40),
),
child: Obx(
(() => indexController.isLoading.value == true
? MaterialButton(
minWidth: double.infinity,
height: 60,
onPressed: () {},
color: Colors.grey[400],
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(40)),
child: const CircularProgressIndicator(),
)
: Column(
children: [
MaterialButton(
minWidth: double.infinity,
height: 40,
onPressed: () async {
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.login(
emailController.text,
passwordController.text,
context);
},
color: Colors.indigoAccent[400],
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(40)),
child: Text(
"login".tr,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: Colors.white70),
),
),
const SizedBox(
height: 5.0,
),
MaterialButton(
minWidth: double.infinity,
height: 36,
onPressed: () {
Get.toNamed(AppPages.REGISTER);
},
color: Colors.redAccent,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(40)),
child: Text(
"sign_up".tr,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: Colors.white70),
),
),
],
)),
),
)),
const SizedBox(
height: 3,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: _showResetPasswordDialog,
child: Text(
"forgot_password".tr,
style: TextStyle(color: Colors.blue),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"app_developed_by_gifu_dx".tr,
style: const TextStyle(
overflow: TextOverflow.ellipsis,
fontSize: 10.0),
),
),
),
],
),
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text(
"※第8回と第9回は、岐阜県の令和年度「清流の国ぎふ」SDGs推進ネットワーク連携促進補助金を受けています",
style: TextStyle(
//overflow: TextOverflow.ellipsis,
fontSize:
10.0, // Consider adjusting the font size if the text is too small.
// Removed overflow: TextOverflow.ellipsis to allow text wrapping.
),
),
),
),
],
),
],
),
],
),
)
: TextButton(
onPressed: () {
indexController.logout();
Get.offAllNamed(AppPages.LOGIN);
},
child: const Text("Already Logged in, Click to logout"),
),
);
}
}
Widget makePasswordInput({
required String label,
required TextEditingController controller,
required bool obscureText,
required VoidCallback onToggleVisibility,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w400, color: Colors.black87),
),
const SizedBox(height: 5),
TextField(
controller: controller,
obscureText: obscureText,
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]!),
),
suffixIcon: IconButton(
icon: Icon(
obscureText ? Icons.visibility : Icons.visibility_off,
color: Colors.grey,
),
onPressed: onToggleVisibility,
),
),
),
const SizedBox(height: 30.0)
],
);
}
Widget makeInput(
{label, required TextEditingController controller, 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,
),
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])!),
),
),
),
const SizedBox(
height: 30.0,
)
],
);
}

View File

@ -207,7 +207,8 @@ class LoginPopupPage extends StatelessWidget {
) )
: TextButton( : TextButton(
onPressed: () { onPressed: () {
indexController.currentUser.clear(); indexController.logout();
Get.offAllNamed(AppPages.LOGIN);
}, },
child: const Text("Already Logged in, Click to logout"), child: const Text("Already Logged in, Click to logout"),
), ),

View File

@ -4,21 +4,174 @@ import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:rogapp/services/location_service.dart'; import 'package:rogapp/services/location_service.dart';
import 'dart:developer' as developer; import 'dart:developer' as developer;
import 'dart:async';
class PermissionController { class PermissionController {
/* static bool _isRequestingPermission = false;
static Future<bool> checkLocationPermissions() async { static Completer<bool>? _permissionCompleter;
debugPrint("(gifunavi)== checkLocationPermissions ==");
final alwaysPermission = await Permission.locationAlways.status;
final whenInUsePermission = await Permission.locationWhenInUse.status;
final locationPermission = await Permission.location.status;
return (alwaysPermission == PermissionStatus.granted || whenInUsePermission == PermissionStatus.granted) && static Future<bool> checkLocationPermissions() async {
(locationPermission == PermissionStatus.granted); final locationPermission = await Permission.location.status;
final whenInUsePermission = await Permission.locationWhenInUse.status;
final alwaysPermission = await Permission.locationAlways.status;
return locationPermission == PermissionStatus.granted &&
(whenInUsePermission == PermissionStatus.granted || alwaysPermission == PermissionStatus.granted);
} }
*/
static Future<bool> checkAndRequestPermissions() async {
if (_isRequestingPermission) {
return _permissionCompleter!.future;
}
_isRequestingPermission = true;
_permissionCompleter = Completer<bool>();
bool hasPermissions = await checkLocationPermissions();
if (!hasPermissions) {
bool userAgreed = await showLocationDisclosure();
if (userAgreed) {
try {
await requestAllLocationPermissions();
hasPermissions = await checkLocationPermissions();
} catch (e) {
print('Error requesting location permissions: $e');
hasPermissions = false;
}
} else {
print('User did not agree to location usage');
hasPermissions = false;
// アプリを終了
SystemNavigator.pop();
}
}
_isRequestingPermission = false;
_permissionCompleter!.complete(hasPermissions);
return _permissionCompleter!.future;
}
static Future<void> requestAllLocationPermissions() async {
await Permission.location.request();
await Permission.locationWhenInUse.request();
await Permission.locationAlways.request();
if (await Permission.locationAlways.isGranted) {
const platform = MethodChannel('location');
try {
await platform.invokeMethod('startLocationService');
} on PlatformException catch (e) {
debugPrint("Failed to start location service: '${e.message}'.");
}
}
}
static Future<bool> showLocationDisclosure() async {
return await Get.dialog<bool>(
AlertDialog(
title: Text('位置情報の使用について'),
content: const SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text('このアプリでは、以下の目的で位置情報を使用します:'),
Text('• チェックポイントの自動チェックイン(アプリが閉じているときも含む)'),
Text('• 移動履歴の記録(バックグラウンドでも継続)'),
Text('• 現在地周辺の情報表示'),
Text('\nバックグラウンドでも位置情報を継続的に取得します。'),
Text('これにより、バッテリーの消費が増加する可能性があります。'),
Text('同意しない場合には、アプリは終了します。'),
],
),
),
actions: <Widget>[
TextButton(
child: const Text('同意しない'),
onPressed: () => Get.back(result: false),
),
TextButton(
child: const Text('同意する'),
onPressed: () => Get.back(result: true),
),
],
),
barrierDismissible: false,
) ?? false;
}
static void showPermissionDeniedDialog(String title,String message) {
Get.dialog(
AlertDialog(
//title: Text('location_permission_needed_title'.tr),
title: Text(title.tr),
// 位置情報への許可が必要です
//content: Text('location_permission_needed_main'.tr),
content: Text(message.tr),
// 岐阜ロゲでは、位置情報を使用してスタート・チェックイン・ゴール等の通過照明及び移動手段の記録のために、位置情報のトラッキングを行なっています。このためバックグラウンドでもトラッキングができるように位置情報の権限が必要です。
// 設定画面で、「岐阜ナビ」に対して、常に位置情報を許可するように設定してください。
actions: [
TextButton(
child: Text('キャンセル'),
onPressed: () => Get.back(),
),
TextButton(
child: Text('設定'),
onPressed: () {
Get.back();
openAppSettings();
},
),
],
),
);
}
/*
static Future<bool> requestLocationPermissions(BuildContext context) async {
if (_isRequestingPermission) {
// If a request is already in progress, wait for it to complete
return _permissionCompleter!.future;
}
_isRequestingPermission = true;
_permissionCompleter = Completer<bool>();
bool userAgreed = await showLocationDisclosure(context);
if (userAgreed) {
try {
final locationStatus = await Permission.location.request();
final whenInUseStatus = await Permission.locationWhenInUse.request();
final alwaysStatus = await Permission.locationAlways.request();
if (locationStatus == PermissionStatus.granted &&
(whenInUseStatus == PermissionStatus.granted || alwaysStatus == PermissionStatus.granted)) {
_permissionCompleter!.complete(true);
} else {
showPermissionDeniedDialog('location_permission_needed_title', 'location_permission_needed_main');
_permissionCompleter!.complete(false);
}
} catch (e) {
print('Error requesting location permission: $e');
_permissionCompleter!.complete(false);
}
} else {
print('User did not agree to location usage');
_permissionCompleter!.complete(false);
// Exit the app
SystemNavigator.pop();
}
_isRequestingPermission = false;
return _permissionCompleter!.future;
}
*/
static Future<bool> checkStoragePermission() async { static Future<bool> checkStoragePermission() async {
//debugPrint("(gifunavi)== checkStoragePermission =="); //debugPrint("(gifunavi)== checkStoragePermission ==");
@ -42,6 +195,7 @@ class PermissionController {
} }
/*
static Future<bool> checkLocationBasicPermission() async { static Future<bool> checkLocationBasicPermission() async {
//debugPrint("(gifunavi)== checkLocationBasicPermission =="); //debugPrint("(gifunavi)== checkLocationBasicPermission ==");
final locationPermission = await Permission.location.status; final locationPermission = await Permission.location.status;
@ -152,31 +306,8 @@ class PermissionController {
} }
} }
static void showPermissionDeniedDialog(String title,String message) {
Get.dialog( */
AlertDialog(
//title: Text('location_permission_needed_title'.tr),
title: Text(title.tr),
// 位置情報への許可が必要です
//content: Text('location_permission_needed_main'.tr),
content: Text(message.tr),
// 岐阜ロゲでは、位置情報を使用してスタート・チェックイン・ゴール等の通過照明及び移動手段の記録のために、位置情報のトラッキングを行なっています。このためバックグラウンドでもトラッキングができるように位置情報の権限が必要です。
// 設定画面で、「岐阜ナビ」に対して、常に位置情報を許可するように設定してください。
actions: [
TextButton(
child: Text('キャンセル'),
onPressed: () => Get.back(),
),
TextButton(
child: Text('設定'),
onPressed: () {
Get.back();
openAppSettings();
},
),
],
),
);
}
} }

View File

@ -2,169 +2,111 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart';
import 'package:rogapp/routes/app_pages.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>(); final IndexController indexController = Get.find<IndexController>();
TextEditingController emailController = TextEditingController(); final TextEditingController emailController = TextEditingController();
TextEditingController passwordController = TextEditingController(); final TextEditingController passwordController = TextEditingController();
TextEditingController confirmPasswordController = TextEditingController(); final TextEditingController confirmPasswordController = TextEditingController();
RegisterPage({Key? key}) : super(key: key); bool _obscurePassword = true;
bool _obscureConfirmPassword = true;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
showHelperDialog(
'登録メールにアクティベーションメールが送信されます。メールにあるリンクをタップすると正式登録になります。',
'register_page'
);
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: true,
backgroundColor: Colors.white, backgroundColor: Colors.white,
appBar: AppBar( appBar: AppBar(
elevation: 0, elevation: 0,
backgroundColor: Colors.white, backgroundColor: Colors.white,
leading: IconButton( leading: IconButton(
onPressed: () { onPressed: () => Navigator.pop(context),
Navigator.pop(context); icon: const Icon(Icons.arrow_back_ios, size: 20, color: Colors.black),
}, ),
icon: const Icon(
Icons.arrow_back_ios,
size: 20,
color: Colors.black,
)),
), ),
body: SafeArea( body: SafeArea(
child: SingleChildScrollView( child: SingleChildScrollView(
child: SizedBox( child: Container(
height: MediaQuery.of(context).size.height, height: MediaQuery.of(context).size.height,
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Column( Text(
children: [ "sign_up".tr,
Column( style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
"sign_up".tr,
style: 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",
"Passwords does not match",
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(milliseconds: 800),
// 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(milliseconds: 800),
//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),
),
),
],
)
],
), ),
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),
makePasswordInput(
label: "password".tr,
controller: passwordController,
obscureText: _obscurePassword,
onToggleVisibility: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
makePasswordInput(
label: "confirm_password".tr,
controller: confirmPasswordController,
obscureText: _obscureConfirmPassword,
onToggleVisibility: () {
setState(() {
_obscureConfirmPassword = !_obscureConfirmPassword;
});
},
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _handleRegister,
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
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 +114,96 @@ class RegisterPage extends StatelessWidget {
), ),
); );
} }
Widget makePasswordInput({
required String label,
required TextEditingController controller,
required bool obscureText,
required VoidCallback onToggleVisibility,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w400, color: Colors.black87)),
const SizedBox(height: 5),
TextField(
controller: controller,
obscureText: obscureText,
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]!)),
suffixIcon: IconButton(
icon: Icon(
obscureText ? Icons.visibility : Icons.visibility_off,
color: Colors.grey,
),
onPressed: onToggleVisibility,
),
),
),
const SizedBox(height: 20),
],
);
}
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,
confirmPasswordController.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( Widget makeInput({required String label, required TextEditingController controller, bool obsureText = false}) {
{label, required TextEditingController controller, obsureText = false}) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w400, color: Colors.black87)),
label, const SizedBox(height: 5),
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w400, color: Colors.black87),
),
const SizedBox(
height: 5,
),
TextField( TextField(
controller: controller, controller: controller,
obscureText: obsureText, obscureText: obsureText,
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10),
const EdgeInsets.symmetric(vertical: 0, horizontal: 10), enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey[400]!)),
enabledBorder: OutlineInputBorder( border: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey[400]!)),
borderSide: BorderSide(
color: (Colors.grey[400])!,
),
),
border: OutlineInputBorder(
borderSide: BorderSide(color: (Colors.grey[400])!),
),
), ),
), ),
const SizedBox( const SizedBox(height: 20),
height: 30,
)
], ],
); );
} }

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

@ -0,0 +1,11 @@
import 'package:get/get.dart';
import 'package:rogapp/pages/team/member_controller.dart';
import 'package:rogapp/services/api_service.dart';
class MemberBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<ApiService>(() => ApiService());
Get.lazyPut<MemberController>(() => MemberController());
}
}

View File

@ -0,0 +1,276 @@
// lib/controllers/member_controller.dart
import 'package:get/get.dart';
import 'package:rogapp/model/user.dart';
import 'package:rogapp/pages/team/team_controller.dart';
import 'package:rogapp/services/api_service.dart';
class MemberController extends GetxController {
late final ApiService _apiService;
final selectedMember = Rx<User?>(null);
int teamId = 0;
final member = Rx<User?>(null);
final email = ''.obs;
final firstname = ''.obs;
final lastname = ''.obs;
final female = false.obs;
final dateOfBirth = Rx<DateTime?>(null);
final isLoading = false.obs; // isLoadingプロパティを追加
final isActive = false.obs;
//MemberController(this._apiService);
@override
void onInit() {
super.onInit();
_apiService = Get.find<ApiService>();
ever(member, (_) => _initializeMemberData());
loadInitialData();
}
bool get isDummyEmail => email.value.startsWith('dummy_');
bool get isApproved => !email.value.startsWith('dummy_') && member.value?.isActive == true;
Future<void> loadInitialData() async {
try {
isLoading.value = true;
if (Get.arguments != null) {
if (Get.arguments['member'] != null) {
member.value = Get.arguments['member'];
}
if (Get.arguments['teamId'] != null) {
teamId = Get.arguments['teamId'];
}
}
// 他の必要な初期データの取得をここで行う
} catch (e) {
print('Error loading initial data: $e');
} finally {
isLoading.value = false;
}
}
void _initializeMemberData() {
if (member.value != null) {
email.value = member.value!.email ?? '';
firstname.value = member.value!.firstname ?? '';
lastname.value = member.value!.lastname ?? '';
dateOfBirth.value = member.value!.dateOfBirth;
}
}
void setSelectedMember(User member) {
this.member.value = member;
email.value = member.email ?? '';
firstname.value = member.firstname ?? '';
lastname.value = member.lastname ?? '';
dateOfBirth.value = member.dateOfBirth;
female.value = member.female ?? false;
isActive.value = member.isActive ?? false;
}
bool _validateInputs() {
if (email.value.isNotEmpty && !isDummyEmail) {
return true; // Emailのみの場合は有効
}
if (firstname.value.isEmpty || lastname.value.isEmpty || dateOfBirth.value == null || female.value == null) {
Get.snackbar('エラー', 'Emailが空の場合、姓名と生年月日及び性別は必須です', snackPosition: SnackPosition.BOTTOM);
return false;
}
return true;
}
void updateFirstName(String value) {
firstname.value = value;
}
void updateLastName(String value) {
lastname.value = value;
}
Future<bool> saveMember() async {
if (!_validateInputs()) return false;
try {
isLoading.value = true;
User updatedMember;
if (member.value == null || member.value!.id == null) {
// 新規メンバー作成
updatedMember = await _apiService.createTeamMember(
teamId,
isDummyEmail ? null : email.value, // dummy_メールの場合はnullを送信
firstname.value,
lastname.value,
dateOfBirth.value,
female.value,
);
} else {
// 既存メンバー更新
updatedMember = await _apiService.updateTeamMember(
teamId,
member.value!.id!,
firstname.value,
lastname.value,
dateOfBirth.value,
female.value,
);
}
member.value = updatedMember;
Get.snackbar('成功', 'メンバーが保存されました', snackPosition: SnackPosition.BOTTOM);
return true;
} catch (e) {
print('Error saving member: $e');
Get.snackbar('エラー', 'メンバーの保存に失敗しました: ${e.toString()}', snackPosition: SnackPosition.BOTTOM);
return false;
} finally {
isLoading.value = false;
}
}
String getDisplayName() {
if (!isActive.value && !isDummyEmail) {
final displayName = email.value.split('@')[0];
return '$displayName(未承認)';
}
return '${lastname.value} ${firstname.value}'.trim();
}
Future<bool> updateMember() async {
if (member.value == null) return false;
int? memberId = member.value?.id;
try {
final updatedMember = await _apiService.updateTeamMember(
teamId,
memberId!,
firstname.value,
lastname.value,
dateOfBirth.value,
female.value,
);
member.value = updatedMember;
return true;
} catch (e) {
print('Error updating member: $e');
return false;
}
}
Future<bool> deleteMember() async {
if (member.value == null || member.value!.id == null) {
Get.snackbar('エラー', 'メンバー情報が不正です', snackPosition: SnackPosition.BOTTOM);
return false;
}
try {
isLoading.value = true;
await _apiService.deleteTeamMember(teamId, member.value!.id!);
Get.snackbar('成功', 'メンバーが削除されました', snackPosition: SnackPosition.BOTTOM);
member.value = null;
isLoading.value = false;
return true;
} catch (e) {
print('Error deleting member: $e');
Get.snackbar('エラー', 'メンバーの削除に失敗しました: ${e.toString()}', snackPosition: SnackPosition.BOTTOM);
isLoading.value = false;
return false;
}
}
/*
Future<void> deleteMember() async {
if (member.value == null) return;
int? memberId = member.value?.id;
try {
await _apiService.deleteTeamMember(teamId,memberId!);
member.value = null;
Get.back();
} catch (e) {
print('Error deleting member: $e');
}
}
*/
Future<void> resendInvitation() async {
if (isDummyEmail) {
Get.snackbar('エラー', 'ダミーメールアドレスには招待メールを送信できません', snackPosition: SnackPosition.BOTTOM);
return;
}
if (member.value == null || member.value!.email == null) return;
int? memberId = member.value?.id;
try {
await _apiService.resendMemberInvitation(memberId!);
Get.snackbar('Success', 'Invitation resent successfully');
} catch (e) {
print('Error resending invitation: $e');
Get.snackbar('Error', 'Failed to resend invitation');
}
}
void updateEmail(String value) => email.value = value;
void updateFirstname(String value) => firstname.value = value;
void updateLastname(String value) => lastname.value = value;
void updateDateOfBirth(DateTime value) => dateOfBirth.value = value;
String getMemberStatus() {
if (member.value == null) return '';
if (member.value!.email == null) return '未登録';
if (member.value!.isActive) return '承認済';
return '招待中';
}
int calculateAge() {
if (dateOfBirth.value == null) return 0;
final today = DateTime.now();
int age = today.year - dateOfBirth.value!.year;
if (today.month < dateOfBirth.value!.month ||
(today.month == dateOfBirth.value!.month && today.day < dateOfBirth.value!.day)) {
age--;
}
return age;
}
String calculateGrade() {
if (dateOfBirth.value == null) return '不明';
final today = DateTime.now();
final birthDate = dateOfBirth.value!;
// 今年の4月1日
final thisYearSchoolStart = DateTime(today.year, 4, 1);
// 生まれた年の翌年の4月1日学齢期の始まり
final schoolStartDate = DateTime(birthDate.year + 1, 4, 1);
// 学齢期の開始からの年数
int yearsFromSchoolStart = today.year - schoolStartDate.year;
// 今年の4月1日より前なら1年引く
if (today.isBefore(thisYearSchoolStart)) {
yearsFromSchoolStart--;
}
if (yearsFromSchoolStart < 7) return '未就学';
if (yearsFromSchoolStart < 13) return '${yearsFromSchoolStart - 6}';
if (yearsFromSchoolStart < 16) return '${yearsFromSchoolStart - 12}';
if (yearsFromSchoolStart < 19) return '${yearsFromSchoolStart - 15}';
return '成人';
}
String getAgeAndGrade() {
final age = calculateAge();
final grade = calculateGrade();
return '$age歳/$grade';
}
bool isOver18() {
return calculateAge() >= 18;
}
}

View File

@ -0,0 +1,281 @@
// lib/pages/team/member_detail_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rogapp/pages/team/member_controller.dart';
import 'package:intl/intl.dart'; // この行を追加
import 'package:flutter_localizations/flutter_localizations.dart'; // 追加
import 'package:flutter/cupertino.dart';
import 'package:rogapp/routes/app_pages.dart';
class MemberDetailPage extends StatefulWidget {
@override
_MemberDetailPageState createState() => _MemberDetailPageState();
}
class _MemberDetailPageState extends State<MemberDetailPage> {
final MemberController controller = Get.find<MemberController>();
late TextEditingController _firstNameController;
late TextEditingController _lastNameController;
late TextEditingController _emailController;
@override
void initState() {
super.initState();
_initializeControllers();
WidgetsBinding.instance.addPostFrameCallback((_) {
final mode = Get.arguments['mode'];
final member = Get.arguments['member'];
if (mode == 'edit' && member != null) {
controller.setSelectedMember(member);
}
});
}
void _initializeControllers() {
_firstNameController =
TextEditingController(text: controller.firstname.value);
_lastNameController =
TextEditingController(text: controller.lastname.value);
_emailController = TextEditingController(text: controller.email.value);
controller.firstname.listen((value) {
if (_firstNameController.text != value) {
_firstNameController.value = TextEditingValue(
text: value,
selection: TextSelection.fromPosition(
TextPosition(offset: value.length)),
);
}
});
controller.lastname.listen((value) {
if (_lastNameController.text != value) {
_lastNameController.value = TextEditingValue(
text: value,
selection: TextSelection.fromPosition(
TextPosition(offset: value.length)),
);
}
});
controller.email.listen((value) {
if (_emailController.text != value) {
_emailController.value = TextEditingValue(
text: value,
selection: TextSelection.fromPosition(
TextPosition(offset: value.length)),
);
}
});
}
Future<void> _handleSaveAndNavigateBack() async {
bool success = await controller.saveMember();
if (success) {
Get.until((route) => Get.currentRoute == AppPages.TEAM_DETAIL);
// スナックバーが表示されるのを待つ
await Future.delayed(Duration(seconds: 1));
// 現在のスナックバーを安全に閉じる
if (Get.isSnackbarOpen) {
await Get.closeCurrentSnackbar();
}
// リストページに戻る
//Get.until((route) => Get.currentRoute == '/team');
// または、リストページの具体的なルート名がある場合は以下を使用
// Get.offNamed('/team');
}
}
Future<void> _handleDeleteAndNavigateBack() 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) {
bool success = await controller.deleteMember();
if (success) {
// リストページに戻る
//Get.offNamed(result: true);
Get.until((route) => Get.currentRoute == AppPages.TEAM_DETAIL);
// スナックバーが表示されるのを待つ
await Future.delayed(Duration(seconds: 1));
// 現在のスナックバーを安全に閉じる
if (Get.isSnackbarOpen) {
await Get.closeCurrentSnackbar();
}
}
}
}
@override
Widget build(BuildContext context) {
final mode = Get.arguments['mode'] as String;
//final member = Get.arguments['member'];
final teamId = Get.arguments['teamId'] as int;
/*
return MaterialApp( // MaterialApp をここに追加
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
const Locale('ja', 'JP'),
],
home:Scaffold(
*/
return Scaffold(
appBar: AppBar(
title: Text(mode == 'new' ? 'メンバー追加' : 'メンバー詳細'),
),
body: Obx(() {
if (controller.isLoading.value) {
return 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(
controller: _emailController,
onChanged: (value) => controller.updateEmail(value),
decoration: InputDecoration(labelText: 'メールアドレス'),
keyboardType: TextInputType.emailAddress, // メールアドレス用のキーボードを表示
autocorrect: false, // 自動修正を無効化
enableSuggestions: false,
)
else if (controller.isDummyEmail)
Text('メールアドレス: (メアド無し)')
else
Text('メールアドレス: ${controller.email.value}'),
if (controller.email.value.isEmpty || controller.isDummyEmail || mode == 'edit') ...[
TextFormField(
decoration: InputDecoration(labelText: ''),
controller: _lastNameController,
onChanged: (value) => controller.updateLastName(value),
//controller: TextEditingController(text: controller.lastname.value),
),
TextFormField(
decoration: InputDecoration(labelText: ''),
controller: _firstNameController,
//onChanged: (value) => controller.firstname.value = value,
onChanged: (value) => controller.updateFirstName(value),
//controller: TextEditingController(text: controller.firstname.value),
),
// 生年月日
if (controller.isDummyEmail || !controller.isOver18())
ListTile(
title: Text('生年月日'),
subtitle: Text(controller.dateOfBirth.value != null
? '${DateFormat('yyyy年MM月dd日').format(controller.dateOfBirth.value!)} (${controller.getAgeAndGrade()})'
: '未設定'),
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: controller.dateOfBirth.value ?? DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
//locale: const Locale('ja', 'JP'),
);
if (date != null) controller.dateOfBirth.value = date;
},
)
else
Text('18歳以上'),
SwitchListTile(
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: _handleDeleteAndNavigateBack,
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: _handleSaveAndNavigateBack,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
),
],
),
),
],
);
}),
);
}
@override
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
_emailController.dispose();
super.dispose();
}
}

View File

@ -0,0 +1,11 @@
import 'package:get/get.dart';
import 'package:rogapp/pages/team/team_controller.dart';
import 'package:rogapp/services/api_service.dart';
class TeamBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<ApiService>(() => ApiService());
Get.lazyPut<TeamController>(() => TeamController());
}
}

View File

@ -0,0 +1,332 @@
// 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';
import 'package:rogapp/model/user.dart';
import 'package:rogapp/services/api_service.dart';
class TeamController extends GetxController {
late final ApiService _apiService;
final teams = <Team>[].obs;
final categories = <NewCategory>[].obs;
final teamMembers = <User>[].obs;
final selectedCategory = Rx<NewCategory?>(null);
final selectedTeam = Rx<Team?>(null);
final currentUser = Rx<User?>(null);
final teamName = ''.obs;
final isLoading = false.obs;
final error = RxString('');
@override
void onInit() async{
super.onInit();
try {
_apiService = Get.find<ApiService>();
isLoading.value = true;
await fetchCategories();
await Future.wait([
fetchTeams(),
getCurrentUser(),
]);
print("selectedCategory=$selectedCategory.value");
// カテゴリが取得できたら、最初のカテゴリを選択状態にする
if (categories.isNotEmpty && selectedCategory.value == null) {
selectedCategory.value = categories.first;
}
}catch(e){
print("Team Controller error: $e");
error.value = e.toString();
}finally{
isLoading.value = false;
}
}
void setSelectedTeam(Team team) {
selectedTeam.value = team;
teamName.value = team.teamName;
if (categories.isNotEmpty) {
selectedCategory.value = categories.firstWhere(
(category) => category.id == team.category.id,
orElse: () => categories.first,
);
} else {
// カテゴリリストが空の場合、teamのカテゴリをそのまま使用
selectedCategory.value = team.category;
}
fetchTeamMembers(team.id);
}
void resetForm() {
selectedTeam.value = null;
teamName.value = '';
if (categories.isNotEmpty) {
selectedCategory.value = categories.first;
} else {
selectedCategory.value = null;
// カテゴリが空の場合、エラーメッセージをセット
error.value = 'カテゴリデータが取得できませんでした。';
} teamMembers.clear();
}
void cleanupForNavigation() {
selectedTeam.value = null;
teamName.value = '';
selectedCategory.value = categories.isNotEmpty ? categories.first : null;
teamMembers.clear();
//teamMembersはクリアしない
// 必要に応じて他のクリーンアップ処理を追加
}
Future<void> fetchTeams() async {
try {
isLoading.value = true;
final fetchedTeams = await _apiService.getTeams();
teams.assignAll(fetchedTeams);
} catch (e) {
error.value = 'チームの取得に失敗しました: $e';
print('Error fetching teams: $e');
} finally {
isLoading.value = false;
}
}
bool checkIfUserHasEntryData(){
if (teams.isEmpty) {
return false;
}else {
return true;
}
}
Future<void> fetchCategories() async {
try {
final fetchedCategories = await _apiService.getCategories();
categories.assignAll(fetchedCategories);
print("Fetched categories: ${categories.length}"); // デバッグ用
} catch (e) {
print('Error fetching categories: $e');
}
}
Future<void> getCurrentUser() async {
try {
final user = await _apiService.getCurrentUser();
currentUser.value = user;
} catch (e) {
print('Error getting current user: $e');
}
}
Future<Team> createTeam(String teamName, int categoryId) async {
final newTeam = await _apiService.createTeam(teamName, categoryId);
// 自分自身をメンバーとして自動登録
await _apiService.createTeamMember(
newTeam.id,
currentUser.value?.email,
currentUser.value!.firstname,
currentUser.value!.lastname,
currentUser.value?.dateOfBirth,
currentUser.value?.female,
);
return newTeam;
}
Future<Team> updateTeam(int teamId, String teamName, int categoryId) async {
// APIサービスを使用してチームを更新
final updatedTeam = await _apiService.updateTeam(teamId, teamName, categoryId);
return updatedTeam;
}
Future<void> deleteTeam(int teamId) async {
bool confirmDelete = await Get.dialog(
AlertDialog(
title: 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('エラー', 'チームとメンバーの削除に失敗しました');
}
}
}
Future<void> fetchTeamMembers(int teamId) async {
try {
isLoading.value = true;
final members = await _apiService.getTeamMembers(teamId);
teamMembers.assignAll(members);
} catch (e) {
error.value = 'メンバーの取得に失敗しました: $e';
print('Error fetching team members: $e');
} finally {
isLoading.value = false;
}
}
Future<void> updateMember(teamId, User member) async {
try {
isLoading.value = true;
await _apiService.updateTeamMember(teamId,member.id, member.firstname, member.lastname, member.dateOfBirth, member.female);
await fetchTeamMembers(selectedTeam.value!.id);
} catch (e) {
error.value = 'メンバーの更新に失敗しました: $e';
print('Error updating member: $e');
} finally {
isLoading.value = false;
}
}
Future<void> deleteMember(int memberId, int teamId) async {
try {
isLoading.value = true;
await _apiService.deleteTeamMember(teamId,memberId);
await fetchTeamMembers(teamId);
} catch (e) {
error.value = 'メンバーの削除に失敗しました: $e';
print('Error deleting member: $e');
} finally {
isLoading.value = false;
}
}
// Future<void> addMember(User member, int teamId) async {
// try {
// isLoading.value = true;
// await _apiService.createTeamMember(teamId, member.email, member.firstname, member.lastname, member.dateOfBirth);
// await fetchTeamMembers(teamId);
// } catch (e) {
// error.value = 'メンバーの追加に失敗しました: $e';
// print('Error adding member: $e');
// } finally {
// isLoading.value = false;
// }
// }
void updateTeamName(String value) {
teamName.value = value;
}
void updateCategory(NewCategory? value) {
if (value != null) {
selectedCategory.value = value;
}
}
//void updateCategory(NewCategory? value) {
// if (value != null) {
// selectedCategory.value = categories.firstWhere(
// (category) => category.id == value.id,
// orElse: () => value,
// );
// }
//}
Future<void> saveTeam() async {
try {
isLoading.value = true;
if (selectedCategory.value == null) {
throw Exception('カテゴリを選択してください');
}
if (selectedTeam.value == null) {
await createTeam(teamName.value, selectedCategory.value!.id);
} else {
await updateTeam(selectedTeam.value!.id, teamName.value, selectedCategory.value!.id);
}
// サーバーから最新のデータを再取得
await fetchTeams();
update(); // UIを強制的に更新
} catch (e) {
error.value = 'チームの保存に失敗しました: $e';
} finally {
isLoading.value = false;
}
}
Future<void> deleteSelectedTeam() async {
if (selectedTeam.value != null) {
await deleteTeam(selectedTeam.value!.id);
selectedTeam.value = null;
}
}
List<NewCategory> getFilteredCategories() {
//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

@ -0,0 +1,215 @@
// lib/pages/team/team_detail_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rogapp/pages/team/team_controller.dart';
import 'package:rogapp/routes/app_pages.dart';
import 'package:rogapp/model/team.dart';
import 'package:rogapp/model/category.dart';
class TeamDetailPage extends StatefulWidget {
@override
_TeamDetailPageState createState() => _TeamDetailPageState();
}
class _TeamDetailPageState extends State<TeamDetailPage> {
late TeamController controller;
late TextEditingController _teamNameController = TextEditingController();
final RxString mode = ''.obs;
final Rx<Team?> team = Rx<Team?>(null);
Worker? _teamNameWorker;
@override
void initState() {
super.initState();
controller = Get.find<TeamController>();
_teamNameController = TextEditingController();
WidgetsBinding.instance.addPostFrameCallback((_) {
_initializeData();
});
}
void _initializeData() {
final args = Get.arguments;
if (args != null && args is Map<String, dynamic>) {
mode.value = args['mode'] as String? ?? '';
team.value = args['team'] as Team?;
if (mode.value == 'edit' && team.value != null) {
controller.setSelectedTeam(team.value!);
} else {
controller.resetForm();
}
} else {
// 引数がない場合は新規作成モードとして扱う
mode.value = 'new';
controller.resetForm();
}
_teamNameController.text = controller.teamName.value;
// Use ever instead of direct listener
_teamNameWorker = ever(controller.teamName, (String value) {
if (_teamNameController.text != value) {
_teamNameController.text = value;
}
});
}
@override
void dispose() {
_teamNameWorker?.dispose();
_teamNameController.dispose();
//controller.resetForm(); // Reset the form when leaving the page
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(Get.arguments['mode'] == 'new' ? '新規チーム作成' : 'チーム詳細'),
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
controller.cleanupForNavigation();
Get.back();
},
),
actions: [
IconButton(
icon: Icon(Icons.save),
onPressed: () async {
try {
await controller.saveTeam();
Get.back();
} catch (e) {
Get.snackbar('エラー', e.toString(), snackPosition: SnackPosition.BOTTOM);
}
},
),
Obx(() {
if (mode.value == 'edit') {
return IconButton(
icon: Icon(Icons.delete),
onPressed: () async {
try {
await controller.deleteSelectedTeam();
Get.back();
} catch (e) {
Get.snackbar('エラー', e.toString(), snackPosition: SnackPosition.BOTTOM);
}
},
);
} else {
return SizedBox.shrink();
}
}),
],
),
body: Obx(() {
if (controller.isLoading.value) {
return Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
decoration: InputDecoration(labelText: 'チーム名'),
controller: _teamNameController,
onChanged: (value) => controller.updateTeamName(value),
),
SizedBox(height: 16),
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),
);
}),
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Text('所有者: ${controller.currentUser.value?.email ?? "未設定"}'),
),
if (mode.value == 'edit') ...[
SizedBox(height: 24),
Text('メンバーリスト', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: controller.teamMembers.length,
itemBuilder: (context, index) {
final member = controller.teamMembers[index];
final isDummyEmail = member.email?.startsWith('dummy_') ?? false;
final displayName = isDummyEmail
? '${member.lastname} ${member.firstname}'
: member.isActive
? '${member.lastname} ${member.firstname}'
: '${member.email?.split('@')[0] ?? ''}'; //(未承認)';
return ListTile(
title: Text(displayName),
subtitle: isDummyEmail ? Text('Email未設定') : null,
onTap: () async {
final result = await Get.toNamed(
AppPages.MEMBER_DETAIL,
arguments: {'mode': 'edit', 'member': member, 'teamId': controller.selectedTeam.value?.id},
);
await controller.fetchTeamMembers(controller.selectedTeam.value!.id);
},
);
},
),
SizedBox(height: 16),
ElevatedButton(
child: Text('新しいメンバーを追加'),
onPressed: () async {
await Get.toNamed(
AppPages.MEMBER_DETAIL,
arguments: {'mode': 'new', 'teamId': controller.selectedTeam.value?.id},
);
if (controller.selectedTeam.value != null) {
controller.fetchTeamMembers(controller.selectedTeam.value!.id);
}
},
),
],
],
),
)
);
}),
);
}
}

View File

@ -0,0 +1,56 @@
// lib/pages/team/team_list_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rogapp/pages/team/team_controller.dart';
import 'package:rogapp/routes/app_pages.dart';
import 'package:rogapp/services/api_service.dart';
class TeamListPage extends GetWidget<TeamController> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('チーム管理'),
actions: [
IconButton(
icon: Icon(Icons.add),
onPressed: () => Get.toNamed(AppPages.TEAM_DETAIL, arguments: {'mode': 'new'}),
),
],
),
body: Obx(() {
if (controller.isLoading.value) {
return Center(child: CircularProgressIndicator());
} else if (controller.error.value.isNotEmpty) {
return Center(child: Text(controller.error.value));
} else if (controller.teams.isEmpty) {
return Center(child: Text('チームがありません'));
} else {
return RefreshIndicator(
onRefresh: controller.fetchTeams,
child: ListView.builder(
itemCount: controller.teams.length,
itemBuilder: (context, index) {
final team = controller.teams[index];
return ListTile(
title: Text(team.teamName),
subtitle: Text('${team.category.categoryName} '),
onTap: () async {
await Get.toNamed(
AppPages.TEAM_DETAIL,
arguments: {'mode': 'edit', 'team': team},
);
controller.fetchTeams();
},
);
},
),
);
}
}),
);
}
}

View File

@ -28,6 +28,19 @@ import 'package:rogapp/spa/spa_binding.dart';
import 'package:rogapp/spa/spa_page.dart'; import 'package:rogapp/spa/spa_page.dart';
import 'package:rogapp/widgets/permission_handler_screen.dart'; import 'package:rogapp/widgets/permission_handler_screen.dart';
import 'package:rogapp/pages/team/team_binding.dart';
import 'package:rogapp/pages/team/team_list_page.dart';
import 'package:rogapp/pages/team/team_detail_page.dart';
import 'package:rogapp/pages/team/member_binding.dart';
import 'package:rogapp/pages/team/member_detail_page.dart';
import 'package:rogapp/pages/entry/entry_list_page.dart';
import 'package:rogapp/pages/entry/entry_detail_page.dart';
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'; part 'app_routes.dart';
class AppPages { class AppPages {
@ -56,6 +69,14 @@ class AppPages {
static const GPS = Routes.GPS; static const GPS = Routes.GPS;
static const SETTINGS = Routes.SETTINGS; static const SETTINGS = Routes.SETTINGS;
static const DEBUG = Routes.DEBUG; static const DEBUG = Routes.DEBUG;
static const TEAM_LIST = Routes.TEAM_LIST;
static const TEAM_DETAIL = Routes.TEAM_DETAIL;
static const MEMBER_DETAIL = Routes.MEMBER_DETAIL;
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 = [ static final routes = [
GetPage( GetPage(
@ -127,7 +148,40 @@ class AppPages {
page: () => DebugPage(), page: () => DebugPage(),
binding: DebugBinding(), binding: DebugBinding(),
), ),
GetPage(
name: Routes.TEAM_LIST,
page: () => TeamListPage(),
binding: TeamBinding(),
),
GetPage(
name: Routes.TEAM_DETAIL,
page: () => TeamDetailPage(),
binding: TeamBinding(),
),
GetPage(
name: Routes.MEMBER_DETAIL,
page: () => MemberDetailPage(),
binding: MemberBinding(),
),
GetPage(
name: Routes.ENTRY_LIST,
page: () => EntryListPage(),
binding: EntryBinding(),
),
GetPage(
name: Routes.ENTRY_DETAIL,
page: () => EntryDetailPage(),
binding: EntryBinding(),
),
GetPage(
name: Routes.EVENT_ENTRIES,
page: () => EventEntriesPage(),
binding: EventEntriesBinding(),
),
GetPage(
name: Routes.USER_DETAILS_EDIT,
page: () => UserDetailsEditPage(),
),
]; ];
} }

View File

@ -28,4 +28,14 @@ abstract class Routes {
static const GPS = '/gp'; static const GPS = '/gp';
static const SETTINGS = '/settings'; static const SETTINGS = '/settings';
static const DEBUG = '/debug'; static const DEBUG = '/debug';
static const TEAM_LIST = '/team-list';
static const TEAM_DETAIL = '/team-detail';
static const MEMBER_DETAIL = '/member-detail';
static const ENTRY_LIST = '/entry-list';
static const ENTRY_DETAIL = '/entry-detail';
static const EVENT_ENTRIES = '/event-entries';
static const USER_DETAILS_EDIT = '/user-details-edit';
} }

View File

@ -0,0 +1,693 @@
// lib/services/api_service.dart
import 'package:get/get.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart';
import 'package:rogapp/model/entry.dart';
import 'package:rogapp/model/event.dart';
import 'package:rogapp/model/team.dart';
import 'package:rogapp/model/category.dart';
import 'package:rogapp/model/user.dart';
import 'package:rogapp/pages/index/index_controller.dart';
import '../utils/const.dart';
import 'package:intl/intl.dart';
class ApiService extends GetxService{
static ApiService get to => Get.find<ApiService>();
String serverUrl = '';
String baseUrl = '';
String token = 'your-auth-token';
Future<ApiService> init() async {
try {
// ここで必要な初期化処理を行う
serverUrl = ConstValues.currentServer();
baseUrl = '$serverUrl/api';
//await Future.delayed(Duration(seconds: 2)); // 仮の遅延(実際の初期化処理に置き換えてください)
print('ApiService initialized successfully');
return this;
} catch(e) {
print('Error in ApiService initialization: $e');
rethrow; // エラーを再スローして、呼び出し元で処理できるようにする
//return this;
}
}
/*
このメソッドは以下のように動作します:
まず、渡された type パラメータに基づいて、どのクラスのフィールドを扱っているかを判断します。
次に、クラス内で fieldName に対応する期待される型を返します。
クラスや フィールド名が予期されていないものである場合、'Unknown' または 'Unknown Type' を返します。
このメソッドを ApiService クラスに追加することで、_printDataComparison メソッドは各フィールドの期待される型を正確に表示できるようになります。
さらに、このメソッドを使用することで、API レスポンスのデータ型が期待と異なる場合に簡単に検出できるようになります。例えば、Category クラスの duration フィールドが整数型秒数で期待されているのに対し、API が文字列を返した場合、すぐに問題を特定できます。
注意点として、API のレスポンス形式が変更された場合や、新しいフィールドが追加された場合は、このメソッドも更新する必要があります。そのため、API の変更とクライアントサイドのコードの同期を保つことが重要です。
*/
String getToken()
{
// IndexControllerの初期化を待つ
final indexController = Get.find<IndexController>();
if (indexController.currentUser.isNotEmpty) {
token = indexController.currentUser[0]['token'] ?? '';
print("Get token = $token");
}else{
token = "";
}
return token;
}
Future<List<Team>> getTeams() async {
init();
getToken();
try {
final response = await http.get(
Uri.parse('$baseUrl/teams/'),
headers: {'Authorization': 'Token $token',"Content-Type": "application/json; charset=UTF-8"},
);
if (response.statusCode == 200) {
// UTF-8でデコード
final decodedResponse = utf8.decode(response.bodyBytes);
//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);
teams.add(Team.fromJson(teamJson));
}
return teams;
} else {
throw Exception('Failed to load teams. Status code: ${response.statusCode}');
}
} catch (e, stackTrace) {
print('Error in getTeams: $e');
print('Stack trace: $stackTrace');
rethrow;
}
}
Future<List<NewCategory>> getCategories() async {
init();
getToken();
try {
final response = await http.get(
Uri.parse('$baseUrl/categories/'),
headers: {'Authorization': 'Token $token',"Content-Type": "application/json; charset=UTF-8"},
);
if (response.statusCode == 200) {
final decodedResponse = utf8.decode(response.bodyBytes);
print('User Response body: $decodedResponse');
List<dynamic> categoriesJson = json.decode(decodedResponse);
List<NewCategory> categories = [];
for (var categoryJson in categoriesJson) {
try {
//print('\nCategory Data:');
//_printDataComparison(categoryJson, NewCategory);
categories.add(NewCategory.fromJson(categoryJson));
}catch(e){
print('Error parsing category: $e');
print('Problematic JSON: $categoryJson');
}
}
return categories;
} else {
throw Exception(
'Failed to load categories. Status code: ${response.statusCode}');
}
}catch(e, stackTrace){
print('Error in getCategories: $e');
print('Stack trace: $stackTrace');
rethrow;
}
}
Future<NewCategory> getZekkenNumber(int categoryId) async {
try {
final response = await http.post(
Uri.parse('$baseUrl/categories-viewset/$categoryId/get_zekken_number/'),
headers: {'Authorization': 'Token $token',"Content-Type": "application/json; charset=UTF-8"},
);
if (response.statusCode == 200) {
final decodedResponse = utf8.decode(response.bodyBytes);
print('User Response body: $decodedResponse');
final categoriesJson = json.decode(decodedResponse);
return NewCategory.fromJson(categoriesJson);
} else {
throw Exception('Failed to increment category number');
}
} catch (e) {
throw Exception('Error incrementing category number: $e');
}
}
Future<User> getCurrentUser() async {
init();
getToken();
try {
final response = await http.get(
Uri.parse('$baseUrl/user/'),
headers: {'Authorization': 'Token $token',"Content-Type": "application/json; charset=UTF-8"},
);
if (response.statusCode == 200) {
final decodedResponse = utf8.decode(response.bodyBytes);
//print('User Response body: $decodedResponse');
final jsonData = json.decode(decodedResponse);
//print('\nUser Data Comparison:');
//_printDataComparison(jsonData, User);
return User.fromJson(jsonData);
} else {
throw Exception('Failed to get current user. Status code: ${response.statusCode}');
}
} catch (e, stackTrace) {
print('Error in getCurrentUser: $e');
print('Stack trace: $stackTrace');
rethrow;
}
}
void _printDataComparison(Map<String, dynamic> data, Type expectedType) {
print('Field\t\t| Expected Type\t| Actual Type\t| Actual Value');
print('------------------------------------------------------------');
data.forEach((key, value) {
String expectedFieldType = _getExpectedFieldType(expectedType, key);
_printComparison(key, expectedFieldType, value);
});
}
String _getExpectedFieldType(Type type, String fieldName) {
// This method should return the expected type for each field based on the class definition
// You might need to implement this based on your class structures
switch (type) {
case NewCategory:
switch (fieldName) {
case 'id': return 'int';
case 'category_name': return 'String';
case 'category_number': return 'int';
case 'duration': return 'int (seconds)';
case 'num_of_member': return 'int';
case 'family': return 'bool';
case 'female': return 'bool';
default: return 'Unknown';
}
case Team:
switch (fieldName) {
case 'id': return 'int';
case 'zekken_number': return 'String';
case 'team_name': return 'String';
case 'category': return 'NewCategory (Object)';
case 'owner': return 'User (Object)';
default: return 'Unknown';
}
case User:
switch (fieldName) {
case 'id': return 'int';
case 'email': return 'String';
case 'firstname': return 'String';
case 'lastname': return 'String';
case 'date_of_birth': return 'String (ISO8601)';
case 'female': return 'bool';
case 'is_active': return 'bool';
default: return 'Unknown';
}
default:
return 'Unknown Type';
}
}
void _printComparison(String fieldName, String expectedType, dynamic actualValue) {
String actualType = actualValue?.runtimeType.toString() ?? 'null';
String displayValue = actualValue.toString();
if (displayValue.length > 50) {
displayValue = '${displayValue.substring(0, 47)}...';
}
print('$fieldName\t\t| $expectedType\t\t| $actualType\t\t| $displayValue');
}
Future<Team> createTeam(String teamName, int categoryId) async {
init();
getToken();
final response = await http.post(
Uri.parse('$baseUrl/teams/'),
headers: {
'Authorization': 'Token $token',
"Content-Type": "application/json; charset=UTF-8",
},
body: json.encode({
'team_name': teamName,
'category': categoryId,
}),
);
if (response.statusCode == 201) {
final decodedResponse = utf8.decode(response.bodyBytes);
return Team.fromJson(json.decode(decodedResponse));
} else {
throw Exception('Failed to create team');
}
}
Future<Team> updateTeam(int teamId, String teamName, int categoryId) async {
init();
getToken();
final response = await http.put(
Uri.parse('$baseUrl/teams/$teamId/'),
headers: {
'Authorization': 'Token $token',
'Content-Type': 'application/json; charset=UTF-8',
},
body: json.encode({
'team_name': teamName,
'category': categoryId,
}),
);
if (response.statusCode == 200) {
final decodedResponse = utf8.decode(response.bodyBytes);
return Team.fromJson(json.decode(decodedResponse));
} else {
throw Exception('Failed to update team');
}
}
Future<void> deleteTeam(int teamId) async {
init();
getToken();
final response = await http.delete(
Uri.parse('$baseUrl/teams/$teamId/'),
headers: {'Authorization': 'Token $token','Content-Type': 'application/json; charset=UTF-8'},
);
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');
}
}
Future<List<User>> getTeamMembers(int teamId) async {
init();
getToken();
final response = await http.get(
Uri.parse('$baseUrl/teams/$teamId/members/'),
headers: {'Authorization': 'Token $token','Content-Type': 'application/json; charset=UTF-8'},
);
if (response.statusCode == 200) {
final decodedResponse = utf8.decode(response.bodyBytes);
print('User Response body: $decodedResponse');
List<dynamic> membersJson = json.decode(decodedResponse);
return membersJson.map((json) => User.fromJson(json)).toList();
} else {
throw Exception('Failed to load team members');
}
}
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);
}
final response = await http.post(
Uri.parse('$baseUrl/teams/$teamId/members/'),
headers: {
'Authorization': 'Token $token',
'Content-Type': 'application/json; charset=UTF-8',
},
body: json.encode({
'email': email,
'firstname': firstname,
'lastname': lastname,
'date_of_birth': formattedDateOfBirth,
'female': female,
}),
);
if (response.statusCode == 200 || response.statusCode == 201) {
final decodedResponse = utf8.decode(response.bodyBytes);
return User.fromJson(json.decode(decodedResponse));
} else {
throw Exception('Failed to create team member');
}
}
Future<User> updateTeamMember(int teamId,int? memberId, String firstname, String lastname, DateTime? dateOfBirth,bool? female) async {
init();
getToken();
String? formattedDateOfBirth;
if (dateOfBirth != null) {
formattedDateOfBirth = DateFormat('yyyy-MM-dd').format(dateOfBirth);
}
final response = await http.put(
Uri.parse('$baseUrl/teams/$teamId/members/$memberId/'),
headers: {
'Authorization': 'Token $token',
'Content-Type': 'application/json; charset=UTF-8',
},
body: json.encode({
'firstname': firstname,
'lastname': lastname,
'date_of_birth': formattedDateOfBirth,
'female': female,
}),
);
if (response.statusCode == 200) {
final decodedResponse = utf8.decode(response.bodyBytes);
return User.fromJson(json.decode(decodedResponse));
} else {
throw Exception('Failed to update team member');
}
}
Future<void> deleteTeamMember(int teamId,int memberId) async {
init();
getToken();
final response = await http.delete(
Uri.parse('$baseUrl/teams/$teamId/members/$memberId/'),
headers: {'Authorization': 'Token $token'},
);
if (response.statusCode != 204) {
throw Exception('Failed to delete team member');
}
}
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();
final response = await http.post(
Uri.parse('$baseUrl/members/$memberId/resend-invitation/'),
headers: {'Authorization': 'Token $token', 'Content-Type': 'application/json; charset=UTF-8',
},
);
if (response.statusCode != 200) {
throw Exception('Failed to resend invitation');
}
}
Future<List<Entry>> getEntries() async {
init();
getToken();
final response = await http.get(
Uri.parse('$baseUrl/entry/'),
headers: {'Authorization': 'Token $token', 'Content-Type': 'application/json; charset=UTF-8',
},
);
if (response.statusCode == 200) {
final decodedResponse = utf8.decode(response.bodyBytes);
List<dynamic> entriesJson = json.decode(decodedResponse);
return entriesJson.map((json) => Entry.fromJson(json)).toList();
} else {
throw Exception('Failed to load entries');
}
}
Future<List<Event>> getEvents() async {
init();
getToken();
final response = await http.get(
Uri.parse('$baseUrl/new-events/',),
headers: {'Authorization': 'Token $token', 'Content-Type': 'application/json; charset=UTF-8',
},
);
if (response.statusCode == 200) {
final decodedResponse = utf8.decode(response.bodyBytes);
print('Response body: $decodedResponse');
List<dynamic> eventsJson = json.decode(decodedResponse);
return eventsJson.map((json) => Event.fromJson(json)).toList();
} else {
throw Exception('Failed to load events');
}
}
Future<Entry> createEntry(int teamId, int eventId, int categoryId, DateTime date,String zekkenNumber) async {
init();
getToken();
String? formattedDate;
if (date != null) {
formattedDate = DateFormat('yyyy-MM-dd').format(date);
}
final response = await http.post(
Uri.parse('$baseUrl/entry/'),
headers: {
'Authorization': 'Token $token',
'Content-Type': 'application/json; charset=UTF-8',
},
body: json.encode({
'team': teamId,
'event': eventId,
'category': categoryId,
'date': formattedDate,
'zekken_number':zekkenNumber,
}),
);
if (response.statusCode == 201) {
final decodedResponse = utf8.decode(response.bodyBytes);
return Entry.fromJson(json.decode(decodedResponse));
} else {
final decodedResponse = utf8.decode(response.bodyBytes);
print("decodedResponse = $decodedResponse");
throw Exception('Failed to create entry');
}
}
Future<void> updateUserInfo(int userId, Entry entry) async {
init();
getToken();
final entryId = entry.id;
DateTime? date = entry.date;
String? formattedDate;
if (date != null) {
formattedDate = DateFormat('yyyy-MM-dd').format(date);
}
final response = await http.put(
Uri.parse('$baseUrl/userinfo/$userId/'),
headers: {
'Authorization': 'Token $token',
'Content-Type': 'application/json; charset=UTF-8',
},
body: json.encode({
'zekken_number': entry.zekkenNumber,
'event_code': entry.event.eventName,
'group': entry.team.category.categoryName,
'team_name': entry.team.teamName,
'date': formattedDate,
}),
);
if (response.statusCode == 200) {
final decodedResponse = utf8.decode(response.bodyBytes);
final updatedUserInfo = json.decode(decodedResponse);
//Get.find<IndexController>().updateUserInfo(updatedUserInfo);
} else {
throw Exception('Failed to update entry');
}
}
Future<Entry> updateEntry(int entryId, int teamId, int eventId, int categoryId, DateTime date,int zekken_number) async {
init();
getToken();
String? formattedDate;
if (date != null) {
formattedDate = DateFormat('yyyy-MM-dd').format(date);
}
final response = await http.put(
Uri.parse('$baseUrl/entry/$entryId/'),
headers: {
'Authorization': 'Token $token',
'Content-Type': 'application/json; charset=UTF-8',
},
body: json.encode({
'team': teamId,
'event': eventId,
'category': categoryId,
'date': formattedDate,
'zekken_number': zekken_number,
}),
);
if (response.statusCode == 200) {
final decodedResponse = utf8.decode(response.bodyBytes);
return Entry.fromJson(json.decode(decodedResponse));
} else {
final decodedResponse = utf8.decode(response.bodyBytes);
final blk = json.decode(decodedResponse);
throw Exception('Failed to update entry');
}
}
Future<void> deleteEntry(int entryId) async {
init();
getToken();
final response = await http.delete(
Uri.parse('$baseUrl/entry/$entryId/'),
headers: {'Authorization': 'Token $token'},
);
if (response.statusCode != 204) {
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;
}
}
Future<bool> resetPassword(String email) async {
init();
try {
final response = await http.post(
Uri.parse('$baseUrl/password-reset/'),
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
body: json.encode({
'email': email,
}),
);
if (response.statusCode == 200) {
return true;
} else {
print('Password reset failed with status code: ${response.statusCode}');
return false;
}
} catch (e) {
print('Error in resetPassword: $e');
return false;
}
}
Future<DateTime?> getLastGoalTime(int userId) async {
try {
final response = await http.get(
Uri.parse('$baseUrl/users/$userId/last-goal/'),
headers: {
'Authorization': 'Token $token',
'Content-Type': 'application/json; charset=UTF-8',
},
);
if (response.statusCode == 200) {
final decodedResponse = json.decode(utf8.decode(response.bodyBytes));
if (decodedResponse['last_goal_time'] != null) {
return DateTime.parse(decodedResponse['last_goal_time']).toLocal();
}
} else {
print('Failed to get last goal time. Status code: ${response.statusCode}');
}
} catch (e) {
print('Error in getLastGoalTime: $e');
}
return null;
}
}

View File

@ -5,6 +5,9 @@ import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../utils/const.dart'; import '../utils/const.dart';
//import 'package:rogapp/services/team_service.dart';
//import 'package:rogapp/services/member_service.dart';
class AuthService { class AuthService {
Future<AuthUser?> userLogin(String email, String password) async { Future<AuthUser?> userLogin(String email, String password) async {
@ -129,22 +132,42 @@ class AuthService {
return cats; return cats;
} }
// ユーザー登録
//
/*
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( static Future<Map<String, dynamic>> register(
String email, String password) async { String email, String password, String password2) async {
Map<String, dynamic> cats = {}; Map<String, dynamic> cats = {};
String serverUrl = ConstValues.currentServer(); String serverUrl = ConstValues.currentServer();
String url = '$serverUrl/api/register/'; String url = '$serverUrl/api/register/';
//print('++++++++$url'); debugPrint('++++++++${url}');
final http.Response response = await http.post( final http.Response response = await http.post(
Uri.parse(url), Uri.parse(url),
headers: <String, String>{ headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8', 'Content-Type': 'application/json; charset=UTF-8',
}, },
body: jsonEncode(<String, String>{'email': email, 'password': password}), body: jsonEncode(<String, String>{'email': email, 'password': password, 'password2': password2}),
); );
//print(response.body); cats = json.decode(utf8.decode(response.bodyBytes));
if (response.statusCode == 200) { print("result=$cats");
cats = json.decode(utf8.decode(response.bodyBytes)); if (response.statusCode == 201) {
}else{
} }
return cats; return cats;
} }
@ -166,6 +189,7 @@ class AuthService {
return cats; return cats;
} }
static Future<List<dynamic>?> userDetails(int userid) async { static Future<List<dynamic>?> userDetails(int userid) async {
List<dynamic> cats = []; List<dynamic> cats = [];
String serverUrl = ConstValues.currentServer(); String serverUrl = ConstValues.currentServer();

View File

@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
class ErrorService { class ErrorService {
static Future<void> reportError(dynamic error, StackTrace stackTrace, Map<String, dynamic> deviceInfo) async { static Future<void> reportError(dynamic error, StackTrace stackTrace, Map<String, dynamic> deviceInfo, List<String> operationLogs) async {
try { try {
final String errorMessage = error.toString(); final String errorMessage = error.toString();
final String stackTraceString = stackTrace.toString(); final String stackTraceString = stackTrace.toString();
@ -19,16 +19,20 @@ class ErrorService {
'stack_trace': stackTraceString, 'stack_trace': stackTraceString,
'estimated_cause': estimatedCause, 'estimated_cause': estimatedCause,
'device_info': deviceInfo, 'device_info': deviceInfo,
'operation_logs': operationLogs.join('\n'), // オペレーションログを改行で結合して送信
}, },
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
// エラー報告が成功した場合の処理(必要に応じて) // エラー報告が成功した場合の処理(必要に応じて)
debugPrint("===== エラーログ送信成功しました。 ====");
} else { } else {
// エラー報告が失敗した場合の処理(必要に応じて) // エラー報告が失敗した場合の処理(必要に応じて)
debugPrint("===== エラーログ送信失敗しました。 ====");
} }
} catch (e) { } catch (e) {
// エラー報告中にエラーが発生した場合の処理(必要に応じて) // エラー報告中にエラーが発生した場合の処理(必要に応じて)
debugPrint("===== エラーログ送信中にエラーになりました。 ====");
} }
} }

View File

@ -4,8 +4,10 @@ import 'package:get/get.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:rogapp/model/rog.dart'; import 'package:rogapp/model/rog.dart';
import 'package:rogapp/model/team.dart';
import 'package:rogapp/pages/destination/destination_controller.dart'; import 'package:rogapp/pages/destination/destination_controller.dart';
import 'package:rogapp/pages/index/index_controller.dart'; import 'package:rogapp/pages/index/index_controller.dart';
import 'package:rogapp/pages/team/team_controller.dart';
import 'package:rogapp/utils/database_gps.dart'; import 'package:rogapp/utils/database_gps.dart';
import 'package:rogapp/utils/database_helper.dart'; import 'package:rogapp/utils/database_helper.dart';
import 'dart:convert'; import 'dart:convert';
@ -32,11 +34,19 @@ class ExternalService {
Future<Map<String, dynamic>> startRogaining() async { Future<Map<String, dynamic>> startRogaining() async {
final IndexController indexController = Get.find<IndexController>(); final IndexController indexController = Get.find<IndexController>();
//final TeamController teamController = Get.find<TeamController>();
debugPrint("== startRogaining =="); debugPrint("== startRogaining ==");
Map<String, dynamic> res = {}; Map<String, dynamic> res = {};
//final teamController = TeamController();
//Team team0 = teamController.teams[0];
//print("team={team0}");
//int teamId = indexController.currentUser[0]["user"]["team"]["id"];
int userId = indexController.currentUser[0]["user"]["id"]; int userId = indexController.currentUser[0]["user"]["id"];
//print("--- Pressed -----"); //print("--- Pressed -----");
String team = indexController.currentUser[0]["user"]['team_name']; String team = indexController.currentUser[0]["user"]['team_name'];
@ -60,8 +70,9 @@ class ExternalService {
} else { } else {
debugPrint("== startRogaining processing=="); debugPrint("== startRogaining processing==");
String url = 'https://rogaining.sumasen.net/gifuroge/start_from_rogapp'; String serverUrl = ConstValues.currentServer();
//print('++++++++$url'); String url = '$serverUrl/gifuroge/start_from_rogapp';
print('++++++++$url');
final http.Response response = await http.post( final http.Response response = await http.post(
Uri.parse(url), Uri.parse(url),
headers: <String, String>{ headers: <String, String>{
@ -82,7 +93,7 @@ class ExternalService {
} }
Future<Map<String, dynamic>> makeCheckpoint( Future<Map<String, dynamic>> makeCheckpoint(
int userId, int userId, // 中身はteamId
String token, String token,
String checkinTime, String checkinTime,
String teamname, String teamname,
@ -93,9 +104,17 @@ class ExternalService {
// print("~~~~ cp is $cp ~~~~"); // print("~~~~ cp is $cp ~~~~");
//print("--cpcp-- ${cp}"); //print("--cpcp-- ${cp}");
Map<String, dynamic> res = {}; Map<String, dynamic> res = {};
String url = 'https://rogaining.sumasen.net/gifuroge/checkin_from_rogapp'; String serverUrl = ConstValues.currentServer();
String url = '$serverUrl/gifuroge/checkin_from_rogapp';
//print('++++++++$url'); //print('++++++++$url');
final IndexController indexController = Get.find<IndexController>(); final IndexController indexController = Get.find<IndexController>();
//final TeamController teamController = Get.find<TeamController>();
// Team team0 = indexController.teamController.teams[0];
// print("team={team0}");
//int teamId = indexController.teamController.teams[0];
if (imageurl != null) { if (imageurl != null) {
if (indexController.connectionStatusName.value != "wifi" && if (indexController.connectionStatusName.value != "wifi" &&
@ -160,7 +179,7 @@ class ExternalService {
'cp_number': cp.toString(), 'cp_number': cp.toString(),
'event_code': eventcode, 'event_code': eventcode,
'image': res["checkinimage"].toString().replaceAll( 'image': res["checkinimage"].toString().replaceAll(
'http://localhost:8100', 'http://rogaining.sumasen.net') 'http://localhost:8100', serverUrl) //'http://rogaining.sumasen.net')
}), }),
); );
var vv = jsonEncode(<String, String>{ var vv = jsonEncode(<String, String>{
@ -168,7 +187,7 @@ class ExternalService {
'cp_number': cp.toString(), 'cp_number': cp.toString(),
'event_code': eventcode, 'event_code': eventcode,
'image': res["checkinimage"].toString().replaceAll( 'image': res["checkinimage"].toString().replaceAll(
'http://localhost:8100', 'http://rogaining.sumasen.net') 'http://localhost:8100', serverUrl) //'http://rogaining.sumasen.net')
}); });
//print("~~~~ api 2 values $vv ~~~~"); //print("~~~~ api 2 values $vv ~~~~");
//print("--json-- $vv"); //print("--json-- $vv");
@ -183,6 +202,7 @@ class ExternalService {
colorText: Colors.white colorText: Colors.white
); );
} }
} }
} else { } else {
Get.snackbar("サーバーエラーがおきました", "サーバーと通信できませんでした", Get.snackbar("サーバーエラーがおきました", "サーバーと通信できませんでした",
@ -253,6 +273,10 @@ class ExternalService {
final DestinationController destinationController = final DestinationController destinationController =
Get.find<DestinationController>(); Get.find<DestinationController>();
// チームIDを取得
//int teamId = indexController.currentUser[0]["user"]["team"]["id"];
debugPrint("== goal Rogaining =="); debugPrint("== goal Rogaining ==");
//if(indexController.connectionStatusName != "wifi" && indexController.connectionStatusName != "mobile"){ //if(indexController.connectionStatusName != "wifi" && indexController.connectionStatusName != "mobile"){
@ -261,7 +285,7 @@ class ExternalService {
id: 1, id: 1,
team_name: teamname, team_name: teamname,
event_code: eventcode, event_code: eventcode,
user_id: userId, user_id: userId, // 中身はteamid
cp_number: -1, cp_number: -1,
checkintime: DateTime.now().toUtc().microsecondsSinceEpoch, checkintime: DateTime.now().toUtc().microsecondsSinceEpoch,
image: image, image: image,
@ -283,7 +307,7 @@ class ExternalService {
}, },
// 'id', 'user', 'goalimage', 'goaltime', 'team_name', 'event_code','cp_number' // 'id', 'user', 'goalimage', 'goaltime', 'team_name', 'event_code','cp_number'
body: jsonEncode(<String, String>{ body: jsonEncode(<String, String>{
'user': userId.toString(), 'user': userId.toString(), //userId.toString(),
'team_name': teamname, 'team_name': teamname,
'event_code': eventcode, 'event_code': eventcode,
'goaltime': goalTime, 'goaltime': goalTime,
@ -292,37 +316,46 @@ class ExternalService {
}), }),
); );
String url = 'https://rogaining.sumasen.net/gifuroge/goal_from_rogapp'; //String serverUrl = ConstValues.currentServer();
String url = '$serverUrl/gifuroge/goal_from_rogapp';
//print('++++++++$url'); //print('++++++++$url');
if (response.statusCode == 201) { if (response.statusCode == 201) {
Map<String, dynamic> res = json.decode(utf8.decode(response.bodyBytes)); try {
// print('----_res : $res ----'); Map<String, dynamic> res = json.decode(utf8.decode(response.bodyBytes));
// print('---- image url ${res["goalimage"]} ----'); // print('----_res : $res ----');
final http.Response response2 = await http.post( // print('---- image url ${res["goalimage"]} ----');
Uri.parse(url), final http.Response response2 = await http.post(
headers: <String, String>{ Uri.parse(url),
'Content-Type': 'application/json; charset=UTF-8', headers: <String, String>{
}, 'Content-Type': 'application/json; charset=UTF-8',
body: jsonEncode(<String, String>{ },
body: jsonEncode(<String, String>{
'team_name': teamname,
'event_code': eventcode,
'goal_time': goalTime,
'image': res["goalimage"].toString().replaceAll(
'http://localhost:8100', serverUrl)
//'http://rogaining.sumasen.net')
}),
);
String rec = jsonEncode(<String, String>{
'team_name': teamname, 'team_name': teamname,
'event_code': eventcode, 'event_code': eventcode,
'goal_time': goalTime, 'goal_time': goalTime,
'image': res["goalimage"].toString().replaceAll( 'image': res["goalimage"]
'http://localhost:8100', 'http://rogaining.sumasen.net') .toString()
}), .replaceAll('http://localhost:8100', serverUrl)
); //'http://rogaining.sumasen.net')
String rec = jsonEncode(<String, String>{ });
'team_name': teamname, //print("-- json -- $rec");
'event_code': eventcode, //print('----- response2 is $response2 --------');
'goal_time': goalTime, if (response2.statusCode == 200) {
'image': res["goalimage"] res2 = json.decode(utf8.decode(response2.bodyBytes));
.toString() } else {
.replaceAll('http://localhost:8100', 'http://rogaining.sumasen.net') res2 = json.decode(utf8.decode(response2.bodyBytes));
}); }
//print("-- json -- $rec"); } catch(e){
//print('----- response2 is $response2 --------'); print( "Error {$e}" );
if (response2.statusCode == 200) {
res2 = json.decode(utf8.decode(response2.bodyBytes));
} }
} }
//} //}
@ -343,8 +376,8 @@ class ExternalService {
indexController.connectionStatusName.value != "mobile") { indexController.connectionStatusName.value != "mobile") {
return Future.value(false); return Future.value(false);
} else { } else {
String url = String serverUrl = ConstValues.currentServer();
'https://rogaining.sumasen.net/gifuroge/remove_checkin_from_rogapp'; String url = '$serverUrl/gifuroge/remove_checkin_from_rogapp';
//print('++++++++$url'); //print('++++++++$url');
final http.Response response = await http.post( final http.Response response = await http.post(
Uri.parse(url), Uri.parse(url),
@ -413,8 +446,8 @@ class ExternalService {
//print("calling push gps step 2 ${payload}"); //print("calling push gps step 2 ${payload}");
String urlS = String serverUrl = ConstValues.currentServer();
'https://rogaining.sumasen.net/gifuroge/get_waypoint_datas_from_rogapp'; String urlS = '$serverUrl/gifuroge/get_waypoint_datas_from_rogapp';
//print('++++++++$url'); //print('++++++++$url');
var url = Uri.parse(urlS); // Replace with your server URL var url = Uri.parse(urlS); // Replace with your server URL
var response = await http.post( var response = await http.post(
@ -440,8 +473,8 @@ class ExternalService {
static Future<Map<String, dynamic>> usersEventCode( static Future<Map<String, dynamic>> usersEventCode(
String teamcode, String password) async { String teamcode, String password) async {
Map<String, dynamic> res = {}; Map<String, dynamic> res = {};
String url = String serverUrl = ConstValues.currentServer();
"https://rogaining.sumasen.net/gifuroge/check_event_code?zekken_number=$teamcode&password=$password"; String url = "$serverUrl/gifuroge/check_event_code?zekken_number=$teamcode&password=$password";
//print('++++++++$url'); //print('++++++++$url');
final http.Response response = final http.Response response =
await http.get(Uri.parse(url), headers: <String, String>{ await http.get(Uri.parse(url), headers: <String, String>{

View File

@ -82,59 +82,93 @@ class LocationService {
double lon3, double lon3,
double lat4, double lat4,
double lon4, double lon4,
String cat) async { String cat,
String event_code) async {
//print("-------- in location for bound -------------"); //print("-------- in location for bound -------------");
final IndexController indexController = Get.find<IndexController>(); final IndexController indexController = Get.find<IndexController>();
String url = ""; final updateTime = indexController.lastUserUpdateTime.value;
String serverUrl = ConstValues.currentServer();
if (cat.isNotEmpty) {
if (indexController.currentUser.isNotEmpty) {
bool rog = indexController.currentUser[0]['user']['is_rogaining'];
String r = rog == true ? 'True' : 'False';
var grp = indexController.currentUser[0]['user']['event_code'];
url =
'$serverUrl/api/inbound?rog=$r&grp=$grp&ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4&cat=$cat';
} else {
url =
'$serverUrl/api/inbound?ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4&cat=$cat';
}
} else {
if (indexController.currentUser.isNotEmpty) {
bool rog = indexController.currentUser[0]['user']['is_rogaining'];
String r = rog == true ? 'True' : 'False';
var grp = indexController.currentUser[0]['user']['event_code'];
//print("-------- requested user group $grp -------------");
url =
'$serverUrl/api/inbound?rog=$r&grp=$grp&ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4';
} else {
url =
'$serverUrl/api/inbound?ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4';
}
}
//print('++++++++$url');
final response = await http.get(
Uri.parse(url),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
);
if (response.statusCode == 500) { // ユーザー情報の更新を最大5秒間待つ
return null; //featuresFromGeoJson(utf8.decode(response.bodyBytes));
}
if (response.statusCode == 200) { try {
DestinationController destinationController = /*
Get.find<DestinationController>(); // ユーザー情報の更新を最大5秒間待つ
GeoJSONFeatureCollection cc = final newUpdateTime = await indexController.lastUserUpdateTime.stream
GeoJSONFeatureCollection.fromJSON(utf8.decode(response.bodyBytes)); .firstWhere(
if (cc.features.isEmpty) { (time) => time.isAfter(updateTime),
orElse: () => updateTime,
).timeout(Duration(seconds: 5));
if (newUpdateTime == updateTime) {
print('ユーザー情報の更新がタイムアウトしました');
// タイムアウト時の処理(例:エラー表示やリトライ)
return null; return null;
} else {
//print("---- feature got from server is ${cc.collection[0].properties} ------");
return cc;
} }
*/
/*
await indexController.lastUserUpdateTime.stream.firstWhere(
(time) => time.isAfter(updateTime),
orElse: () => updateTime,
).timeout(Duration(seconds: 2), onTimeout: () => updateTime);
*/
String url = "";
String serverUrl = ConstValues.currentServer();
if (cat.isNotEmpty) {
if (indexController.currentUser.isNotEmpty) {
bool rog = indexController.currentUser[0]['user']['is_rogaining'];
String r = rog == true ? 'True' : 'False';
var grp = event_code; //indexController.currentUser[0]['user']['event_code'];
print("Group=$grp");
url =
'$serverUrl/api/inbound2?rog=$r&grp=$grp&ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4&cat=$cat';
} else {
url =
'$serverUrl/api/inbound2?ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4&cat=$cat';
}
} else {
if (indexController.currentUser.isNotEmpty) {
bool rog = indexController.currentUser[0]['user']['is_rogaining'];
String r = rog == true ? 'True' : 'False';
var grp = indexController.currentUser[0]['user']['event_code'];
print("-------- requested user group $grp -------------");
url =
'$serverUrl/api/inbound2?rog=$r&grp=$grp&ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4';
} else {
url =
'$serverUrl/api/inbound2?ln1=$lon1&la1=$lat1&ln2=$lon2&la2=$lat2&ln3=$lon3&la3=$lat3&ln4=$lon4&la4=$lat4';
}
print('++++++++$url');
final response = await http.get(
Uri.parse(url),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
);
if (response.statusCode == 500) {
return null; //featuresFromGeoJson(utf8.decode(response.bodyBytes));
}
if (response.statusCode == 200) {
DestinationController destinationController =
Get.find<DestinationController>();
GeoJSONFeatureCollection cc =
GeoJSONFeatureCollection.fromJSON(utf8.decode(response.bodyBytes));
if (cc.features.isEmpty) {
return null;
} else {
//print("---- feature got from server is ${cc.collection[0].properties} ------");
return cc;
}
}
}
} catch(e) {
print("Error: $e");
} }
return null; return null;
} }

View File

@ -2,7 +2,9 @@
class ConstValues{ class ConstValues{
static const container_svr = "http://container.intranet.sumasen.net:8100"; //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 server_uri = "https://rogaining.sumasen.net";
static const dev_server = "http://localhost:8100"; static const dev_server = "http://localhost:8100";
static const dev_ip_server = "http://192.168.8.100:8100"; static const dev_ip_server = "http://192.168.8.100:8100";

View File

@ -7,7 +7,8 @@ class StringValues extends Translations{
'drawer_title':'Rogaining participants can view checkpoints by logging in', 'drawer_title':'Rogaining participants can view checkpoints by logging in',
'app_title': '- Rogaining -', 'app_title': '- Rogaining -',
'address':'address', 'address':'address',
'email':'Email', 'bib':'Bib number',
'email':'Email address',
'password':'Password', 'password':'Password',
'web':'Web', 'web':'Web',
'wikipedia':'Wikipedia', 'wikipedia':'Wikipedia',
@ -29,7 +30,7 @@ class StringValues extends Translations{
'visit_history': 'Visit History', 'visit_history': 'Visit History',
'rog_web': 'rog website', 'rog_web': 'rog website',
'no_values': 'No Values', 'no_values': 'No Values',
'email_and_password_required': 'Email and password required', 'email_and_password_required': 'Email and password are required to register user',
'rogaining_user_need_tosign_up': "Rogaining participants do need to sign up.", 'rogaining_user_need_tosign_up': "Rogaining participants do need to sign up.",
'add_location': 'Gifu', 'add_location': 'Gifu',
'select_travel_mode':'Select your travel mode', 'select_travel_mode':'Select your travel mode',
@ -70,6 +71,8 @@ class StringValues extends Translations{
"Not reached the goal yet": "Not reached the goal yet", "Not reached the goal yet": "Not reached the goal yet",
"You have not reached the goal yet.":"You have not reached the goal yet.", "You have not reached the goal yet.":"You have not reached the goal yet.",
"delete_account": "Delete account", "delete_account": "Delete account",
"delete_account_title": "Are you ok to delete your account?",
"delete_account_middle": "All your account information and data history will be removed from local device and server side.",
"accounted_deleted": "Account deleted", "accounted_deleted": "Account deleted",
"account_deleted_message": "your account has beed successfully deleted", "account_deleted_message": "your account has beed successfully deleted",
"privacy": "Privacy policy", "privacy": "Privacy policy",
@ -95,7 +98,6 @@ class StringValues extends Translations{
'already_have_account': 'Already have an account?', 'already_have_account': 'Already have an account?',
'sign_up': 'Sign Up', 'sign_up': 'Sign Up',
'create_account': 'Create an account, it\'s free', 'create_account': 'Create an account, it\'s free',
'email': 'Email',
'confirm_password': 'Confirm Password', 'confirm_password': 'Confirm Password',
'cancel_checkin': 'Cancel Check-in', 'cancel_checkin': 'Cancel Check-in',
'go_here': 'Show route', 'go_here': 'Show route',
@ -203,13 +205,24 @@ class StringValues extends Translations{
'location_permission_required_title': 'Location Permission Required', 'location_permission_required_title': 'Location Permission Required',
'location_permission_required_message': 'This app requires access to your location. Please grant permission to continue.', 'location_permission_required_message': 'This app requires access to your location. Please grant permission to continue.',
'cancel': 'Cancel', 'cancel': 'Cancel',
'checkins': 'Check-ins' 'checkins': 'Check-ins',
'reset_button': 'Reset data',
'reset_title': 'Reset the data in this device.',
'reset_message': 'Are you ok to reset all data in this device?',
'reset_done': 'Reset Done.',
'reset_explain': 'All data has been reset. You should tap start rogaining to start game.',
'no_match': 'No match!',
'password_does_not_match':'The passwords you entered were not match.',
'forgot_password':'Forgot password',
'user_registration_successful':'Sent activation mail to you. Pls click activation link on the email.',
}, },
'ja_JP': { 'ja_JP': {
'drawer_title':'ロゲイニング参加者はログイン するとチェックポイントが参照 できます', 'drawer_title':'ロゲイニング参加者はログイン するとチェックポイントが参照 できます',
'app_title': '旅行工程表', 'app_title': '旅行工程表',
'address':'住所', 'address':'住所',
'email':'ゼッケン番号', 'bib':'ゼッケン番号',
'email':'メールアドレス',
'password':'パスワード', 'password':'パスワード',
'web':'ウェブ', 'web':'ウェブ',
'wikipedia':'ウィキペディア', 'wikipedia':'ウィキペディア',
@ -233,7 +246,7 @@ class StringValues extends Translations{
'visit_history': '訪問履歴', 'visit_history': '訪問履歴',
'rog_web': 'ロゲイニングウェブサイト', 'rog_web': 'ロゲイニングウェブサイト',
'no_values': '値なし', 'no_values': '値なし',
'email_and_password_required': 'メールとパスワードが必要です', 'email_and_password_required': 'メールとパスワードの入力が必要です',
'rogaining_user_need_tosign_up': "ロゲイニング参加者はサインアップの必要はありません。", 'rogaining_user_need_tosign_up': "ロゲイニング参加者はサインアップの必要はありません。",
'add_location': '岐阜', 'add_location': '岐阜',
'select_travel_mode':'移動モードを選択してください', 'select_travel_mode':'移動モードを選択してください',
@ -245,12 +258,12 @@ class StringValues extends Translations{
'confirm': '確認', 'confirm': '確認',
'cancel': 'キャンセル', 'cancel': 'キャンセル',
'all_destinations_are_deleted_successfully' : 'すべての宛先が正常に削除されました', 'all_destinations_are_deleted_successfully' : 'すべての宛先が正常に削除されました',
'deleted': "削除された", 'deleted': "削除されまし",
'remarks' : '備考', 'remarks' : '備考',
'old_password' : '以前のパスワード', 'old_password' : '以前のパスワード',
'new_password' : '新しいパスワード', 'new_password' : '新しいパスワード',
'values_required' : '必要な値', 'values_required' : '必要な値',
'failed' : '失敗した', 'failed' : '失敗',
'password_change_failed_please_try_again' : 'パスワードの変更に失敗しました。もう一度お試しください', 'password_change_failed_please_try_again' : 'パスワードの変更に失敗しました。もう一度お試しください',
'user_registration_failed_please_try_again' : 'ユーザー登録に失敗しました。もう一度お試しください', 'user_registration_failed_please_try_again' : 'ユーザー登録に失敗しました。もう一度お試しください',
'all': '全て', 'all': '全て',
@ -273,11 +286,13 @@ class StringValues extends Translations{
"You have not started rogaining yet.":"あなたはまだロゲイニングを始めていません。", "You have not started rogaining yet.":"あなたはまだロゲイニングを始めていません。",
"Not reached the goal yet": "まだ目標に達していない", "Not reached the goal yet": "まだ目標に達していない",
"You have not reached the goal yet.":"あなたはまだゴールに達していません。", "You have not reached the goal yet.":"あなたはまだゴールに達していません。",
"delete_account": "アカウントを削除す", "delete_account": "アカウントを削除しま",
"delete_account_title": "アカウントを削除しますがよろしいですか?",
"delete_account_middle": "これにより、アカウント情報とすべてのゲーム データが削除され、すべての状態が削除されます",
"accounted_deleted": "アカウントが削除されました", "accounted_deleted": "アカウントが削除されました",
"account_deleted_message": "あなたのアカウントは正常に削除されました", "account_deleted_message": "あなたのアカウントは正常に削除されました",
"privacy": "プライバシーポリシー", "privacy": "プライバシーポリシー",
"app_developed_by_gifu_dx": "※このアプリは令和年度岐阜県DX補助金事業で開発されました。", "app_developed_by_gifu_dx": "※このアプリは令和4、6年度岐阜県DX補助金事業で開発されました。",
'location_permission_title': 'ロケーション許可', 'location_permission_title': 'ロケーション許可',
'location_permission_content': 'このアプリでは、位置情報の収集を行います。\n岐阜ナビアプリではチェックポイントの自動チェックインの機能を可能にするために、現在地のデータが収集されます。アプリを閉じている時や、使用していないときにも収集されます。位置情報は、個人を特定できない統計的な情報として、ユーザーの個人情報とは一切結びつかない形で送信されます。お知らせの配信、位置情報の利用を許可しない場合は、この後表示されるダイアログで「許可しない」を選択してください。', 'location_permission_content': 'このアプリでは、位置情報の収集を行います。\n岐阜ナビアプリではチェックポイントの自動チェックインの機能を可能にするために、現在地のデータが収集されます。アプリを閉じている時や、使用していないときにも収集されます。位置情報は、個人を特定できない統計的な情報として、ユーザーの個人情報とは一切結びつかない形で送信されます。お知らせの配信、位置情報の利用を許可しない場合は、この後表示されるダイアログで「許可しない」を選択してください。',
@ -299,8 +314,7 @@ class StringValues extends Translations{
'already_have_account': 'すでにアカウントをお持ちですか?', 'already_have_account': 'すでにアカウントをお持ちですか?',
'sign_up': 'サインアップ', 'sign_up': 'サインアップ',
'create_account': 'アカウントを無料で作成します', 'create_account': 'アカウントを無料で作成します',
'email': 'ゼッケン番号', 'confirm_password': '確認用パスワード',
'confirm_password': 'パスワードを認証する',
'cancel_checkin': 'チェックイン取消', 'cancel_checkin': 'チェックイン取消',
'go_here': 'ルート表示', 'go_here': 'ルート表示',
'cancel_route':'ルート消去', 'cancel_route':'ルート消去',
@ -410,6 +424,15 @@ class StringValues extends Translations{
'location_permission_required_message': 'このアプリを使用するには、位置情報へのアクセスが必要です。続行するには許可を付与してください。', 'location_permission_required_message': 'このアプリを使用するには、位置情報へのアクセスが必要です。続行するには許可を付与してください。',
'cancel': 'キャンセル', 'cancel': 'キャンセル',
'checkins': 'チェックイン', 'checkins': 'チェックイン',
'reset_button': 'リセット',
'reset_title': 'リセットしますがよろしいですか?',
'reset_message': 'これにより、すべてのゲーム データが削除され、すべての状態が削除されます',
'reset_done': 'リセット完了',
'reset_explain': 'すべてリセットされました。ロゲ開始から再開して下さい。',
'no_match': '不一致',
'password_does_not_match':'入力したパスワードが一致しません',
'forgot_password':'パスワードを忘れた場合',
'user_registration_successful':'ユーザー認証のメールをお届けしました。メール上のリンクをクリックして正式登録してください。',
}, },
}; };
} }

View File

@ -21,6 +21,10 @@ import 'package:rogapp/widgets/bottom_sheet_controller.dart';
import 'package:rogapp/widgets/debug_widget.dart'; import 'package:rogapp/widgets/debug_widget.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tz;
import 'package:get/get.dart';
// BottomSheetNewは、StatelessWidgetを継承したクラスで、目的地の詳細情報を表示するボトムシートのUIを構築します。 // BottomSheetNewは、StatelessWidgetを継承したクラスで、目的地の詳細情報を表示するボトムシートのUIを構築します。
// コンストラクタでは、destination目的地オブジェクトとisAlreadyCheckedInすでにチェックイン済みかどうかのフラグを受け取ります。 // コンストラクタでは、destination目的地オブジェクトとisAlreadyCheckedInすでにチェックイン済みかどうかのフラグを受け取ります。
// buildメソッドでは、detailsSheetメソッドを呼び出して、目的地の詳細情報を表示します。 // buildメソッドでは、detailsSheetメソッドを呼び出して、目的地の詳細情報を表示します。
@ -38,6 +42,15 @@ class BottomSheetNew extends GetView<BottomSheetController> {
final RxBool isButtonDisabled = false.obs; final RxBool isButtonDisabled = false.obs;
static bool _timezoneInitialized = false;
void _initializeTimezone() {
if (!_timezoneInitialized) {
tz.initializeTimeZones();
_timezoneInitialized = true;
}
}
// 目的地の画像を取得するためのメソッドです。 // 目的地の画像を取得するためのメソッドです。
// indexController.rogModeの値に基づいて、適切な画像を返します。画像が見つからない場合は、デフォルトの画像を返します。 // indexController.rogModeの値に基づいて、適切な画像を返します。画像が見つからない場合は、デフォルトの画像を返します。
// //
@ -146,6 +159,7 @@ class BottomSheetNew extends GetView<BottomSheetController> {
// ボタンがタップされたときの処理も含まれています。 // ボタンがタップされたときの処理も含まれています。
// //
Widget getActionButton(BuildContext context, Destination destination) { Widget getActionButton(BuildContext context, Destination destination) {
_initializeTimezone(); // タイムゾーンの初期化
/* /*
debugPrint("getActionButton ${destinationController.rogainingCounted.value}"); debugPrint("getActionButton ${destinationController.rogainingCounted.value}");
debugPrint("getActionButton ${destinationController.distanceToStart()}"); debugPrint("getActionButton ${destinationController.distanceToStart()}");
@ -181,6 +195,51 @@ class BottomSheetNew extends GetView<BottomSheetController> {
onPressed: destinationController.isInRog.value onPressed: destinationController.isInRog.value
? null ? null
: () async { : () async {
// Check if the event is for today
bool isEventToday = await checkIfEventIsToday();
if (!isEventToday) {
Get.dialog(
AlertDialog(
title: Text("警告"),
content: Text("参加したエントリーは別日のものですので、ロゲの開始はできません。当日のエントリーを選択するか、エントリーを今日に変更してからロゲ開始を行ってください。"),
actions: <Widget>[
TextButton(
child: Text("OK"),
onPressed: () {
Get.back(); // Close the dialog
Get.back(); // Close the bottom sheet
},
),
],
),
);
return;
}
// Check if user has already completed a rogaining event today
bool hasCompletedToday = await checkIfCompletedToday();
if (hasCompletedToday) {
Get.dialog(
AlertDialog(
title: Text("警告"),
content: Text("すでにロゲの参加を行いゴールをしています。ロゲは1日1回に制限されています。ご了承ください。"),
actions: <Widget>[
TextButton(
child: Text("OK"),
onPressed: () {
Get.back(); // Close the dialog
Get.back(); // Close the bottom sheet
},
),
],
),
);
return;
}
destinationController.isInRog.value = true; destinationController.isInRog.value = true;
@ -216,6 +275,7 @@ class BottomSheetNew extends GetView<BottomSheetController> {
); );
saveGameState(); saveGameState();
//int teamId = indexController.teamId.value; // teamIdを使用
await ExternalService().startRogaining(); await ExternalService().startRogaining();
Get.back(); Get.back();
Get.back();// Close the dialog and potentially navigate away Get.back();// Close the dialog and potentially navigate away
@ -249,12 +309,17 @@ class BottomSheetNew extends GetView<BottomSheetController> {
//goal //goal
return ElevatedButton( return ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red), style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: Colors.red
),
onPressed: destinationController.rogainingCounted.value == true && onPressed: destinationController.rogainingCounted.value == true &&
destinationController.distanceToStart() <= 500 && destinationController.distanceToStart() <= 500 &&
(destination.cp == 0 || destination.cp == -2|| destination.cp == -1) && (destination.cp == 0 || destination.cp == -2|| destination.cp == -1) &&
DestinationController.ready_for_goal == true DestinationController.ready_for_goal == true
? () async { ? () async {
destinationController.isAtGoal.value = true; destinationController.isAtGoal.value = true;
destinationController.photos.clear(); destinationController.photos.clear();
await showModalBottomSheet( await showModalBottomSheet(
@ -317,7 +382,7 @@ class BottomSheetNew extends GetView<BottomSheetController> {
? "in_game".tr ? "in_game".tr
: isAlreadyCheckedIn == true : isAlreadyCheckedIn == true
? "in_game".tr ? "in_game".tr
: destinationController.isInRog.value == true : (destinationController.isInRog.value == true || (destination.buy_point != null && destination.buy_point! > 0))
? "checkin".tr ? "checkin".tr
: "rogaining_not_started".tr, : "rogaining_not_started".tr,
style: TextStyle(color: Theme.of(context).colorScheme.onSecondary), style: TextStyle(color: Theme.of(context).colorScheme.onSecondary),
@ -327,6 +392,62 @@ class BottomSheetNew extends GetView<BottomSheetController> {
return Container(); return Container();
} }
// Add these new methods to check event date and completion status
Future<bool> checkIfEventIsToday() async {
try {
final now = tz.TZDateTime.now(tz.getLocation('Asia/Tokyo'));
// Null チェックと安全な操作を追加
final userEventDate = indexController.currentUser.isNotEmpty &&
indexController.currentUser[0] != null &&
indexController.currentUser[0]["user"] != null
? indexController.currentUser[0]["user"]["event_date"]
: null;
if (userEventDate == null || userEventDate.toString().isEmpty) {
print('Event date is null or empty');
return false; // イベント日付が設定されていない場合は false を返す
}
final eventDate = tz.TZDateTime.from(
DateTime.parse(userEventDate.toString()),
tz.getLocation('Asia/Tokyo')
);
return eventDate.year == now.year &&
eventDate.month == now.month &&
eventDate.day == now.day;
} catch (e) {
print('Error in checkIfEventIsToday: $e');
// エラーが発生した場合はダイアログを表示
Get.dialog(
AlertDialog(
title: Text('エラー'),
content: Text('イベント日付の確認中にエラーが発生しました。\nアプリを再起動するか、管理者にお問い合わせください。'),
actions: <Widget>[
TextButton(
child: Text('OK'),
onPressed: () => Get.back(),
),
],
),
);
return false; // エラーが発生した場合は false を返す
}
}
Future<bool> checkIfCompletedToday() async {
final IndexController indexController = Get.find<IndexController>();
final lastGoalTime = await indexController.getLastGoalTime();
if (lastGoalTime == null) return false;
final now = DateTime.now();
return lastGoalTime.year == now.year &&
lastGoalTime.month == now.month &&
lastGoalTime.day == now.day;
}
// 継承元のbuild をオーバーライドし、detailsSheetメソッドを呼び出して、目的地の詳細情報を表示します。 // 継承元のbuild をオーバーライドし、detailsSheetメソッドを呼び出して、目的地の詳細情報を表示します。
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -443,7 +564,7 @@ class BottomSheetNew extends GetView<BottomSheetController> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
//checkin or remove checkin //checkin or remove checkin
destinationController.isInRog.value == true (destinationController.isInRog.value == true || (destination.buy_point != null && destination.buy_point! > 0))
&& (distanceToDest <= && (distanceToDest <=
destinationController.getForcedChckinDistance(destination) || destination.checkin_radious==-1 ) destinationController.getForcedChckinDistance(destination) || destination.checkin_radious==-1 )
&& destination.cp != 0 && destination.cp != -1 && destination.cp != -2 && destination.cp != 0 && destination.cp != -1 && destination.cp != -2

View File

@ -12,6 +12,9 @@ class LogManager {
List<String> _logs = []; List<String> _logs = [];
List<VoidCallback> _listeners = []; List<VoidCallback> _listeners = [];
List<String> _operationLogs = [];
List<String> get operationLogs => _operationLogs;
List<String> get logs => _logs; List<String> get logs => _logs;
void addLog(String log) { void addLog(String log) {
@ -24,6 +27,16 @@ class LogManager {
_notifyListeners(); // Notify all listeners _notifyListeners(); // Notify all listeners
} }
void addOperationLog(String log) {
_operationLogs.add(log);
_notifyListeners();
}
void clearOperationLogs() {
_operationLogs.clear();
_notifyListeners();
}
void addListener(VoidCallback listener) { void addListener(VoidCallback listener) {
_listeners.add(listener); _listeners.add(listener);
} }

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));
}
}

383
login_page.dart Normal file
View File

@ -0,0 +1,383 @@
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';
import 'package:rogapp/services/api_service.dart';
// 要検討:ログインボタンとサインアップボタンの配色を見直すことを検討してください。現在の配色では、ボタンの役割がわかりにくい可能性があります。
// エラーメッセージをローカライズすることを検討してください。
// ログイン処理中にエラーが発生した場合のエラーハンドリングを追加することをお勧めします。
//
class LoginPage extends StatefulWidget {
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
//class LoginPage extends StatelessWidget {
final IndexController indexController = Get.find<IndexController>();
final ApiService apiService = Get.find<ApiService>();
TextEditingController emailController = TextEditingController();
TextEditingController passwordController = TextEditingController();
bool _obscureText = true;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
showHelperDialog(
'参加するにはユーザー登録が必要です。サインアップからユーザー登録してください。',
'login_page'
);
});
}
void _showResetPasswordDialog() {
TextEditingController resetEmailController = TextEditingController();
Get.dialog(
AlertDialog(
title: Text('パスワードのリセット'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('パスワードをリセットするメールアドレスを入力してください。'),
SizedBox(height: 10),
TextField(
controller: resetEmailController,
decoration: InputDecoration(
labelText: 'メールアドレス',
border: OutlineInputBorder(),
),
),
],
),
actions: [
TextButton(
child: Text('キャンセル'),
onPressed: () => Get.back(),
),
ElevatedButton(
child: Text('リセット'),
onPressed: () async {
if (resetEmailController.text.isNotEmpty) {
bool success = await apiService.resetPassword(resetEmailController.text);
Get.back();
if (success) {
Get.dialog(
AlertDialog(
title: Text('パスワードリセット'),
content: Text('パスワードリセットメールを送信しました。メールのリンクからパスワードを設定してください。'),
actions: [
TextButton(
child: Text('OK'),
onPressed: () => Get.back(),
),
],
),
);
} else {
Get.snackbar('エラー', 'パスワードリセットに失敗しました。もう一度お試しください。',
snackPosition: SnackPosition.BOTTOM);
}
}
},
),
],
),
);
}
//LoginPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: Colors.white,
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.white,
automaticallyImplyLeading: false,
),
body: indexController.currentUser.isEmpty
? SizedBox(
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Column(
children: [
Column(
children: [
Container(
height: MediaQuery.of(context).size.height / 6,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(
'assets/images/login_image.jpg'))),
),
const SizedBox(
height: 5,
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
children: [
makeInput(
label: "email".tr, controller: emailController),
makePasswordInput(
label: "password".tr,
controller: passwordController,
obscureText: _obscureText,
onToggleVisibility: () {
setState(() {
_obscureText = !_obscureText;
});
}),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Container(
padding: const EdgeInsets.only(top: 3, left: 3),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40),
),
child: Obx(
(() => indexController.isLoading.value == true
? MaterialButton(
minWidth: double.infinity,
height: 60,
onPressed: () {},
color: Colors.grey[400],
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(40)),
child: const CircularProgressIndicator(),
)
: Column(
children: [
MaterialButton(
minWidth: double.infinity,
height: 40,
onPressed: () async {
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.login(
emailController.text,
passwordController.text,
context);
},
color: Colors.indigoAccent[400],
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(40)),
child: Text(
"login".tr,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: Colors.white70),
),
),
const SizedBox(
height: 5.0,
),
MaterialButton(
minWidth: double.infinity,
height: 36,
onPressed: () {
Get.toNamed(AppPages.REGISTER);
},
color: Colors.redAccent,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(40)),
child: Text(
"sign_up".tr,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: Colors.white70),
),
),
],
)),
),
)),
const SizedBox(
height: 3,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: _showResetPasswordDialog,
child: Text(
"forgot_password".tr,
style: TextStyle(color: Colors.blue),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"app_developed_by_gifu_dx".tr,
style: const TextStyle(
overflow: TextOverflow.ellipsis,
fontSize: 10.0),
),
),
),
],
),
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text(
"※第8回と第9回は、岐阜県の令和年度「清流の国ぎふ」SDGs推進ネットワーク連携促進補助金を受けています",
style: TextStyle(
//overflow: TextOverflow.ellipsis,
fontSize:
10.0, // Consider adjusting the font size if the text is too small.
// Removed overflow: TextOverflow.ellipsis to allow text wrapping.
),
),
),
),
],
),
],
),
],
),
)
: TextButton(
onPressed: () {
indexController.logout();
Get.offAllNamed(AppPages.LOGIN);
},
child: const Text("Already Logged in, Click to logout"),
),
);
}
}
Widget makePasswordInput({
required String label,
required TextEditingController controller,
required bool obscureText,
required VoidCallback onToggleVisibility,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w400, color: Colors.black87),
),
const SizedBox(height: 5),
TextField(
controller: controller,
obscureText: obscureText,
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]!),
),
suffixIcon: IconButton(
icon: Icon(
obscureText ? Icons.visibility : Icons.visibility_off,
color: Colors.grey,
),
onPressed: onToggleVisibility,
),
),
),
const SizedBox(height: 30.0)
],
);
}
Widget makeInput(
{label, required TextEditingController controller, 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,
),
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])!),
),
),
),
const SizedBox(
height: 30.0,
)
],
);
}

View File

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

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. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 4.8.0+480 version: 4.8.17+497
environment: environment:
sdk: ">=3.2.0 <4.0.0" sdk: ">=3.2.0 <4.0.0"
@ -29,6 +29,8 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations:
sdk: flutter
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
@ -39,6 +41,7 @@ dependencies:
geolocator: ^10.1.0 geolocator: ^10.1.0
permission_handler: ^11.3.1 permission_handler: ^11.3.1
logging: ^1.0.2 logging: ^1.0.2
timezone: ^0.9.1
# flutter_dev_tools: ^0.0.2 # flutter_dev_tools: ^0.0.2
# permission_handler: ^11.1.0 <== older # permission_handler: ^11.1.0 <== older
@ -81,7 +84,7 @@ dependencies:
circular_menu: ^2.0.1 circular_menu: ^2.0.1
camera: ^0.10.0+3 camera: ^0.10.0+3
camera_camera: ^3.0.0 camera_camera: ^3.0.0
intl: ^0.18.1 intl: ^0.19.0 #^0.18.1
modal_bottom_sheet: ^3.0.0-pre modal_bottom_sheet: ^3.0.0-pre
connectivity_plus: ^5.0.2 connectivity_plus: ^5.0.2
flutter_map_tile_caching: ^9.0.0-dev.5 flutter_map_tile_caching: ^9.0.0-dev.5