diff --git a/android/app/build.gradle b/android/app/build.gradle index 85cd96a3c930e03299120ebbe5ea302926fb476c..26fd88b3319b6c1c60538186ba1d7909045c7b3e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -23,6 +23,7 @@ if (flutterVersionName == null) { apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'com.google.gms.google-services' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { @@ -65,4 +66,6 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.google.firebase:firebase-messaging:20.3.0' } + diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000000000000000000000000000000000000..ad2c9482b18241521d164ded12dd63776f4b17c7 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,47 @@ +{ + "project_info": { + "project_number": "927565375305", + "project_id": "up-smf-47fd3", + "storage_bucket": "up-smf-47fd3.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:927565375305:android:72f8b2fed08ff47dc6dc98", + "android_client_info": { + "package_name": "com.example.smf_mobile" + } + }, + "oauth_client": [ + { + "client_id": "927565375305-57h44768ioqql1j8lm5q0jrq4es9lqj8.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.example.smf_mobile", + "certificate_hash": "7157475a2fccb7c2857112262c12be29359f845d" + } + }, + { + "client_id": "927565375305-den7asbeb5ucg36rpbn76ppnnd10pm4r.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDmWSOiwgEnSoTt6GKiArZT9wHREe-f1rw" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "927565375305-den7asbeb5ucg36rpbn76ppnnd10pm4r.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a471d0b3a87d369f3d4095470571d75d5dac0c68..f6f287cbcbeb4961e33dc5edae8fe98bba965cf2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.smf_mobile"> - <application + <uses-permission android:name="android.permission.INTERNET"/> + <application android:label="UP SMF" android:name="${applicationName}" android:icon="@mipmap/ic_launcher"> @@ -24,6 +25,10 @@ <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> + <intent-filter> + <action android:name="FLUTTER_NOTIFICATION_CLICK" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> </activity> <!-- Don't delete the meta-data below. This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> diff --git a/android/app/src/main/res/drawable/flutter_devs.png b/android/app/src/main/res/drawable/flutter_devs.png new file mode 100644 index 0000000000000000000000000000000000000000..275d9ad266027529356df93df0097f05ea3e9889 Binary files /dev/null and b/android/app/src/main/res/drawable/flutter_devs.png differ diff --git a/android/build.gradle b/android/build.gradle index 4256f9173625d2b6c3810e1946a6d1ee138034d3..94d72e4b887df493da4493223ca52560f1e18e54 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -8,6 +8,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:4.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'com.google.gms:google-services:4.3.4' } } diff --git a/lib/constants/api_endpoints.dart b/lib/constants/api_endpoints.dart index c0293d43b68e6f8e46da98fcaa899b90e3d350a1..8e575b0ec809e068c665e54d8527672face90bc4 100644 --- a/lib/constants/api_endpoints.dart +++ b/lib/constants/api_endpoints.dart @@ -2,6 +2,8 @@ class ApiUrl { static const baseUrl = 'https://smfdev.idc.tarento.com'; static const getOtp = '$baseUrl/api/user/requestOTP'; static const validateOtp = '$baseUrl/api/signIn'; + static const updateUserDeviceToken = + '$baseUrl/api/user/updateUserDeviceToken'; static const getAllApplications = '$baseUrl/api/forms/getAllApplications'; static const submitInspection = '$baseUrl/api/forms/submitInspection'; static const getAllUsers = '$baseUrl/api/user/v1/getAllUser'; diff --git a/lib/constants/app_constants.dart b/lib/constants/app_constants.dart index d8e423505b8f7a1a579cb01805ebb41d818d9568..5c659991a16618a66a3dd1e7372ee7fa96a80d21 100644 --- a/lib/constants/app_constants.dart +++ b/lib/constants/app_constants.dart @@ -18,6 +18,13 @@ class FieldType { } class InspectionStatus { + static const String newInspection = 'NEW'; static const String sentForInspection = 'SENTFORINS'; static const String inspectionCompleted = 'INSCOMPLETED'; + static const String returned = 'RETURNED'; +} + +class FieldValue { + static const String correct = 'Correct'; + static const String inCorrect = 'Incorrect'; } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 23d7cae59bca24664127b1779a00c5cc76753530..c319c9dcfa952f80a99fedf927eafe854fc400ab 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -13,5 +13,11 @@ "viewPastApplications": "View past applications", "next": "Next", "previous": "previous", - "inspectionCompleted": "Inspection completed" + "inspectionCompleted": "Inspection completed", + "sessionExpiredMessage": "Your session has expired.", + "isGivenInformationCorrect": "Is the given information found correct?", + "typeHere": "Type here", + "cancel": "Cancel", + "submit": "Submit", + "actualValue": "Actual value(s)" } \ No newline at end of file diff --git a/lib/landing_page.dart b/lib/landing_page.dart index 5500a1ad03ebdef6cc643232fcae5a44e29c9f63..888d48f0b818a1b633cb7415e86b05c749666605 100644 --- a/lib/landing_page.dart +++ b/lib/landing_page.dart @@ -8,11 +8,13 @@ import 'package:smf_mobile/repositories/application_repository.dart'; import 'package:smf_mobile/repositories/form_repository.dart'; import 'package:smf_mobile/repositories/login_repository.dart'; import 'package:smf_mobile/repositories/user_repository.dart'; +import 'package:smf_mobile/util/helper.dart'; import 'constants/app_constants.dart'; import 'constants/app_urls.dart'; import 'constants/color_constants.dart'; import 'routes.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:firebase_core/firebase_core.dart'; class LandingPage extends StatefulWidget { static const route = AppUrl.landingPage; @@ -28,6 +30,12 @@ class LandingPage extends StatefulWidget { class _LandingPageState extends State<LandingPage> { final client = HttpClient(); Locale _locale = const Locale('en', 'US'); + bool _isTokenExpired = false; + + @override + void initState() { + super.initState(); + } void setLocale(Locale value) { setState(() { @@ -35,40 +43,62 @@ class _LandingPageState extends State<LandingPage> { }); } + Future<dynamic> _initilizeApp() async { + _isTokenExpired = await Helper.isTokenExpired(); + await Firebase.initializeApp(); + // await Future.delayed(const Duration(microseconds: 100)); + return true; + } + @override Widget build(BuildContext context) { - return MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: LoginRespository()), - ChangeNotifierProvider.value(value: ApplicationRespository()), - ChangeNotifierProvider.value(value: UserRespository()), - ChangeNotifierProvider.value(value: FormRespository()), - ], - child: MaterialApp( - title: appName, - locale: _locale, - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [ - Locale('en', 'US'), - // Locale('es', 'ES'), - ], - theme: ThemeData( - scaffoldBackgroundColor: AppColors.scaffoldBackground, - primaryColor: Colors.white, - visualDensity: VisualDensity.adaptivePlatformDensity, - dividerColor: AppColors.black08, - canvasColor: Colors.white, - unselectedWidgetColor: AppColors.black40), - debugShowCheckedModeBanner: false, - onGenerateRoute: Routes.generateRoute, - onUnknownRoute: Routes.errorRoute, - home: const LoginEmailPage(), - // home: const HomePage(), - )); + return Container( + color: AppColors.scaffoldBackground, + child: FutureBuilder( + // Initialize FlutterFire + future: _initilizeApp(), + builder: (context, AsyncSnapshot<dynamic> snapshot) { + // Check for errors + if (snapshot.hasData) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: LoginRespository()), + ChangeNotifierProvider.value( + value: ApplicationRespository()), + ChangeNotifierProvider.value(value: UserRespository()), + ChangeNotifierProvider.value(value: FormRespository()), + ], + child: MaterialApp( + title: appName, + locale: _locale, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('en', 'US'), + // Locale('es', 'ES'), + ], + theme: ThemeData( + scaffoldBackgroundColor: AppColors.scaffoldBackground, + primaryColor: Colors.white, + visualDensity: VisualDensity.adaptivePlatformDensity, + dividerColor: AppColors.black08, + canvasColor: Colors.white, + unselectedWidgetColor: AppColors.black40), + debugShowCheckedModeBanner: false, + onGenerateRoute: Routes.generateRoute, + onUnknownRoute: Routes.errorRoute, + home: !_isTokenExpired + ? const HomePage() + : const LoginEmailPage(), + // home: const HomePage(), + )); + } else { + return const Center(); + } + })); } } diff --git a/lib/pages/application_details_page.dart b/lib/pages/application_details_page.dart index 5828389694fe7af7dd6bcb6f5db180e8ce445391..8c66091618d51fca75cbd0ff8f24f303a82517ca 100644 --- a/lib/pages/application_details_page.dart +++ b/lib/pages/application_details_page.dart @@ -32,6 +32,7 @@ class _ApplicationDetailsPageState extends State<ApplicationDetailsPage> int _activeTabIndex = 0; final Map _data = {}; final Map _fieldTypes = {}; + final Map _fieldOptions = {}; final List<String> _tabs = []; final List<Map> _fields = []; @@ -59,6 +60,8 @@ class _ApplicationDetailsPageState extends State<ApplicationDetailsPage> if (_formData.fields[i]['fieldType'] != FieldType.heading) { _fieldTypes[_formData.fields[i]['name']] = _formData.fields[i]['fieldType']; + _fieldOptions[_formData.fields[i]['name']] = + _formData.fields[i]['values']; } } } @@ -98,12 +101,13 @@ class _ApplicationDetailsPageState extends State<ApplicationDetailsPage> }) } }); + // print(fieldData); } void _validateUser() async { bool tokenExpired = await Helper.isTokenExpired(); if (tokenExpired) { - Helper.toastMessage('Your session has expired.'); + Helper.toastMessage(AppLocalizations.of(context)!.sessionExpiredMessage); Navigator.of(context).pushReplacement(MaterialPageRoute( builder: (context) => const LoginEmailPage(), )); @@ -241,6 +245,8 @@ class _ApplicationDetailsPageState extends State<ApplicationDetailsPage> field.keys.elementAt(i)], fieldType: _fieldTypes[ field.keys.elementAt(i)], + fieldOptions: _fieldOptions[ + field.keys.elementAt(i)], applicationStatus: widget.application.status, parentAction: updateField, diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 54262c7d776d8af5b3c2a46d3a8b87fdb139c68f..20f857ddbb603e48dcb9dfb8db26ea97d11209fe 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -34,7 +34,7 @@ class _HomePageState extends State<HomePage> { void _validateUser() async { bool tokenExpired = await Helper.isTokenExpired(); if (tokenExpired) { - Helper.toastMessage('Your session has expired.'); + Helper.toastMessage(AppLocalizations.of(context)!.sessionExpiredMessage); Navigator.of(context).pushReplacement(MaterialPageRoute( builder: (context) => const LoginEmailPage(), )); diff --git a/lib/pages/inspection_summary.dart b/lib/pages/inspection_summary.dart index e529200d230518e7368590df35eae2b00018055a..91a6039ffcee2a8ebe476bef6cbd5bf28f1dcf3a 100644 --- a/lib/pages/inspection_summary.dart +++ b/lib/pages/inspection_summary.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; import 'package:smf_mobile/constants/app_constants.dart'; @@ -12,6 +11,7 @@ import 'package:smf_mobile/repositories/form_repository.dart'; import 'package:smf_mobile/util/helper.dart'; import 'package:smf_mobile/widgets/people_card.dart'; import 'inspection_completed.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class InspectionSummaryPage extends StatefulWidget { static const route = AppUrl.inspectionSummary; @@ -65,7 +65,7 @@ class _InspectionSummaryPageState extends State<InspectionSummaryPage> { void _validateUser() async { bool tokenExpired = await Helper.isTokenExpired(); if (tokenExpired) { - Helper.toastMessage('Your session has expired.'); + Helper.toastMessage(AppLocalizations.of(context)!.sessionExpiredMessage); Navigator.of(context).pushReplacement(MaterialPageRoute( builder: (context) => const LoginEmailPage(), )); diff --git a/lib/pages/login_email_page.dart b/lib/pages/login_email_page.dart index 629962d0b6fc1ecc46e95875ea611111c1304d83..e623aa3098499adaeac4b0b1534302e8975529e6 100644 --- a/lib/pages/login_email_page.dart +++ b/lib/pages/login_email_page.dart @@ -1,3 +1,4 @@ +// import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; @@ -8,6 +9,8 @@ import 'package:smf_mobile/pages/login_otp_page.dart'; import 'package:smf_mobile/repositories/login_repository.dart'; import 'package:smf_mobile/util/helper.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:smf_mobile/util/notification_helper.dart'; class LoginEmailPage extends StatefulWidget { static const route = AppUrl.loginEmailPage; @@ -20,19 +23,39 @@ class LoginEmailPage extends StatefulWidget { class _LoginEmailPageState extends State<LoginEmailPage> { final TextEditingController _emailController = TextEditingController(); final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); + FirebaseMessaging messaging = FirebaseMessaging.instance; String _errorMessage = ''; - late Locale locale; @override void initState() { super.initState(); + // _configureMessaging(); } - // This method should be triggered on some event - // _setLang() async { - // locale = const Locale('es', ''); - // LandingPage.of(context)?.setLocale(locale); + // _configureMessaging() async { + // NotificationSettings settings = await messaging.requestPermission( + // alert: true, + // announcement: false, + // badge: true, + // carPlay: false, + // criticalAlert: false, + // provisional: false, + // sound: true, + // ); + // print('User granted permission: ${settings.authorizationStatus}'); + + // FirebaseMessaging.onMessage.listen((RemoteMessage message) { + // if (message.notification != null) { + // var random = Random(); + // int notificationId = random.nextInt(999999); + // String body = + // message.notification!.body.toString() + message.data.toString(); + // NotificationHelper.scheduleNotification(context, DateTime.now(), + // notificationId, message.notification!.title.toString(), body); + // } + // print('Message data: $message'); + // }); // } Future<void> _generateOtp() async { @@ -45,7 +68,7 @@ class _LoginEmailPageState extends State<LoginEmailPage> { try { final responseCode = await Provider.of<LoginRespository>(context, listen: false) - .getOtp(email); + .getOtp(email.trim()); if (responseCode == 200) { Navigator.of(context).pushReplacement(MaterialPageRoute( builder: (context) => const LoginOtpPage(), diff --git a/lib/pages/login_otp_page.dart b/lib/pages/login_otp_page.dart index 3bb11aea8edca017d6358b5e5f6a07b4ff6fcb81..4423329054eeabd8efc99cbed8db31ba3fadfa3c 100644 --- a/lib/pages/login_otp_page.dart +++ b/lib/pages/login_otp_page.dart @@ -10,6 +10,8 @@ import 'package:otp_text_field/style.dart'; import 'package:smf_mobile/repositories/login_repository.dart'; import 'package:smf_mobile/util/helper.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:unique_identifier/unique_identifier.dart'; +import 'package:flutter/services.dart'; class LoginOtpPage extends StatefulWidget { static const route = AppUrl.loginOtpPage; @@ -23,10 +25,27 @@ class _LoginOtpPageState extends State<LoginOtpPage> { final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); String _errorMessage = ''; String _otp = ''; + late String _identifier; @override void initState() { super.initState(); + initUniqueIdentifierState(); + } + + Future<void> initUniqueIdentifierState() async { + String? identifier; + try { + identifier = await UniqueIdentifier.serial; + } on PlatformException { + identifier = 'Failed to get Unique Identifier'; + } + + if (!mounted) return; + + setState(() { + _identifier = identifier!; + }); } Future<void> _validateOtp() async { @@ -34,7 +53,7 @@ class _LoginOtpPageState extends State<LoginOtpPage> { try { final responseCode = await Provider.of<LoginRespository>(context, listen: false) - .validateOtp(otp); + .validateOtp(context, otp, _identifier); if (responseCode == 200) { Navigator.of(context).pushReplacement(MaterialPageRoute( builder: (context) => const HomePage(), @@ -144,19 +163,26 @@ class _LoginOtpPageState extends State<LoginOtpPage> { color: Colors.white, ), child: OTPTextField( - length: 6, - width: MediaQuery.of(context).size.width, - fieldWidth: 38, - style: const TextStyle(fontSize: 14), - textFieldAlignment: - MainAxisAlignment.spaceAround, - fieldStyle: FieldStyle.underline, - onCompleted: (pin) { - setState(() { - _otp = pin; - }); - }, - ), + length: 6, + width: + MediaQuery.of(context).size.width, + fieldWidth: 38, + style: const TextStyle(fontSize: 14), + textFieldAlignment: + MainAxisAlignment.spaceAround, + fieldStyle: FieldStyle.underline, + onCompleted: (pin) { + setState(() { + _otp = pin; + }); + }, + onChanged: (String? pin) { + if (pin?.length == 6) { + setState(() { + _otp = pin.toString(); + }); + } + }), ) ], ), diff --git a/lib/repositories/login_repository.dart b/lib/repositories/login_repository.dart index b98c877418006daa041cc380b89c25120231a709..295db22eee5aaeb5056ea9381f438cce8448f948 100644 --- a/lib/repositories/login_repository.dart +++ b/lib/repositories/login_repository.dart @@ -1,13 +1,17 @@ import 'dart:convert'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/widgets.dart'; import 'package:smf_mobile/models/login_model.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:smf_mobile/services/login_service.dart'; +import 'package:smf_mobile/util/helper.dart'; +import 'package:smf_mobile/util/notification_helper.dart'; class LoginRespository with ChangeNotifier { late Map _data; late Login _loginDetails; String _errorMessage = ''; + final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; final _storage = const FlutterSecureStorage(); @@ -27,7 +31,7 @@ class LoginRespository with ChangeNotifier { return _data['statusInfo']['statusCode']; } - Future<dynamic> validateOtp(String otp) async { + Future<dynamic> validateOtp(context, String otp, String identifier) async { try { final username = await _storage.read(key: 'username'); final request = await LoginService.validateOtp(username!, otp); @@ -45,10 +49,48 @@ class LoginRespository with ChangeNotifier { _storage.write(key: 'firstName', value: _loginDetails.firstName); _storage.write(key: 'lastName', value: _loginDetails.lastName); _storage.write(key: 'authToken', value: _loginDetails.authToken); + _firebaseMessaging.getToken().then((token) async { + final request = await LoginService.updateUserDeviceToken( + token.toString(), + identifier, + _loginDetails.id, + ); + _data = json.decode(request.body); + // print(_data); + if (_data['statusInfo']['statusCode'] == 200) { + // print('_configureMessaging...'); + _configureMessaging(context); + } + }); } return _data['statusInfo']['statusCode']; } + _configureMessaging(context) async { + NotificationSettings settings = await _firebaseMessaging.requestPermission( + alert: true, + announcement: false, + badge: true, + carPlay: false, + criticalAlert: false, + provisional: false, + sound: true, + ); + print('User granted permission: ${settings.authorizationStatus}'); + + FirebaseMessaging.onMessage.listen((RemoteMessage message) { + print('message.notification...'); + if (message.notification != null) { + // int uniqueNotificationId = Helper.getUniqueId(); + String body = message.notification!.body.toString(); + NotificationHelper.scheduleNotification(context, DateTime.now(), 0, + message.notification!.title.toString(), body); + } + print('Message data: $message'); + }); + return; + } + Future<void> clearData() async { await _storage.deleteAll(); } diff --git a/lib/services/login_service.dart b/lib/services/login_service.dart index e9efcae654f433d694d0c362e8135bae3385f7a2..6df9e5ac6fa65eaa8af7a9e2d741d59dc6a1c04b 100644 --- a/lib/services/login_service.dart +++ b/lib/services/login_service.dart @@ -9,9 +9,7 @@ class LoginService extends BaseService { LoginService(HttpClient client) : super(client); static Future<dynamic> getOtp(String username) async { - Map requestData = { - 'username': username, - }; + Map requestData = {'username': username, 'isMobile': true}; var body = json.encode(requestData); Map<String, String> headers = await BaseService.getHeaders(); final response = @@ -27,4 +25,18 @@ class LoginService extends BaseService { headers: headers, body: body); return response; } + + static Future<dynamic> updateUserDeviceToken( + String token, String identifier, int userId) async { + Map requestData = { + "deviceToken": token, + "deviceId": identifier, + "userId": userId + }; + var body = json.encode(requestData); + Map<String, String> headers = await BaseService.getHeaders(); + final response = await http.post(Uri.parse(ApiUrl.updateUserDeviceToken), + headers: headers, body: body); + return response; + } } diff --git a/lib/util/helper.dart b/lib/util/helper.dart index 0582bd18d4b1939890423a320f9fd405ad7bfe15..92e1dde7010a64e2ded097d24c88f57b24eb27d2 100644 --- a/lib/util/helper.dart +++ b/lib/util/helper.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:jwt_decoder/jwt_decoder.dart'; @@ -33,7 +35,9 @@ class Helper { static Future<bool> isTokenExpired() async { bool isTokenExpired = true; var authToken = await _storage.read(key: 'authToken'); - isTokenExpired = JwtDecoder.isExpired(authToken!); + if (authToken != null) { + isTokenExpired = JwtDecoder.isExpired(authToken); + } return isTokenExpired; } @@ -47,4 +51,13 @@ class Helper { textColor: Colors.white, fontSize: 16.0); } + + static getUniqueId() { + DateTime _now = DateTime.now(); + var random = Random(); + int id1 = random.nextInt(99999); + int id2 = random.nextInt(99999); + int notificationId = id1 + id2 + _now.millisecond; + return notificationId; + } } diff --git a/lib/util/notification_helper.dart b/lib/util/notification_helper.dart new file mode 100644 index 0000000000000000000000000000000000000000..c03623fcb10c9c831b3f489c3feac1f148726ddd --- /dev/null +++ b/lib/util/notification_helper.dart @@ -0,0 +1,61 @@ +// import 'dart:convert'; +// import 'package:crypto/crypto.dart'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:smf_mobile/pages/home_page.dart'; + +class NotificationHelper { + // static BuildContext get context => null; + static void onSelectNotification( + BuildContext context, + String payload, + ) { + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (context) => const HomePage(), + )); + } + + static Future<void> scheduleNotification( + BuildContext context, + DateTime scheduledNotificationDateTime, + int notificationId, + String title, + String notes) async { + FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + var initializationSettingsAndroid = + const AndroidInitializationSettings('flutter_devs'); + var initializationSettingsIOs = const IOSInitializationSettings(); + var initSetttings = InitializationSettings( + android: initializationSettingsAndroid, iOS: initializationSettingsIOs); + flutterLocalNotificationsPlugin.initialize(initSetttings, + onSelectNotification: (payload) async { + onSelectNotification(context, payload!); + }); + + var androidPlatformChannelSpecifics = const AndroidNotificationDetails( + 'channel id', + 'channel name', + icon: 'flutter_devs', + enableVibration: true, + enableLights: true, + playSound: true, + // sound: RawResourceAndroidNotificationSound('mixkit_happy_bell'), + largeIcon: DrawableResourceAndroidBitmap('flutter_devs'), + styleInformation: BigTextStyleInformation(''), + ); + + var iOSPlatformChannelSpecifics = const IOSNotificationDetails(); + var platformChannelSpecifics = NotificationDetails( + android: androidPlatformChannelSpecifics, + iOS: iOSPlatformChannelSpecifics); + + Map payload = {}; + // ignore: deprecated_member_use + await flutterLocalNotificationsPlugin.schedule(notificationId, title, notes, + scheduledNotificationDateTime, platformChannelSpecifics, + payload: jsonEncode(payload)); + } +} diff --git a/lib/widgets/application_card.dart b/lib/widgets/application_card.dart index 72a8c5ccc1742d8f86c2746fffa1dc50c080ae4b..0838d56b480d2f4c6e8d40aeba0d63e937a9e35e 100644 --- a/lib/widgets/application_card.dart +++ b/lib/widgets/application_card.dart @@ -17,11 +17,29 @@ class ApplicationCard extends StatefulWidget { } class _ApplicationCardState extends State<ApplicationCard> { + String _inspectionStatus = ''; @override void initState() { super.initState(); + _getInspectionStatus(); } + void _getInspectionStatus() { + if (widget.application.status == InspectionStatus.inspectionCompleted) { + _inspectionStatus = 'Completed'; + } else if (widget.application.status == + InspectionStatus.sentForInspection) { + _inspectionStatus = 'Pending'; + } else { + _inspectionStatus = _toCapitalized(widget.application.status); + } + setState(() {}); + } + + _toCapitalized(String string) => string.isNotEmpty + ? '${string[0].toUpperCase()}${string.substring(1).toLowerCase()}' + : ''; + @override Widget build(BuildContext context) { return InkWell( @@ -89,28 +107,16 @@ class _ApplicationCardState extends State<ApplicationCard> { fontWeight: FontWeight.w400, )), ), - widget.application.status == - InspectionStatus.inspectionCompleted - ? Padding( - padding: const EdgeInsets.only(bottom: 10), - child: Text('Completed', - style: GoogleFonts.lato( - color: AppColors.black60, - fontSize: 14.0, - letterSpacing: 0.12, - fontWeight: FontWeight.w700, - )), - ) - : Padding( - padding: const EdgeInsets.only(bottom: 10), - child: Text('Pending', - style: GoogleFonts.lato( - color: AppColors.black60, - fontSize: 14.0, - letterSpacing: 0.12, - fontWeight: FontWeight.w700, - )), - ) + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Text(_inspectionStatus, + style: GoogleFonts.lato( + color: AppColors.black60, + fontSize: 14.0, + letterSpacing: 0.12, + fontWeight: FontWeight.w700, + )), + ), ], ) ], diff --git a/lib/widgets/application_field.dart b/lib/widgets/application_field.dart index da777e1c23bbaa64d02024a9a563f142aebbd0f7..ac8f84414cf3f4ddaa62702ed2901dcb24a5577c 100644 --- a/lib/widgets/application_field.dart +++ b/lib/widgets/application_field.dart @@ -3,11 +3,13 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:smf_mobile/constants/app_constants.dart'; import 'package:smf_mobile/constants/color_constants.dart'; import 'package:smf_mobile/widgets/application_field_dialog.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class ApplicationField extends StatefulWidget { final String fieldName; final Map fieldData; final String fieldType; + final List fieldOptions; final String applicationStatus; final ValueChanged<Map> parentAction; const ApplicationField({ @@ -15,6 +17,7 @@ class ApplicationField extends StatefulWidget { required this.fieldName, required this.fieldData, required this.fieldType, + required this.fieldOptions, required this.applicationStatus, required this.parentAction, }) : super(key: key); @@ -27,15 +30,21 @@ class _ApplicationFieldState extends State<ApplicationField> { late String _radioValue; String _inspectionValue = ''; String _summaryText = ''; - final List<String> _options = ['Correct', 'Incorrect']; + final List<String> _options = [FieldValue.correct, FieldValue.inCorrect]; @override void initState() { super.initState(); - // print(widget.fieldName + ', ' + widget.fieldType); + _data = widget.fieldData[widget.fieldData.keys.elementAt(0)]; + // print('Field: ' + _data.toString()); _radioValue = _data[_data.keys.elementAt(0)]; _summaryText = _data[_data.keys.elementAt(1)]; + try { + _inspectionValue = _data[_data.keys.elementAt(2)]; + } catch (_) { + return; + } } triggerUpdate(Map dialogData) { @@ -64,6 +73,7 @@ class _ApplicationFieldState extends State<ApplicationField> { summaryText: _summaryText, inspectionValue: _inspectionValue, fieldType: widget.fieldType, + fieldOptions: widget.fieldOptions, parentAction: triggerUpdate, ); })); @@ -158,7 +168,8 @@ class _ApplicationFieldState extends State<ApplicationField> { width: MediaQuery.of(context).size.width, padding: const EdgeInsets.only(bottom: 10), child: Text( - 'Is the given information found correct?', + AppLocalizations.of(context)! + .sessionExpiredMessage, style: GoogleFonts.lato( color: AppColors.black60, fontWeight: FontWeight.w700, @@ -182,7 +193,7 @@ class _ApplicationFieldState extends State<ApplicationField> { _radioValue = _options[i]; }); if (_options[i] == - 'Incorrect') { + FieldValue.inCorrect) { _displayCommentDialog(); } Map data = { @@ -239,7 +250,8 @@ class _ApplicationFieldState extends State<ApplicationField> { }; triggerUpdate(data); if (_options[i] == - 'Incorrect') { + FieldValue + .inCorrect) { _displayCommentDialog(); } } @@ -264,24 +276,27 @@ class _ApplicationFieldState extends State<ApplicationField> { if (widget.applicationStatus == InspectionStatus .sentForInspection && - _radioValue != 'Correct') { + _radioValue != + FieldValue.correct) { _displayCommentDialog(); } }, - icon: _radioValue != 'Correct' - ? const Icon( - Icons.edit, - color: AppColors.black40, - ) - : const Icon( - Icons.message, - color: AppColors.black40, - ), + icon: + _radioValue != FieldValue.correct + ? const Icon( + Icons.edit, + color: AppColors.black40, + ) + : const Icon( + Icons.message, + color: AppColors.black40, + ), ), ) ], )), - _radioValue != 'Correct' && _summaryText != '' + _radioValue != FieldValue.correct && + _summaryText != '' ? Container( margin: const EdgeInsets.only(top: 10), padding: const EdgeInsets.fromLTRB( @@ -304,7 +319,8 @@ class _ApplicationFieldState extends State<ApplicationField> { ), ) : const Center(), - _radioValue != 'Correct' && _inspectionValue != '' + _radioValue != FieldValue.correct && + _inspectionValue != '' ? Container( width: MediaQuery.of(context).size.width, padding: const EdgeInsets.only(top: 20), @@ -318,7 +334,8 @@ class _ApplicationFieldState extends State<ApplicationField> { ), )) : const Center(), - _radioValue != 'Correct' && _inspectionValue != '' + _radioValue != FieldValue.correct && + _inspectionValue != '' ? Container( margin: const EdgeInsets.only(top: 10), padding: const EdgeInsets.fromLTRB( diff --git a/lib/widgets/application_field_dialog.dart b/lib/widgets/application_field_dialog.dart index df381b639937bb01180b96a43410c49432fc5add..616befd2a3ae45dfa72a7c8d2085a778f12f5161 100644 --- a/lib/widgets/application_field_dialog.dart +++ b/lib/widgets/application_field_dialog.dart @@ -1,19 +1,26 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:smf_mobile/constants/app_constants.dart'; import 'package:smf_mobile/constants/color_constants.dart'; +import 'package:smf_mobile/widgets/questions/dropdown_question.dart'; +import 'package:smf_mobile/widgets/questions/multi_select_question.dart'; +import 'package:smf_mobile/widgets/questions/radio_question.dart'; import 'package:smf_mobile/widgets/questions/text_question.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class ApplicationFieldDialog extends StatefulWidget { final String summaryText; final String inspectionValue; final String fieldType; + final List fieldOptions; final ValueChanged<Map> parentAction; const ApplicationFieldDialog({ Key? key, required this.summaryText, required this.inspectionValue, required this.fieldType, + required this.fieldOptions, required this.parentAction, }) : super(key: key); @@ -29,14 +36,20 @@ class _ApplicationFieldDialogState extends State<ApplicationFieldDialog> { @override void initState() { super.initState(); + // print('Dialog: ' + widget.inspectionValue); _summaryController.text = widget.summaryText; + _inspectionValue = widget.inspectionValue; } saveData(String inspectionValue) { setState(() { _inspectionValue = inspectionValue; }); - // _submitData(); + data = { + 'summaryText': _summaryController.text, + 'inspectionValue': _inspectionValue + }; + widget.parentAction(data); } _submitData() { @@ -44,6 +57,7 @@ class _ApplicationFieldDialogState extends State<ApplicationFieldDialog> { 'summaryText': _summaryController.text, 'inspectionValue': _inspectionValue }; + // print('inspectionValue: $_inspectionValue'); widget.parentAction(data); } @@ -57,7 +71,7 @@ class _ApplicationFieldDialogState extends State<ApplicationFieldDialog> { margin: const EdgeInsets.only(top: 150), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4)), - constraints: const BoxConstraints(minHeight: 300, maxHeight: 400), + constraints: const BoxConstraints(minHeight: 300, maxHeight: 440), width: MediaQuery.of(context).size.width - 40, child: Material( child: Padding( @@ -66,7 +80,7 @@ class _ApplicationFieldDialogState extends State<ApplicationFieldDialog> { crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - height: 290, + height: 330, child: ListView( shrinkWrap: true, physics: const AlwaysScrollableScrollPhysics(), @@ -94,7 +108,7 @@ class _ApplicationFieldDialogState extends State<ApplicationFieldDialog> { borderRadius: BorderRadius.circular(8), child: Focus( child: TextFormField( - autofocus: true, + // autofocus: true, controller: _summaryController, textCapitalization: TextCapitalization.sentences, @@ -103,7 +117,11 @@ class _ApplicationFieldDialogState extends State<ApplicationFieldDialog> { minLines: 8, //Normal textInputField will be displayed maxLines: 8, // wh - // controller: notesController, + onEditingComplete: () { + SystemChannels.textInput + .invokeMethod('TextInput.hide'); + return; + }, style: const TextStyle( color: AppColors.black87, fontSize: 14), decoration: const InputDecoration( @@ -118,25 +136,52 @@ class _ApplicationFieldDialogState extends State<ApplicationFieldDialog> { ), ), ), - Padding( - padding: const EdgeInsets.only(top: 20), - child: Text( - 'Actual value(s)', - style: GoogleFonts.lato( - color: AppColors.black87, - fontWeight: FontWeight.w700, - fontSize: 14, - letterSpacing: 0.25, - ), - )), + widget.fieldType != FieldType.file + ? Padding( + padding: const EdgeInsets.only(top: 20), + child: Text( + 'Actual value(s)', + style: GoogleFonts.lato( + color: AppColors.black87, + fontWeight: FontWeight.w700, + fontSize: 14, + letterSpacing: 0.25, + ), + )) + : const Center(), widget.fieldType == FieldType.text || widget.fieldType == FieldType.numeric || - widget.fieldType == FieldType.email + widget.fieldType == FieldType.email || + widget.fieldType == FieldType.date || + widget.fieldType == FieldType.textarea ? TextQuestion( fieldType: widget.fieldType, answerGiven: widget.inspectionValue, parentAction: saveData) - : const Center(), + : widget.fieldType == FieldType.dropdown + ? DropdownQuestion( + items: widget.fieldOptions, + selectedItem: widget.inspectionValue, + parentAction: saveData) + : widget.fieldType == FieldType.radio || + widget.fieldType == + FieldType.boolean + ? RadioQuestion( + items: widget.fieldOptions, + fieldType: widget.fieldType, + selectedItem: + widget.inspectionValue, + parentAction: saveData) + : widget.fieldType == + FieldType.checkbox || + widget.fieldType == + FieldType.multiselect + ? MultiSelectQuestion( + items: widget.fieldOptions, + selectedItems: + widget.inspectionValue, + parentAction: saveData) + : const Center(), ]), ), Container( @@ -148,6 +193,7 @@ class _ApplicationFieldDialogState extends State<ApplicationFieldDialog> { child: OutlinedButton( onPressed: () { Navigator.of(context).pop(false); + _submitData(); }, style: OutlinedButton.styleFrom( // primary: Colors.white, diff --git a/lib/widgets/questions/dropdown_question.dart b/lib/widgets/questions/dropdown_question.dart new file mode 100644 index 0000000000000000000000000000000000000000..9044e26fc66c3dc0dba7971ae5325b2e721d877c --- /dev/null +++ b/lib/widgets/questions/dropdown_question.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:smf_mobile/constants/color_constants.dart'; + +class DropdownQuestion extends StatefulWidget { + final List items; + final String selectedItem; + final ValueChanged<String> parentAction; + const DropdownQuestion( + {Key? key, + required this.items, + required this.selectedItem, + required this.parentAction}) + : super(key: key); + + @override + _DropdownQuestionState createState() => _DropdownQuestionState(); +} + +class _DropdownQuestionState extends State<DropdownQuestion> { + String _dropdownValue = ''; + final List<String> _dropdownItems = []; + + @override + void initState() { + super.initState(); + initializeDropdown(); + } + + void initializeDropdown() { + for (var item in widget.items) { + _dropdownItems.add(item['value']); + } + setState(() { + _dropdownValue = + widget.selectedItem != '' ? widget.selectedItem : _dropdownItems[0]; + }); + } + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(top: 15), + padding: const EdgeInsets.only(left: 10, right: 10), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: AppColors.black16), + ), + child: DropdownButton<String>( + value: _dropdownValue.isNotEmpty ? _dropdownValue : null, + icon: const Icon(Icons.arrow_drop_down_outlined), + iconSize: 26, + elevation: 16, + isExpanded: true, + style: const TextStyle(color: AppColors.black87), + underline: Container( + // height: 2, + color: AppColors.black08, + ), + selectedItemBuilder: (BuildContext context) { + return _dropdownItems.map<Widget>((String item) { + return Row( + children: [ + Text( + _dropdownValue, + style: GoogleFonts.lato( + color: AppColors.black87, + fontSize: 14.0, + letterSpacing: 0.25, + fontWeight: FontWeight.w400, + ), + ), + ], + ); + }).toList(); + }, + onChanged: (newValue) { + setState(() { + _dropdownValue = newValue.toString(); + widget.parentAction(newValue.toString()); + }); + }, + items: _dropdownItems.map<DropdownMenuItem<String>>((String value) { + return DropdownMenuItem<String>( + value: value, + child: Text( + value, + style: GoogleFonts.lato( + color: AppColors.black87, + fontSize: 13.0, + letterSpacing: 0.25, + fontWeight: FontWeight.w400, + ), + ), + ); + }).toList(), + )); + } +} diff --git a/lib/widgets/questions/multi_select_question.dart b/lib/widgets/questions/multi_select_question.dart index 30fc6926d6ad22f77bcc90ba800ee4027e445590..be747e5fc6f7813236bbe2f5f28740f2072cbfca 100644 --- a/lib/widgets/questions/multi_select_question.dart +++ b/lib/widgets/questions/multi_select_question.dart @@ -1,170 +1,107 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; -import './../../../../feedback/constants.dart'; +import 'package:smf_mobile/constants/color_constants.dart'; class MultiSelectQuestion extends StatefulWidget { - final question; - final int currentIndex; - final answerGiven; - final bool showAnswer; - final ValueChanged<Map> parentAction; - MultiSelectQuestion(this.question, this.currentIndex, this.answerGiven, - this.showAnswer, this.parentAction); + final List items; + final String selectedItems; + final ValueChanged<String> parentAction; + + const MultiSelectQuestion( + {Key? key, + required this.items, + required this.selectedItems, + required this.parentAction}) + : super(key: key); @override _MultiSelectQuestionQuestionState createState() => _MultiSelectQuestionQuestionState(); } class _MultiSelectQuestionQuestionState extends State<MultiSelectQuestion> { - Map<int, bool> isChecked = { - 1: false, - 2: false, - 3: false, - 4: false, - }; - List selectedOptions = []; - List<int> _correctAnswer = []; + final Map _isChecked = {}; + List _selectedOptions = []; + final List _checkboxItems = []; @override void initState() { super.initState(); - if (widget.answerGiven != null) { - selectedOptions = widget.answerGiven; - for (int i = 0; i < widget.question['options'].length; i++) { - if (selectedOptions - .contains(widget.question['options'][i]['optionId'])) { - isChecked[i + 1] = true; - } - if (widget.question['options'][i]['isCorrect']) { - _correctAnswer.add(i); - } + initializeDropdown(); + } + + void initializeDropdown() { + _selectedOptions = widget.selectedItems.split(","); + for (int i = 0; i < widget.items.length; i++) { + _checkboxItems.add(widget.items[i]['value']); + _isChecked[i] = + _selectedOptions.contains(widget.items[i]['value']) ? true : false; + } + List temp = []; + for (int i = 0; i < _selectedOptions.length; i++) { + if (_checkboxItems.contains(_selectedOptions[i])) { + temp.add(_selectedOptions[i]); } } + _selectedOptions = temp; } @override Widget build(BuildContext context) { return Container( - height: MediaQuery.of(context).size.height- 30, - padding: const EdgeInsets.all(32), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.only(bottom: 15), - child: HtmlWidget( - widget.question['question'], - textStyle: GoogleFonts.lato( - color: FeedbackColors.black87, - fontWeight: FontWeight.w600, - fontSize: 16.0, - ), + padding: const EdgeInsets.only(top: 10), + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: _checkboxItems.length, + itemBuilder: (context, index) { + return Container( + width: MediaQuery.of(context).size.width, + // padding: const EdgeInsets.all(15), + margin: const EdgeInsets.only(top: 10, bottom: 10), + decoration: BoxDecoration( + color: _isChecked[index] + ? const Color.fromRGBO(0, 116, 182, 0.05) + : Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + border: Border.all( + color: _isChecked[index] + ? AppColors.primaryBlue + : AppColors.black16, ), ), - ListView.builder( - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: widget.question['options'].length, - itemBuilder: (context, index) { - return Container( - width: MediaQuery.of(context).size.width, - // padding: const EdgeInsets.all(15), - margin: const EdgeInsets.only(top: 10, bottom: 10), - decoration: BoxDecoration( - color: isChecked[index + 1] && - _correctAnswer.contains(index) && - widget.showAnswer - ? FeedbackColors.positiveLightBg - : isChecked[index + 1] && - !_correctAnswer.contains(index) && - widget.showAnswer - ? FeedbackColors.negativeLightBg - : _correctAnswer.contains(index) && - widget.showAnswer - ? FeedbackColors.positiveLightBg - : _correctAnswer.contains(index) && - widget.showAnswer - ? FeedbackColors.negativeLightBg - : isChecked[index + 1] && - _correctAnswer.contains(index) && - widget.showAnswer - ? FeedbackColors.positiveLightBg - : Colors.white, - borderRadius: BorderRadius.all(const Radius.circular(4.0)), - border: Border.all( - color: isChecked[index + 1] && - _correctAnswer.contains(index) && - widget.showAnswer - ? FeedbackColors.positiveLight - : isChecked[index + 1] && - !_correctAnswer.contains(index) && - widget.showAnswer - ? FeedbackColors.negativeLight - : _correctAnswer.contains(index) && - widget.showAnswer - ? FeedbackColors.positiveLight - : _correctAnswer.contains(index) && - widget.showAnswer - ? FeedbackColors.negativeLight - : isChecked[index + 1] && - _correctAnswer.contains(index) && - widget.showAnswer - ? FeedbackColors.positiveLight - : isChecked[index + 1] - ? FeedbackColors.primaryBlue - : FeedbackColors.black16, - ), + child: CheckboxListTile( + controlAffinity: ListTileControlAffinity.leading, + activeColor: AppColors.primaryBlue, + dense: true, + title: Text( + _checkboxItems[index], + style: GoogleFonts.lato( + color: AppColors.black87, + fontWeight: FontWeight.w400, + fontSize: 14, ), - child: CheckboxListTile( - controlAffinity: ListTileControlAffinity.leading, - activeColor: _correctAnswer.contains(index) && - widget.showAnswer - ? FeedbackColors.positiveLight - : !_correctAnswer.contains(index) && widget.showAnswer - ? FeedbackColors.negativeLight - : FeedbackColors.primaryBlue, - dense: true, - //font change - title: Text( - widget.question['options'][index]['text'], - style: GoogleFonts.lato( - color: FeedbackColors.black87, - fontWeight: FontWeight.w400, - fontSize: 14, - ), - ), - value: isChecked[index + 1], - onChanged: (bool value) { - if (!widget.showAnswer) { - if (value) { - if (!selectedOptions.contains(widget - .question['options'][index]['optionId'])) { - selectedOptions.add(widget.question['options'] - [index]['optionId']); - } - } else { - if (selectedOptions.contains(widget - .question['options'][index]['optionId'])) { - selectedOptions.remove(widget.question['options'] - [index]['optionId']); - } - } - widget.parentAction({ - 'index': widget.question['questionId'], - 'isCorrect': widget.question['options'][index] - ['isCorrect'], - 'value': selectedOptions - }); - setState(() { - isChecked[index + 1] = value; - }); - } - }), - ); - }, - ), - ], - )); + ), + value: _isChecked[index], + onChanged: (value) { + setState(() { + _isChecked[index] = value; + + if (_isChecked[index]) { + _selectedOptions.add(_checkboxItems[index]); + } else { + int keyIndex = + _selectedOptions.indexOf(_checkboxItems[index]); + if (keyIndex >= 0) { + _selectedOptions.removeAt(keyIndex); + } + } + }); + // print(_selectedOptions); + widget.parentAction(_selectedOptions.join(",")); + }), + ); + }, + ), + ); } } diff --git a/lib/widgets/questions/radio_question.dart b/lib/widgets/questions/radio_question.dart new file mode 100644 index 0000000000000000000000000000000000000000..83e5c0cad366a1161551d542e1b9a66edd5278bb --- /dev/null +++ b/lib/widgets/questions/radio_question.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:smf_mobile/constants/app_constants.dart'; +import 'package:smf_mobile/constants/color_constants.dart'; + +class RadioQuestion extends StatefulWidget { + final List items; + final String fieldType; + final String selectedItem; + final ValueChanged<String> parentAction; + + const RadioQuestion( + {Key? key, + required this.items, + required this.fieldType, + required this.selectedItem, + required this.parentAction}) + : super(key: key); + @override + _RadioQuestionState createState() => _RadioQuestionState(); +} + +class _RadioQuestionState extends State<RadioQuestion> { + int _radioValue = 0; + List<String> _radioItems = []; + + @override + void initState() { + super.initState(); + initializeDropdown(); + } + + void initializeDropdown() { + if (widget.fieldType == FieldType.boolean) { + _radioItems = ['True', 'False']; + } else { + for (var item in widget.items) { + _radioItems.add(item['value']); + } + } + setState(() { + _radioValue = widget.selectedItem != '' + ? widget.items.indexOf(widget.selectedItem) + : 0; + }); + // if (widget.selectedItem.isEmpty) { + // widget.parentAction(_radioItems[_radioValue]); + // } + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(top: 15), + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: _radioItems.length, + itemBuilder: (context, index) { + return Container( + width: MediaQuery.of(context).size.width, + // padding: const EdgeInsets.all(15), + margin: const EdgeInsets.only(top: 10, bottom: 10), + decoration: BoxDecoration( + color: _radioValue == index + ? const Color.fromRGBO(0, 116, 182, 0.05) + : Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + border: Border.all( + color: _radioValue == index + ? AppColors.primaryBlue + : AppColors.black16), + ), + child: RadioListTile( + groupValue: _radioValue, + title: Text( + _radioItems[index], + style: GoogleFonts.lato( + color: AppColors.black87, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + ), + value: index, + onChanged: (value) { + int selectedIndex = int.parse(value.toString()); + setState(() { + _radioValue = selectedIndex; + }); + // print(value); + widget.parentAction(_radioItems[selectedIndex]); + }, + )); + }, + ), + ); + } +} diff --git a/lib/widgets/questions/single_answer_question.dart b/lib/widgets/questions/single_answer_question.dart deleted file mode 100644 index fe4f67dbd75f812ae1df864e0e20f8c730ac7820..0000000000000000000000000000000000000000 --- a/lib/widgets/questions/single_answer_question.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:flutter/material.dart'; -import './../../../../models/_models/assessment_question_model.dart'; -import 'package:google_fonts/google_fonts.dart'; -import './../../../../constants/index.dart'; - -class SingleAnswerQuestion extends StatefulWidget { - final AssessmentQuestion question; - SingleAnswerQuestion(this.question); - @override - _SingleAnswerQuestionState createState() => _SingleAnswerQuestionState(); -} - -class _SingleAnswerQuestionState extends State<SingleAnswerQuestion> { - int _radioValue = 0; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(32), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.only(bottom: 15), - child: Text( - 'Q' + widget.question.id.toString() + ' of 3', - style: GoogleFonts.lato( - color: AppColors.greys60, - fontWeight: FontWeight.w400, - fontSize: 14.0, - ), - ), - ), - Container( - padding: const EdgeInsets.only(bottom: 15), - child: Text( - widget.question.question, - style: GoogleFonts.lato( - color: AppColors.greys87, - fontWeight: FontWeight.w600, - fontSize: 16.0, - ), - ), - ), - ListView.builder( - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: widget.question.options.length, - itemBuilder: (context, index) { - return Container( - width: MediaQuery.of(context).size.width, - // padding: const EdgeInsets.all(15), - margin: const EdgeInsets.only(top: 10, bottom: 10), - decoration: BoxDecoration( - color: _radioValue == index + 1 - ? Color.fromRGBO(0, 116, 182, 0.05) - : Colors.white, - borderRadius: - BorderRadius.all(const Radius.circular(4.0)), - border: Border.all( - color: _radioValue == index + 1 - ? AppColors.primaryThree - : AppColors.grey16), - ), - child: RadioListTile( - groupValue: _radioValue, - title: Text( - widget.question.options[index], - style: GoogleFonts.lato( - color: AppColors.greys87, - fontWeight: FontWeight.w400, - fontSize: 14, - ), - ), - value: index + 1, - onChanged: (value) { - setState(() { - _radioValue = value; - }); - }, - )); - }, - ), - ], - )); - } -} diff --git a/lib/widgets/questions/text_question.dart b/lib/widgets/questions/text_question.dart index 049cbc4693f91c903c5468d7434b52bc1b80ea99..6a7ccb7e52fe214427165f10d84dd819839dba80 100644 --- a/lib/widgets/questions/text_question.dart +++ b/lib/widgets/questions/text_question.dart @@ -36,51 +36,58 @@ class _TextQuestionState extends State<TextQuestion> { @override Widget build(BuildContext context) { return Container( - width: MediaQuery.of(context).size.width, - margin: const EdgeInsets.only(top: 15, bottom: 20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(4), - border: Border.all(color: AppColors.black16), - ), - child: Focus( - child: TextFormField( - onEditingComplete: () { - widget.parentAction(_textController.text); - SystemChannels.textInput.invokeMethod('TextInput.hide'); - return; - }, - controller: _textController, - style: GoogleFonts.lato(fontSize: 14.0), - textInputAction: TextInputAction.done, - textCapitalization: TextCapitalization.sentences, - keyboardType: widget.fieldType == FieldType.numeric - ? TextInputType.number - : widget.fieldType == FieldType.email - ? TextInputType.emailAddress - : TextInputType.name, - decoration: InputDecoration( - contentPadding: const EdgeInsets.fromLTRB(10.0, 0.0, 10.0, 0.0), - // border: OutlineInputBorder( - // borderSide: BorderSide(color: AppColors.grey16)), - // enabledBorder: const UnderlineInputBorder( - // borderSide: BorderSide(color: AppColors.black16), - // ), - // focusedBorder: const UnderlineInputBorder( - // borderSide: BorderSide(color: AppColors.primaryBlue), - // ), - border: InputBorder.none, - hintText: 'Type here', - hintStyle: GoogleFonts.lato( - color: AppColors.black40, - fontSize: 14.0, - fontWeight: FontWeight.w400), - // focusedBorder: OutlineInputBorder( - // borderSide: const BorderSide( - // color: AppColors.primaryThree, width: 1.0), - // ), - ), + width: MediaQuery.of(context).size.width, + margin: const EdgeInsets.only(top: 15, bottom: 20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + border: Border.all(color: AppColors.black16), + ), + child: Focus( + child: TextFormField( + onEditingComplete: () { + // widget.parentAction(_textController.text.toString()); + SystemChannels.textInput.invokeMethod('TextInput.hide'); + // return; + }, + controller: _textController, + style: GoogleFonts.lato(fontSize: 14.0), + textInputAction: TextInputAction.done, + textCapitalization: TextCapitalization.sentences, + keyboardType: widget.fieldType == FieldType.numeric || + widget.fieldType == FieldType.date + ? TextInputType.number + : widget.fieldType == FieldType.email + ? TextInputType.emailAddress + : TextInputType.name, + decoration: InputDecoration( + contentPadding: const EdgeInsets.fromLTRB(10.0, 0.0, 10.0, 0.0), + // border: OutlineInputBorder( + // borderSide: BorderSide(color: AppColors.grey16)), + // enabledBorder: const UnderlineInputBorder( + // borderSide: BorderSide(color: AppColors.black16), + // ), + // focusedBorder: const UnderlineInputBorder( + // borderSide: BorderSide(color: AppColors.primaryBlue), + // ), + border: InputBorder.none, + hintText: 'Type here', + hintStyle: GoogleFonts.lato( + color: AppColors.black40, + fontSize: 14.0, + fontWeight: FontWeight.w400), + // focusedBorder: OutlineInputBorder( + // borderSide: const BorderSide( + // color: AppColors.primaryThree, width: 1.0), + // ), ), - )); + ), + onFocusChange: (value) { + if (!value) { + widget.parentAction(_textController.text.toString()); + } + }, + ), + ); } } diff --git a/pubspec.lock b/pubspec.lock index c6ffcc79e6eacf286d7472cd141e2c8fa4da9720..bac53e3df62e897c59a04b72512450a7d889e859 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -78,6 +78,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" + dbus: + dependency: transitive + description: + name: dbus + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.1" fake_async: dependency: transitive description: @@ -99,6 +106,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.2" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + url: "https://pub.dartlang.org" + source: hosted + version: "1.12.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.4" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + url: "https://pub.dartlang.org" + source: hosted + version: "11.2.6" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.6" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.7" flutter: dependency: "direct main" description: flutter @@ -118,6 +167,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + url: "https://pub.dartlang.org" + source: hosted + version: "9.3.2" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.2" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -411,6 +481,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.8" + timezone: + dependency: transitive + description: + name: timezone + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.0" typed_data: dependency: transitive description: @@ -418,6 +495,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + unique_identifier: + dependency: "direct main" + description: + name: unique_identifier + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.2" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9f808313f5ec66a60bf2b1b9e8cf948c7e3df7a9..c9369854fe713007e093d18f8d000256a4d8dab1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,10 @@ dependencies: fluttertoast: ^8.0.8 jwt_decoder: ^2.0.1 intl: ^0.17.0 + firebase_messaging: ^11.0.0 + firebase_core: ^1.12.0 + flutter_local_notifications: ^9.3.2 + unique_identifier: ^0.2.2 dev_dependencies: flutter_test: