diff --git a/lib/api/login_api_client.dart b/lib/api/login_api_client.dart index 217a11f..f487b80 100644 --- a/lib/api/login_api_client.dart +++ b/lib/api/login_api_client.dart @@ -191,9 +191,14 @@ class LoginApiClient { }; return await ApiClient().postJsonForObject((json) { GenericResponseModel responseData = GenericResponseModel.fromJson(json); - if (responseData.logInTokenID != null) { + if (responseData.logInTokenID != null && responseData.logInTokenID!.isNotEmpty) { AppState().postParamsObject?.setLogInTokenID = responseData.logInTokenID; } + AppState().setUserName = nationalId; + AppState().postParamsObject?.setUserName = nationalId; + AppState().postParamsObject?.setPUserName = nationalId; + AppState().postParamsObject?.channel = 31; + AppState().postParamsObject?.versionID = 20; return responseData; }, url, postParams); } @@ -244,6 +249,25 @@ class LoginApiClient { }; return await ApiClient().postJsonForObject((json) { GenericResponseModel responseData = GenericResponseModel.fromJson(json); + String effectiveToken = (responseData.tokenID != null && responseData.tokenID!.isNotEmpty) + ? responseData.tokenID! + : ((responseData.logInTokenID != null && responseData.logInTokenID!.isNotEmpty) + ? responseData.logInTokenID! + : logInTokenID); + + AppState().postParamsObject?.setTokenID = effectiveToken; + AppState().postParamsObject?.setLogInTokenID = (responseData.logInTokenID != null && responseData.logInTokenID!.isNotEmpty) + ? responseData.logInTokenID! + : logInTokenID; + AppState().postParamsObject?.setUserName = nationalId; + AppState().postParamsObject?.setPUserName = nationalId; + AppState().postParamsObject?.setMobileNumer = mobileNumber; + AppState().postParamsObject?.channel = 31; + AppState().postParamsObject?.versionID = 20; + AppState().setUserName = nationalId; + AppState().isAuthenticated = true; + AppState().setLogged = true; + return responseData; }, url, postParams); } diff --git a/lib/api/offers_and_discounts_api_client.dart b/lib/api/offers_and_discounts_api_client.dart index 8c92539..4f0d1b4 100644 --- a/lib/api/offers_and_discounts_api_client.dart +++ b/lib/api/offers_and_discounts_api_client.dart @@ -1,10 +1,12 @@ import 'dart:convert'; +import 'package:flutter/material.dart'; import 'package:mohem_flutter_app/api/api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/consts.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/get_categories_list.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart'; class OffersAndDiscountsApiClient { static final OffersAndDiscountsApiClient _instance = OffersAndDiscountsApiClient._internal(); @@ -68,4 +70,37 @@ class OffersAndDiscountsApiClient { postParams, ); } + + Future> getWalaPlusCategories({BuildContext? context}) async { + String url = ApiConsts.walaPlusCategories; + + String? pUserName = AppState().getUserName; + + String? tokenID = AppState().postParamsObject?.tokenID; + + String? logInTokenID = AppState().postParamsObject?.logInTokenID; + + Map postParams = { + "VersionID": 20, + "Channel": 31, + "LanguageID": context != null ? AppState().getLanguageID(context) : 2, + "LogInTokenID": logInTokenID, + "TokenID": tokenID, + "P_USER_NAME": pUserName, + }; + + return await ApiClient().postJsonForObject( + (response) { + List categories = []; + if (response != null && response['WalaPlus_Categories'] != null) { + response['WalaPlus_Categories'].forEach((v) { + categories.add(WalaPlusCategoryModel.fromJson(v)); + }); + } + return categories; + }, + url, + postParams, + ); + } } diff --git a/lib/app_state/app_state.dart b/lib/app_state/app_state.dart index 818d91f..e518ea5 100644 --- a/lib/app_state/app_state.dart +++ b/lib/app_state/app_state.dart @@ -110,9 +110,23 @@ class AppState { this._postParams = _postParams; } - bool isArabic(context) => EasyLocalization.of(context)?.locale.languageCode == "ar"; + bool isArabic(context) { + try { + if (context != null) { + return EasyLocalization.of(context)?.locale.languageCode == "ar"; + } + } catch (_) {} + return false; + } - int getLanguageID(context) => EasyLocalization.of(context)?.locale.languageCode == "ar" ? 1 : 2; + int getLanguageID(context) { + try { + if (context != null) { + return EasyLocalization.of(context)?.locale.languageCode == "ar" ? 1 : 2; + } + } catch (_) {} + return 2; + } String? _username; diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index b5f7197..65ff8a3 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -39,6 +39,9 @@ class ApiConsts { static String mohemmSendOTPDependent = erpRest + "Mohemm_SendOTP_Dependent"; static String mohemmCheckOTPDependent = erpRest + "Mohemm_CheckOTP_Dependent"; + // WalaPlus Endpoints + static String walaPlusCategories = erpRest + "WalaPlus_Categories"; + static String ssoAuthRedirection = ssoBaseUrl + "/api/auth/connect"; diff --git a/lib/dialogs/otp_dialog.dart b/lib/dialogs/otp_dialog.dart index da2611e..e819b6f 100644 --- a/lib/dialogs/otp_dialog.dart +++ b/lib/dialogs/otp_dialog.dart @@ -60,7 +60,7 @@ class OtpDialog { // static String signature; void displayDialog(BuildContext context) async { - return showDialog( + showDialog( context: context, barrierColor: Colors.black.withOpacity(0.63), builder: (context) { @@ -187,6 +187,8 @@ class OtpDialog { ); }), ); + }).then((_) { + stopTimer = true; }); } @@ -240,30 +242,38 @@ class OtpDialog { } void startTimer(setState) { + if (stopTimer) return; remainingTime--; if (stopTimer) return; - setState(() { - displayTime = getSecondsAsDigitalClock(remainingTime); - }); + try { + setState(() { + displayTime = getSecondsAsDigitalClock(remainingTime); + }); + } catch (_) {} timer = Future.delayed(const Duration(seconds: 1), () { + if (stopTimer) return; if (remainingTime > 0) { startTimer(setState); } else { - setState(() { - stopTimer = true; - }); + if (stopTimer) return; + try { + setState(() { + stopTimer = true; + }); + } catch (_) {} } }); } void hideSMSBox(context) { + stopTimer = true; onFailure(); } void _onOtpCallBack(String otpCode, bool? isAutofill) { if (otpCode.length == 4) { - // stopTimer = true; + stopTimer = true; otpFieldClear.value = otpCode; onSuccess(otpCode, _pinPutController); } diff --git a/lib/models/generic_response_model.dart b/lib/models/generic_response_model.dart index c4bd672..b6f4ef8 100644 --- a/lib/models/generic_response_model.dart +++ b/lib/models/generic_response_model.dart @@ -107,6 +107,7 @@ import 'package:mohem_flutter_app/models/vacation_rule/respond_attributes_list_m import 'package:mohem_flutter_app/models/vacation_rule/vr_item_types_list_model.dart'; import 'package:mohem_flutter_app/models/vacation_rule/wf_look_up_list_model.dart'; import 'package:mohem_flutter_app/models/validate_eit_transaction_list_model.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart'; import 'package:mohem_flutter_app/models/worklist/GetRFCEmployeeList.dart'; import 'package:mohem_flutter_app/models/worklist/get_fad_notifications.dart'; import 'package:mohem_flutter_app/models/worklist/get_favorite_replacements_model.dart'; @@ -402,6 +403,7 @@ class GenericResponseModel { GetWalaplusDepInfoModel? getWalaplusDepInfoModel; dynamic getWalaplusDepInfoModelInternal; + List? walaPlusCategories; GenericResponseModel({ this.date, @@ -677,6 +679,7 @@ class GenericResponseModel { this.Mohemm_ENSAT_GetItgToken_Response, this.getWalaplusDepInfoModel, this.getWalaplusDepInfoModelInternal, + this.walaPlusCategories, }); GenericResponseModel.fromJson(Map json) { @@ -1474,6 +1477,13 @@ class GenericResponseModel { getWalaplusDepInfoModel = json['GET_WALAPLUS_DEP_INFO_Model'] != null ? GetWalaplusDepInfoModel.fromJson(json['GET_WALAPLUS_DEP_INFO_Model']) : null; getWalaplusDepInfoModelInternal = json['GET_WALAPLUS_DEP_INFO_Model_Internal']; + + if (json['WalaPlus_Categories'] != null) { + walaPlusCategories = []; + json['WalaPlus_Categories'].forEach((v) { + walaPlusCategories!.add(WalaPlusCategoryModel.fromJson(v)); + }); + } } Map toJson() { @@ -1966,6 +1976,10 @@ class GenericResponseModel { data['GET_WALAPLUS_DEP_INFO_Model'] = this.getWalaplusDepInfoModel!.toJson(); } + if (walaPlusCategories != null) { + data['WalaPlus_Categories'] = walaPlusCategories!.map((WalaPlusCategoryModel v) => v.toJson()).toList(); + } + return data; } } diff --git a/lib/models/offers_and_discounts/wala_plus_category_model.dart b/lib/models/offers_and_discounts/wala_plus_category_model.dart new file mode 100644 index 0000000..51e835f --- /dev/null +++ b/lib/models/offers_and_discounts/wala_plus_category_model.dart @@ -0,0 +1,36 @@ +class WalaPlusCategoryModel { + String? id; + String? image; + String? imageThumb; + String? name; + String? parentId; + + WalaPlusCategoryModel({ + this.id, + this.image, + this.imageThumb, + this.name, + this.parentId, + }); + + WalaPlusCategoryModel.fromJson(Map json) { + id = json['id']?.toString(); + image = json['image']?.toString(); + imageThumb = json['image_thumb']?.toString(); + name = json['name']?.toString(); + parentId = json['parent_id']?.toString(); + } + + Map toJson() { + Map data = {}; + data['id'] = id; + data['image'] = image; + data['image_thumb'] = imageThumb; + data['name'] = name; + data['parent_id'] = parentId; + return data; + } + + bool get isMainCategory => parentId == null || parentId!.isEmpty; + bool get isSvg => (image ?? '').toLowerCase().contains('.svg'); +} diff --git a/lib/ui/landing/dashboard_screen_for_dependet.dart b/lib/ui/landing/dashboard_screen_for_dependet.dart index bb060ae..4f4c455 100644 --- a/lib/ui/landing/dashboard_screen_for_dependet.dart +++ b/lib/ui/landing/dashboard_screen_for_dependet.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; import 'package:mohem_flutter_app/classes/consts.dart'; @@ -11,6 +12,7 @@ import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart'; import 'package:mohem_flutter_app/widgets/button/default_button.dart'; import 'package:mohem_flutter_app/widgets/dynamic_forms/dynamic_textfield_widget.dart'; @@ -24,13 +26,51 @@ class DashboardScreenForDependet extends StatefulWidget { class _DashboardScreenForDependetState extends State { List offersList = []; List _filteredOffersList = []; + List categoriesList = []; + String currentCategoryId = "0"; bool isLoading = false; + bool isCategoriesLoading = false; String searchQuery = ""; @override void initState() { super.initState(); - fetchOffers(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + fetchCategories(); + fetchOffers(); + } + }); + } + + Future fetchCategories() async { + setState(() { + isCategoriesLoading = true; + }); + + try { + List list = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context); + if (list.isNotEmpty) { + bool isAr = mounted ? AppState().isArabic(context) : false; + categoriesList = [ + WalaPlusCategoryModel( + id: "0", + name: isAr ? "الجميع" : "All", + image: null, + parentId: null, + ), + ...list, + ]; + } + } catch (e) { + print("Error fetching WalaPlus categories: $e"); + } finally { + if (mounted) { + setState(() { + isCategoriesLoading = false; + }); + } + } } /// Offers API placeholder - will be connected when the endpoint is provided @@ -44,7 +84,7 @@ class _DashboardScreenForDependetState extends State // Example: // offersList = await OffersApiClient().getDependentOffers(); await Future.delayed(const Duration(milliseconds: 300)); - _filteredOffersList = offersList; + _runFilter(searchQuery); } catch (e) { print("Error fetching dependent offers: $e"); } finally { @@ -56,18 +96,28 @@ class _DashboardScreenForDependetState extends State } } + Future _onRefresh() async { + await Future.wait([ + fetchCategories(), + fetchOffers(), + ]); + } + void _runFilter(String query) { searchQuery = query; - if (query.isEmpty) { - _filteredOffersList = offersList; - } else { + List temp = offersList; + if (currentCategoryId != "0") { + temp = temp.where((offer) => offer.categoryId?.toString() == currentCategoryId).toList(); + } + if (query.isNotEmpty) { bool isAr = AppState().isArabic(context); - _filteredOffersList = - offersList.where((offer) { + temp = + temp.where((offer) { String title = isAr ? (offer.titleAr ?? "") : (offer.titleEn ?? ""); return title.toLowerCase().contains(query.toLowerCase()); }).toList(); } + _filteredOffersList = temp; setState(() {}); } @@ -78,6 +128,7 @@ class _DashboardScreenForDependetState extends State onTap: () { ApiConsts.selectedMemberType = MemberType.employee; ApiConsts.ISDependetLogin = false; + AppState().setPostParamsInitConfig(); Navigator.pushNamedAndRemoveUntil(context, AppRoutes.login, (Route route) => false); }, ); @@ -115,7 +166,7 @@ class _DashboardScreenForDependetState extends State ], ), body: RefreshIndicator( - onRefresh: fetchOffers, + onRefresh: _onRefresh, color: MyColors.textMixColor, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()), @@ -139,6 +190,63 @@ class _DashboardScreenForDependetState extends State ), 20.height, + // Categories Section + if (isCategoriesLoading) + _buildCategoriesShimmer() + else if (categoriesList.isNotEmpty) ...[ + LocaleKeys.browseCategories.tr().toText17(isBold: true, color: MyColors.darkTextColor), + 12.height, + SizedBox( + height: 110.0, + child: ListView.separated( + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, + itemCount: categoriesList.length, + separatorBuilder: (cxt, index) => 12.width, + itemBuilder: (BuildContext cxt, int index) { + WalaPlusCategoryModel item = categoriesList[index]; + bool isSelected = currentCategoryId == item.id; + String title = item.id == "0" ? (isAr ? "الجميع" : "All") : (item.name ?? ""); + + return AspectRatio( + aspectRatio: 1 / 1, + child: InkWell( + borderRadius: BorderRadius.circular(15), + onTap: () { + setState(() { + currentCategoryId = item.id ?? "0"; + }); + _runFilter(searchQuery); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildCategoryIcon(item), + if (isSelected) + const Icon(Icons.check_circle_rounded, color: MyColors.greenColor, size: 16.0), + ], + ).expanded, + title.toText10( + isBold: isSelected, + color: isSelected ? MyColors.textMixColor : MyColors.darkTextColor, + maxlines: 2, + ), + ], + ).paddingOnly(left: 10, right: 10, bottom: 10, top: 12).expanded.objectContainerView(disablePadding: true), + ), + ); + }, + ), + ), + 20.height, + ], + // Header Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -229,4 +337,53 @@ class _DashboardScreenForDependetState extends State ), ); } + + Widget _buildCategoryIcon(WalaPlusCategoryModel item) { + if (item.id == "0" || item.image == null || item.image!.isEmpty) { + return const Icon(Icons.grid_view_rounded, size: 24, color: MyColors.textMixColor); + } + if (item.isSvg) { + return SvgPicture.network( + item.image!, + width: 25, + height: 25, + fit: BoxFit.contain, + placeholderBuilder: (_) => const SizedBox(width: 25, height: 25), + ); + } + return Image.network( + item.imageThumb ?? item.image!, + width: 25, + height: 25, + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => const Icon(Icons.category_outlined, size: 24, color: MyColors.grey70Color), + ); + } + + Widget _buildCategoriesShimmer() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(width: 120, height: 16, color: Colors.white).toShimmer(), + 12.height, + SizedBox( + height: 110.0, + child: ListView.separated( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + itemCount: 4, + separatorBuilder: (_, __) => 12.width, + itemBuilder: (_, __) => AspectRatio( + aspectRatio: 1 / 1, + child: Container( + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15)), + ).toShimmer(), + ), + ), + ), + 20.height, + ], + ); + } } diff --git a/lib/ui/login/login_screen.dart b/lib/ui/login/login_screen.dart index 8594174..cf6e804 100644 --- a/lib/ui/login/login_screen.dart +++ b/lib/ui/login/login_screen.dart @@ -522,6 +522,25 @@ class _LoginScreenState extends State { } Navigator.pop(context); + String effectiveToken = (checkOtpResponse?.tokenID != null && checkOtpResponse!.tokenID!.isNotEmpty) + ? checkOtpResponse.tokenID! + : ((checkOtpResponse?.logInTokenID != null && checkOtpResponse!.logInTokenID!.isNotEmpty) + ? checkOtpResponse.logInTokenID! + : logInTokenID); + + AppState().postParamsObject?.setTokenID = effectiveToken; + AppState().postParamsObject?.setLogInTokenID = (checkOtpResponse?.logInTokenID != null && checkOtpResponse!.logInTokenID!.isNotEmpty) + ? checkOtpResponse.logInTokenID! + : logInTokenID; + AppState().postParamsObject?.setUserName = nationalId.text.trim(); + AppState().postParamsObject?.setPUserName = nationalId.text.trim(); + AppState().postParamsObject?.setMobileNumer = mobileNumber; + AppState().postParamsObject?.channel = 31; + AppState().postParamsObject?.versionID = 20; + AppState().setUserName = nationalId.text.trim(); + AppState().isAuthenticated = true; + AppState().setLogged = true; + Utils.showToast(AppState().isArabic(context) ? "تم التحقق بنجاح" : "Verified successfully"); if (ApiConsts.ISDependetLogin) { Navigator.pushNamedAndRemoveUntil(context, AppRoutes.dashboardScreenForDependet, (route) => false); diff --git a/lib/ui/parking_qr/parking_qr.dart b/lib/ui/parking_qr/parking_qr.dart index d9e86f5..08d7b10 100644 --- a/lib/ui/parking_qr/parking_qr.dart +++ b/lib/ui/parking_qr/parking_qr.dart @@ -49,11 +49,11 @@ class _RequestDetailsState extends State { children: [ // QR Code Image if (qrData!.getEmployeeParking!.qrCodeImageBase64 != null && qrData!.getEmployeeParking!.qrCodeImageBase64!.isNotEmpty) - Image.memory(Utils.dataFromBase64String(qrData!.getEmployeeParking!.qrCodeImageBase64!), width: 300, height: 300, fit: BoxFit.contain) + Image.memory(Utils.dataFromBase64String(qrData!.getEmployeeParking!.qrCodeImageBase64!), width: 150, height: 150, fit: BoxFit.contain) else Container( - width: 300, - height: 300, + width: 200, + height: 200, color: MyColors.lightGreyEFColor, child: Center(child: Text(LocaleKeys.noDataAvailable.tr(), style: const TextStyle(color: MyColors.grey67Color))), ),