From b6ac85e22092659c46fdd2b8235eed97d3aac805 Mon Sep 17 00:00:00 2001 From: "mirza.shafique" Date: Tue, 15 Nov 2022 11:28:08 +0300 Subject: [PATCH] structure 1.0 --- assets/langs/ar-SA.json | 502 +++++++++++++++++ assets/langs/en-US.json | 502 +++++++++++++++++ lib/api/api_client.dart | 321 +++++++++++ lib/api/login_api_client.dart | 148 +++++ lib/app_state/app_state.dart | 132 +++++ lib/classes/app_permissions.dart | 48 ++ lib/classes/colors.dart | 64 +++ lib/classes/consts.dart | 36 ++ lib/classes/date_uitl.dart | 460 ++++++++++++++++ lib/classes/decorations_helper.dart | 25 + lib/classes/file_process.dart | 40 ++ lib/classes/lottie_consts.dart | 7 + lib/classes/utils.dart | 346 ++++++++++++ lib/config/app_provider.dart | 19 + lib/config/routes.dart | 300 ++++++++++ lib/dialogs/otp_dialog.dart | 274 ++++++++++ lib/exceptions/api_exception.dart | 29 + lib/extensions/int_extensions.dart | 15 + lib/extensions/string_extensions.dart | 234 ++++++++ lib/extensions/widget_extensions.dart | 107 ++++ lib/provider/dashboard_provider_model.dart | 274 ++++++++++ lib/theme/app_theme.dart | 60 ++ lib/theme/colors.dart | 13 + lib/ui/a.dart | 0 lib/ui/login/splash_page.dart | 0 lib/widgets/Updater.dart | 39 ++ lib/widgets/app_bar_widget.dart | 70 +++ lib/widgets/app_logo.dart | 26 + lib/widgets/bottom_sheet.dart | 86 +++ lib/widgets/button/default_button.dart | 139 +++++ lib/widgets/button/simple_button.dart | 42 ++ lib/widgets/circular_avatar.dart | 41 ++ lib/widgets/circular_step_progress_bar.dart | 515 ++++++++++++++++++ lib/widgets/dialogs/confirm_dialog.dart | 63 +++ lib/widgets/dialogs/dialogs.dart | 14 + lib/widgets/input_widget.dart | 142 +++++ lib/widgets/item_detail_view_widget.dart | 150 +++++ lib/widgets/loading_dialog.dart | 47 ++ lib/widgets/otp_widget.dart | 373 +++++++++++++ lib/widgets/radio/show_radio.dart | 45 ++ .../shimmer/dashboard_shimmer_widget.dart | 256 +++++++++ lib/widgets/swipe/nfc_reader_sheet.dart | 194 +++++++ 42 files changed, 6198 insertions(+) create mode 100644 assets/langs/ar-SA.json create mode 100644 assets/langs/en-US.json create mode 100644 lib/api/api_client.dart create mode 100644 lib/api/login_api_client.dart create mode 100644 lib/app_state/app_state.dart create mode 100644 lib/classes/app_permissions.dart create mode 100644 lib/classes/colors.dart create mode 100644 lib/classes/consts.dart create mode 100644 lib/classes/date_uitl.dart create mode 100644 lib/classes/decorations_helper.dart create mode 100644 lib/classes/file_process.dart create mode 100644 lib/classes/lottie_consts.dart create mode 100644 lib/classes/utils.dart create mode 100644 lib/config/app_provider.dart create mode 100644 lib/config/routes.dart create mode 100644 lib/dialogs/otp_dialog.dart create mode 100644 lib/exceptions/api_exception.dart create mode 100644 lib/extensions/int_extensions.dart create mode 100644 lib/extensions/string_extensions.dart create mode 100644 lib/extensions/widget_extensions.dart create mode 100644 lib/provider/dashboard_provider_model.dart create mode 100644 lib/theme/app_theme.dart create mode 100644 lib/theme/colors.dart create mode 100644 lib/ui/a.dart create mode 100644 lib/ui/login/splash_page.dart create mode 100644 lib/widgets/Updater.dart create mode 100644 lib/widgets/app_bar_widget.dart create mode 100644 lib/widgets/app_logo.dart create mode 100644 lib/widgets/bottom_sheet.dart create mode 100644 lib/widgets/button/default_button.dart create mode 100644 lib/widgets/button/simple_button.dart create mode 100644 lib/widgets/circular_avatar.dart create mode 100644 lib/widgets/circular_step_progress_bar.dart create mode 100644 lib/widgets/dialogs/confirm_dialog.dart create mode 100644 lib/widgets/dialogs/dialogs.dart create mode 100644 lib/widgets/input_widget.dart create mode 100644 lib/widgets/item_detail_view_widget.dart create mode 100644 lib/widgets/loading_dialog.dart create mode 100644 lib/widgets/otp_widget.dart create mode 100644 lib/widgets/radio/show_radio.dart create mode 100644 lib/widgets/shimmer/dashboard_shimmer_widget.dart create mode 100644 lib/widgets/swipe/nfc_reader_sheet.dart diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json new file mode 100644 index 0000000..f72ea36 --- /dev/null +++ b/assets/langs/ar-SA.json @@ -0,0 +1,502 @@ +{ + "mohemm": "Mohemm", + "english": "English", + "arabic": "عربي", + "login": "تسجيل الدخول", + "pleaseEnterLoginDetails": "الرجاء إدخال التفاصيل أدناه لتسجيل الدخول", + "username": "اسم المستخدم", + "password": "كلمة المرور", + "welcomeBack": "مرحبا بعودتك", + "wouldYouLikeToLoginWithCurrentUsername": "هل ترغب في تسجيل الدخول باسم المستخدم الحالي؟", + "lastLoginDetails": "تفاصيل تسجيل الدخول الأخير:", + "verificationType": "نوع التحقق:", + "pleaseVerify": "ارجوك تحقق", + "pleaseVerifyForBio": "الرجاء التحقق من تسجيل الدخول باستخدام أحد هذه الخيارات", + "verifyThroughFace": "تحقق من خلال الوجه", + "verifyThroughFingerprint": "تحقق من خلال بصمة الإصبع", + "verifyThroughSMS": "تحقق من خلال الرسائل القصيرة", + "verifyThroughWhatsapp": "تحقق من خلال Whatsapp", + "useAnotherAccount": "استخدم حسابا آخر", + "pleaseEnterTheVerificationCodeSentTo": "الرجاء إدخال رمز التحقق المرسل إلى ", + "theVerificationCodeWillExpireIn": "ستنتهي صلاحية رمز التحقق في ", + "goodMorning": "صباح الخير", + "markAttendance": "علامة الحضور", + "timeLeftToday": "الوقت المتبقي اليوم", + "checkIn": "تحقق في", + "workList": "قائمة العمل", + "leaveBalance": "رصيد الاجازات", + "missingSwipes": "تسجيل بصمة حضور", + "ticketBalance": "رصيد التذكرة", + "other": "آخر", + "services": "خدمات", + "viewAllServices": "عرض جميع الخدمات", + "monthlyAttendance": "الحضور الشهري", + "vacationRule": "قاعدة الاجازات", + "vacationType": "نوع الاجازة", + "startDateT": "تاريخ البدء", + "endDateT": "تاريخ الانتهاء", + "workFromHome": "العمل من المنزل", + "ticketRequest": "طلب تذكرة", + "viewAllOffers": "مشاهدة جميع العروض", + "offers": "عروض & ", + "discounts": "الخصومات", + "newString": "جديد", + "setTheNewPassword": "قم بتعيين كلمة المرور الجديدة", + "typeYourNewPasswordBelow": "اكتب كلمة المرور الجديدة أدناه", + "confirmPassword": "تأكيد كلمة المرور", + "update": "تحديث", + "title": "عنوان", + "home": "الرئيسية", + "mySalary": "راتبي", + "createRequest": "إنشاء طلب", + "forgotPassword": "هل نسيت كلمة السر", + "employeeId": "هوية الموظف", + "loginCodeWillSentToMobileNumber": "الرجاء إدخال معرف الموظف الخاص بك ، وسيتم إرسال رمز تسجيل الدخول إلى رقم هاتفك المحمول", + "changePassword": "تغيير كلمة المرور", + "ok": "موافق", + "confirm": "تؤكد", + "passwordChangedSuccessfully": "تم تغيير الرقم السري بنجاح", + "itemsForSale": "سلع للبيع", + "attendanceDetails": "تفاصيل الحضور", + "order": "الطلبات", + "earlyOut": "الخروج مبكرا", + "shortage": "ساعات التقصير", + "excess": "فائض", + "lateIn": "القدوم المتاخر", + "approvedCheckOut": "اعتماد وقت الخروج", + "approvedCheckIn": "اعتماد وقت الدخول", + "actualCheckOut": "وقت الخروج", + "actualCheckIn": "وقت الدخول", + "present": "حضور", + "pres": "حضور", + "shiftTime": "وقت التناوب", + "absent": "غياب", + "attendance": "الحضور", + "scheduleDays": "ايام العمل", + "offDays": "ايام الراحه", + "nonAnalyzed": "لايوجد تحليل", + "shortageHour": "ساعات التقصير", + "stats": "الحاله", + "completed": "تم اكمال", + "msg": "Hello {} in the {} world ", + "msg_named": "{} are written in the {lang} language", + "clickMe": "Click me", + "doNotUseRecentPassword": "لا تستخدم كلمة مرور حديثة", + "atLeastOneLowercase": "حرف صغير واحد على الأقل", + "atLeastOneUppercase": "حرف كبير واحد على الأقل", + "atLeastOneNumeric": "رقم واحد على الأقل", + "minimum8Characters": "8 أحرف على الأقل", + "doNotAddRepeatingLetters": "لا تقم بإضافة أحرف متكررة", + "itShouldContainSpecialCharacter": "يجب أن يحتوي على طابع خاص", + "confirmPasswordMustMatch": "يجب أن يتطابق تأكيد كلمة المرور", + "sms": "رسالة قصيرة", + "fingerPrint": "بصمة", + "face": "التعرف على الوجه", + "whatsapp": "واتس اب", + "reject": "يرفض", + "approve": "يوافق", + "cancel": "إلغاء", + "requestedItems": "العناصر المطلوبة", + "request": "طلب", + "myRequest": "طلبي", + "actions": "أجراءات", + "delegate": "مندوب", + "request_info": "اطلب معلومات", + "attachments": "المرفقات", + "info": "معلومات", + "employeeNumber": "رقم الموظف", + "assignmentNumber": "رقم الواجب", + "employeeName": "اسم الموظف", + "scheduleDate": "تاريخ الجدول الزمني", + "shiftType": "نوع التحول", + "shift": "يحول", + "breakText": "استراحة", + "actualSwipeStart": "بدء التمرير الفعلي", + "actualSwipeEnd": "التمرير الفعلي للنهاية", + "approvedSwipeStart": "وافق انتقاد البدء", + "approvedSwipeStartReason": "تمت الموافقة على سبب بدء التمرير السريع", + "approvedSwipeEnd": "تمت الموافقة على تمرير النهاية", + "approvedSwipeEndReason": "الموافقة على سبب إنهاء التمرير", + "from": "من", + "to": "ل", + "sent": "أرسلت", + "closed": "مغلق", + "id": "هوية شخصية", + "responder": "المستجيب", + "jobTitle": "عنوان وظيفي", + "grade": "درجة", + "jobCategory": "تصنيف الوظيفة", + "category": "فئة", + "employeeEmailAddress": "عنوان البريد الإلكتروني للموظف", + "payrollBranch": "فرع الرواتب", + "yourChangeHasBeenSavedSuccessfully": "تم حفظ التغيير الخاص بك بنجاح", + "code": "شفرة", + "unit": "وحدة", + "quantity": "كمية", + "dateRequired": "التاريخ مطلوب", + "lineStatus": "حالة الخط", + "statusDate": "تاريخ الحالة", + "transactionType": "نوع المعاملة", + "operatingUnit": "وحدة التشغيل", + "organizationCode": "كود المنظمة", + "organization": "منظمة", + "fromSubInventory": "من الجرد الفرعي", + "fromLocator": "من محدد المواقع", + "toSubInventory": "إلى الجرد الفرعي", + "toLocator": "إلى محدد المواقع", + "shipToLocator": "شحن إلى محدد المواقع", + "itemHistory": "تاريخ العنصر", + "mfg": "مبدع", + "lineType": "نوع الخط", + "price": "السعر", + "lineAmount": "مبلغ الخط", + "lineDiscount": "خصم الخط٪", + "needByDate": "القادمة إلى الأمام", + "promisedDate": "التسجيل وعد", + "deliverToLocation": "تسليم إلى الموقع", + "requisitionNumber": "رقم الطلب", + "requester": "مقدم الطلب", + "quotationAnalysis": "تحليل الاقتباس", + "subject": "موضوعات", + "description": "وصف", + "supplier": "المورد", + "site": "موقع", + "buyer": "مشتر", + "preparer": "معد", + "creationDate": "تاريخ الإنشاء", + "shipToLocation": "الشحن الى الموقع", + "quotationNumber": "رقم الإقتباس", + "quotationDate": "تاريخ الاقتباس", + "paymentTerms": "شروط الدفع", + "currency": "عملة", + "grossAmount": "المبلغ الإجمالي", + "discountAmount": "مقدار الخصم", + "customDuty": "الرسوم الجمركية", + "shipHandle": "مقبض السفينة", + "otherCharges": "رسوم أخرى", + "totalPOAmountWithVAT": "إجمالي مبلغ الشراء مع ضريبة القيمة المضافة", + "totalPOAmountInWords": "إجمالي مبلغ أمر الشراء بالكلمات", + "requestNumber": "رقم الطلب", + "uom": "UOM", + "operatingCode": "كود التشغيل", + "poNumber": "PO عدد", + "revision": "مراجعة", + "quantityOrdered": "الكمية المطلوبة", + "quantityReceived": "الكمية المستلمة", + "bonusQuantity": "كمية المكافأة", + "purchasePrice": "سعر الشراء", + "discountPer": "خصم ٪", + "balanceQuantity": "كمية التوازن", + "netPrice": "السعر الصافي", + "closureStatus": "حالة الإغلاق", + "quotationNetPrice": "صافي سعر الاقتباس", + "quotationUOM": "اقتباس UOM", + "quotationQty": "اقتباس الكمية", + "itemCode": "رمز الصنف", + "vendorName": "اسم البائع", + "quotationMFGPartNumber": "رقم الجزء MFG الاقتباس", + "quotationDeliveryDate": "تاريخ تسليم عرض الأسعار", + "quotationBonusQuantity": "كمية مكافأة الاقتباس", + "quotationLineTotal": "مجموع خط الاقتباس", + "rfqUOM": "RFQ UOM", + "rfqQty": "RFQ الكمية", + "rfqNumber": "رقم RFQ", + "human": "بشري", + "resources": "موارد", + "details": "تفاصيل", + "noDataAvailable": "لا تتوافر بيانات", + "productName": "اسم المنتج", + "productDescription": "وصف المنتج", + "unitPrice": "سعر الوحده", + "manufacturerName": "اسم المصنع", + "manufacturerPartName": "اسم جزء الشركة المصنعة", + "supplierName": "اسم المورد", + "supplierContact": "الاتصال بالمورد", + "chargeToPatient": "المسؤول عن المريض", + "justification": "التبرير", + "itemDescription": "وصف السلعة", + "groupCode": "كود المجموعة", + "primaryUOM": "UOM الابتدائية", + "subgroupDescription": "وصف المجموعة الفرعية", + "subgroupCode": "رمز المجموعة الفرعية", + "groupDescription": "وصف المجموعة", + "templateName": "اسم القالب", + "itemCreationStatus": "حالة إنشاء العنصر", + "standardizationApprovalStatus": "حالة الموافقة على التقييس", + "standardizationApprovalRejectionReason": "سبب رفض الموافقة على التقييس", + "analyzedBy": "تحليل بواسطة", + "approvedDate": "تاريخ الموافقة", + "itemType": "نوع العنصر", + "relatedTo": "متعلق ب", + "requestDate": "تاريخ الطلب", + "analyzedDate": "تاريخ التحليل", + "urgent": "العاجلة", + "requestDetails": "طلب تفاصيل", + "approvalLevel": "مستوى الموافقة", + "requesterDetails": "تفاصيل مقدم الطلب", + "myAttendance": "حضوري", + "workOnBreak": "التعويض عن العمل اثناءالاستراحه", + "next": "التالي", + "apply": "يتقدم", + "mobile": "التليفون المحمول", + "completingYear": "نحن نقدر لك لاستكمال خدمة", + "year": "سنة", + "month": "شهر", + "day": "يوم", + "address": "العنوان", + "phoneNumber": "رقم الجوال", + "businessGroup": "مجموعة العمل", + "Payroll": "الراتب", + "civilIdentityNumber": "رقم الهويه", + "dateOfBirth": "تاريخ الميلاد", + "maritalStatus ": "الحالة الاجتماعية", + "fullName": "الأسم الكامل", + "remove": "حذف", + "submit": "ارسال", + "areYouSureYouWantToSubmit": "هل أنت متأكد أنك تريد أن تقدم؟", + "comments": "تعليقات", + "writeComment": "أكتب تعليقا", + "approversList": "قائمة الموافقين", + "yourRequestHasBeenSubmittedForApprovals": "تم تقديم طلبك للموافقات", + "monthlyPaySlip": "قسيمة الراتب الشهرية", + "particular": "خاص", + "earnings": "أرباح", + "deductions": "الخصومات", + "paymentMethodName": "اسم طريقة الدفع", + "bankName": "اسم البنك", + "branchCode": "رمز الفرع", + "accountNo": "رقم الحساب", + "summaryOfInformation": "ملخص المعلومات", + "totalPayAmount": "المبلغ الإجمالي للدفع", + "paymentInformation": "معلومات الدفع", + "performance": "تقييم الاداء", + "performanceEvaluation": "تقييم الأداء في", + "performanceEvaluationIn": "تقييم أدائك في", + "valuationIn": "تقييم الأداء في", + "amount": "مقدار", + "correctCurrentDatails": "تعديل او اكمال التفاصيل الحالية", + "selectType": " حدد نوع التغيير الذي تريد القيام به", + "enterNewInfo": " أدخل معلومات جديدة بسبب تغيير حقيقي في التفاصيل الحالية (على سبيل المثال بسبب تغيير في الحالة الاجتماعية", + "endDate": "تاريخ الانتهاء", + "removeThisMember": "هل انت متأكد تريد ازالة هذا العضو؟", + "wantUpdateThisMember ": "هل انت متأكد تريد تحديث بيانات هذا العضو؟", + "addNewFamilyMember": "اضافة عضو جديد", + "addRow": "اضافة صف جديد", + "pleaseSelect": "الرجاء اختيار", + "delete": "حذف", + "edit": "تعديل", + "add": "اضافه", + "myProfile": "معلوماتي", + "mowadhafhi": "موظفي", + "searchAnnouncements": "بحث الاعلانات", + "announcements": "اعلانات", + "swipeRequest": "طلب تسجيل حضور", + "serviceType": "نوع الخدمه", + "departmentName": "اسم القسم", + "selectDepartment": "اختر القسم", + "relatedSection": "قسم ذو صله", + "selectSection": "اختيار القسم", + "relatedTopic": "عنوان ذو صله", + "selectTopic": "اختر العنوان", + "supportingDocument": "ارفاق مستند", + "mowadhafhiRequest": "طلب موظفي", + "ticketReference": "مرجع التذكره", + "section": "القسم", + "topic": "العنوان", + "actionBy": "الرد بواسطة", + "pending": "معلق", + "pendingTransactions": "المعاملات المعلقه", + "selectRequestType": "الرجاء اختيار نوع الطلب", + "dateFrom": "من تاريخ", + "dateTo": "الى تاريخ", + "requestName": "اسم الطلب", + "createdFor": "انشاء لأجل", + "requestCreatedSuccessfully": "تم انشاء الطلب بنجاح", + "search": "بحث", + "wantToReject": "هل انت متأكد تريد الرفض", + "requestType": "نوع الطلب", + "employeeDigitalID": "هويةالموظف الرقمية", + "businessCard": "بطاقة العمل", + "viewBusinessCard": "عرض بطاقة العمل", + "logout": "تسجيل خروج", + "checkOut": "وقت الخروج", + "regular": "منتظم", + "mark": "علامة", + "selectMethodOfAttendance": "اختر طريقة تسجيل الحضور", + "comeNearHMGWifi": "HMG wifi من فضلك اقترب من", + "deliverNotificationToMeRegardless": "تسليم الإخطارات إلي بغض النظر عن أي قواعد عامة", + "close": "أغلق", + "respond": "يرد", + "vacationRuleAdded": "تمت إضافة قاعدة الإجازة", + "selectTypeT": "اختر صنف", + "notification": "تنبيه", + "selectNotification": "حدد إعلام", + "ifAllSelectedYouWillSkip": "* إذا تم تحديد الكل ، فستنتقل إلى الخطوة 3", + "applyForVacationRule": "التقدم بطلب للحصول على قانون الإجازة", + "step1": "الخطوة 1", + "step2": "الخطوة 2", + "step3": "الخطوه 3", + "message": "رسالة", + "writeAMessage": "اكتب رسالة", + "notificationReassign": "إعادة تعيين الإخطار", + "selectEmployee": "حدد الموظف", + "searchEmployeeForReplacement": "ابحث عن موظف بديل", + "searchForEmployee": "ابحث عن موظف", + "pleaseSpecifyEndTime": "الرجاء تحديد وقت الانتهاء", + "pleaseSelectNotificationReassign": "يرجى تحديد إعادة تعيين الإخطار", + "pleaseSelectEmployeeForReplacement": "الرجاء تحديد موظف للاستبدال", + "pleaseSelectAction": "الرجاء تحديد الإجراء", + "pleaseSelectDate": "الرجاء تحديد التاريخ", + "todayAttendance": "حضور اليوم", + "viewAttendance": "عرض الحضور", + "teamMembers": "اعضاءالفريق", + "profileDetails": "الملف الشخصي", + "noResultsFound": "لايوجد نتائج", + "searchBy": "بحث بواسطة", + "myTeamMembers": "اعضاء فريقي", + "save": "حفظ", + "TurnNotificationsFor": "تفعيل الاشعارات", + "worklistSettings": "اعدادات الاشعارات", + "absenceType": "نوع الغياب", + "absenceCategory": "فئة الغياب", + "days": "أيام", + "hours": "ساعات", + "approvalStatus": "حالة القبول", + "absenceStatus": "حالة الغياب", + "subordinateLeave": "إجازة التابعيين", + "numberDays": "عدد الأيام", + "poweredBy": "مشغل بواسطة", + "cloudSolutions": "حلول السحابة", + "selectTemplate": "حدد قالب", + "myPostedAds": "إعلاناتي المنشورة", + "browseCategories": "تصفح الفئات", + "searchItems": "عناصر البحث", + "offerAndDiscounts": "العروض والخصومات", + "offerValid": "العرض صالح", + "offerExpired": "انتهى العرض", + "whatAreYouOffering": "ما الذي تعرضه؟", + "selectCategory": "اختر الفئة", + "inProgress": "في تَقَدم", + "locked": "مقفل", + "addDetails": "أضف التفاصيل", + "reviewAndSell": "مراجعة وبيع", + "itemTitle": "عنوان البند", + "itemCondition": "حالة السلعة", + "used": "تستخدم", + "region": "منطقة", + "selectRegion": "اختر المنطقة", + "itemPrice": "سعر السلعة", + "itemPhotos": "صور البند", + "itemInfo": "معلومات العنصر", + "uploadAttachment": "تحميل المرفق", + "selectFromGalleryOrOpenCamera": "اختر من المعرض أو فتح الكاميرا", + "openCamera": "فتح\nالكاميرا", + "uploadFromGallery": "تحميل من\nملفات الجهاز", + "name": "الأسم", + "email": "ايميل", + "noHistoryAvailable": "لايوجد سجل بيانات سابقة ", + "purchaseRequisition": "طلب شراء", + "moveOrder": "طلب تغيير", + "humanResource": "الموارد البشريه", + "purchaseOrder": "امر شراء", + "ITGForms": "ITG نماذج", + "itemCreation": "أنشاء عنصر", + "stamp": "ختم", + "addFavoriteList": "هل تريد اضافة {name} لقائمة المفضله", + "feedbackUserExperience": "هذا للحصول على تعليقات حول تجربة المستخدم", + "rateUI": ".1 كيف تريد تقييم التطبيق", + "submitSurvey": "ارسال الاستبيان", + "typeHere": "اكتب هنا", + "infoDetail": "تفاصيل المعلومات", + "amount_detail": "تفاصيل المبلغ", + "currentBalance": "الرصيد الحالي", + "currentLeaveBalance": "رصيد الاجازات الحالي", + "calculatedDays": "الايام المحسوبه", + "totalDays": "مجموع الأيام", + "usedBalance": "المستخدم", + "infants": "رضيع", + "child": "طفل", + "adult": "بالغ", + "updateMember": "هل انت متأكد تريد تحديث بيانات هذا العضو؟", + "fieldIsEmpty": "'{data}' الحقل فارغ. الرجاء التحديد", + "pleaseEnterComments": "الرجاء إدخال التعليقات", + "skip": "يتخطى", + "typeCurrentPasswordBelow": "اكتب كلمة المرور الحاليه", + "currentPassword": "كلمة المرور الحاليه", + "concurrentReports": "التقارير المتزامنه", + "profile": { + "reset_password": { + "label": "Reset Password", + "username": "Username", + "password": "password" + }, + "profileCompletionPer": "استكمال الملف الشخصي", + "completeProfile": "الملف الشخصي الكامل", + "personalInformation": "معلومات شخصية", + "basicDetails": "تفاصيل أساسية", + "address": "العنوان", + "contactDetails": "بيانات التواصل", + "familyDetails": "تفاصيل عائلية", + "effectiveDate": "تاريخ النفاذ", + "country": "دولة" + }, + "clicked": { + "zero": "You clicked {} times!", + "one": "You clicked {} time!", + "two": "You clicked {} times!", + "few": "You clicked {} times!", + "many": "You clicked {} times!", + "other": "You clicked {} times!" + }, + "gender": { + "male": "Hi man ;) ", + "female": "Hello girl :)", + "with_arg": { + "male": "Hi man ;) {}", + "female": "Hello girl :) {}" + } + }, + "reset_locale": "إعادة ضبط اللغة", + "chat": "دردشة", + "mychats": "دردشاتي", + "advancedSearch": "بحث متقدم", + "openNot": "التبليغات المفتوحة", + "fyi": "تبليغات للعلم", + "toDo": "تبليغات الأعمال", + "all": "كل التبليغات", + "meNot": "تبليغات صادرة مني", + "view": "عرض", + "fromUserName": "من", + "sentDate": "تاريخ الإرسال", + "itemTypeDisplayName": "اسم العرض", + "none": "بدون", + "createNewChat": "إنشاء محادثة جديدة", + "brainMarathon": "ماراثون الدماغ", + "contestTopicAbout": "سيكون موضوع المسابقة حول:", + "gameDate": "تاريخ اللعبة:", + "gameTime": "وقت اللعب:", + "joinMarathon": "انضم إلى ماراثون", + "joinDemoMarathon": "انضم إلى الماراثون التجريبي", + "minutes": "الدقائق", + "seconds": "ثواني", + "note": "ملحوظة:", + "demoMarathonNoteP1": "يمكنك لعب ماراثون العرض لتتعلم كيف يعمل. يمكنك الانضمام إلى ماراثون", + "demoMarathonNoteP2": "خمس دقائق", + "demoMarathonNoteP3": "قبل الوقت الفعلي.", + "sponsoredBy": "برعاية:", + "question": "سؤال", + "marathoners": "الماراثون", + "prize": "جائزة:", + "winnerSelection": "اختيار الفائز", + "qualifiers": "تصفيات", + "getReadyForContest": "استعد للمسابقة القادمة:", + "winnerSelectedRandomly": "سيتم اختيار الفائز عشوائياً من بين التصفيات.", + "fingersCrossed": "تشابك الاصابع!!!", + "congrats": "مبروك !!!", + "allQuestionsCorrect": "لقد أجبت على جميع الأسئلة بشكل صحيح.", + "otp": "OTP", + "verification": "تَحَقّق", + "resend": "إعادة إرسال", + "codeExpire": "انتهت صلاحية رمز التحقق", + "typeheretoreply": "اكتب هنا للرد" +} \ No newline at end of file diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json new file mode 100644 index 0000000..e0b9019 --- /dev/null +++ b/assets/langs/en-US.json @@ -0,0 +1,502 @@ +{ + "mohemm": "Mohemm", + "english": "English", + "arabic": "عربي", + "login": "Login", + "pleaseEnterLoginDetails": "Please enter the detail below to login", + "username": "Username", + "password": "Password", + "welcomeBack": "Welcome back", + "wouldYouLikeToLoginWithCurrentUsername": "Would you like to login with current Username?", + "lastLoginDetails": "Last Login Details:", + "verificationType": "Verification Type:", + "pleaseVerify": "Please Verify", + "pleaseVerifyForBio": "Please verify login with one of the following options", + "verifyThroughFace": "Verify Through Face", + "verifyThroughFingerprint": "Verify Through Fingerprint", + "verifyThroughSMS": "Verify Through SMS", + "verifyThroughWhatsapp": "Verify Through Whatsapp", + "useAnotherAccount": "Use Another Account", + "pleaseEnterTheVerificationCodeSentTo": "Please enter the verification code sent to ", + "theVerificationCodeWillExpireIn": "The verification code will expire in ", + "goodMorning": "Good Morning", + "markAttendance": "Mark Attendance", + "timeLeftToday": "Time Left Today", + "checkIn": "Check In", + "workList": "Work List", + "leaveBalance": "Leave Balance", + "missingSwipes": "Missing Swipes", + "ticketBalance": "Ticket Balance", + "other": "Other", + "services": "Services", + "viewAllServices": "View All Services", + "monthlyAttendance": "Monthly Attendance", + "vacationRule": "Vacation Rule", + "vacationType": "Vacation Type", + "startDateT": "Start Date", + "endDateT": "End Date", + "workFromHome": "Work From Home", + "ticketRequest": "Ticket Request", + "viewAllOffers": "View All Offers", + "offers": "Offers & ", + "discounts": "Discounts", + "newString": "New", + "setTheNewPassword": "Set the new password", + "typeYourNewPasswordBelow": "Type your new password below", + "confirmPassword": "Confirm Password", + "update": "Update", + "title": "Title", + "home": "Home", + "mySalary": "My Salary", + "createRequest": "Create Request", + "forgotPassword": "Forgot Password", + "employeeId": "Employee ID", + "loginCodeWillSentToMobileNumber": "Please Enter your Employee ID, A login code will be sent to your mobile number", + "changePassword": "Change Password", + "ok": "OK", + "confirm": "Confirm", + "passwordChangedSuccessfully": "Password changed successfully", + "itemsForSale": "Items for Sale", + "attendanceDetails": "Attendance Details", + "order": "order", + "earlyOut": "Early Out", + "shortage": "Shortage", + "excess": "Excess", + "lateIn": "Late In", + "approvedCheckOut": "Approved Check Out", + "approvedCheckIn": "Approved Check In", + "actualCheckOut": "Actual Check Out", + "actualCheckIn": "Actual Check In", + "present": "PRESENT", + "pres": "present", + "shiftTime": "Shift Time", + "absent": "ABSENT", + "attendance": "Attendance", + "scheduleDays": "Schedule\nDays", + "offDays": "Off\nDays", + "nonAnalyzed": "Non\nAnalyzed", + "shortageHour": "Shortage\nHour", + "stats": "Stats", + "completed": "Completed", + "doNotUseRecentPassword": "Do not use recent password", + "atLeastOneLowercase": "At least one lowercase", + "atLeastOneUppercase": "At least one uppercase", + "atLeastOneNumeric": "At least one numeric", + "minimum8Characters": "Minimum 8 characters", + "doNotAddRepeatingLetters": "Do not add repeating letters", + "itShouldContainSpecialCharacter": "It should contain special character", + "confirmPasswordMustMatch": "Confirm password must match", + "sms": "SMS", + "fingerPrint": "Fingerprint", + "face": "Face", + "whatsapp": "Whatsapp", + "reject": "Reject", + "approve": "Approve", + "cancel": "Cancel", + "requestedItems": "Requested Items", + "request": "Request", + "myRequest": "My Request", + "actions": "Actions", + "delegate": "Delegate", + "request_info": "Request Info", + "attachments": "Attachments", + "info": "Info.", + "employeeNumber": "Employee Number", + "assignmentNumber": "Assignment Number", + "employeeName": "Employee Name", + "scheduleDate": "Schedule Date", + "shiftType": "Shift Type", + "shift": "Shift", + "breakText": "Break", + "actualSwipeStart": "Actual Swipe Start", + "actualSwipeEnd": "Actual Swipe End", + "approvedSwipeStart": "Approved Swipe Start", + "approvedSwipeStartReason": "Approved Swipe Start Reason", + "approvedSwipeEnd": "Approved Swipe End", + "approvedSwipeEndReason": "Approved Swipe End Reason", + "from": "From", + "to": "To", + "sent": "Sent", + "closed": "Closed", + "id": "ID", + "responder": "Responder", + "jobTitle": "Job Title", + "grade": "Grade", + "jobCategory": "Job Category", + "category": "Category", + "employeeEmailAddress": "Employee Email Address", + "payrollBranch": "Payroll Branch", + "yourChangeHasBeenSavedSuccessfully": "Your change has been saved successfully", + "code": "Code", + "unit": "Unit", + "quantity": "Quantity", + "dateRequired": "Date Required", + "lineStatus": "Line Status", + "statusDate": "Status Date", + "transactionType": "Transaction Type", + "operatingUnit": "Operating Unit", + "organizationCode": "Organization Code", + "organization": "Organization", + "fromSubInventory": "From Sub Inventory", + "fromLocator": "From Locator", + "toSubInventory": "To Sub Inventory", + "toLocator": "To Locator", + "shipToLocator": "Ship To Locator", + "itemHistory": "Item History", + "mfg": "MFG", + "lineType": "Line Type", + "price": "Price", + "lineAmount": "Line Amount", + "lineDiscount": "Line Discount %", + "needByDate": "Need By Date", + "promisedDate": "Promised Date", + "deliverToLocation": "Deliver To Location", + "requisitionNumber": "Requisition Number", + "requester": "Requester", + "subject": "Subject", + "quotationAnalysis": "Quotation Analysis", + "description": "Description", + "supplier": "Supplier", + "site": "Site", + "buyer": "Buyer", + "preparer": "Preparer", + "creationDate": "Creation Date", + "shipToLocation": "Ship To Location", + "quotationNumber": "Quotation Number", + "quotationDate": "Quotation Date", + "paymentTerms": "Payment Terms", + "currency": "Currency", + "grossAmount": "Gross Amount", + "discountAmount": "Discount Amount", + "customDuty": "Custom Duty", + "shipHandle": "Ship Handle", + "otherCharges": "Other Charges", + "totalPOAmountWithVAT": "Total PO Amount With VAT", + "totalPOAmountInWords": "Total PO Amount In Words", + "requestNumber": "Request Number", + "uom": "UOM", + "operatingCode": "Operating Code", + "poNumber": "PO Number", + "revision": "Revision", + "quantityOrdered": "Quantity Ordered", + "quantityReceived": "Quantity Received", + "bonusQuantity": "Bonus Quantity", + "purchasePrice": "Purchase Price", + "discountPer": "Discount %", + "balanceQuantity": "Balance Quantity", + "netPrice": "Net Price", + "closureStatus": "Closure Status", + "quotationNetPrice": "Quotation Net Price", + "quotationUOM": "Quotation UOM", + "quotationQty": "Quotation Qty", + "itemCode": "item Code", + "vendorName": "Vendor Name", + "quotationMFGPartNumber": "Quotation MFG Part Number", + "quotationDeliveryDate": "Quotation Delivery Date", + "quotationBonusQuantity": "Quotation Bonus Quantity", + "quotationLineTotal": "Quotation Line Total", + "rfqUOM": "RFQ UOM", + "rfqQty": "RFQ Qty", + "rfqNumber": "RFQ Number", + "msg": "Hello {} in the {} world ", + "msg_named": "{} are written in the {lang} language", + "clickMe": "Click me", + "human": "Human", + "resources": "Resources", + "details": "Details", + "noDataAvailable": "No Data Available", + "productName": "Product Name", + "productDescription": "Product Description", + "unitPrice": "Unit Price", + "manufacturerName": "Manufacturer Name", + "manufacturerPartName": "Manufacturer Part Name", + "supplierName": "Supplier Name", + "supplierContact": "Supplier Contact", + "chargeToPatient": "Charge To Patient", + "justification": "Justification", + "itemDescription": "Item Description", + "groupCode": "Group Code", + "primaryUOM": "Primary UOM", + "subgroupDescription": "Subgroup Description", + "subgroupCode": "Subgroup Code", + "groupDescription": "Group Description", + "templateName": "Template Name", + "itemCreationStatus": "Item Creation Status", + "standardizationApprovalStatus": "Standardization Approval Status", + "standardizationApprovalRejectionReason": "Standardization Approval Rejection Reason", + "analyzedBy": "Analyzed By", + "approvedDate": "Approved Date", + "itemType": "Item Type", + "relatedTo": "Related To", + "requestDate": "Request Date", + "analyzedDate": "Analyzed Date", + "urgent": "Urgent", + "requestDetails": "Request Details", + "approvalLevel": "Approval Level", + "requesterDetails": "Requester Details", + "myAttendance": "My Attendance", + "workOnBreak": "Work On Break", + "next": "Next", + "apply": "Apply", + "mobile": "Mobile", + "year": "Year", + "month": "Month", + "day": "Day", + "completingYear": "We appreciate you for completing the service of", + "address": "Address", + "phoneNumber": "Phone Number", + "businessGroup": "Business", + "Payroll": "Payroll", + "civilIdentityNumber": "Civil Identity Number", + "dateOfBirth": "Date of Birth", + "maritalStatus ": "Marital Status ", + "fullName": "Full Name", + "remove": "Remove", + "Attendance": "Attendance", + "submit": "Submit", + "areYouSureYouWantToSubmit": "Are you sure you want to submit?", + "comments": "Comments", + "writeComment": "Write a comment", + "approversList": "Approvers List", + "yourRequestHasBeenSubmittedForApprovals": "Your request has been submitted for approvals", + "monthlyPaySlip": "Monthly Pay Slip", + "particular": "Particular", + "earnings": "Earnings", + "deductions": "Deductions", + "paymentMethodName": "Payment Method Name", + "bankName": "Bank Name", + "branchCode": "Branch Code", + "accountNo": "Account No", + "summaryOfInformation": "Summary of Information", + "totalPayAmount": "Total Pay Amount", + "paymentInformation": "Payment Information", + "amount": "Amount", + "correctCurrentDatails": "correct or complete the current details", + "selectType": "Select the type of change you want to make", + "enterNewInfo": "Enter new Information because of a real change to the current details (e.g because of a change in marital status)", + "endDate": "*End Date", + "removeThisMember": "Are You Sure You Want to Remove this Member?", + "wantUpdateThisMember": "Are You Sure You Want to Update this Member?", + "addNewFamilyMember": "Add New Family Member", + "addRow": "Add new row", + "pleaseSelect": "Please Select *", + "delete": "delete", + "add": "Add", + "edit": "Edit", + "myProfile": "My Profile", + "mowadhafhi": "Mowadhafi", + "searchAnnouncements": "Search Announcements", + "announcements": "Announcements", + "swipeRequest": "Swipe Request", + "serviceType": "Service Type", + "departmentName": "Department Name", + "selectDepartment": "Select Department", + "relatedSection": "Related Section", + "selectSection": "Select Section", + "relatedTopic": "Related Topic", + "selectTopic": "Select Topic", + "supportingDocument": "Supporting Document", + "mowadhafhiRequest": "Mowadhafi Request", + "ticketReference": "Ticket Reference", + "section": "Section", + "topic": "Topic", + "actionBy": "Action By", + "pendingTransactions": "Pending Transactions", + "selectRequestType": "Please select request type", + "dateFrom": "Date From", + "dateTo": "Date To", + "requestName": "Request Name", + "createdFor": "Created For", + "requestType": "Request Type", + "requestCreatedSuccessfully": "Request created successfully", + "search": "Search", + "wantToReject": "Are you sure want to reject?", + "employeeDigitalID": "Employee Digital ID", + "businessCard": "Business Card", + "checkOut": "Check Out", + "regular": "Regular", + "mark": "Mark", + "performance": "Performance Evaluation", + "performanceEvaluationIn": "Your performance Evaluation in", + "valuationIn": "Performance Evaluation in", + "viewBusinessCard": "View Business Card", + "performanceEvaluation": "Performance Evaluation", + "logout": "Logout", + "selectMethodOfAttendance": "Select the method to mark the attendance", + "comeNearHMGWifi": "Please come near to HMG wifi", + "deliverNotificationToMeRegardless": "Deliver notifications to me regardless of any general rules", + "close": "Close", + "respond": "Respond", + "vacationRuleAdded": "Vacation rule added", + "selectTypeT": "Select Type", + "notification": "Notification", + "selectNotification": "Select Notification", + "ifAllSelectedYouWillSkip": "*If All is selected, you will skip to step 3", + "applyForVacationRule": "Apply for Vacation Rule", + "step1": "Step 1", + "step2": "Step 2", + "step3": "Step 3", + "message": "Message", + "writeAMessage": "Write a message", + "notificationReassign": "Notification Reassign", + "selectEmployee": "Select Employee", + "searchEmployeeForReplacement": "Search employee for replacement", + "searchForEmployee": "Search for Employee", + "pleaseSpecifyEndTime": "Please specify End Time", + "pleaseSelectNotificationReassign": "Please select notification reassign", + "pleaseSelectEmployeeForReplacement": "Please select employee for replacement", + "pleaseSelectAction": "Please select action", + "pleaseSelectDate": "Please select date", + "todayAttendance": "Today's Attendance", + "viewAttendance": "View Attendance", + "teamMembers": "Team Members", + "profileDetails": "Profile Details", + "noResultsFound": "No Results Found", + "searchBy": "Search by", + "myTeamMembers": "My Team Members", + "save": "Save", + "TurnNotificationsFor": "Turn on notifications for", + "worklistSettings": "Worklist Settings", + "absenceType": "Absence Type", + "absenceCategory": "Absence Category", + "days": "Days", + "hours": "Hours", + "approvalStatus": "Approval Status", + "absenceStatus": "Absence Status", + "poweredBy": "Powered By", + "cloudSolutions": "Cloud Solutions", + "subordinateLeave": "Subordinate Leave", + "numberDays": "Number of days", + "selectTemplate": "Select Template", + "myPostedAds": "My posted ads", + "browseCategories": "Browse Categories", + "searchItems": "Search Items", + "offerAndDiscounts": "Offer & Discounts", + "offerValid": "Offer Valid", + "offerExpired": "Offer Expired", + "whatAreYouOffering": "What are you offering?", + "selectCategory": "Select Category", + "inProgress": "InProgress", + "locked": "Locked", + "addDetails": "Add Details", + "reviewAndSell": "Review & Sell", + "itemTitle": "Item Title", + "itemCondition": "Item Condition", + "used": "Used", + "region": "Region", + "selectRegion": "Select Region", + "itemPrice": "Item Price", + "itemPhotos": "Item Photos", + "itemInfo": "Item Info", + "uploadAttachment": "Upload Attachment", + "selectFromGalleryOrOpenCamera": "Select from gallery or open camera", + "openCamera": "Open\nCamera", + "uploadFromGallery": "Upload from\nGallery", + "name": "Name", + "email": "Email", + "noHistoryAvailable": "No History Available", + "purchaseRequisition": "Purchase Requisition", + "moveOrder": "Move Order", + "humanResource": "Human Resource", + "purchaseOrder": "Purchase Order", + "ITGForms": "ITG Forms", + "itemCreation": "Item Creation", + "stamp": "Stamp", + "addFavoriteList": "Do you want to add {name} in your favorite list", + "feedbackUserExperience": "This is to get the feedback about the user experience", + "rateUI": "1. How would you rate this UI?", + "submitSurvey": "Submit Survey", + "typeHere": "Type here", + "infoDetail": "Info Detail", + "amount_detail": "Amount Detail", + "currentBalance": "Current Balance", + "currentLeaveBalance": "Current Leave Balance", + "calculatedDays": "Calculated Days", + "totalDays": "Total Days", + "usedBalance": "Used", + "infants": "Infants", + "child": "Child", + "adult": "Adult", + "updateMember": "Are You Sure You Want to Update this Member?", + "fieldIsEmpty": "'{data}' Field is empty. Please select", + "pleaseEnterComments": "Please enter comments", + "skip": "Skip", + "typeCurrentPasswordBelow": "Type Your Current password below", + "currentPassword": "Current password", + "concurrentReports": "Concurrent Reports", + "profile": { + "reset_password": { + "label": "Reset Password", + "username": "Username", + "password": "password" + }, + "profileCompletionPer": "Profile Completion", + "completeProfile": "Complete Profile", + "personalInformation": "Personal Information", + "basicDetails": "Basic Details", + "address": "Address", + "contactDetails": "Contact Details", + "familyDetails": "Family Members", + "effectiveDate": "Effective Date", + "country": "Country" + }, + "clicked": { + "zero": "You clicked {} times!", + "one": "You clicked {} time!", + "two": "You clicked {} times!", + "few": "You clicked {} times!", + "many": "You clicked {} times!", + "other": "You clicked {} times!" + }, + "gender": { + "male": "Hi man ;) ", + "female": "Hello girl :)", + "with_arg": { + "male": "Hi man ;) {}", + "female": "Hello girl :) {}" + } + }, + "reset_locale": "Reset Language", + "chat": "Chat", + "mychats": "My Chats", + "createNewChat": "Create New Chat", + "brainMarathon": "Brain Marathon", + "contestTopicAbout": "Contest Topic will be about:", + "gameDate": "Game Date:", + "gameTime": "Game Time:", + "joinMarathon": "Join Marathon", + "joinDemoMarathon": "Join Demo Marathon", + "minutes": "Minutes", + "seconds": "Seconds", + "note": "Note:", + "demoMarathonNoteP1": "You can play the demo Marathon to learn how it works. You can join the Marathon", + "demoMarathonNoteP2": "5 Minutes", + "demoMarathonNoteP3": "before the actual time.", + "sponsoredBy": "Sponsored By:", + "question": "Question", + "marathoners": "Marathoners", + "prize": "Prize:", + "advancedSearch": "Advanced Search", + "openNot": "Open Notifications", + "fyi": "FYI Notifications", + "toDo": "To Do Notifications", + "all": "All Notifications", + "meNot": "Notifications from Me", + "view": "View", + "fromUserName": "From User Name", + "sentDate": "Sent Date", + "itemTypeDisplayName": "Item Type Display Name", + "none": "None", + "winnerSelection": "Winner Selection", + "qualifiers": "Qualifiers", + "getReadyForContest": "Get Ready for the coming contest:", + "winnerSelectedRandomly": "The winner will be selected randomly among the qualifiers.", + "fingersCrossed": "Fingers Crossed!!!", + "congrats": "Congratulations!!!", + "otp": "OTP", + "verification": "Verification", + "resend": "Resend", + "codeExpire": "The verification code has been expired", + "allQuestionsCorrect": "You have answered all questions correct", + "typeheretoreply": "Type here to reply" +} \ No newline at end of file diff --git a/lib/api/api_client.dart b/lib/api/api_client.dart new file mode 100644 index 0000000..7a6f668 --- /dev/null +++ b/lib/api/api_client.dart @@ -0,0 +1,321 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; +import 'package:http/io_client.dart'; +import 'package:mohem_flutter_app/app_state/app_state.dart'; +import 'package:mohem_flutter_app/exceptions/api_exception.dart'; +import 'package:mohem_flutter_app/main.dart'; +// ignore_for_file: avoid_annotating_with_dynamic + +typedef FactoryConstructor = U Function(dynamic); + +class APIError { + int? errorCode; + String? errorMessage; + + APIError(this.errorCode, this.errorMessage); + + Map toJson() => + {'errorCode': errorCode, 'errorMessage': errorMessage}; + + @override + String toString() { + return jsonEncode(this); + } +} + +APIException _throwAPIException(Response response) { + switch (response.statusCode) { + case 200: + APIError? apiError; + if (response.body != null && response.body.isNotEmpty) { + var jsonError = jsonDecode(response.body); + print(jsonError); + apiError = APIError(jsonError['ErrorCode'], jsonError['ErrorMessage']); + } + return APIException(APIException.BAD_REQUEST, error: apiError); + case 400: + APIError? apiError; + if (response.body != null && response.body.isNotEmpty) { + var jsonError = jsonDecode(response.body); + apiError = APIError(jsonError['ErrorCode'], jsonError['ErrorMessage']); + } + return APIException(APIException.BAD_REQUEST, error: apiError); + case 401: + return APIException(APIException.UNAUTHORIZED); + case 403: + return APIException(APIException.FORBIDDEN); + case 404: + return APIException(APIException.NOT_FOUND); + case 500: + return APIException(APIException.INTERNAL_SERVER_ERROR); + case 444: + var downloadUrl = response.headers["location"]; + return APIException(APIException.UPGRADE_REQUIRED, + arguments: downloadUrl); + default: + return APIException(APIException.OTHER); + } +} + +class ApiClient { + static final ApiClient _instance = ApiClient._internal(); + + ApiClient._internal(); + + factory ApiClient() => _instance; + + Future postJsonForObject( + FactoryConstructor factoryConstructor, String url, T jsonObject, + {String? token, + Map? queryParameters, + Map? headers, + int retryTimes = 0, + bool isFormData = false}) async { + var _headers = {'Accept': 'application/json'}; + if (headers != null && headers.isNotEmpty) { + _headers.addAll(headers); + } + if (!kReleaseMode) { + print("Url:$url"); + var bodyJson = json.encode(jsonObject); + print("body:$bodyJson"); + } + var response = await postJsonForResponse(url, jsonObject, + token: token, + queryParameters: queryParameters, + headers: _headers, + retryTimes: retryTimes, + isFormData: isFormData); + // try { + if (!kReleaseMode) { + logger.i("res: " + response.body); + } + var jsonData = jsonDecode(response.body); + if (jsonData["IsAuthenticated"] != null) { + AppState().setIsAuthenticated = jsonData["IsAuthenticated"]; + } + if (jsonData["ErrorMessage"] == null) { + return factoryConstructor(jsonData); + } else { + APIError? apiError; + apiError = + APIError(jsonData['ErrorCode'], jsonData['ErrorEndUserMessage']); + throw APIException(APIException.BAD_REQUEST, error: apiError); + } + // } catch (ex) { + // if (ex is APIException) { + // rethrow; + // } else { + // throw APIException(APIException.BAD_RESPONSE_FORMAT, arguments: ex); + // } + // } + } + + Future postJsonForResponse(String url, T jsonObject, + {String? token, + Map? queryParameters, + Map? headers, + int retryTimes = 0, + bool isFormData = false}) async { + String? requestBody; + late Map stringObj; + if (jsonObject != null) { + requestBody = jsonEncode(jsonObject); + if (headers == null) { + headers = {'Content-Type': 'application/json'}; + } else { + headers['Content-Type'] = 'application/json'; + } + } + + if (isFormData) { + headers = {'Content-Type': 'application/x-www-form-urlencoded'}; + stringObj = ((jsonObject ?? {}) as Map) + .map((key, value) => MapEntry(key, value?.toString() ?? "")); + } + + return await _postForResponse(url, isFormData ? stringObj : requestBody, + token: token, + queryParameters: queryParameters, + headers: headers, + retryTimes: retryTimes); + } + + Future _postForResponse(String url, requestBody, + {String? token, + Map? queryParameters, + Map? headers, + int retryTimes = 0}) async { + try { + var _headers = {}; + if (token != null) { + _headers['Authorization'] = 'Bearer $token'; + } + + if (headers != null && headers.isNotEmpty) { + _headers.addAll(headers); + } + + if (queryParameters != null) { + var queryString = new Uri(queryParameters: queryParameters).query; + url = url + '?' + queryString; + } + var response = + await _post(Uri.parse(url), body: requestBody, headers: _headers) + .timeout(Duration(seconds: 120)); + + if (response.statusCode >= 200 && response.statusCode < 300) { + return response; + } else { + throw _throwAPIException(response); + } + } on SocketException catch (e) { + if (retryTimes > 0) { + print('will retry after 3 seconds...'); + await Future.delayed(Duration(seconds: 3)); + return await _postForResponse(url, requestBody, + token: token, + queryParameters: queryParameters, + headers: headers, + retryTimes: retryTimes - 1); + } else { + throw APIException(APIException.OTHER, arguments: e); + } + } on HttpException catch (e) { + if (retryTimes > 0) { + print('will retry after 3 seconds...'); + await Future.delayed(Duration(seconds: 3)); + return await _postForResponse(url, requestBody, + token: token, + queryParameters: queryParameters, + headers: headers, + retryTimes: retryTimes - 1); + } else { + throw APIException(APIException.OTHER, arguments: e); + } + } on TimeoutException catch (e) { + throw APIException(APIException.TIMEOUT, arguments: e); + } on ClientException catch (e) { + if (retryTimes > 0) { + print('will retry after 3 seconds...'); + await Future.delayed(Duration(seconds: 3)); + return await _postForResponse(url, requestBody, + token: token, + queryParameters: queryParameters, + headers: headers, + retryTimes: retryTimes - 1); + } else { + throw APIException(APIException.OTHER, arguments: e); + } + } + } + + Future getJsonForResponse(String url, + {String? token, + Map? queryParameters, + Map? headers, + int retryTimes = 0}) async { + if (headers == null) { + headers = {'Content-Type': 'application/json'}; + } else { + headers['Content-Type'] = 'application/json'; + } + return await _getForResponse(url, + token: token, + queryParameters: queryParameters, + headers: headers, + retryTimes: retryTimes); + } + + Future _getForResponse(String url, + {String? token, + Map? queryParameters, + Map? headers, + int retryTimes = 0}) async { + try { + var _headers = {}; + if (token != null) { + _headers['Authorization'] = 'Bearer $token'; + } + + if (headers != null && headers.isNotEmpty) { + _headers.addAll(headers); + } + + if (queryParameters != null) { + var queryString = new Uri(queryParameters: queryParameters).query; + url = url + '?' + queryString; + } + var response = await _get(Uri.parse(url), headers: _headers) + .timeout(Duration(seconds: 60)); + + if (response.statusCode >= 200 && response.statusCode < 300) { + return response; + } else { + throw _throwAPIException(response); + } + } on SocketException catch (e) { + if (retryTimes > 0) { + print('will retry after 3 seconds...'); + await Future.delayed(Duration(seconds: 3)); + return await _getForResponse(url, + token: token, + queryParameters: queryParameters, + headers: headers, + retryTimes: retryTimes - 1); + } else { + throw APIException(APIException.OTHER, arguments: e); + } + } on HttpException catch (e) { + if (retryTimes > 0) { + print('will retry after 3 seconds...'); + await Future.delayed(Duration(seconds: 3)); + return await _getForResponse(url, + token: token, + queryParameters: queryParameters, + headers: headers, + retryTimes: retryTimes - 1); + } else { + throw APIException(APIException.OTHER, arguments: e); + } + } on TimeoutException catch (e) { + throw APIException(APIException.TIMEOUT, arguments: e); + } on ClientException catch (e) { + if (retryTimes > 0) { + print('will retry after 3 seconds...'); + await Future.delayed(Duration(seconds: 3)); + return await _getForResponse(url, + token: token, + queryParameters: queryParameters, + headers: headers, + retryTimes: retryTimes - 1); + } else { + throw APIException(APIException.OTHER, arguments: e); + } + } + } + + Future _get(url, {Map? headers}) => + _withClient((client) => client.get(url, headers: headers)); + + bool _certificateCheck(X509Certificate cert, String host, int port) => true; + + Future _withClient(Future Function(Client) fn) async { + var httpClient = HttpClient()..badCertificateCallback = _certificateCheck; + var client = IOClient(httpClient); + try { + return await fn(client); + } finally { + client.close(); + } + } + + Future _post(url, + {Map? headers, body, Encoding? encoding}) => + _withClient((client) => + client.post(url, headers: headers, body: body, encoding: encoding)); +} diff --git a/lib/api/login_api_client.dart b/lib/api/login_api_client.dart new file mode 100644 index 0000000..7da7754 --- /dev/null +++ b/lib/api/login_api_client.dart @@ -0,0 +1,148 @@ +import 'dart:async'; + +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/basic_member_information_model.dart'; +import 'package:mohem_flutter_app/models/check_mobile_app_version_model.dart'; +import 'package:mohem_flutter_app/models/generic_response_model.dart'; +import 'package:mohem_flutter_app/models/get_mobile_login_info_list_model.dart'; +import 'package:mohem_flutter_app/models/member_login_list_model.dart'; + +class LoginApiClient { + static final LoginApiClient _instance = LoginApiClient._internal(); + + LoginApiClient._internal(); + + factory LoginApiClient() => _instance; + + Future getMobileLoginInfoNEW(String deviceToken, String deviceType) async { + String url = "${ApiConsts.erpRest}Mohemm_GetMobileLoginInfo_NEW"; + Map postParams = {}; + postParams["DeviceToken"] = deviceToken; + postParams["DeviceType"] = deviceType; + return await ApiClient().postJsonForObject((json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return (responseData.mohemmGetMobileLoginInfoList?.length ?? 0) > 0 ? (responseData.mohemmGetMobileLoginInfoList!.first) : null; + }, url, postParams); + } + + Future insertMobileLoginInfoNEW( + String email, int sessionId, String employeeName, int loginType, String mobileNumber, String userName, String deviceToken, String deviceType) async { + String url = "${ApiConsts.erpRest}Mohemm_InsertMobileLoginInfo"; + Map postParams = { + "MobileNumber": mobileNumber, + "P_USER_NAME": userName, + "UserName": userName, + "CompanyID": 1, // todo 'sikander' @discuss umer for companyID + "DeviceToken": deviceToken, + "LoginType": loginType, + "EmployeeName": employeeName, + "P_SESSION_ID": sessionId, + "P_EMAIL_ADDRESS": email + }; + postParams["DeviceToken"] = deviceToken; + postParams["DeviceType"] = deviceType; + postParams.addAll(AppState().postParamsJson); + return await ApiClient().postJsonForObject((json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + AppState().setLogged = true; + return responseData; + }, url, postParams); + } + + Future checkMobileAppVersion() async { + String url = "${ApiConsts.utilitiesRest}CheckMobileAppVersion"; + Map postParams = {}; + postParams.addAll(AppState().postParamsJson); + return await ApiClient().postJsonForObject((json) => CheckMobileAppVersionModel.fromJson(json), url, postParams); + } + + Future memberLogin(String username, String password) async { + String url = "${ApiConsts.erpRest}MemberLogin"; + Map postParams = {"P_APP_VERSION": "CS", "P_LANGUAGE": "US", "P_PASSWORD": password, "P_USER_NAME": username}; + postParams.addAll(AppState().postParamsJson); + return await ApiClient().postJsonForObject((json) { + GenericResponseModel responseData = GenericResponseModel.fromJson(json); + AppState().postParamsObject?.setLogInTokenID = responseData.logInTokenID; + return responseData.memberLoginList; + }, url, postParams); + } + + Future mohemmSendActivationCodeByOTPNotificationType(int isMobileFingerPrint, String? mobileNumber, int optSendType, String? pUserName) async { + String url = "${ApiConsts.erpRest}Mohemm_SendActivationCodebyOTPNotificationType"; + Map postParams = {"IsMobileFingerPrint": isMobileFingerPrint, "MobileNumber": mobileNumber, "OTP_SendType": optSendType, "P_USER_NAME": pUserName}; + postParams.addAll(AppState().postParamsJson); + return await ApiClient().postJsonForObject((json) => GenericResponseModel.fromJson(json).basicMemberInformation, url, postParams); + } + + Future checkActivationCode(bool isDeviceNFC, String? mobileNumber, String activationCode, String? pUserName) async { + String url = "${ApiConsts.erpRest}CheckActivationCode"; + Map postParams = {"IsDeviceNFC": isDeviceNFC, "MobileNumber": mobileNumber, "activationCode": activationCode, "P_USER_NAME": pUserName}; + postParams.addAll(AppState().postParamsJson); + return await ApiClient().postJsonForObject((json) { + GenericResponseModel responseData = GenericResponseModel.fromJson(json); + AppState().postParamsObject?.setTokenID = responseData.tokenID; + AppState().postParamsObject?.mobileNumber = responseData.basicMemberInformation!.pMOBILENUMBER; + AppState().postParamsObject?.userName = AppState().getUserName; + AppState().postParamsObject?.pEmailAddress = responseData.basicMemberInformation!.pEMAILADDRESS; + AppState().postParamsObject?.pSessionId = responseData.pSESSIONID; + AppState().postParamsObject?.pUserName = AppState().getUserName; + AppState().postParamsObject?.pSelectedEmployeeNumber = AppState().getUserName; + AppState().postParamsObject?.setPLegislationCode = responseData.basicMemberInformation!.pLEGISLATIONCODE; + AppState().postParamsObject?.setPayrollCodeStr = responseData.memberInformationList!.first.pAYROLLCODE; + AppState().setBusinessCardPrivilege = responseData.businessCardPrivilege ?? false; + return responseData; + }, url, postParams); + } + + Future getBasicUserInformation(String pAppVersion, String pUsername) async { + String url = "${ApiConsts.erpRest}Get_BasicUserInformation"; + Map postParams = {"P_APP_VERSION": pAppVersion, "P_USER_NAME": pUsername}; + postParams.addAll(AppState().postParamsJson); + return await ApiClient().postJsonForObject((json) => GenericResponseModel.fromJson(json).basicMemberInformation, url, postParams); + } + + Future sendPublicActivationCode(String? mobileNumber, String? pUsername) async { + String url = "${ApiConsts.erpRest}SendPublicActivationCode"; + Map postParams = {"MobileNumber": mobileNumber, "P_MOBILE_NUMBER": mobileNumber, "P_USER_NAME": pUsername}; + postParams.addAll(AppState().postParamsJson); + return await ApiClient().postJsonForObject((json) { + GenericResponseModel responseData = GenericResponseModel.fromJson(json); + AppState().postParamsObject?.setLogInTokenID = responseData.logInTokenID; + return responseData; + }, url, postParams); + } + + Future checkPublicActivationCode(String activationCode, String? pUserName) async { + String url = "${ApiConsts.erpRest}checkPublicActivationCode"; + Map postParams = {"activationCode": activationCode, "P_USER_NAME": pUserName}; + postParams.addAll(AppState().postParamsJson); + return await ApiClient().postJsonForObject((json) { + GenericResponseModel responseData = GenericResponseModel.fromJson(json); + AppState().setForgetPasswordTokenID = responseData.forgetPasswordTokenID; + return responseData; + }, url, postParams); + } + + Future changePasswordForget(String forgetPasswordTokenID, String pNewPassword, String pConfirmNewPassword, String? pUserName) async { + String url = "${ApiConsts.erpRest}ChangePassword_Forget"; + Map postParams = {"P_USER_NAME": pUserName, "ForgetPasswordTokenID": forgetPasswordTokenID, "P_Confirm_NEW_PASSWORD": pConfirmNewPassword, "P_NEW_PASSWORD": pNewPassword}; + postParams.addAll(AppState().postParamsJson); + return await ApiClient().postJsonForObject((json) { + GenericResponseModel responseData = GenericResponseModel.fromJson(json); + + return responseData; + }, url, postParams); + } + + Future changePasswordFromActiveSession(String pOldPassword, String pNewPassword, String pConfirmNewPassword) async { + String url = "${ApiConsts.erpRest}ChangePassword_FromActiveSession"; + Map postParams = {"P_OLD_PASSWORD": pOldPassword, "P_Confirm_NEW_PASSWORD": pConfirmNewPassword, "P_NEW_PASSWORD": pNewPassword}; + postParams.addAll(AppState().postParamsJson); + return await ApiClient().postJsonForObject((json) { + GenericResponseModel responseData = GenericResponseModel.fromJson(json); + return responseData; + }, url, postParams); + } +} diff --git a/lib/app_state/app_state.dart b/lib/app_state/app_state.dart new file mode 100644 index 0000000..9e61872 --- /dev/null +++ b/lib/app_state/app_state.dart @@ -0,0 +1,132 @@ +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:mohem_flutter_app/models/chat/get_user_login_token_model.dart'; +import 'package:mohem_flutter_app/models/itg_forms_models/request_detail_model.dart'; +import 'package:mohem_flutter_app/models/member_information_list_model.dart'; +import 'package:mohem_flutter_app/models/member_login_list_model.dart'; +import 'package:mohem_flutter_app/models/post_params_model.dart'; +import 'package:mohem_flutter_app/models/privilege_list_model.dart'; +import 'package:mohem_flutter_app/models/worklist_response_model.dart'; + +class AppState { + static final AppState _instance = AppState._internal(); + + AppState._internal(); + + factory AppState() => _instance; + + bool isAuthenticated = false; + + set setIsAuthenticated(v) => isAuthenticated = v; + + bool isLogged = false; + + set setLogged(v) => isLogged = v; + + bool? get getIsLogged => isLogged; + + bool businessCardPrivilege = false; + + set setBusinessCardPrivilege(bool v) => businessCardPrivilege = v; + + bool? get getBusinessCardPrivilege => businessCardPrivilege; + + String? forgetPasswordTokenID; + + set setForgetPasswordTokenID(token) => forgetPasswordTokenID = token; + + String? get getForgetPasswordTokenID => forgetPasswordTokenID; + + //Wifi info + String? _mohemmWifiSSID; + + set setMohemmWifiSSID(v) => _mohemmWifiSSID = v; + + String? get getMohemmWifiSSID => _mohemmWifiSSID; + + String? _mohemmWifiPassword; + + set setMohemmWifiPassword(v) => _mohemmWifiPassword = v; + + String? get getMohemmWifiPassword => _mohemmWifiPassword; + + final PostParamsModel _postParamsInitConfig = PostParamsModel(channel: 31, versionID: 5.0, mobileType: Platform.isAndroid ? "android" : "ios"); + + void setPostParamsInitConfig() { + isAuthenticated = false; + isLogged = false; + _memberLoginList = null; + _memberInformationList = null; + _postParams = _postParamsInitConfig; + } + + PostParamsModel? _postParams; + + PostParamsModel? get postParamsObject => _postParams; + + Map get postParamsJson => isLogged ? (_postParams?.toJsonAfterLogin() ?? {}) : (_postParams?.toJson() ?? {}); + + void setPostParamsModel(PostParamsModel _postParams) { + this._postParams = _postParams; + } + + bool isArabic(context) => EasyLocalization.of(context)?.locale.languageCode == "ar"; + + int getLanguageID(context) => EasyLocalization.of(context)?.locale.languageCode == "ar" ? 1 : 2; + + String? _username; + + // todo ''sikander' added password for now, later will remove & improve + String? password; + + set setUserName(v) => _username = v; + + String? get getUserName => _username; + + set setUserPassword(_password) => password = _password; + + MemberLoginListModel? _memberLoginList; + + MemberLoginListModel? get memberLoginList => _memberLoginList; + + set setMemberLoginListModel(MemberLoginListModel? _memberLoginList) => this._memberLoginList = _memberLoginList; + + MemberInformationListModel? _memberInformationList; + + MemberInformationListModel? get memberInformationList => _memberInformationList; + + set setMemberInformationListModel(MemberInformationListModel? _memberInformationList) => this._memberInformationList = _memberInformationList; + + List? _privilegeListModel; + + List? get privilegeListModel => _privilegeListModel; + + set setPrivilegeListModel(List? _privilegeListModel) => this._privilegeListModel = _privilegeListModel; + + List? workList; + + set setWorkList(List? _workList) => workList = _workList; + + int? workListIndex; + + set setWorkListIndex(int? _workListIndex) => workListIndex = _workListIndex; + + List? requestAllList; + + set setRequestAllList(List? _requestAllList) => requestAllList = _requestAllList; + + int? itgWorkListIndex; + + set setItgWorkListIndex(int? _itgWorkListIndex) => itgWorkListIndex = _itgWorkListIndex; + + UserAutoLoginModel? chatDetails; + + set setchatUserDetails(UserAutoLoginModel details) => chatDetails = details; + + UserAutoLoginModel? get getchatUserDetails => chatDetails; + + String? _base64ImageEmp = "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"; + + String? get getBase64ImageEmp => _base64ImageEmp; +} diff --git a/lib/classes/app_permissions.dart b/lib/classes/app_permissions.dart new file mode 100644 index 0000000..1509b57 --- /dev/null +++ b/lib/classes/app_permissions.dart @@ -0,0 +1,48 @@ +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class AppPermissions { + static void location(Function(bool) completion, BuildContext context) { + Permission.location.isGranted.then((isGranted){ + if(!isGranted){ + Permission.location.request().then((granted){ + completion(granted == PermissionStatus.granted); + }); + } + completion(isGranted); + }); + } + + static void showErrorLocationDialog(bool isPermissionError, BuildContext context) { + showDialog( + context: context, + builder: (cxt) => ConfirmDialog( + message: "Please provide location permission", + onTap: () { + if (isPermissionError) { + Geolocator.openAppSettings(); + } else { + Geolocator.openLocationSettings(); + } + Navigator.pop(context); + // createVacationRule(list); + }, + ), + ); + } + + static void checkAll(Function(bool) completion) { + [Permission.location].request().then((value) { + bool allGranted = false; + value.values.forEach((element) { + allGranted = allGranted && element == PermissionStatus.granted; + }); + + completion(allGranted); + }); + } +} diff --git a/lib/classes/colors.dart b/lib/classes/colors.dart new file mode 100644 index 0000000..f99cc31 --- /dev/null +++ b/lib/classes/colors.dart @@ -0,0 +1,64 @@ +import 'package:flutter/cupertino.dart'; + +class MyColors { + static const Color kWhiteColor = Color(0xffffffff); + static const Color darkIconColor = Color(0xff28323A); + static const Color darkTextColor = Color(0xff2B353E); + static const Color normalTextColor = Color(0xff5A5A5A); + static const Color lightTextColor = Color(0xffBFBFBF); + static const Color gradiantStartColor = Color(0xff33c0a5); + static const Color gradiantEndColor = Color(0xff259db7); + static const Color textMixColor = Color(0xff2BB8A6); + static const Color backgroundColor = Color(0xffF8F8F8); + static const Color grey41Color = Color(0xff414141); + static const Color grey57Color = Color(0xff575757); + static const Color grey67Color = Color(0xff676767); + static const Color grey77Color = Color(0xff777777); + static const Color greyF7Color = Color(0xffF7F7F7); + static const Color grey80Color = Color(0xff808080); + static const Color grey70Color = Color(0xff707070); + static const Color greyACColor = Color(0xffACACAC); + static const Color grey98Color = Color(0xff989898); + static const Color lightGreyEFColor = Color(0xffEFEFEF); + static const Color lightGreyEDColor = Color(0xffEDEDED); + static const Color lightGreyDeColor = Color(0xffDEDEDE); + static const Color lightGreyE3Color = Color(0xffE3E3E3); + static const Color lightGreyE6Color = Color(0xffE6E6E6); + static const Color lightGreyEAColor = Color(0xffEAEAEA); + static const Color lightGreyE5Color = Color(0xffE5E5E5); + static const Color darkWhiteColor = Color(0xffE0E0E0); + static const Color redColor = Color(0xffD02127); + static const Color pinkColor = Color(0xffEBA9A9); + static const Color pinkDarkColor = Color(0xffe3797d); + static const Color yellowColor = Color(0xffF4E31C); + static const Color orange = Color(0xFFCC9B14); + static const Color yellowFavColor = Color(0xffEAC321); + static const Color yellowColorII = Color(0xffEAA118); + static const Color backgroundBlackColor = Color(0xff202529); + static const Color black = Color(0xff000000); + static const Color white = Color(0xffffffff); + static const Color borderColor = Color(0xffE8E8E8); + static const Color borderE3Color = Color(0xffE3E3E3); + static const Color borderCEColor = Color(0xffCECECE); + //static const Color grey67Color = Color(0xff676767); + static const Color whiteColor = Color(0xFFEEEEEE); + static const Color greenColor = Color(0xff1FA269); + static const Color lightGreenColor = Color(0xff2AB2AB); + static const Color darkGreyColor = Color(0xff464646); + static const Color greyA5Color = Color(0xffA5A5A5); + static const Color blackColor = Color(0xff000014); + static const Color grey3AColor = Color(0xff2E303A); + static const Color darkColor = Color(0xff000015); + static const Color lightGrayColor = Color(0xff808080); + static const Color darkRedColor = Color(0xffD02127); + static const Color lightGreyColor = Color(0xffC7C7C7); + static const Color green69Color = Color(0xff1FA169); + static const Color redA3Color = Color(0xffCA3332); + static const Color green9CColor = Color(0xff259CB8); + static const Color green2DColor = Color(0xff32D892); + static const Color greyC4Color = Color(0xffC4C4C4); + static const Color grey35Color = Color(0xff535353); + static const Color grey9DColor = Color(0xff9D9D9D); + static const Color darkDigitColor = Color(0xff2D2F39); + static const Color grey71Color = Color(0xff717171); +} diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart new file mode 100644 index 0000000..646e498 --- /dev/null +++ b/lib/classes/consts.dart @@ -0,0 +1,36 @@ +class ApiConsts { + //static String baseUrl = "http://10.200.204.20:2801/"; // Local server + static String baseUrl = "https://uat.hmgwebservices.com"; // UAT server + // static String baseUrl = "https://hmgwebservices.com"; // Live server + static String baseUrlServices = baseUrl + "/Services/"; // server + // static String baseUrlServices = "https://api.cssynapses.com/tangheem/"; // Live server + static String utilitiesRest = baseUrlServices + "Utilities.svc/REST/"; + static String erpRest = baseUrlServices + "ERP.svc/REST/"; + static String swpRest = baseUrlServices + "SWP.svc/REST/"; + static String user = baseUrlServices + "api/User/"; + static String cocRest = baseUrlServices + "COCWS.svc/REST/"; + + //Chat + static String chatServerBaseUrl = "https://apiderichat.hmg.com"; + static String chatServerBaseApiUrl = "https://apiderichat.hmg.com/api/"; + static String chatHubConnectionUrl = chatServerBaseUrl + "/ConnectionChatHub"; + static String chatSearchMember = "user/getUserWithStatusAndFavAsync/"; + static String chatRecentUrl = "UserChatHistory/getchathistorybyuserid"; //For a Mem + static String chatSingleUserHistoryUrl = "UserChatHistory/GetUserChatHistory"; + static String chatMediaImageUploadUrl = "shared/upload"; + static String chatFavoriteUsers = "FavUser/getFavUserById/"; +} + +class SharedPrefsConsts { + static String isRememberMe = "remember_me"; + static String username = "username"; + static String password = "password"; + static String privilegeList = "privilegeList"; + static String firebaseToken = "firebaseToken"; + static String memberInformation = "memberInformation"; + static String welcomeVideoUrl = "welcomeVideoUrl"; + static String doNotShowWelcomeVideo = "doNotShowWelcomeVideo"; + static String mohemmWifiSSID = "mohemmWifiSSID"; + static String mohemmWifiPassword = "mohemmWifiPassword"; + static String editItemForSale = "editItemForSale"; +} diff --git a/lib/classes/date_uitl.dart b/lib/classes/date_uitl.dart new file mode 100644 index 0000000..73945f8 --- /dev/null +++ b/lib/classes/date_uitl.dart @@ -0,0 +1,460 @@ +import 'package:intl/intl.dart'; + +class DateUtil { + /// convert String To Date function + /// [date] String we want to convert + static DateTime convertStringToDate(String date) { + // /Date(1585774800000+0300)/ + if (date != null) { + const start = "/Date("; + const end = "+0300)"; + int startIndex = date.indexOf(start); + int endIndex = date.indexOf(end, startIndex + start.length); + return DateTime.fromMillisecondsSinceEpoch( + int.parse( + date.substring(startIndex + start.length, endIndex), + ), + ); + } else + return DateTime.now(); + } + + static DateTime convertSimpleStringDateToDate(String date) { + return DateFormat("MM/dd/yyyy hh:mm:ss").parse(date); + } + + static DateTime convertSimpleStringDateToDateddMMyyyy(String date) { + return DateFormat("MM/dd/yyyy hh:mm:ss").parse(date); + } + + static DateTime convertStringToDateNoTimeZone(String date) { + // /Date(1585774800000+0300)/ + if (date != null) { + const start = "/Date("; + const end = ")"; + int startIndex = date.indexOf(start); + int endIndex = date.indexOf(end, startIndex + start.length); + return DateTime.fromMillisecondsSinceEpoch( + int.parse( + date.substring(startIndex + start.length, endIndex), + ), + ); + } else + return DateTime.now(); + } + + static DateTime convertStringToDateTime(String date) { + if (date != null) { + try { + var dateT = date.split('/'); + var year = dateT[2].substring(0, 4); + var dateP = DateTime(int.parse(year), int.parse(dateT[1]), int.parse(dateT[0])); + return dateP; + } catch (e) { + print(e); + } + + return DateTime.now(); + } else + return DateTime.now(); + } + + static String convertDateToString(DateTime date) { + const start = "/Date("; + const end = "+0300)/"; + int milliseconds = date.millisecondsSinceEpoch; + + return start + "$milliseconds" + end; + } + + static String convertDateToStringLocation(DateTime date) { + const start = "/Date("; + const end = ")/"; + int milliseconds = date.millisecondsSinceEpoch; + + return start + "$milliseconds" + end; + } + + static String formatDateToDate(DateTime date, bool isArabic) { + return DateFormat('dd MMM yyy', isArabic ? "ar_SA" : "en_US").format(date); + } + + static String formatDateToTime(DateTime date) { + return DateFormat('hh:mm a').format(date); + } + + static String yearMonthDay(DateTime dateTime) { + String dateFormat = '${dateTime.year}-${dateTime.month}-${dateTime.day}'; + return dateFormat; + } + + static String time(DateTime dateTime) { + String dateFormat = '${dateTime.hour}:${dateTime.minute}:00'; + return dateFormat; + } + + static String convertDateMSToJsonDate(utc) { + var dt = new DateTime.fromMicrosecondsSinceEpoch(utc); + + return "/Date(" + (dt.millisecondsSinceEpoch * 1000).toString() + '+0300' + ")/"; + } + + /// check Date + /// [dateString] String we want to convert + static String checkDate(DateTime checkedTime) { + DateTime currentTime = DateTime.now(); + if ((currentTime.year == checkedTime.year) && (currentTime.month == checkedTime.month) && (currentTime.day == checkedTime.day)) { + return "Today"; + } else if ((currentTime.year == checkedTime.year) && (currentTime.month == checkedTime.month)) { + if ((currentTime.day - checkedTime.day) == 1) { + return "YESTERDAY"; + } else if ((currentTime.day - checkedTime.day) == -1) { + return "Tomorrow"; + } + + if ((currentTime.day - checkedTime.day) <= -2) { + return "Next Week"; + } else { + return "Old Date"; + } + } + return "Old Date"; + } + + static String getDateFormatted(String date) { + DateTime dateObj = DateUtil.convertStringToDate(date); + return DateUtil.getWeekDay(dateObj.weekday) + ", " + dateObj.day.toString() + " " + DateUtil.getMonth(dateObj.month) + " " + dateObj.year.toString(); + } + + static String getISODateFormat(DateTime dateTime) { + // 2020-04-30T00:00:00.000 + return dateTime.toIso8601String(); + } + + /// get month by + /// [month] convert month number in to month name + static String getMonth(int month) { + switch (month) { + case 1: + return "January"; + case 2: + return "February"; + case 3: + return "March"; + case 4: + return "April"; + case 5: + return "May"; + case 6: + return "June"; + case 7: + return "July"; + case 8: + return "August"; + case 9: + return "September"; + case 10: + return "October"; + case 11: + return "November"; + case 12: + return "December"; + default: + return ""; + } + } + + /// get month by + /// [month] convert month number in to month name in Arabic + static String getMonthArabic(int month) { + switch (month) { + case 1: + return "يناير"; + case 2: + return " فبراير"; + case 3: + return "مارس"; + case 4: + return "أبريل"; + case 5: + return "مايو"; + case 6: + return "يونيو"; + case 7: + return "يوليو"; + case 8: + return "أغسطس"; + case 9: + return "سبتمبر"; + case 10: + return " اكتوبر"; + case 11: + return " نوفمبر"; + case 12: + return "ديسمبر"; + default: + return ""; + } + } + + static int getMonthByName(String month) { + switch (month.toLowerCase()) { + case 'january': + return 1; + case 'february': + return 2; + case 'march': + return 3; + case 'april': + return 4; + case 'may': + return 5; + case 'june': + return 6; + case 'july': + return 7; + case 'august': + return 8; + case 'september': + return 9; + case 'october': + return 10; + case 'november': + return 11; + case 'december': + return 12; + default: + return 0; + } + } + + static DateTime getMonthDateTime(String month, yearName) { + DateTime? date; + try { + date = DateTime(int.parse(yearName), getMonthByName(month)); + } catch (e) { + print(e); + } + return date ?? DateTime.now(); + } + + /// get month by + /// [weekDay] convert week day in int to week day name + static String getWeekDay(int weekDay) { + switch (weekDay) { + case 1: + return "Monday"; + case 2: + return "Tuesday"; + case 3: + return "Wednesday"; + case 4: + return "Thursday"; + case 5: + return "Friday"; + case 6: + return "Saturday "; + case 7: + return "Sunday"; + default: + return ""; + } + } + + /// get month by + /// [weekDay] convert week day in int to week day name arabic + static String getWeekDayArabic(int weekDay) { + switch (weekDay) { + case 1: + return "الاثنين"; + case 2: + return "الثلاثاء"; + case 3: + return "الاربعاء"; + case 4: + return "الخميس"; + case 5: + return "الجمعه"; + case 6: + return "السبت "; + case 7: + return "الاحد"; + default: + return ""; + } + } + + static String getWeekDayEnglish(int weekDay) { + switch (weekDay) { + case 1: + return "Monday"; + case 2: + return "Tuesday"; + case 3: + return "Wednesday"; + case 4: + return "Thursday"; + case 5: + return "Friday"; + case 6: + return "Saturday "; + case 7: + return "Sunday"; + default: + return ""; + } + } + + /// get data formatted like Apr 26,2020 + /// [dateTime] convert DateTime to data formatted + static String getMonthDayYearDateFormatted(DateTime dateTime) { + if (dateTime != null) + return getMonth(dateTime.month) + " " + dateTime.day.toString() + ", " + dateTime.year.toString(); + else + return ""; + } + + /// get data formatted like Apr 26,2020 + /// [dateTime] convert DateTime to data formatted Arabic + static String getMonthDayYearDateFormattedAr(DateTime dateTime) { + if (dateTime != null) + return getMonthArabic(dateTime.month) + " " + dateTime.day.toString() + ", " + dateTime.year.toString(); + else + return ""; + } + + /// get data formatted like Thursday, Apr 26,2020 + /// [dateTime] convert DateTime to date formatted + static String getWeekDayMonthDayYearDateFormatted(DateTime dateTime, String lang) { + // print(dateTime); + // print(dateTime.weekday); + // print(dateTime.weekday.getDayOfWeekEnumValue.value); + if (dateTime != null) + return lang == 'en' + ? getWeekDayEnglish(dateTime.weekday) + ", " + getMonth(dateTime.month) + " " + dateTime.day.toString() + " " + dateTime.year.toString() + : getWeekDayArabic(dateTime.weekday) + ", " + dateTime.day.toString() + " " + getMonthArabic(dateTime.month) + " " + dateTime.year.toString(); + else + return ""; + } + + static String getMonthDayYearLangDateFormatted(DateTime dateTime, String lang) { + if (dateTime != null) + return lang == 'en' + ? getMonth(dateTime.month) + " " + dateTime.day.toString() + " " + dateTime.year.toString() + : dateTime.day.toString() + " " + getMonthArabic(dateTime.month) + " " + dateTime.year.toString(); + else + return ""; + } + + /// get data formatted like 26/4/2020 + static String getDayMonthYearLangDateFormatted(DateTime dateTime, String lang) { + if (dateTime != null) + return lang == 'en' + ? dateTime.day.toString() + " " + getMonth(dateTime.month) + " " + dateTime.year.toString() + : dateTime.day.toString() + " " + getMonthArabic(dateTime.month) + " " + dateTime.year.toString(); + else + return ""; + } + + static String getMonthYearLangDateFormatted(DateTime dateTime, String lang) { + if (dateTime != null) + return lang == 'en' ? getMonth(dateTime.month) + " " + dateTime.year.toString() : getMonthArabic(dateTime.month) + " " + dateTime.year.toString(); + else + return ""; + } + + /// get data formatted like 26/4/2020 + /// [dateTime] convert DateTime to data formatted + static String getDayMonthYearDateFormatted(DateTime dateTime) { + if (dateTime != null) + return dateTime.day.toString() + "/" + dateTime.month.toString() + "/" + dateTime.year.toString(); + else + return ""; + } + + /// get data formatted like 26/4/2020 + /// [dateTime] convert DateTime to data formatted + static String getDayMonthDateFormatted(DateTime dateTime) { + if (dateTime != null) + return DateFormat('dd/MM').format(dateTime); + else + return ""; + } + + /// get data formatted like 26/4/2020 + /// [dateTime] convert DateTime to data formatted according to language + static String getDayMonthYearDateFormattedLang(DateTime dateTime, bool isArabic) { + if (dateTime != null) + return DateFormat('dd/MM/yyyy', isArabic ? "ar_SA" : "en_US").format(dateTime); + else + return ""; + } + + /// get data formatted like 10:30 according to lang + static String formatDateToTimeLang(DateTime date, bool isArabic) { + return DateFormat('HH:mm', isArabic ? "ar_SA" : "en_US").format(date); + } + + /// get data formatted like 26/4/2020 10:30 + /// [dateTime] convert DateTime to data formatted + static String getDayMonthYearHourMinuteDateFormatted(DateTime dateTime) { + if (dateTime != null) + return dateTime.day.toString() + "/" + dateTime.month.toString() + "/" + dateTime.year.toString() + " " + DateFormat('HH:mm').format(dateTime); + else + return ""; + } + + /// get data formatted like 2020-8-13 09:43:00 + /// [dateTime] convert DateTime to data formatted + static String getYearMonthDayHourMinSecDateFormatted(DateTime dateTime) { + if (dateTime != null) + return dateTime.year.toString() + + "-" + + dateTime.month.toString() + + "-" + + dateTime.day.toString() + + " " + + dateTime.hour.toString() + + ":" + + dateTime.minute.toString() + + ":" + + dateTime.second.toString(); + else + return ""; + } + + static String getFormattedDate(DateTime dateTime, String formattedString) { + return DateFormat(formattedString).format(dateTime); + } + + static String convertISODateToJsonDate(String isoDate) { + return "/Date(" + DateFormat('mm-dd-yyy').parse(isoDate).millisecondsSinceEpoch.toString() + ")/"; + } + + // static String getDay(DayOfWeek dayOfWeek) { + // switch (dayOfWeek) { + // case DayOfWeek.Monday: + // return "Monday"; + // break; + // case DayOfWeek.Tuesday: + // return "Tuesday"; + // break; + // case DayOfWeek.Wednesday: + // return "Wednesday"; + // break; + // case DayOfWeek.Thursday: + // return "Thursday"; + // break; + // case DayOfWeek.Friday: + // return "Friday"; + // break; + // case DayOfWeek.Saturday: + // return "Saturday"; + // break; + // case DayOfWeek.Sunday: + // return "Sunday"; + // break; + // } + // return ""; + // } +} diff --git a/lib/classes/decorations_helper.dart b/lib/classes/decorations_helper.dart new file mode 100644 index 0000000..77ea4a4 --- /dev/null +++ b/lib/classes/decorations_helper.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; + +class MyDecorations { + static Decoration shadowDecoration = BoxDecoration( + color: MyColors.kWhiteColor, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: const Color(0xff000000).withOpacity(.05), + blurRadius: 26, + offset: const Offset(0, -3), + ), + ], + ); + + static Decoration getContainersDecoration(Color color) { + Decoration answerContainerDecoration = BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: color, + border: Border.all(width: 1, color: MyColors.lightGreyEFColor), + ); + return answerContainerDecoration; + } +} diff --git a/lib/classes/file_process.dart b/lib/classes/file_process.dart new file mode 100644 index 0000000..6974702 --- /dev/null +++ b/lib/classes/file_process.dart @@ -0,0 +1,40 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:open_file/open_file.dart'; +import 'package:path_provider/path_provider.dart'; + +class FileProcess { + static bool isFolderCreated = false; + static Directory? directory; + + static Future checkDocumentFolder() async { + try { + if (!isFolderCreated) { + directory = await getApplicationDocumentsDirectory(); + await directory!.exists().then((value) { + if (value) directory!.create(); + isFolderCreated = true; + }); + } + } catch (e) { + print(e.toString()); + } + } + + static void openFile(String fileName) { + String dir = directory!.path + "/$fileName.pdf"; + OpenFile.open(dir); + } + + static Future downloadFile(String base64Content, String fileName) async { + Uint8List bytes = base64.decode(base64Content); + await checkDocumentFolder(); + String dir = directory!.path + "/" + fileName + ".pdf"; + File file = File(dir); + if (!file.existsSync()) file.create(); + await file.writeAsBytes(bytes); + return file; + } +} diff --git a/lib/classes/lottie_consts.dart b/lib/classes/lottie_consts.dart new file mode 100644 index 0000000..1b714a4 --- /dev/null +++ b/lib/classes/lottie_consts.dart @@ -0,0 +1,7 @@ +class MyLottieConsts { + static const String hourGlassLottie = "assets/lottie/hourglass.json"; + static const String celebrate1Lottie = "assets/lottie/celebrate1.json"; + static const String celebrate2Lottie = "assets/lottie/celebrate2.json"; + static const String winnerLottie = "assets/lottie/winner3.json"; + static const String allQuestions = "assets/lottie/all_questions.json"; +} diff --git a/lib/classes/utils.dart b/lib/classes/utils.dart new file mode 100644 index 0000000..7e7e83c --- /dev/null +++ b/lib/classes/utils.dart @@ -0,0 +1,346 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:mohem_flutter_app/app_state/app_state.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/config/routes.dart'; +import 'package:mohem_flutter_app/exceptions/api_exception.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +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/widgets/dialogs/confirm_dialog.dart'; +import 'package:mohem_flutter_app/widgets/loading_dialog.dart'; +import 'package:nfc_manager/nfc_manager.dart'; +import 'package:nfc_manager/platform_tags.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +// ignore_for_file: avoid_annotating_with_dynamic + +class Utils { + static bool _isLoadingVisible = false; + + static bool get isLoading => _isLoadingVisible; + + static void showToast(String message, {bool longDuration = true}) { + Fluttertoast.showToast( + msg: message, + toastLength: longDuration ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: Colors.black54, + textColor: Colors.white, + fontSize: 16.0); + } + + static dynamic getNotNullValue(List list, int index) { + try { + return list[index]; + } catch (ex) { + return null; + } + } + + static int stringToHex(String colorCode) { + try { + return int.parse(colorCode.replaceAll("#", "0xff")); + } catch (ex) { + return (0xff000000); + } + } + + static Future delay(int millis) async { + return await Future.delayed(Duration(milliseconds: millis)); + } + + static void showLoading(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _isLoadingVisible = true; + showDialog( + context: context, + barrierColor: Colors.black.withOpacity(0.5), + builder: (BuildContext context) => LoadingDialog(), + ).then((value) { + _isLoadingVisible = false; + }); + }); + } + + static void hideLoading(BuildContext context) { + if (_isLoadingVisible) { + _isLoadingVisible = false; + Navigator.of(context).pop(); + } + _isLoadingVisible = false; + } + + static Future getStringFromPrefs(String key) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getString(key) ?? ""; + } + + static Future saveStringFromPrefs(String key, String value) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + return await prefs.setString(key, value); + } + + static void handleException(dynamic exception, cxt, Function(String)? onErrorMessage) { + String errorMessage; + if (exception is APIException) { + if (exception.message == APIException.UNAUTHORIZED) { + return; + } else { + errorMessage = exception.error?.errorMessage ?? exception.message; + } + } else { + errorMessage = APIException.UNKNOWN; + } + if (onErrorMessage != null) { + onErrorMessage(errorMessage); + } else { + if (!AppState().isAuthenticated) { + showDialog( + context: cxt, + builder: (cxt) => ConfirmDialog( + message: errorMessage, + onTap: () { + Navigator.pushNamedAndRemoveUntil(cxt, AppRoutes.login, (Route route) => false); + }, + ), + ); + } else { + showToast(errorMessage); + } + } + } + + static void confirmDialog(cxt, String message) { + showDialog( + context: cxt, + builder: (cxt) => ConfirmDialog( + message: message, + ), + ); + } + + static Widget getNoDataWidget(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SvgPicture.asset('assets/images/not_found.svg', width: 110.0, height: 110.0), + LocaleKeys.noDataAvailable.tr().toText16().paddingOnly(top: 15), + ], + ).center; + } + + static Widget getNoChatWidget(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SvgPicture.asset('assets/images/not_found.svg', width: 110.0, height: 110.0), + LocaleKeys.noDataAvailable.tr().toText16().paddingOnly(top: 15), + ], + ).center; + } + + static Uint8List getPostBytes(img) { + try { + var b64 = img.replaceFirst('data:image/png;base64,', ''); + if (img != null && Utils.isBase64(b64)) return Utils.dataFromBase64String(b64); + } catch (e) {} + return Uint8List.fromList([]); + } + + static String getBase64FromJpeg(img) { + try { + var b64 = img.replaceFirst('data:image/jpeg;base64,', ''); + return b64; + } catch (e) {} + return ""; + } + + static bool isBase64(String str) { + RegExp _base64 = RegExp(r'^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$'); + return _base64.hasMatch(str); + } + + static Uint8List dataFromBase64String(String base64String) { + return base64Decode(base64String); + } + + static Widget tableColumnTitle(String? text, {bool showDivider = true, bool alignCenter = false}) { + text ??= ""; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + 6.height, + alignCenter ? text.toText12().center : text.toText12(), + 5.height, + if (showDivider) + const Divider( + height: 1, + color: Color(0xff2E303A), + thickness: 1, + ) + ], + ); + } + + static Decoration containerRadius(Color background, double radius) { + return BoxDecoration( + color: background, + border: Border.all( + width: 1, // + color: background // <--- border width here + ), + borderRadius: BorderRadius.circular(radius), + ); + } + + static Widget mHeight(double h) { + return Container( + height: h, + ); + } + + static Widget mDivider(Color color) { + return Divider( + // width: double.infinity, + height: 1, + color: color, + ); + } + + static Widget tableColumnValue(String text, {bool isCapitable = true, bool alignCenter = false}) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + 12.height, + if (alignCenter) + (isCapitable ? text.toLowerCase().capitalizeFirstofEach : text).toText12(color: MyColors.normalTextColor).center + else + (isCapitable ? text.toLowerCase().capitalizeFirstofEach : text).toText12(color: MyColors.normalTextColor), + 12.height, + ], + ); + } + + /// EIT Forms date formats + + static String getMonthNamedFormat(DateTime date) { + /// it will return like "29-Sep-2022" + return DateFormat('dd-MMM-yyyy').format(date); + } + + static String reverseFormatDate(String date) { + String formattedDate; + if (date.isNotEmpty) { + formattedDate = date.replaceAll('/', '-'); + formattedDate = formattedDate.replaceAll(' 00:00:00', ''); + } else { + formattedDate = date; + } + return formattedDate; + } + + static String formatStandardDate(String date) { + String formattedDate; + if (date.isNotEmpty) { + formattedDate = date.replaceAll('-', '/'); + } else { + formattedDate = date; + } + return formattedDate; + } + + static String reverseFormatStandardDate(String date) { + String formattedDate; + if (date.isNotEmpty) { + formattedDate = date.replaceAll('/', '-'); + } else { + formattedDate = date; + } + return formattedDate; + } + + static String formatDate(String date) { + String formattedDate; + + if (date.isNotEmpty) { + date = date.substring(0, 10); + formattedDate = date.replaceAll('-', '/'); + formattedDate = formattedDate + ' 00:00:00'; + } else { + formattedDate = date; + } + return formattedDate; + } + + static String formatDateNew(String date) { + String formattedDate; + if (date.isNotEmpty) { + formattedDate = date.split('T')[0]; + if(!formattedDate.contains("00:00:00")) { + formattedDate = formattedDate + ' 00:00:00'; + } + } else { + formattedDate = date; + } + return formattedDate; + } + + static Future selectDate(BuildContext context, DateTime selectedDate) async { + if (!Platform.isIOS) { + await showCupertinoModalPopup( + context: context, + builder: (cxt) => Container( + height: 250, + color: Colors.white, + child: CupertinoDatePicker( + backgroundColor: Colors.white, + mode: CupertinoDatePickerMode.date, + onDateTimeChanged: (value) { + if (value != null && value != selectedDate) { + selectedDate = value; + } + }, + initialDateTime: selectedDate, + ), + ), + ); + } else { + DateTime? picked = await showDatePicker(context: context, initialDate: selectedDate, initialEntryMode: DatePickerEntryMode.calendarOnly, firstDate: DateTime(2015, 8), lastDate: DateTime(2101)); + if (picked != null && picked != selectedDate) { + selectedDate = picked; + } + } + return selectedDate; + } + + static void readNFc({required Function(String) onRead}) { + + NfcManager.instance.startSession(onDiscovered: (NfcTag tag) async { + print(tag.data); + var f; + if (Platform.isAndroid) { + f = MifareUltralight(tag: tag, identifier: tag.data["nfca"]["identifier"], type: 2, maxTransceiveLength: 252, timeout: 22); + } else { + f = MifareUltralight(tag: tag, identifier: tag.data["mifare"]["identifier"], type: 2, maxTransceiveLength: 252, timeout: 22); + } + String identifier = f.identifier.map((e) => e.toRadixString(16).padLeft(2, '0')).join(''); + NfcManager.instance.stopSession(); + onRead(identifier); + }).catchError((err) { + print(err); + }); + } +} diff --git a/lib/config/app_provider.dart b/lib/config/app_provider.dart new file mode 100644 index 0000000..9ccf065 --- /dev/null +++ b/lib/config/app_provider.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class AppProvider extends StatelessWidget { + final Widget child; + + AppProvider({required this.child}); + + @override + Widget build(BuildContext context) { + return child; + return MultiProvider( + providers: [ + // ChangeNotifierProvider(create: (_) => Counter()), + ], + child: child, + ); + } +} diff --git a/lib/config/routes.dart b/lib/config/routes.dart new file mode 100644 index 0000000..1fcbf2e --- /dev/null +++ b/lib/config/routes.dart @@ -0,0 +1,300 @@ +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/ui/attendance/add_vacation_rule_screen.dart'; +import 'package:mohem_flutter_app/ui/attendance/monthly_attendance_screen.dart'; +import 'package:mohem_flutter_app/ui/attendance/vacation_rule_screen.dart'; +import 'package:mohem_flutter_app/ui/bottom_sheets/attendence_details_bottom_sheet.dart'; +import 'package:mohem_flutter_app/ui/chat/chat_detailed_screen.dart'; +import 'package:mohem_flutter_app/ui/chat/chat_home.dart'; +import 'package:mohem_flutter_app/ui/landing/dashboard_screen.dart'; +import 'package:mohem_flutter_app/ui/landing/itg/survey_screen.dart'; +import 'package:mohem_flutter_app/ui/landing/today_attendance_screen.dart'; +import 'package:mohem_flutter_app/ui/landing/today_attendance_screen2.dart'; +import 'package:mohem_flutter_app/ui/leave_balance/add_leave_balance_screen.dart'; +import 'package:mohem_flutter_app/ui/leave_balance/leave_balance_screen.dart'; +import 'package:mohem_flutter_app/ui/login/change_password_screen.dart'; +import 'package:mohem_flutter_app/ui/login/forgot_password_screen.dart'; +import 'package:mohem_flutter_app/ui/login/login_screen.dart'; +import 'package:mohem_flutter_app/ui/login/new_password_screen.dart'; +import 'package:mohem_flutter_app/ui/login/verify_last_login_screen.dart'; +import 'package:mohem_flutter_app/ui/login/verify_login_screen.dart'; +import 'package:mohem_flutter_app/ui/marathon/marathon_intro_screen.dart'; +import 'package:mohem_flutter_app/ui/marathon/marathon_screen.dart'; +import 'package:mohem_flutter_app/ui/marathon/marathon_winner_selection.dart'; +import 'package:mohem_flutter_app/ui/marathon/winner_screen.dart'; +import 'package:mohem_flutter_app/ui/misc/request_submit_screen.dart'; +import 'package:mohem_flutter_app/ui/my_attendance/dynamic_screens/dynamic_input_screen.dart'; +import 'package:mohem_flutter_app/ui/my_attendance/dynamic_screens/dynamic_listview_screen.dart'; +import 'package:mohem_flutter_app/ui/my_attendance/services_menu_list_screen.dart'; + +// import 'package:mohem_flutter_app/ui/my_attendance/my_attendance_screen.dart'; +import 'package:mohem_flutter_app/ui/my_team/create_request.dart'; +import 'package:mohem_flutter_app/ui/my_team/employee_details.dart'; +import 'package:mohem_flutter_app/ui/my_team/my_team.dart'; +import 'package:mohem_flutter_app/ui/my_team/profile_details.dart'; +import 'package:mohem_flutter_app/ui/my_team/subordinate_leave.dart'; +import 'package:mohem_flutter_app/ui/my_team/team_members.dart'; +import 'package:mohem_flutter_app/ui/my_team/view_attendance.dart'; +import 'package:mohem_flutter_app/ui/payslip/monthly_pay_slip_screen.dart'; +import 'package:mohem_flutter_app/ui/performance/performance_appraisal_screen.dart'; +import 'package:mohem_flutter_app/ui/profile/add_update_family_member.dart'; +import 'package:mohem_flutter_app/ui/profile/basic_details.dart'; +import 'package:mohem_flutter_app/ui/profile/contact_details.dart'; +import 'package:mohem_flutter_app/ui/profile/delete_family_member.dart'; +import 'package:mohem_flutter_app/ui/profile/dynamic_screens/dynamic_input_address_screen.dart'; +import 'package:mohem_flutter_app/ui/profile/dynamic_screens/dynamic_input_basic_details_screen.dart'; +import 'package:mohem_flutter_app/ui/profile/family_members.dart'; +import 'package:mohem_flutter_app/ui/profile/personal_info.dart'; +import 'package:mohem_flutter_app/ui/profile/profile_screen.dart'; +import 'package:mohem_flutter_app/ui/screens/announcements/announcement_details.dart'; +import 'package:mohem_flutter_app/ui/screens/announcements/announcements.dart'; + +// import 'package:mohem_flutter_app/ui/my_attendance/work_from_home_screen.dart'; +import 'package:mohem_flutter_app/ui/screens/eit/add_eit.dart'; +import 'package:mohem_flutter_app/ui/screens/items_for_sale/add_new_item_for_sale.dart'; +import 'package:mohem_flutter_app/ui/screens/items_for_sale/item_for_sale_detail.dart'; +import 'package:mohem_flutter_app/ui/screens/items_for_sale/items_for_sale_home.dart'; +import 'package:mohem_flutter_app/ui/screens/mowadhafhi/mowadhafhi_home.dart'; +import 'package:mohem_flutter_app/ui/screens/mowadhafhi/mowadhafhi_hr_request.dart'; +import 'package:mohem_flutter_app/ui/screens/mowadhafhi/request_details.dart'; +import 'package:mohem_flutter_app/ui/screens/my_requests/my_requests.dart'; +import 'package:mohem_flutter_app/ui/screens/my_requests/new_request.dart'; +import 'package:mohem_flutter_app/ui/screens/offers_and_discounts/offers_and_discounts_details.dart'; +import 'package:mohem_flutter_app/ui/screens/offers_and_discounts/offers_and_discounts_home.dart'; +import 'package:mohem_flutter_app/ui/screens/pending_transactions/pending_transactions.dart'; +import 'package:mohem_flutter_app/ui/screens/pending_transactions/pending_transactions_details.dart'; +import 'package:mohem_flutter_app/ui/screens/submenu_screen.dart'; +import 'package:mohem_flutter_app/ui/termination/end_employement.dart'; +import 'package:mohem_flutter_app/ui/work_list/item_history_screen.dart'; +import 'package:mohem_flutter_app/ui/work_list/itg_detail_screen.dart'; +import 'package:mohem_flutter_app/ui/work_list/work_list_screen.dart'; +import 'package:mohem_flutter_app/ui/work_list/worklist_detail_screen.dart'; +import 'package:mohem_flutter_app/ui/work_list/worklist_settings.dart'; + +class AppRoutes { + static const String splash = "/splash"; + static const String registerSelection = "/registerSelection"; + static const String loginVerifyAccount = "/loginVerifyAccount"; + static const String login = "/login"; + static const String verifyLogin = "/verifyLogin"; + static const String verifyLastLogin = "/verifyLastLogin"; + static const String forgotPassword = "/forgotPassword"; + static const String newPassword = "/newPassword"; + static const String loginVerification = "/loginVerification"; + static const String dashboard = "/dashboard"; + static const String todayAttendance = "/todayAttendance"; + static const String subMenuScreen = "/submenuScreen"; + static const String addEitScreen = "/addeitScreen"; + static const String initialRoute = login; + static const String survey = "/survey"; + + //Work List + static const String workList = "/workList"; + static const String workListDetail = "/workListDetail"; + static const String itgDetail = "/itgDetail"; + static const String itemHistory = "/itemHistory"; + static const String worklistSettings = "/worklistSettings"; + + // Leave Balance + static const String leaveBalance = "/leaveBalance"; + static const String addLeaveBalance = "/addLeaveBalance"; + + static const String servicesMenuListScreen = "/servicesMenuListScreen"; + static const String dynamicScreen = "/dynamicScreen"; + static const String addDynamicInput = "/addDynamicInput"; + static const String requestSubmitScreen = "/requestSubmitScreen"; + + //profile + + static const String addDynamicInputProfile = 'addDynamicInputProfile'; + static const String addDynamicAddressScreen = 'addDynamicAddressProfile'; + + //Attendance + static const String attendance = "/attendance"; + static const String monthlyAttendance = "/monthlyAttendance"; + static const String vacationRule = "/vacationRule"; + static const String addVacationRule = "/addVacationRule"; + + //Bottom Sheet + static const String attendanceDetailsBottomSheet = + "/attendanceDetailsBottomSheet"; + + //Profile + static const String profile = "/profile"; + static const String personalInfo = "/personalInfo"; + static const String basicDetails = "/basicDetails"; + static const String contactDetails = "/contactDetails"; + static const String familyMembers = "/familyMembers"; + static const String deleteFamilyMember = "/deleteFamilyMember"; + static const String addUpdateFamilyMember = "/addUpdateFamilyMember"; + + // Mowadhafhi + static const String mowadhafhi = "/mowadhafhi"; + static const String mowadhafhiDetails = "/mowadhafhiDetails"; + static const String mowadhafhiHRRequest = "/mowadhafhiHRRequest"; + + // Pending Transactions + static const String pendingTransactions = "/pendingTransactions"; + static const String pendingTransactionsDetails = + "/pendingTransactionsDetails"; + + // Announcements + static const String announcements = "/announcements"; + static const String announcementsDetails = "/announcementsDetails"; + + // My Requests + static const String myRequests = "/myRequests"; + static const String newRequest = "/newRequests"; + + // Items For Sale + static const String itemsForSale = "/itemsForSale"; + static const String itemsForSaleDetail = "/itemsForSaleDetail"; + static const String addNewItemForSale = "/addNewItemForSale"; + + // Offers & Discounts + static const String offersAndDiscounts = "/offersAndDiscounts"; + static const String offersAndDiscountsDetails = "/offersAndDiscountsDetails"; + + //Pay slip + static const String monthlyPaySlip = "/monthlyPaySlip"; + + //performance evaluation + + static const String performanceEvaluation = "/performanceEvaluation"; + + static const String endEmploymentScreen = "/endEmploymentScreen"; + + //My Team + static const String myTeam = "/myTeam"; + static const String employeeDetails = "/employeeDetails"; + static const String profileDetails = "/profileDetails"; + static const String viewAttendance = "/viewAttendance"; + static const String teamMembers = "/teamMembers"; + static const String createRequest = "/createRequest"; + static const String subordinateLeave = "/subordinateLeave"; + + static const String changePassword = "/changePassword"; + + //Chat + static const String chat = "/chat"; + static const String chatDetailed = "/chatDetailed"; + + //Marathon + static const String marathonIntroScreen = "/marathonIntroScreen"; + static const String marathonScreen = "/marathonScreen"; + static const String marathonWinnerSelection = "/marathonWinnerSelection"; + static const String marathonWinnerScreen = "/marathonWinnerScreen"; + + static final Map routes = { + login: (BuildContext context) => LoginScreen(), + verifyLogin: (BuildContext context) => VerifyLoginScreen(), + verifyLastLogin: (BuildContext context) => VerifyLastLoginScreen(), + dashboard: (BuildContext context) => DashboardScreen(), + survey: (BuildContext context) => SurveyScreen(), + + subMenuScreen: (BuildContext context) => SubMenuScreen(), + newPassword: (BuildContext context) => NewPasswordScreen(), + forgotPassword: (BuildContext context) => ForgotPasswordScreen(), + todayAttendance: (BuildContext context) => TodayAttendanceScreen2(), + //eit + + addEitScreen: (BuildContext context) => AddEITScreen(), + + //Work List + workList: (BuildContext context) => WorkListScreen(), + workListDetail: (BuildContext context) => WorkListDetailScreen(), + itgDetail: (BuildContext context) => ItgDetailScreen(), + itemHistory: (BuildContext context) => ItemHistoryScreen(), + worklistSettings: (BuildContext context) => WorklistSettings(), + + // Leave Balance + + leaveBalance: (BuildContext context) => LeaveBalance(), + addLeaveBalance: (BuildContext context) => AddLeaveBalanceScreen(), + + servicesMenuListScreen: (BuildContext context) => ServicesMenuListScreen(), + // workFromHome: (BuildContext context) => WorkFromHomeScreen(), + // addWorkFromHome: (BuildContext context) => AddWorkFromHomeScreen(), + profile: (BuildContext context) => ProfileScreen(), + //Attendance + monthlyAttendance: (BuildContext context) => MonthlyAttendanceScreen(), + vacationRule: (BuildContext context) => VacationRuleScreen(), + addVacationRule: (BuildContext context) => AddVacationRuleScreen(), + + //Bottom Sheet + attendanceDetailsBottomSheet: (BuildContext context) => + AttendenceDetailsBottomSheet(), + + //Profile + //profile: (BuildContext context) => Profile(), + //profile: (BuildContext context) => Profile(), + personalInfo: (BuildContext context) => PersonalInfo(), + basicDetails: (BuildContext context) => BasicDetails(), + contactDetails: (BuildContext context) => ContactDetails(), + familyMembers: (BuildContext context) => FamilyMembers(), + dynamicScreen: (BuildContext context) => DynamicListViewScreen(), + addDynamicInput: (BuildContext context) => DynamicInputScreen(), + addDynamicInputProfile: (BuildContext context) => + DynamicInputScreenProfile(), + addDynamicAddressScreen: (BuildContext context) => + DynamicInputScreenAddress(), + + deleteFamilyMember: (BuildContext context) => + DeleteFamilyMember(ModalRoute.of(context)!.settings.arguments as int), + requestSubmitScreen: (BuildContext context) => RequestSubmitScreen(), + addUpdateFamilyMember: (BuildContext context) => AddUpdateFamilyMember(), + + //mowadhafhi + mowadhafhi: (BuildContext context) => MowadhafhiHome(), + mowadhafhiDetails: (BuildContext context) => MowadhafhiRequestDetails(), + mowadhafhiHRRequest: (BuildContext context) => MowadhafhiHRRequest(), + + pendingTransactions: (BuildContext context) => PendingTransactions(), + pendingTransactionsDetails: (BuildContext context) => + PendingTransactionsDetails(), + + announcements: (BuildContext context) => Announcements(), + announcementsDetails: (BuildContext context) => AnnouncementDetails(), + + //My Requests + myRequests: (BuildContext context) => MyRequests(), + newRequest: (BuildContext context) => NewRequest(), + + // Items for sale + itemsForSale: (BuildContext context) => ItemsForSale(), + itemsForSaleDetail: (BuildContext context) => ItemForSaleDetailPage(), + addNewItemForSale: (BuildContext context) => AddNewItemForSale(), + + // Offers & Discounts + offersAndDiscounts: (BuildContext context) => OffersAndDiscountsHome(), + offersAndDiscountsDetails: (BuildContext context) => + OffersAndDiscountsDetails(), + + //pay slip + monthlyPaySlip: (BuildContext context) => MonthlyPaySlipScreen(), + performanceEvaluation: (BuildContext context) => PerformanceAppraisal(), + + endEmploymentScreen: (BuildContext context) => EndEmploymentScreen(), + + //My Team + myTeam: (BuildContext context) => MyTeam(), + employeeDetails: (BuildContext context) => EmployeeDetails(), + profileDetails: (BuildContext context) => ProfileDetails(), + viewAttendance: (BuildContext context) => ViewAttendance(), + teamMembers: (BuildContext context) => TeamMembers(), + createRequest: (BuildContext context) => CreateRequest(), + subordinateLeave: (BuildContext context) => SubordinateLeave(), + + changePassword: (BuildContext context) => ChangePasswordScreen(), + + //Chat + chat: (BuildContext context) => ChatHomeScreen(), + chatDetailed: (BuildContext context) => ChatDetailScreen(), + + // Marathon + marathonIntroScreen: (BuildContext context) => MarathonIntroScreen(), + marathonScreen: (BuildContext context) => MarathonScreen(), + marathonWinnerSelection: (BuildContext context) => + MarathonWinnerSelection(), + marathonWinnerScreen: (BuildContext context) => WinnerScreen(), + }; +} diff --git a/lib/dialogs/otp_dialog.dart b/lib/dialogs/otp_dialog.dart new file mode 100644 index 0000000..9158b94 --- /dev/null +++ b/lib/dialogs/otp_dialog.dart @@ -0,0 +1,274 @@ +import 'dart:async'; + +import 'package:easy_localization/src/public_ext.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/widgets/button/default_button.dart'; +import 'package:mohem_flutter_app/widgets/otp_widget.dart'; +import 'package:sizer/sizer.dart'; + +final ValueNotifier otpFieldClear = ValueNotifier(""); + +class OtpDialog { + final int type; + final int? mobileNo; + final Function(String, TextEditingController _pinPutController) onSuccess; + final Function onFailure; + final BuildContext context; + final Function onResendCode; + + int remainingTime = 120; + + Future? timer; + + static BuildContext? _context; + + static bool? _loading; + + OtpDialog(this.context, this.type, this.mobileNo, this.onSuccess, this.onFailure, {required this.onResendCode}); + + GlobalKey? verifyAccountForm = GlobalKey(); + + final TextEditingController _pinPutController = TextEditingController(); + + TextEditingController digit1 = TextEditingController(text: ""); + TextEditingController digit2 = TextEditingController(text: ""); + TextEditingController digit3 = TextEditingController(text: ""); + TextEditingController digit4 = TextEditingController(text: ""); + + Map verifyAccountFormValue = { + 'digit1': '', + 'digit2': '', + 'digit3': '', + 'digit4': '', + }; + final focusD1 = FocusNode(); + final focusD2 = FocusNode(); + final focusD3 = FocusNode(); + final focusD4 = FocusNode(); + String? errorMsg; + + // ProjectViewModel projectProvider; + String displayTime = ''; + String? _code; + dynamic setState; + bool stopTimer = false; + + // static String signature; + + void displayDialog(BuildContext context) async { + return showDialog( + context: context, + barrierColor: Colors.black.withOpacity(0.63), + builder: (context) { + // projectProvider = Provider.of(context); + return Dialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + insetPadding: const EdgeInsets.only(left: 21, right: 21), + child: StatefulBuilder(builder: (context, setState) { + if (displayTime == '') { + startTimer(setState); + } + + return Container( + padding: EdgeInsets.all(21), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + type == 1 + ? "assets/images/login/verify_sms.svg" + : type == 2 + ? "assets/images/login/verify_whatsapp.svg" + : type == 3 + ? "assets/images/login/verify_face.svg" + : 'assets/images/login/verify_thumb.svg', + height: 50, + width: 50, + ), + 12.height, + LocaleKeys.otp.tr().toText14(), + LocaleKeys.verification.tr().toText24(isBold: true), + 6.height, + (LocaleKeys.pleaseEnterTheVerificationCodeSentTo.tr() + ' xxxxxxxx' + mobileNo.toString().substring(mobileNo.toString().length - 3)).toText16(), + 18.height, + ValueListenableBuilder( + builder: (BuildContext context, String value, Widget? child) { + // This builder will only get called when the _counter + // is updated. + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: OTPWidget( + autoFocus: true, + controller: _pinPutController, + defaultBorderColor: const Color(0xffD8D8D8), + maxLength: 4, + onTextChanged: (text) {}, + pinBoxColor: Colors.white, + onDone: (code) => _onOtpCallBack(code, null), + textBorderColor: const Color(0xffD8D8D8), + pinBoxWidth: 60, + pinBoxHeight: 60, + pinTextStyle: const TextStyle(fontSize: 24.0, color: MyColors.darkTextColor), + pinTextAnimatedSwitcherTransition: ProvidedPinBoxTextAnimation.scalingTransition, + pinTextAnimatedSwitcherDuration: const Duration(milliseconds: 300), + pinBoxRadius: 10, + keyboardType: TextInputType.number, + ), + ), + ), + 10.height, + stopTimer + ? Row( + children: [ + Expanded( + child: LocaleKeys.codeExpire.tr().toText16( + color: MyColors.redColor, + ), + ), + 12.width, + Image.asset( + "assets/icons/ic_alarm.png", + width: 20, + height: 20, + color: MyColors.redColor, + ), + ], + ) + : RichText( + text: TextSpan( + text: LocaleKeys.theVerificationCodeWillExpireIn.tr() + '\n', + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.darkTextColor, letterSpacing: -0.48), + children: [ + TextSpan( + text: displayTime, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.textMixColor, letterSpacing: -0.48), + ), + ], + ), + ), + ], + ); + }, + valueListenable: otpFieldClear, + ), + 18.height, + DefaultButton( + stopTimer ? LocaleKeys.resend.tr() : LocaleKeys.cancel.tr(), + () async { + if (stopTimer) { + hideSMSBox(context); + onResendCode(); + } else { + stopTimer = true; + // onFailure(); + hideSMSBox(context); + } + }, + colors: stopTimer + ? null + : [ + MyColors.pinkDarkColor, + MyColors.pinkDarkColor, + ], + ), + ], + ), + ); + }), + ); + }); + } + + InputDecoration buildInputDecoration(BuildContext context) { + return InputDecoration( + counterText: " ", + enabledBorder: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + borderSide: BorderSide(color: Colors.black), + ), + focusedBorder: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + borderSide: BorderSide(color: Theme.of(context).primaryColor), + ), + errorBorder: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + borderSide: BorderSide(color: Theme.of(context).errorColor), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + borderSide: BorderSide(color: Theme.of(context).errorColor), + ), + ); + } + + // String validateCodeDigit(value) { + // if (value.isEmpty) { + // return ' '; + // } else if (value.length == 3) { + // print(value); + // } else { + // return null; + // } + // } + + String getSecondsAsDigitalClock(int inputSeconds) { + int secNum = int.parse(inputSeconds.toString()); // don't forget the second param + int hours = (secNum / 3600).floor(); + int minutes = ((secNum - hours * 3600) / 60).floor(); + double seconds = secNum - hours * 3600 - minutes * 60; + String minutesString = ""; + String secondsString = ""; + minutesString = minutes < 10 ? "0" + minutes.toString() : minutes.toString(); + secondsString = seconds < 10 ? "0" + seconds.toStringAsFixed(0) : seconds.toStringAsFixed(0); + return minutesString + ":" + secondsString; + } + + void startTimer(setState) { + remainingTime--; + if (stopTimer) return; + setState(() { + displayTime = getSecondsAsDigitalClock(remainingTime); + }); + + timer = Future.delayed(const Duration(seconds: 1), () { + if (remainingTime > 0) { + startTimer(setState); + } else { + setState(() { + stopTimer = true; + }); + } + }); + } + + void hideSMSBox(context) { + onFailure(); + } + + void _onOtpCallBack(String otpCode, bool? isAutofill) { + if (otpCode.length == 4) { + // stopTimer = true; + otpFieldClear.value = otpCode; + onSuccess(otpCode, _pinPutController); + } + } + +// static getSignature() async { +// // if (Platform.isAndroid) { +// // return await SmsRetriever.getAppSignature(); +// // } else { +// // return null; +// // } +// } +} diff --git a/lib/exceptions/api_exception.dart b/lib/exceptions/api_exception.dart new file mode 100644 index 0000000..e3046ca --- /dev/null +++ b/lib/exceptions/api_exception.dart @@ -0,0 +1,29 @@ +import 'dart:convert'; + +import 'package:mohem_flutter_app/api/api_client.dart'; + +class APIException implements Exception { + static const String BAD_REQUEST = 'api_common_bad_request'; + static const String UNAUTHORIZED = 'api_common_unauthorized'; + static const String FORBIDDEN = 'api_common_forbidden'; + static const String NOT_FOUND = 'api_common_not_found'; + static const String INTERNAL_SERVER_ERROR = 'api_common_internal_server_error'; + static const String UPGRADE_REQUIRED = 'api_common_upgrade_required'; + static const String BAD_RESPONSE_FORMAT = 'api_common_bad_response_format'; + static const String OTHER = 'api_common_http_error'; + static const String TIMEOUT = 'api_common_http_timeout'; + static const String UNKNOWN = 'unexpected_error'; + + final String message; + final APIError? error; + final arguments; + + const APIException(this.message, {this.arguments, this.error}); + + Map toJson() => {'message': message, 'error': error, 'arguments': '$arguments'}; + + @override + String toString() { + return jsonEncode(this); + } +} diff --git a/lib/extensions/int_extensions.dart b/lib/extensions/int_extensions.dart new file mode 100644 index 0000000..16bdd29 --- /dev/null +++ b/lib/extensions/int_extensions.dart @@ -0,0 +1,15 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; + +extension IntExtensions on int { + Widget get height => SizedBox(height: toDouble()); + + Widget get width => SizedBox(width: toDouble()); + + Widget get divider => Divider(height: toDouble(), thickness: toDouble(), color: MyColors.lightGreyEFColor); + + Widget get makeItSquare => SizedBox(width: toDouble(), height: toDouble()); + + +} diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart new file mode 100644 index 0000000..9af8f2b --- /dev/null +++ b/lib/extensions/string_extensions.dart @@ -0,0 +1,234 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:intl/intl.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; + +extension CapExtension on String { + String get toCamelCase => "${this[0].toUpperCase()}${this.substring(1)}"; + + String get inCaps => '${this[0].toUpperCase()}${this.substring(1)}'; + + String get allInCaps => this.toUpperCase(); + + String get capitalizeFirstofEach => this.trim().length > 0 ? this.trim().toLowerCase().split(" ").map((str) => str.inCaps).join(" ") : ""; +} + +extension EmailValidator on String { + Widget get toWidget => Text(this); + + Widget toText10({Color? color, bool isBold = false, int? maxlines, FontStyle? fontStyle}) => Text( + this, + maxLines: maxlines, + style: TextStyle(fontSize: 10, fontStyle: fontStyle ?? FontStyle.normal, fontWeight: isBold ? FontWeight.bold : FontWeight.w600, color: color ?? MyColors.darkTextColor, letterSpacing: -0.4), + ); + + Widget toText11({Color? color, FontWeight? weight, bool isUnderLine = false, bool isBold = false}) => Text( + this, + style: TextStyle( + fontSize: 11, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.w600), + color: color ?? MyColors.darkTextColor, + letterSpacing: -0.33, + decoration: isUnderLine ? TextDecoration.underline : null, + ), + ); + + Widget toText12({Color? color, bool isUnderLine = false, bool isBold = false, bool isCenter = false, int maxLine = 0}) => Text( + this, + textAlign: isCenter ? TextAlign.center : null, + maxLines: (maxLine > 0) ? maxLine : null, + style: TextStyle( + fontSize: 12, + fontWeight: isBold ? FontWeight.bold : FontWeight.w600, + color: color ?? MyColors.darkTextColor, + letterSpacing: -0.72, + decoration: isUnderLine ? TextDecoration.underline : null, + ), + ); + + Widget toText12Auto({Color? color, bool isUnderLine = false, bool isBold = false, bool isCenter = false, int maxLine = 0}) => AutoSizeText( + this, + textAlign: isCenter ? TextAlign.center : null, + maxLines: (maxLine > 0) ? maxLine : null, + minFontSize: 8, + style: TextStyle( + fontSize: 12, + fontWeight: isBold ? FontWeight.bold : FontWeight.w600, + color: color ?? MyColors.darkTextColor, + letterSpacing: -0.72, + decoration: isUnderLine ? TextDecoration.underline : null, + ), + ); + + Widget toTextAuto({ + Color? color, + bool isUnderLine = false, + bool isBold = false, + bool isCenter = false, + int maxLine = 0, + double fontSize = 12, + double letterSpacing = -0.72, + double height = 1, + }) => + AutoSizeText( + this, + textAlign: isCenter ? TextAlign.center : null, + maxLines: (maxLine > 0) ? maxLine : null, + minFontSize: 5, + style: TextStyle( + fontSize: fontSize, + fontWeight: isBold ? FontWeight.bold : FontWeight.w600, + color: color ?? MyColors.darkTextColor, + letterSpacing: letterSpacing, + decoration: isUnderLine ? TextDecoration.underline : null, + ), + ); + + Widget toText13({Color? color, bool isUnderLine = false}) => Text( + this, + style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: color ?? MyColors.darkTextColor, letterSpacing: -0.52, decoration: isUnderLine ? TextDecoration.underline : null), + ); + + Widget toText14({Color? color, bool isUnderLine = false, bool isBold = false, FontWeight? weight, int? maxlines}) => Text( + this, + maxLines: maxlines, + style: TextStyle( + color: color ?? MyColors.darkTextColor, + fontSize: 14, + letterSpacing: -0.48, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.w600), + decoration: isUnderLine ? TextDecoration.underline : null), + ); + + Widget toText16({Color? color, bool isUnderLine = false, bool isBold = false, int? maxlines}) => Text( + this, + maxLines: maxlines, + style: TextStyle( + color: color ?? MyColors.darkTextColor, + fontSize: 16, + letterSpacing: -0.64, + fontWeight: isBold ? FontWeight.bold : FontWeight.w600, + decoration: isUnderLine ? TextDecoration.underline : null, + ), + ); + + Widget toText17({Color? color, bool isBold = false}) => Text( + this, + style: TextStyle(color: color ?? MyColors.darkTextColor, fontSize: 17, letterSpacing: -0.68, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + ); + + Widget toText18({Color? color, bool isBold = false}) => Text( + this, + style: TextStyle(fontSize: 18, fontWeight: isBold ? FontWeight.bold : FontWeight.w600, color: color ?? MyColors.darkTextColor, letterSpacing: -1.08), + ); + + Widget toText19({Color? color, bool isBold = false}) => Text( + this, + style: TextStyle(fontSize: 19, fontWeight: isBold ? FontWeight.bold : FontWeight.w600, color: color ?? MyColors.darkTextColor, letterSpacing: -1.14), + ); + + Widget toText20({Color? color, bool isBold = false}) => Text( + this, + style: TextStyle(fontSize: 20, fontWeight: isBold ? FontWeight.bold : FontWeight.w600, color: color ?? MyColors.darkTextColor, letterSpacing: -0.4), + ); + + Widget toText21({Color? color, bool isBold = false, FontWeight? weight, int? maxlines}) => Text( + this, + maxLines: maxlines, + style: TextStyle(color: color ?? MyColors.grey3AColor, fontSize: 21, letterSpacing: -0.31, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.w600)), + ); + + Widget toText22({Color? color, bool isBold = false}) => Text( + this, + style: TextStyle(height: 1, color: color ?? MyColors.darkTextColor, fontSize: 22, letterSpacing: -1.44, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + ); + + Widget toText24({Color? color, bool isBold = false}) => Text( + this, + style: TextStyle(height: 23 / 24, color: color ?? MyColors.darkTextColor, fontSize: 24, letterSpacing: -1.44, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + ); + + Widget toText32({Color? color, bool isBold = false}) => Text( + this, + style: TextStyle(height: 32 / 32, color: color ?? MyColors.darkTextColor, fontSize: 32, letterSpacing: -1.92, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + ); + + Widget toText44({Color? color, bool isBold = false}) => Text( + this, + style: TextStyle(height: 32 / 32, color: color ?? MyColors.darkTextColor, fontSize: 44, letterSpacing: -2.64, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + ); + + Widget toSectionHeading({String upperHeading = "", String lowerHeading = ""}) { + String upper = ""; + String lower = ""; + String heading = this; + if (heading.isNotEmpty) { + List data = heading.split(" "); + + if (data.length > 1) { + upper = data[0]; + data.removeAt(0); + lower = data.join(" "); + } else { + lower = data[0]; + } + } + if (upperHeading.isNotEmpty) { + upper = upperHeading; + } + if (lowerHeading.isNotEmpty) { + lower = lowerHeading; + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (upper.isNotEmpty) upper.toText12(), + lower.toText24(isBold: true), + ], + ); + } + + bool isValidEmail() { + return RegExp(r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$').hasMatch(this); + } + + String toFormattedDate() { + String date = this.split("T")[0]; + String time = this.split("T")[1]; + var dates = date.split("-"); + return "${dates[2]} ${getMonth(int.parse(dates[1]))} ${dates[0]} ${DateFormat('hh:mm a').format(DateFormat('hh:mm:ss').parse(time))}"; + } + + String getMonth(int month) { + switch (month) { + case 1: + return "January"; + case 2: + return "February"; + case 3: + return "March"; + case 4: + return "April"; + case 5: + return "May"; + case 6: + return "June"; + case 7: + return "July"; + case 8: + return "August"; + case 9: + return "September"; + case 10: + return "October"; + case 11: + return "November"; + case 12: + return "December"; + default: + return ""; + } + } +} diff --git a/lib/extensions/widget_extensions.dart b/lib/extensions/widget_extensions.dart new file mode 100644 index 0000000..64aaae8 --- /dev/null +++ b/lib/extensions/widget_extensions.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:shimmer/shimmer.dart'; + +extension WidgetExtensions on Widget { + Widget onPress(VoidCallback onTap) => InkWell(onTap: onTap, child: this); + + Widget get expanded => Expanded(child: this); + + Widget get center => Center(child: this); + + Widget circle(double _value) => ClipRRect(borderRadius: BorderRadius.circular(_value), child: this); + + Widget paddingAll(double _value) => Padding(padding: EdgeInsets.all(_value), child: this); + + Widget paddingOnly({double left = 0.0, double right = 0.0, double top = 0.0, double bottom = 0.0}) => + Padding(padding: EdgeInsets.only(left: left, right: right, top: top, bottom: bottom), child: this); + + Widget toExpanded({int flex = 1}) => Expanded(flex: flex, child: this); + + Widget toShimmer({bool isShow = true}) => isShow + ? Shimmer.fromColors( + baseColor: Color(0xffe8eff0), + highlightColor: Colors.white, + child: Container( + color: Colors.white, + child: this, + ), + ) + : Container( + child: this, + ); + + Widget animatedSwither() => AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + // transitionBuilder: (Widget child, Animation animation) { + // return ScaleTransition(scale: animation, child: child); + // }, + switchInCurve: Curves.linearToEaseOut, + switchOutCurve: Curves.linearToEaseOut, + child: this, + ); + + Widget objectContainerView({String title = "", String note = "", bool disablePadding = false, double radius = 15}) { + return Container( + padding: disablePadding ? EdgeInsets.zero : const EdgeInsets.only(top: 15, bottom: 15, left: 14, right: 14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(radius), + boxShadow: [ + BoxShadow( + color: const Color(0xff000000).withOpacity(.05), + blurRadius: 26, + offset: const Offset(0, -3), + ), + ], + ), + alignment: Alignment.center, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (title.isNotEmpty) title.toText16(), + if (title.isNotEmpty) 12.height, + this, + if (note.isNotEmpty) note.toText11(), + ], + ), + ); + } + + Widget objectContainerBorderView( + {String title = "", + String note = "", + bool disablePadding = false, + double radius = 15, + Color? color, + Color borderColor = MyColors.lightGreyEFColor, + bool disableWidth = false, + bool isAlignment = false}) { + return Container( + padding: disablePadding ? EdgeInsets.zero : const EdgeInsets.only(top: 15, bottom: 15, left: 14, right: 14), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(radius), + color: color, + border: Border.all( + color: borderColor, + width: disableWidth ? 2 : 1, + ), + ), + alignment: isAlignment ? Alignment.center : null, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (title.isNotEmpty) title.toText16(), + if (title.isNotEmpty) 12.height, + this, + if (note.isNotEmpty) note.toText11(), + ], + ), + ); + } +} diff --git a/lib/provider/dashboard_provider_model.dart b/lib/provider/dashboard_provider_model.dart new file mode 100644 index 0000000..5b9f8f1 --- /dev/null +++ b/lib/provider/dashboard_provider_model.dart @@ -0,0 +1,274 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/api/dashboard_api_client.dart'; +import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; +import 'package:mohem_flutter_app/config/routes.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/main.dart'; +import 'package:mohem_flutter_app/models/dashboard/drawer_menu_item_model.dart'; +import 'package:mohem_flutter_app/models/dashboard/get_accrual_balances_list_model.dart'; +import 'package:mohem_flutter_app/models/dashboard/get_attendance_tracking_list_model.dart'; +import 'package:mohem_flutter_app/models/dashboard/get_open_notifications_list.dart'; +import 'package:mohem_flutter_app/models/dashboard/itg_forms_model.dart'; +import 'package:mohem_flutter_app/models/dashboard/list_menu.dart'; +import 'package:mohem_flutter_app/models/dashboard/menu_entries.dart'; +import 'package:mohem_flutter_app/models/dashboard/menus.dart'; +import 'package:mohem_flutter_app/models/dashboard/mohemm_itg_pending_task_responseitem.dart'; +import 'package:mohem_flutter_app/models/generic_response_model.dart'; +import 'package:mohem_flutter_app/models/itg/itg_response_model.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart'; +import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart'; + +/// Mix-in [DiagnosticableTreeMixin] to have access to [debugFillProperties] for the devtool +// ignore: prefer_mixin +class DashboardProviderModel with ChangeNotifier, DiagnosticableTreeMixin { + //Attendance Tracking + bool isAttendanceTrackingLoading = true; + int endTime = 0, isTimeRemainingInSeconds = 0; + double progress = 0.0; + GetAttendanceTracking? attendanceTracking; + + //Work List + bool isWorkListLoading = true; + int workListCounter = 0; + + //Misssing Swipe + bool isMissingSwipeLoading = true; + int missingSwipeCounter = 0; + + //Leave and Ticket Balance + bool isLeaveTicketBalanceLoading = true; + List? accrualList; + GetAccrualBalancesList? leaveBalanceAccrual; + + double get leaveBalance => leaveBalanceAccrual?.accrualNetEntitlement ?? 0; + double ticketBalance = 0; + + //Menu Entries + bool isServicesMenusLoading = true; + List? homeMenus; + List? getMenuEntriesList; + + //Offers And Discounts + bool isOffersLoading = true; + List getOffersList = []; + + //Attendance Tracking API's & Methods + Future fetchAttendanceTracking(context) async { + try { + attendanceTracking = await DashboardApiClient().getAttendanceTracking(); + isAttendanceTrackingLoading = false; + // isTimeRemainingInSeconds = calculateSeconds( "00:00:00"); + if (attendanceTracking?.pSwipeIn != null) { + isTimeRemainingInSeconds = calculateSeconds(attendanceTracking!.pRemainingHours ?? "00:00:00"); + int totalShiftTimeInSeconds = calculateSeconds(attendanceTracking!.pScheduledHours ?? "00:00:00"); + progress = (isTimeRemainingInSeconds / totalShiftTimeInSeconds); + endTime = DateTime.now().millisecondsSinceEpoch + Duration(seconds: isTimeRemainingInSeconds).inMilliseconds; + } + notifyListeners(); + } catch (ex) { + Utils.handleException(ex, context, null); + } + return true; + } + + void initProvider() { + isAttendanceTrackingLoading = true; + endTime = 0; + isTimeRemainingInSeconds = 0; + progress = 0.0; + attendanceTracking = null; + + isWorkListLoading = true; + workListCounter = 0; + + isMissingSwipeLoading = true; + missingSwipeCounter = 0; + + isLeaveTicketBalanceLoading = true; + accrualList = null; + leaveBalanceAccrual = null; + + ticketBalance = 0; + isServicesMenusLoading = true; + homeMenus = null; + getMenuEntriesList = null; + isOffersLoading = true; + getOffersList = []; + + drawerMenuItemList = [ + DrawerMenuItem("assets/images/drawer/my_profile.svg", LocaleKeys.myProfile.tr(), AppRoutes.profile), + DrawerMenuItem("assets/images/drawer/performance_evaluation.svg", LocaleKeys.performanceEvaluation.tr(), AppRoutes.performanceEvaluation), + DrawerMenuItem("assets/images/drawer/mowadhafi.svg", LocaleKeys.mowadhafhi.tr(), AppRoutes.mowadhafhi), + DrawerMenuItem("assets/images/drawer/pending_trasactions.svg", LocaleKeys.pendingTransactions.tr(), AppRoutes.pendingTransactions), + DrawerMenuItem("assets/images/drawer/drawer_marathon.svg", LocaleKeys.brainMarathon.tr(), AppRoutes.marathonIntroScreen), + DrawerMenuItem("assets/images/drawer/change_password.svg", LocaleKeys.changePassword.tr(), AppRoutes.changePassword), + ]; + + notifyListeners(); + } + + int calculateSeconds(String time) { + int hour = int.parse(time.split(":")[0]); + int mints = int.parse(time.split(":")[1]); + int seconds = int.parse(time.split(":")[2]); + return ((hour * 60 * 60) + (mints * 60) + seconds); + } + + void update(context) { + fetchAttendanceTracking(context); + } + + ItgFormsModel? itgFormsModel; + List? getOpenNotificationsList; + MohemmITGPendingTaskResponseItem? cocCount; + int cocFinalCount = 0; + + //Work List API's & Methods + Future fetchWorkListCounter(context, {bool showLoading = false}) async { + try { + if (showLoading) Utils.showLoading(context); + GenericResponseModel? genericResponseModel = await DashboardApiClient().getOpenNotifications(); + + isWorkListLoading = false; + getOpenNotificationsList = genericResponseModel?.getOpenNotificationsList; + + workListCounter = genericResponseModel?.pOPENNTFNUMBER ?? 0; + + itgFormsModel = await DashboardApiClient().getItgFormsPendingTask(); + workListCounter = workListCounter + (itgFormsModel?.totalCount ?? 0); + GenericResponseModel? cocGenericResponseModel = await DashboardApiClient().getCOCNotifications(); + cocCount = cocGenericResponseModel?.mohemmITGPendingTaskResponseItem; + if (cocCount != null) { + cocFinalCount = (cocCount?.escalation ?? 0) + (cocCount?.waitingToClose ?? 0) + (cocCount?.waitingForAcceptance ?? 0) + (cocCount?.extendTATRequest ?? 0); + workListCounter += cocFinalCount; + } + if (showLoading) Utils.hideLoading(context); + notifyListeners(); + } catch (ex) { + isWorkListLoading = false; + logger.wtf(ex); + if (showLoading) Utils.hideLoading(context); + notifyListeners(); + Utils.handleException(ex, context, (err) { + Utils.hideLoading(context); + showDialog( + context: context, + builder: (cxt) => ConfirmDialog( + message: err, + onTap: () { + Navigator.pushNamedAndRemoveUntil(cxt, AppRoutes.login, (Route route) => false); + }, + ), + ); + }); + } + } + + //Missing Siwpe API's & Methods + Future fetchMissingSwipe(context) async { + try { + GenericResponseModel? genericResponseModel = await DashboardApiClient().getOpenMissingSwipes(); + isMissingSwipeLoading = false; + missingSwipeCounter = genericResponseModel!.getOpenMissingSwipesList!.pOpenMissingSwipes ?? 0; + notifyListeners(); + } catch (ex) { + isMissingSwipeLoading = false; + logger.wtf(ex); + notifyListeners(); + Utils.handleException(ex, context, null); + } + } + + //Leave and Ticket Balance API's & Methods + Future fetchLeaveTicketBalance(context, DateTime date) async { + try { + accrualList = await DashboardApiClient().getAccrualBalances(DateFormat("MM/dd/yyyy").format(date)); + isLeaveTicketBalanceLoading = false; + leaveBalanceAccrual = accrualList![0]; + ticketBalance = (accrualList![1].accrualNetEntitlement ?? 0.0) + (accrualList![2].accrualNetEntitlement ?? 0.0); + notifyListeners(); + } catch (ex) { + isLeaveTicketBalanceLoading = false; + logger.wtf(ex); + notifyListeners(); + Utils.handleException(ex, context, null); + } + } + + //List Menu API's & Methods + + List drawerMenuItemList = []; + + void fetchListMenu() async { + try { + List menuList = await DashboardApiClient().getListMenu(); + List findMyRequest = menuList.where((element) => element.menuType == "E").toList(); + if (findMyRequest.isNotEmpty) { + drawerMenuItemList.insert(3, DrawerMenuItem("assets/images/drawer/my_requests.svg", LocaleKeys.myRequest.tr(), AppRoutes.myRequests)); + } + List findMyTeam = menuList.where((element) => element.menuType == "M").toList(); + if (findMyTeam.isNotEmpty) { + drawerMenuItemList.insert(2, DrawerMenuItem("assets/images/drawer/my_team.svg", LocaleKeys.myTeamMembers.tr(), AppRoutes.myTeam)); + } + } catch (ex) { + logger.wtf(ex); + Utils.handleException(ex, null, null); + } + } + + //Menu Entries API's & Methods + void fetchMenuEntries() async { + try { + GenericResponseModel? genericResponseModel = await DashboardApiClient().getGetMenuEntries(); + getMenuEntriesList = genericResponseModel!.getMenuEntriesList; + homeMenus = parseMenus(getMenuEntriesList ?? []); + if (homeMenus!.isNotEmpty) { + homeMenus!.first.menuEntiesList.insert(0, GetMenuEntriesList(requestType: "MONTHLY_ATTENDANCE", prompt: LocaleKeys.monthlyAttendance.tr())); + homeMenus!.first.menuEntiesList.add(GetMenuEntriesList(requestType: "VACATION_RULE", prompt: LocaleKeys.vacationRule.tr())); + } + isServicesMenusLoading = false; + notifyListeners(); + } catch (ex) { + logger.wtf(ex); + notifyListeners(); + Utils.handleException(ex, null, null); + } + } + + void getCategoryOffersListAPI(BuildContext context) async { + try { + // Utils.showLoading(context); + getOffersList = await OffersAndDiscountsApiClient().getOffersList(0, 6); + isOffersLoading = false; + notifyListeners(); + } catch (ex) { + // Utils.hideLoading(context); + notifyListeners(); + Utils.handleException(ex, context, null); + } + } + + List parseMenus(List getMenuEntriesList) { + List menus = []; + for (int i = 0; i < getMenuEntriesList.length; i++) { + if (getMenuEntriesList[i].parentMenuName!.isEmpty) { + menus.add(Menus(getMenuEntriesList[i], getMenuEntriesList.where((element) => getMenuEntriesList[i].menuName == element.parentMenuName).toList())); + } + } + return menus; + } + + Future getITGNotification() async { + MohemmItgResponseItem? res = await DashboardApiClient().getITGPageNotification(); + return res; + } + + + + void notify() { + notifyListeners(); + } +} diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart new file mode 100644 index 0000000..586c3aa --- /dev/null +++ b/lib/theme/app_theme.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/theme/colors.dart'; + +class AppTheme { + static ThemeData getTheme(isArabic) => ThemeData( + fontFamily: isArabic ? 'Cairo' : 'Poppins', + primarySwatch: Colors.red, + visualDensity: VisualDensity.adaptivePlatformDensity, + brightness: Brightness.light, + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + }, + ), + hintColor: Colors.grey[400], + colorScheme: ColorScheme.fromSwatch(accentColor: MyColors.backgroundColor), + disabledColor: Colors.grey[300], + errorColor: const Color.fromRGBO(235, 80, 60, 1.0), + scaffoldBackgroundColor: MyColors.backgroundColor, + textSelectionTheme: const TextSelectionThemeData(cursorColor: Colors.grey, selectionColor: Color.fromRGBO(80, 100, 253, 0.5), selectionHandleColor: Colors.grey), + canvasColor: Colors.white, + backgroundColor: const Color.fromRGBO(255, 255, 255, 1), + highlightColor: Colors.grey[100]!.withOpacity(0.4), + splashColor: Colors.transparent, + primaryColor: primaryColor, + primaryColorDark: primaryColor, + buttonColor: Colors.black, + toggleableActiveColor: secondaryColor, + indicatorColor: secondaryColor, + bottomSheetTheme: const BottomSheetThemeData( + backgroundColor: Color(0xFFE0E0E0), + ), + primaryTextTheme: const TextTheme( + bodyText2: TextStyle(color: Colors.white), + ), + iconTheme: const IconThemeData(color: MyColors.darkIconColor), + textTheme: const TextTheme( + bodyText1: TextStyle(color: Colors.black, letterSpacing: 0.6), + headline1: TextStyle(color: Colors.white, letterSpacing: 0.6), + headline2: TextStyle(color: Colors.white, letterSpacing: 0.6), + ), + floatingActionButtonTheme: const FloatingActionButtonThemeData(highlightElevation: 2, disabledElevation: 0, elevation: 2), + appBarTheme: AppBarTheme( + color: const Color(0xff515A5D), + brightness: Brightness.light, + elevation: 0.0, + actionsIconTheme: IconThemeData( + color: Colors.grey[800], + ), + ), + ); +} + +extension ExtendedRevoCheckTheme on TextTheme { + //add custom styles and colors here + //taken from https://medium.com/@crizantlai/flutter-how-to-extend-themedata-b5b987a95bb5 + TextStyle get price => const TextStyle(color: Colors.redAccent); +} diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart new file mode 100644 index 0000000..67b5a6f --- /dev/null +++ b/lib/theme/colors.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +const Color primaryColor = Color(0xffF8F8F8); +const Color secondaryColor = Color(0xff2BB8A6); +const Color accentColor = Colors.blue; +const Color appBackgroundColor = Colors.white; +Color? accentColorDark = Colors.green[800]; +const Color borderColor = Color(0xffE8E8E8); +Color? borderLightColor = Colors.blueGrey[50]; +Color backgroudColor = Colors.blueGrey[50]!.withOpacity(0.5); +const Color iconColor = Colors.blueGrey; +Color? headingColor = Colors.blueGrey[800]; +Color? txtColor = Colors.blueGrey[500]; diff --git a/lib/ui/a.dart b/lib/ui/a.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/ui/login/splash_page.dart b/lib/ui/login/splash_page.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/widgets/Updater.dart b/lib/widgets/Updater.dart new file mode 100644 index 0000000..82cc172 --- /dev/null +++ b/lib/widgets/Updater.dart @@ -0,0 +1,39 @@ +/* ZiK */ + +import 'dart:async'; +import 'package:flutter/cupertino.dart'; + +typedef ChildProvider = Widget Function(BuildContext context, E? data); + +class Updater extends StatelessWidget{ + final ChildProvider childProvider; + StreamController? sink; + T? initialData; + List _history = []; + + Stream? _stream; + Updater({T? initialData, required this.childProvider}){ + this.sink = StreamController(); + this.initialData = initialData; + _stream = this.sink?.stream; + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + initialData: this.initialData, + stream: _stream, + builder: (ctx, snapshot){ + return childProvider(context, snapshot.data); + }); + } + + void pushData(T? data) { + _history.add(data); + sink?.sink.add(data); + } + + List getDataHistory() => _history; + T? getLatestData() => _history.last; + +} \ No newline at end of file diff --git a/lib/widgets/app_bar_widget.dart b/lib/widgets/app_bar_widget.dart new file mode 100644 index 0000000..745dedf --- /dev/null +++ b/lib/widgets/app_bar_widget.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/config/routes.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; + +AppBar AppBarWidget(BuildContext context, + {required String title, + bool showHomeButton = true, + bool showNotificationButton = false, + bool showMemberButton = false, + String? image}) { + return AppBar( + leadingWidth: 0, + // leading: GestureDetector( + // behavior: HitTestBehavior.opaque, + // onTap: Feedback.wrapForTap(() => Navigator.maybePop(context), context), + // child: const Icon(Icons.arrow_back_ios, color: MyColors.darkIconColor), + // ), + //titleSpacing: -1.44, + title: Row( + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: + Feedback.wrapForTap(() => Navigator.maybePop(context), context), + child: + const Icon(Icons.arrow_back_ios, color: MyColors.darkIconColor), + ), + 4.width, + if (image != null) SvgPicture.asset( + image, + height: 40, + width: 40, + ), + if (image != null) 14.width, + title.toText24(color: MyColors.darkTextColor, isBold: true).expanded, + ], + ), + centerTitle: false, + elevation: 0, + backgroundColor: Colors.white, + actions: [ + if (showHomeButton) + IconButton( + onPressed: () { + Navigator.popUntil( + context, ModalRoute.withName(AppRoutes.dashboard)); + }, + icon: const Icon(Icons.home, color: MyColors.darkIconColor), + ), + if (showNotificationButton) + IconButton( + onPressed: () { + Navigator.pushNamed(context, AppRoutes.worklistSettings); + }, + icon: const Icon(Icons.notifications, color: MyColors.textMixColor), + ), + if (showMemberButton) + IconButton( + onPressed: () { + Navigator.pushNamed(context, AppRoutes.subordinateLeave); + }, + icon: const Icon(Icons.people, color: MyColors.textMixColor), + ), + ], + ); +} diff --git a/lib/widgets/app_logo.dart b/lib/widgets/app_logo.dart new file mode 100644 index 0000000..3140c29 --- /dev/null +++ b/lib/widgets/app_logo.dart @@ -0,0 +1,26 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; + +class AppLogo extends StatelessWidget { + AppLogo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + SvgPicture.asset( + "assets/mohemm_logo.svg", + height: 48, + width: 48, + alignment: Alignment.centerRight, + ), + 8.width, + LocaleKeys.mohemm.tr().toText14() + ], + ); + } +} diff --git a/lib/widgets/bottom_sheet.dart b/lib/widgets/bottom_sheet.dart new file mode 100644 index 0000000..a15fc3b --- /dev/null +++ b/lib/widgets/bottom_sheet.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; + +void showMyBottomSheet(BuildContext context, {required Widget child, required VoidCallback callBackFunc}) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topRight: Radius.circular(25), + topLeft: Radius.circular(25), + ), + ), + clipBehavior: Clip.antiAlias, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + 13.height, + Container( + height: 6, + width: 60, + decoration: const BoxDecoration( + color: Color(0xff9A9A9A), + borderRadius: BorderRadius.all( + Radius.circular(20), + ), + ), + ), + 8.height, + child, + ], + ), + ); + }, + ).then((value) { + // print("BACK FROM DELEGATE!!!!"); + // print("value: $value"); + if (value == "delegate_reload") { + callBackFunc(); + } + }); +} + +class BottomSheetItem extends StatelessWidget { + final Function onTap; + final IconData icon; + final String title; + final Color color; + + const BottomSheetItem({Key? key, required this.onTap, required this.title, required this.icon, this.color = Colors.black}) : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + if (onTap != null) { + Navigator.pop(context); + onTap(); + } + }, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 18.0, vertical: 18.0), + child: Row( + children: [ + if (icon != null) + Icon( + icon, + color: color, + size: 18.0, + ), + if (icon != null) SizedBox(width: 24.0), + Text( + title ?? "", + style: TextStyle(color: color), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/button/default_button.dart b/lib/widgets/button/default_button.dart new file mode 100644 index 0000000..71de642 --- /dev/null +++ b/lib/widgets/button/default_button.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; + +extension WithContainer on Widget { + Widget get insideContainer => Container( + color: Colors.white, + padding: + const EdgeInsets.only(top: 16, bottom: 16, right: 21, left: 21), + child: this, + ); +} + +class DefaultButton extends StatelessWidget { + final String text; + final VoidCallback? onPress; + final Color textColor; + final Color? color; + final Color? disabledColor; + final IconData? iconData; + final String? svgIcon; + final double? fontSize; + final bool isTextExpanded; + final int count; + final List? colors; + + const DefaultButton( + this.text, + this.onPress, { + Key? key, + this.color, + this.isTextExpanded = true, + this.svgIcon, + this.disabledColor, + this.count = 0, + this.textColor = Colors.white, + this.iconData, + this.fontSize, + this.colors, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onPress, + child: Container( + height: 43, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.0), + gradient: onPress == null + ? const LinearGradient( + colors: [ + Color(0xffEAEAEA), + Color(0xffEAEAEA), + ], + ) + : color == MyColors.yellowColorII + ? const LinearGradient( + colors: [ + MyColors.yellowColorII, + MyColors.yellowColorII, + ], + ) + : LinearGradient( + transform: const GradientRotation(.83), + begin: Alignment.topRight, + end: Alignment.bottomLeft, + colors: colors ?? + [ + MyColors.gradiantEndColor, + MyColors.gradiantStartColor, + ], + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (iconData != null) Icon(iconData, color: textColor), + if (svgIcon != null) + SvgPicture.asset(svgIcon ?? "", color: textColor), + if (!isTextExpanded) + Padding( + padding: EdgeInsets.only( + left: (iconData ?? svgIcon) != null ? 6 : 0, + ), + child: Text( + text, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: fontSize ?? 16, + fontWeight: FontWeight.w600, + color: textColor, + letterSpacing: -0.48, + ), + ), + ), + if (isTextExpanded) + Expanded( + child: Text( + text, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: fontSize ?? 16, + fontWeight: FontWeight.w600, + color: textColor, + letterSpacing: -0.48, + ), + ), + ), + if (count > 0) + Align( + alignment: Alignment.topCenter, + child: Container( + margin: const EdgeInsets.only(top: 6, bottom: 6), + padding: const EdgeInsets.only(left: 5, right: 5), + alignment: Alignment.center, + height: 16, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + color: Colors.white, + ), + child: Text( + "$count", + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: Color(0xffD02127), + letterSpacing: -0.6, + ), + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/widgets/button/simple_button.dart b/lib/widgets/button/simple_button.dart new file mode 100644 index 0000000..eecce21 --- /dev/null +++ b/lib/widgets/button/simple_button.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; + +class SimpleButton extends StatelessWidget { + final String text; + final VoidCallback? onPress; + final Color textColor; + final Color? color; + final double? fontSize; + final List? colors; + + SimpleButton(this.text, this.onPress, {this.color, this.textColor = Colors.white, this.fontSize, this.colors}); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onPress, + child: Container( + alignment: Alignment.center, + padding: const EdgeInsets.only(left: 12, right: 12, top: 4, bottom: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.0), + gradient: onPress == null + ? const LinearGradient(colors: [Color(0xffEAEAEA), Color(0xffEAEAEA)]) + : LinearGradient( + transform: const GradientRotation(.83), + begin: Alignment.topRight, + end: Alignment.bottomLeft, + colors: colors ?? + [ + MyColors.gradiantEndColor, + MyColors.gradiantStartColor, + ]), + ), + child: Text( + text, + style: TextStyle(fontSize: fontSize ?? 16, fontWeight: FontWeight.w600, color: textColor, letterSpacing: -0.48), + ), + ), + ); + } +} diff --git a/lib/widgets/circular_avatar.dart b/lib/widgets/circular_avatar.dart new file mode 100644 index 0000000..a18cf03 --- /dev/null +++ b/lib/widgets/circular_avatar.dart @@ -0,0 +1,41 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +class CircularAvatar extends StatelessWidget { + final String? url; + final double radius; + final double width; + final double height; + final bool isImageBase64; + + CircularAvatar({Key? key, this.radius = 70.0, this.width = 70, this.height = 60, this.url, this.isImageBase64 = false}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + width: width, + height: height, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: isImageBase64 + ? null + : DecorationImage( + fit: BoxFit.cover, + image: NetworkImage(url ?? "https://cdn4.iconfinder.com/data/icons/professions-2-2/151/89-512.png"), + ), + ), + child: (isImageBase64 && url != null) ? imageFromBase64String(url!) : Container(), + ); + } + + Widget imageFromBase64String(String base64String) { + return ClipRRect( + borderRadius: BorderRadius.circular(100), + child: Image.memory( + base64Decode(base64String), + fit: BoxFit.cover, + ), + ); + } +} diff --git a/lib/widgets/circular_step_progress_bar.dart b/lib/widgets/circular_step_progress_bar.dart new file mode 100644 index 0000000..b137dc6 --- /dev/null +++ b/lib/widgets/circular_step_progress_bar.dart @@ -0,0 +1,515 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +class CircularStepProgressBar extends StatelessWidget { + /// Defines if steps grow from + /// clockwise [CircularDirection.clockwise] or + /// counterclockwise [CircularDirection.counterclockwise] + final CircularDirection circularDirection; + + /// Number of steps to underline, all the steps with + /// index <= [currentStep] will have [Color] equal to + /// [selectedColor] + /// + /// Only used when [customColor] is [null] + /// + /// Default value: 0 + final int currentStep; + + /// Total number of step of the complete indicator + final int totalSteps; + + /// Radial spacing between each step. Remember to + /// define the value in radiant units + /// + /// Default value: math.pi / 20 + final double padding; + + /// Height of the indicator's box container + final double? height; + + /// Width of the indicator's box container + final double? width; + + /// Assign a custom [Color] for each step + /// + /// Takes a [int], index of the current step starting from 0, and + /// must return a [Color] + /// + /// **NOTE**: If provided, it overrides + /// [selectedColor] and [unselectedColor] + final Color Function(int)? customColor; + + /// [Color] of the selected steps + /// + /// All the steps with index <= [currentStep] + /// + /// Default value: [Colors.blue] + final Color? selectedColor; + + /// [Color] of the unselected steps + /// + /// All the steps with index between + /// [currentStep] and [totalSteps] + /// + /// Default value: [Colors.grey] + final Color? unselectedColor; + + /// The size of a single step in the indicator + /// + /// Default value: 6.0 + final double stepSize; + + /// Specify a custom size for selected steps + /// + /// Only applicable when not custom setting (customColor, customStepSize) is defined + /// + /// This value will replace the [stepSize] only for selected steps + final double? selectedStepSize; + + /// Specify a custom size for unselected steps + /// + /// Only applicable when not custom setting (customColor, customStepSize) is defined + /// + /// This value will replace the [stepSize] only for unselected steps + final double? unselectedStepSize; + + /// Assign a custom size [double] for each step + /// + /// Function takes a [int], index of the current step starting from 0, and + /// a [bool], which tells if the step is selected based on [currentStep], + /// and must return a [double] size of the step + /// + /// **NOTE**: If provided, it overrides [stepSize] + final double Function(int, bool)? customStepSize; + + /// [Widget] contained inside the circular indicator + final Widget? child; + + /// Height of the indicator container in case no [height] parameter + /// given and parent height is [double.infinity] + /// + /// Default value: 100.0 + final double fallbackHeight; + + /// Height of the indicator container in case no [width] parameter + /// given and parent height is [double.infinity] + /// + /// Default value: 100.0 + final double fallbackWidth; + + /// Angle in radiants in which the first step of the indicator is placed. + /// The initial value is on the top of the indicator (- math.pi / 2) + /// - 0 => TOP + /// - math.pi / 2 => LEFT + /// - math.pi => BOTTOM + /// - math.pi / 2 * 3 => RIGHT + /// - math.pi / 2 => TOP (again) + final double startingAngle; + + /// Angle in radiants which represents the size of the arc used to display the indicator. + /// It allows you to draw a semi-circle instead of a full 360° (math.pi * 2) circle. + final double arcSize; + + /// Adds rounded edges at the beginning and at the end of the circular indicator + /// given a [int], index of each step, and a [bool], + /// which tells if the step is selected based on [currentStep], and must return a + /// [bool] that tells if the edges are rounded or not + /// + /// **NOTE**: For continuous circular indicators (`padding: 0`), to check if to apply + /// the rounded edges the packages uses index 0 (for the first arc painted) and + /// 1 (for the second arc painted) + /// + /// ```dart + /// // Example: Add rounded edges for all the steps + /// roundedCap: (index, _) => true + /// ``` + /// + /// ```dart + /// // Example: Add rounded edges for the selected arc of the indicator + /// roundedCap: (index, _) => index == 0, + /// padding: 0 + /// ``` + final bool Function(int, bool)? roundedCap; + + /// Adds a gradient color to the circular indicator + /// + /// **NOTE**: If provided, it overrides [selectedColor], [unselectedColor], and [customColor] + final Gradient? gradientColor; + + /// Removes the extra angle caused by [StrokeCap.round] when [roundedCap] is applied + final bool removeRoundedCapExtraAngle; + + const CircularStepProgressBar({ + required this.totalSteps, + this.child, + this.height, + this.width, + this.customColor, + this.customStepSize, + this.selectedStepSize, + this.unselectedStepSize, + this.roundedCap, + this.gradientColor, + this.circularDirection = CircularDirection.clockwise, + this.fallbackHeight = 100.0, + this.fallbackWidth = 100.0, + this.currentStep = 0, + this.selectedColor = Colors.blue, + this.unselectedColor = Colors.grey, + this.padding = math.pi / 20, + this.stepSize = 6.0, + this.startingAngle = 0, + this.arcSize = math.pi * 2, + this.removeRoundedCapExtraAngle = false, + Key? key, + }) : assert(totalSteps > 0, "Number of total steps (totalSteps) of the CircularStepProgressBar must be greater than 0"), + assert(currentStep >= 0, "Current step (currentStep) of the CircularStepProgressBar must be greater than or equal to 0"), + assert(padding >= 0.0, "Padding (padding) of the CircularStepProgressBar must be greater or equal to 0"), + super(key: key); + + @override + Widget build(BuildContext context) { + // Print warning when arcSize greater than math.pi * 2 which causes steps to overlap + if (arcSize > math.pi * 2) print("WARNING (step_progress_indicator): arcSize of CircularStepProgressBar is greater than 360° (math.pi * 2), this will cause some steps to overlap!"); + TextDirection textDirection = Directionality.of(context); + + return LayoutBuilder( + builder: (context, constraints) => SizedBox( + // Apply fallback for both height and width + // if their value is null and no parent size limit + height: height != null + ? height + : constraints.maxHeight != double.infinity + ? constraints.maxHeight + : fallbackHeight, + width: width != null + ? width + : constraints.maxWidth != double.infinity + ? constraints.maxWidth + : fallbackWidth, + child: CustomPaint( + painter: _CircularIndicatorPainter( + totalSteps: totalSteps, + currentStep: currentStep, + customColor: customColor, + padding: padding, + circularDirection: circularDirection, + selectedColor: selectedColor, + unselectedColor: unselectedColor, + arcSize: arcSize, + stepSize: stepSize, + customStepSize: customStepSize, + maxDefinedSize: maxDefinedSize, + selectedStepSize: selectedStepSize, + unselectedStepSize: unselectedStepSize, + startingAngle: startingAngleTopOfIndicator, + roundedCap: roundedCap, + gradientColor: gradientColor, + textDirection: textDirection, + removeRoundedCapExtraAngle: removeRoundedCapExtraAngle, + ), + // Padding needed to show the indicator when child is placed on top of it + child: Padding( + padding: EdgeInsets.all(maxDefinedSize), + child: child, + ), + ), + ), + ); + } + + /// Compute the maximum possible size of the indicator between + /// [stepSize] and [customStepSize] + double get maxDefinedSize { + if (customStepSize == null) { + return math.max(stepSize, math.max(selectedStepSize ?? 0, unselectedStepSize ?? 0)); + } + + // When customSize defined, compute and return max possible size + double currentMaxSize = 0; + + for (int step = 0; step < totalSteps; ++step) { + // Consider max between selected and unselected case + var customSizeValue = math.max(customStepSize!(step, false), customStepSize!(step, true)); + if (customSizeValue > currentMaxSize) { + currentMaxSize = customSizeValue; + } + } + + return currentMaxSize; + } + + /// Make [startingAngle] to top-center of indicator (0°) by default + double get startingAngleTopOfIndicator => startingAngle - math.pi / 2; +} + +class _CircularIndicatorPainter implements CustomPainter { + final int totalSteps; + final int currentStep; + final double padding; + final Color? selectedColor; + final Color? unselectedColor; + final double stepSize; + final double? selectedStepSize; + final double? unselectedStepSize; + final double Function(int, bool)? customStepSize; + final double maxDefinedSize; + final Color Function(int)? customColor; + final CircularDirection circularDirection; + final double startingAngle; + final double arcSize; + final bool Function(int, bool)? roundedCap; + final Gradient? gradientColor; + final TextDirection textDirection; + final bool removeRoundedCapExtraAngle; + + _CircularIndicatorPainter({ + required this.totalSteps, + required this.circularDirection, + required this.customColor, + required this.currentStep, + required this.selectedColor, + required this.unselectedColor, + required this.padding, + required this.stepSize, + required this.selectedStepSize, + required this.unselectedStepSize, + required this.customStepSize, + required this.startingAngle, + required this.arcSize, + required this.maxDefinedSize, + required this.roundedCap, + required this.gradientColor, + required this.textDirection, + required this.removeRoundedCapExtraAngle, + }); + + @override + void paint(Canvas canvas, Size size) { + var w = size.width; + var h = size.height; + + // Step length is user-defined arcSize + // divided by the total number of steps (each step same size) + var stepLength = arcSize / totalSteps; + + // Define general arc paint + Paint paint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = maxDefinedSize; + + var rect = Rect.fromCenter( + // Rect created from the center of the widget + center: Offset(w / 2, h / 2), + // For both height and width, subtract maxDefinedSize to fit indicator inside the parent container + height: h - maxDefinedSize, + width: w - maxDefinedSize, + ); + + if (gradientColor != null) { + paint.shader = gradientColor!.createShader(rect, textDirection: textDirection); + } + + // Change color selected or unselected based on the circularDirection + var isClockwise = circularDirection == CircularDirection.clockwise; + + // Make a continuous arc without rendering all the steps when possible + if (padding == 0 && customColor == null && customStepSize == null && roundedCap == null) { + _drawContinuousArc(canvas, paint, rect, isClockwise); + } else { + _drawStepArc(canvas, paint, rect, isClockwise, stepLength); + } + } + + /// Draw a series of arcs, each composing the full steps of the indicator + void _drawStepArc(Canvas canvas, Paint paint, Rect rect, bool isClockwise, double stepLength) { + // Draw a series of circular arcs to compose the indicator + // Starting based on startingAngle attribute + // + // When clockwise: + // - Start drawing counterclockwise so to have the selected steps on top of the unselected + int step = isClockwise ? totalSteps - 1 : 0; + double stepAngle = isClockwise ? startingAngle - stepLength : startingAngle; + for (; isClockwise ? step >= 0 : step < totalSteps; isClockwise ? stepAngle -= stepLength : stepAngle += stepLength, isClockwise ? --step : ++step) { + // Check if the current step is selected or unselected + var isSelectedColor = _isSelectedColor(step, isClockwise); + + // Size of the step + var indexStepSize = customStepSize != null + // Consider step index inverted when counterclockwise + ? customStepSize!(_indexOfStep(step, isClockwise), isSelectedColor) + : isSelectedColor + ? selectedStepSize ?? stepSize + : unselectedStepSize ?? stepSize; + + // Use customColor if defined + var stepColor = customColor != null + // Consider step index inverted when counterclockwise + ? customColor!(_indexOfStep(step, isClockwise)) + : isSelectedColor + ? selectedColor! + : unselectedColor!; + + // Apply stroke cap to each step + var hasStrokeCap = roundedCap != null ? roundedCap!(_indexOfStep(step, isClockwise), isSelectedColor) : false; + var strokeCap = hasStrokeCap ? StrokeCap.round : StrokeCap.butt; + + // Remove extra size caused by rounded stroke cap + // https://github.com/SandroMaglione/step-progress-indicator/issues/20#issue-786114745 + var extraCapSize = indexStepSize / 2; + var extraCapAngle = extraCapSize / (rect.width / 2); + var extraCapRemove = hasStrokeCap && removeRoundedCapExtraAngle; + + // Draw arc steps of the indicator + _drawArcOnCanvas( + canvas: canvas, + rect: rect, + startingAngle: stepAngle + (extraCapRemove ? extraCapAngle : 0), + sweepAngle: stepLength - padding - (extraCapRemove ? extraCapAngle * 2 : 0), + paint: paint, + color: stepColor, + strokeWidth: indexStepSize, + strokeCap: strokeCap, + ); + } + } + + /// Draw optimized continuous indicator instead of multiple steps + void _drawContinuousArc(Canvas canvas, Paint paint, Rect rect, bool isClockwise) { + // Compute color of the selected and unselected bars + var firstStepColor = isClockwise ? selectedColor : unselectedColor; + var secondStepColor = !isClockwise ? selectedColor : unselectedColor; + + // Selected and unselected step sizes if defined, otherwise use stepSize + var firstStepSize = isClockwise ? selectedStepSize ?? stepSize : unselectedStepSize ?? stepSize; + var secondStepSize = !isClockwise ? selectedStepSize ?? stepSize : unselectedStepSize ?? stepSize; + + // Compute length and starting angle of the selected and unselected bars + var firstArcLength = arcSize * (currentStep / totalSteps); + var secondArcLength = arcSize - firstArcLength; + + // firstArcStartingAngle = startingAngle + var secondArcStartingAngle = startingAngle + firstArcLength; + + // Apply stroke cap to both arcs + // NOTE: For continuous circular indicator, it uses 0 and 1 as index to + // apply the rounded cap + var firstArcStrokeCap = roundedCap != null + ? isClockwise + ? roundedCap!(0, true) + : roundedCap!(1, false) + : false; + var secondArcStrokeCap = roundedCap != null + ? isClockwise + ? roundedCap!(1, false) + : roundedCap!(0, true) + : false; + var firstCap = firstArcStrokeCap ? StrokeCap.round : StrokeCap.butt; + var secondCap = secondArcStrokeCap ? StrokeCap.round : StrokeCap.butt; + + // When clockwise, draw the second arc first and the first on top of it + // Required when stroke cap is rounded to make the selected step on top of the unselected + if (circularDirection == CircularDirection.clockwise) { + // Second arc, selected when counterclockwise, unselected otherwise + _drawArcOnCanvas( + canvas: canvas, + rect: rect, + paint: paint, + startingAngle: secondArcStartingAngle, + sweepAngle: secondArcLength, + strokeWidth: secondStepSize, + color: secondStepColor!, + strokeCap: secondCap, + ); + + // First arc, selected when clockwise, unselected otherwise + _drawArcOnCanvas( + canvas: canvas, + rect: rect, + paint: paint, + startingAngle: startingAngle, + sweepAngle: firstArcLength, + strokeWidth: firstStepSize, + color: firstStepColor!, + strokeCap: firstCap, + ); + } else { + // First arc, selected when clockwise, unselected otherwise + _drawArcOnCanvas( + canvas: canvas, + rect: rect, + paint: paint, + startingAngle: startingAngle, + sweepAngle: firstArcLength, + strokeWidth: firstStepSize, + color: firstStepColor!, + strokeCap: firstCap, + ); + + // Second arc, selected when counterclockwise, unselected otherwise + _drawArcOnCanvas( + canvas: canvas, + rect: rect, + paint: paint, + startingAngle: secondArcStartingAngle, + sweepAngle: secondArcLength, + strokeWidth: secondStepSize, + color: secondStepColor!, + strokeCap: secondCap, + ); + } + } + + /// Draw the actual arc for a continuous indicator + void _drawArcOnCanvas({ + required Canvas canvas, + required Rect rect, + required double startingAngle, + required double sweepAngle, + required Paint paint, + required Color color, + required double strokeWidth, + required StrokeCap strokeCap, + }) => + canvas.drawArc( + rect, + startingAngle, + sweepAngle, + false /*isRadial*/, + paint + ..color = color + ..strokeWidth = strokeWidth + ..strokeCap = strokeCap, + ); + + bool _isSelectedColor(int step, bool isClockwise) => isClockwise ? step < currentStep : (step + 1) > (totalSteps - currentStep); + + /// Start counting indexes from the right if clockwise and on the left if counterclockwise + int _indexOfStep(int step, bool isClockwise) => isClockwise ? step : totalSteps - step - 1; + + @override + bool shouldRepaint(CustomPainter oldDelegate) => oldDelegate != this; + + @override + bool hitTest(Offset position) => false; + + @override + void addListener(listener) {} + + @override + void removeListener(listener) {} + + @override + Null get semanticsBuilder => null; + + @override + bool shouldRebuildSemantics(CustomPainter oldDelegate) => false; +} + +/// Used to define the [circularDirection] attribute of the [CircularStepProgressBar] +enum CircularDirection { + clockwise, + counterclockwise, +} diff --git a/lib/widgets/dialogs/confirm_dialog.dart b/lib/widgets/dialogs/confirm_dialog.dart new file mode 100644 index 0000000..4c94340 --- /dev/null +++ b/lib/widgets/dialogs/confirm_dialog.dart @@ -0,0 +1,63 @@ +import 'package:easy_localization/src/public_ext.dart'; +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +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/widgets/button/default_button.dart'; + +class ConfirmDialog extends StatelessWidget { + final String? title; + final String message; + final String? okTitle; + final VoidCallback? onTap; + + const ConfirmDialog({Key? key, this.title, required this.message, this.okTitle, this.onTap}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder(), + insetPadding: const EdgeInsets.only(left: 21, right: 21), + child: Padding( + padding: const EdgeInsets.only(left: 20, right: 20, top: 18, bottom: 28), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + title ?? LocaleKeys.confirm.tr(), + style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: MyColors.darkTextColor, height: 35 / 24, letterSpacing: -0.96), + ).paddingOnly(top: 16), + ), + IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.close), + color: MyColors.darkTextColor, + constraints: const BoxConstraints(), + onPressed: () { + Navigator.pop(context); + }, + ) + ], + ), + message.toText16(color: MyColors.lightGrayColor), + 28.height, + DefaultButton( + okTitle ?? LocaleKeys.ok.tr(), + onTap ?? () => Navigator.pop(context), + textColor: Colors.white, + //color: Ap.green, + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/dialogs/dialogs.dart b/lib/widgets/dialogs/dialogs.dart new file mode 100644 index 0000000..debb147 --- /dev/null +++ b/lib/widgets/dialogs/dialogs.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +void showMDialog(context, {Widget? child,Color? backgroundColor,bool isDismissable=true}) async { + return showDialog( + context: context, + barrierDismissible: isDismissable, + builder: (context) { + return Dialog( + backgroundColor: backgroundColor, + child: child, + ); + }, + ); +} diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart new file mode 100644 index 0000000..5059733 --- /dev/null +++ b/lib/widgets/input_widget.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; + +class InputWidget extends StatefulWidget { + final String labelText; + final String hintText; + final TextEditingController controller; + final VoidCallback? suffixTap; + final bool isEnable; + final bool hasSelection; + final int? lines; + final bool isInputTypeNum; + final bool isTextIsPassword; + final bool isBackgroundEnable; + final bool isEnableBorder; + final double verticalPadding; + final double horizontalPadding; + final Function(String)? onChange; + + InputWidget( + this.labelText, + this.hintText, + this.controller, { + Key? key, + this.isTextIsPassword = false, + this.suffixTap, + this.isEnable = true, + this.hasSelection = false, + this.isEnableBorder = true, + this.lines = 1, + this.onChange, + this.isInputTypeNum = false, + this.isBackgroundEnable = false, + this.verticalPadding = 15, + this.horizontalPadding = 16, + }) : super(key: key); + + @override + _InputWidgetState createState() { + return _InputWidgetState(); + } +} + +class _InputWidgetState extends State { + late bool isObscureText; + + @override + void initState() { + super.initState(); + isObscureText = widget.isTextIsPassword; + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only(left: widget.horizontalPadding, right: widget.horizontalPadding, bottom: widget.verticalPadding, top: widget.verticalPadding), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: widget.isBackgroundEnable ? Color(0xffF7F7F7) : Colors.white, + border: Border.all( + color: widget.isEnableBorder ? Color(0xffefefef) : Colors.transparent, + width: 1, + ), + ), + child: InkWell( + onTap: widget.hasSelection ? () {} : null, + child: Row( + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.labelText, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xff2B353E), + letterSpacing: -0.44, + ), + ), + TextField( + enabled: widget.isEnable, + scrollPadding: EdgeInsets.zero, + keyboardType: widget.isInputTypeNum ? TextInputType.number : TextInputType.text, + controller: widget.controller, + maxLines: widget.lines, + obscuringCharacter: "*", + obscureText: isObscureText, + onChanged: widget.onChange, + style: const TextStyle( + fontSize: 14, + height: 21 / 14, + fontWeight: FontWeight.w400, + color: Color(0xff2B353E), + letterSpacing: -0.44, + ), + decoration: InputDecoration( + isDense: true, + hintText: widget.hintText, + hintStyle: const TextStyle( + fontSize: 14, + height: 21 / 14, + fontWeight: FontWeight.w400, + color: Color(0xff575757), + letterSpacing: -0.56, + ), + suffixIconConstraints: const BoxConstraints(minWidth: 50), + suffixIcon: widget.suffixTap == null ? null : IconButton(icon: const Icon(Icons.mic, color: MyColors.darkTextColor), onPressed: widget.suffixTap), + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + ), + ), + ], + ), + ), + if (widget.isTextIsPassword) ...[ + 16.width, + Icon(isObscureText ? Icons.visibility_rounded : Icons.visibility_off_rounded).onPress(() { + setState(() { + isObscureText = !isObscureText; + }); + }) + ], + if (widget.hasSelection) Icon(Icons.keyboard_arrow_down_outlined), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/item_detail_view_widget.dart b/lib/widgets/item_detail_view_widget.dart new file mode 100644 index 0000000..5148389 --- /dev/null +++ b/lib/widgets/item_detail_view_widget.dart @@ -0,0 +1,150 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; +import 'package:mohem_flutter_app/models/itg_forms_models/itg_worklist_table_model.dart'; + +class ItemDetailView extends StatelessWidget { + final String title; + final String value; + + const ItemDetailView(this.title, this.value, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "$title:".toText12(isBold: true, color: const Color(0xff2D3238)), + 6.width, + (value.isEmpty ? "--" : value).toText12(color: MyColors.normalTextColor).expanded, + ], + ); + } +} + +class ItemDetailViewCol extends StatelessWidget { + final String title; + final String value; + + const ItemDetailViewCol(this.title, this.value, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "$title:".toText12(isBold: true, color: const Color(0xff2BB8A6)), + 4.width, + (value.isEmpty ? "--" : value).toText12(color: MyColors.normalTextColor), + ], + ); + } +} + +class ItemDetailViewGridItem extends StatelessWidget { + int index; + final String? title; + final String? value; + final String? type; + final bool isNeedToShowEmptyDivider; + + ItemDetailViewGridItem(this.index, this.title, this.value, {Key? key, this.isNeedToShowEmptyDivider = false, this.type = ""}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only(left: index % 2 != 0 ? 12 : 0), + decoration: BoxDecoration( + color: Colors.transparent, + border: Border( + left: BorderSide( + //MyColors.lightGreyEFColor + // <--- left side + color: index % 2 != 0 ? MyColors.lightGreyEFColor : Colors.transparent, + width: 1.5, + ), + top: BorderSide( + // <--- left side + color: index > 1 ? MyColors.lightGreyEFColor : Colors.transparent, + width: 1.5, + ), + ), + ), + child: isNeedToShowEmptyDivider + ? Container() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + title != null ? Flexible(child: "$title:".toText12Auto(isBold: true, color: const Color(0xff2BB8A6))) : Container(), + 4.width, + type != null + ? type!.toLowerCase() == "table" + ? getStringFromJSON(value!) + : Flexible(child: (value!.isEmpty ? "--" : value).toString().toText12Auto(color: MyColors.normalTextColor)) + : Container(), + ], + ), + ); + } +} + +class ItemDetailGrid extends StatelessWidget { + Widget child1, child2; + + bool isItLast; + + ItemDetailGrid(this.child1, this.child2, {this.isItLast = false}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + flex: 1, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: child1, + ), + ), + Container( + width: 1, + height: double.infinity, + color: MyColors.lightGreyEFColor, + margin: EdgeInsets.symmetric(horizontal: 8), + ), + Expanded( + flex: 1, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: child2, + ), + ), + ], + ), + ), + if (!isItLast) 1.divider, + ], + ); + } +} + +Widget getStringFromJSON(String jsonString) { + var body = json.decode(jsonString); + ITGWorkListTableModel itgWorkListTableModel = ITGWorkListTableModel(); + if (body.length != 0) { + itgWorkListTableModel = ITGWorkListTableModel.fromJson(body[0][0]); + return Flexible(child: (itgWorkListTableModel.textvalue).toString().toText12Auto(color: MyColors.normalTextColor)); + } else { + return Flexible(child: ("-").toString().toText12Auto(color: MyColors.normalTextColor)); + } +} diff --git a/lib/widgets/loading_dialog.dart b/lib/widgets/loading_dialog.dart new file mode 100644 index 0000000..9c88a39 --- /dev/null +++ b/lib/widgets/loading_dialog.dart @@ -0,0 +1,47 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class LoadingDialog extends StatefulWidget { + LoadingDialog({Key? key}) : super(key: key); + + @override + _LoadingDialogState createState() { + return _LoadingDialogState(); + } +} + +class _LoadingDialogState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Dialog( + insetPadding: const EdgeInsets.symmetric(horizontal: 60.0, vertical: 24.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + backgroundColor: Colors.transparent, + child: Directionality( + textDirection: TextDirection.rtl, + child: Center( + child: Image.asset( + "assets/images/logos/loading_mohemm_logo.gif", + height: 96.0, + width: 96.0, + ), + ), + ), + ); + } +} diff --git a/lib/widgets/otp_widget.dart b/lib/widgets/otp_widget.dart new file mode 100644 index 0000000..e90308d --- /dev/null +++ b/lib/widgets/otp_widget.dart @@ -0,0 +1,373 @@ +import 'dart:async'; + +import 'package:flutter/animation.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +typedef OnDone = void Function(String text); + +class ProvidedPinBoxTextAnimation { + static AnimatedSwitcherTransitionBuilder scalingTransition = (child, animation) { + return ScaleTransition( + child: child, + scale: animation, + ); + }; + + static AnimatedSwitcherTransitionBuilder defaultNoTransition = (Widget child, Animation animation) { + return child; + }; +} + +class OTPWidget extends StatefulWidget { + final int maxLength; + final TextEditingController? controller; + + final Color defaultBorderColor; + final Color pinBoxColor; + final double pinBoxBorderWidth; + final double pinBoxRadius; + final bool hideDefaultKeyboard; + + final TextStyle? pinTextStyle; + final double pinBoxHeight; + final double pinBoxWidth; + final OnDone? onDone; + final bool hasError; + final Color errorBorderColor; + final Color textBorderColor; + final Function(String)? onTextChanged; + final bool autoFocus; + final FocusNode? focusNode; + final AnimatedSwitcherTransitionBuilder? pinTextAnimatedSwitcherTransition; + final Duration pinTextAnimatedSwitcherDuration; + final TextDirection textDirection; + final TextInputType keyboardType; + final EdgeInsets pinBoxOuterPadding; + + const OTPWidget({ + Key? key, + this.maxLength: 4, + this.controller, + this.pinBoxWidth: 70.0, + this.pinBoxHeight: 70.0, + this.pinTextStyle, + this.onDone, + this.defaultBorderColor: Colors.black, + this.textBorderColor: Colors.black, + this.pinTextAnimatedSwitcherTransition, + this.pinTextAnimatedSwitcherDuration: const Duration(), + this.hasError: false, + this.errorBorderColor: Colors.red, + this.onTextChanged, + this.autoFocus: false, + this.focusNode, + this.textDirection: TextDirection.ltr, + this.keyboardType: TextInputType.number, + this.pinBoxOuterPadding = const EdgeInsets.symmetric(horizontal: 4.0), + this.pinBoxColor = Colors.white, + this.pinBoxBorderWidth = 2.0, + this.pinBoxRadius = 0, + this.hideDefaultKeyboard = false, + }) : super(key: key); + + @override + State createState() { + return OTPWidgetState(); + } +} + +class OTPWidgetState extends State with SingleTickerProviderStateMixin { + AnimationController? _highlightAnimationController; + FocusNode? focusNode; + String text = ""; + int currentIndex = 0; + List strList = []; + bool hasFocus = false; + + @override + void didUpdateWidget(OTPWidget oldWidget) { + super.didUpdateWidget(oldWidget); + focusNode = widget.focusNode ?? focusNode; + + if (oldWidget.maxLength < widget.maxLength) { + setState(() { + currentIndex = text.length; + }); + widget.controller?.text = text; + widget.controller?.selection = TextSelection.collapsed(offset: text.length); + } else if (oldWidget.maxLength > widget.maxLength && widget.maxLength > 0 && text.length > 0 && text.length > widget.maxLength) { + setState(() { + text = text.substring(0, widget.maxLength); + currentIndex = text.length; + }); + widget.controller?.text = text; + widget.controller?.selection = TextSelection.collapsed(offset: text.length); + } + } + + void calculateStrList() { + if (strList.length > widget.maxLength) { + strList.length = widget.maxLength; + } + while (strList.length < widget.maxLength) { + strList.add(""); + } + } + + @override + void initState() { + super.initState(); + focusNode = widget.focusNode ?? FocusNode(); + + _initTextController(); + calculateStrList(); + widget.controller?.addListener(_controllerListener); + focusNode?.addListener(_focusListener); + } + + void _controllerListener() { + if (mounted == true) { + setState(() { + _initTextController(); + }); + var onTextChanged = widget.onTextChanged; + if (onTextChanged != null) { + onTextChanged(widget.controller?.text ?? ""); + } + } + } + + void _focusListener() { + if (mounted == true) { + setState(() { + hasFocus = focusNode?.hasFocus ?? false; + }); + } + } + + void _initTextController() { + if (widget.controller == null) { + return; + } + strList.clear(); + var text = widget.controller?.text ?? ""; + if (text.isNotEmpty) { + if (text.length > widget.maxLength) { + throw Exception("TextEditingController length exceeded maxLength!"); + } + } + for (var i = 0; i < text.length; i++) { + strList.add(text[i]); + } + } + + double get _width { + var width = 0.0; + for (var i = 0; i < widget.maxLength; i++) { + width += widget.pinBoxWidth; + if (i == 0) { + width += widget.pinBoxOuterPadding.left; + } else if (i + 1 == widget.maxLength) { + width += widget.pinBoxOuterPadding.right; + } else { + width += widget.pinBoxOuterPadding.left; + } + } + return width; + } + + @override + void dispose() { + if (widget.focusNode == null) { + focusNode?.dispose(); + } else { + focusNode?.removeListener(_focusListener); + } + _highlightAnimationController?.dispose(); + widget.controller?.removeListener(_controllerListener); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + _otpTextInput(), + _touchPinBoxRow(), + ], + ); + } + + Widget _touchPinBoxRow() { + return widget.hideDefaultKeyboard + ? _pinBoxRow(context) + : GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (hasFocus) { + FocusScope.of(context).requestFocus(FocusNode()); + Future.delayed(Duration(milliseconds: 100), () { + FocusScope.of(context).requestFocus(focusNode); + }); + } else { + FocusScope.of(context).requestFocus(focusNode); + } + }, + child: _pinBoxRow(context), + ); + } + + Widget _otpTextInput() { + var transparentBorder = OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + width: 0.0, + ), + ); + return Container( + width: _width, + height: widget.pinBoxHeight, + child: TextField( + autofocus: !kIsWeb ? widget.autoFocus : false, + enableInteractiveSelection: false, + focusNode: focusNode, + controller: widget.controller, + keyboardType: widget.keyboardType, + inputFormatters: widget.keyboardType == TextInputType.number ? [FilteringTextInputFormatter.digitsOnly] : null, + style: TextStyle( + height: 0.1, + color: Colors.transparent, + ), + decoration: InputDecoration( + contentPadding: EdgeInsets.all(0), + focusedErrorBorder: transparentBorder, + errorBorder: transparentBorder, + disabledBorder: transparentBorder, + enabledBorder: transparentBorder, + focusedBorder: transparentBorder, + counterText: null, + counterStyle: null, + helperStyle: TextStyle( + height: 0.0, + color: Colors.transparent, + ), + labelStyle: TextStyle(height: 0.1), + fillColor: Colors.transparent, + border: InputBorder.none, + ), + cursorColor: Colors.transparent, + showCursor: false, + maxLength: widget.maxLength, + onChanged: _onTextChanged, + ), + ); + } + + void _onTextChanged(text) { + var onTextChanged = widget.onTextChanged; + if (onTextChanged != null) { + onTextChanged(text); + } + setState(() { + this.text = text; + if (text.length >= currentIndex) { + for (int i = currentIndex; i < text.length; i++) { + strList[i] = text[i]; + } + } + currentIndex = text.length; + }); + if (text.length == widget.maxLength) { + FocusScope.of(context).requestFocus(FocusNode()); + var onDone = widget.onDone; + if (onDone != null) { + onDone(text); + } + } + } + + Widget _pinBoxRow(BuildContext context) { + calculateStrList(); + List pinCodes = List.generate(widget.maxLength, (int i) { + return _buildPinCode(i, context); + }); + return Row(children: pinCodes, mainAxisSize: MainAxisSize.min); + } + + Widget _buildPinCode(int i, BuildContext context) { + Color borderColor; + Color pinBoxColor = widget.pinBoxColor; + + if (widget.hasError) { + borderColor = widget.errorBorderColor; + } else if (i < text.length) { + borderColor = widget.textBorderColor; + } else { + borderColor = widget.defaultBorderColor; + pinBoxColor = widget.pinBoxColor; + } + + EdgeInsets insets; + if (i == 0) { + insets = EdgeInsets.only( + left: 0, + top: widget.pinBoxOuterPadding.top, + right: widget.pinBoxOuterPadding.right, + bottom: widget.pinBoxOuterPadding.bottom, + ); + } else if (i == strList.length - 1) { + insets = EdgeInsets.only( + left: widget.pinBoxOuterPadding.left, + top: widget.pinBoxOuterPadding.top, + right: 0, + bottom: widget.pinBoxOuterPadding.bottom, + ); + } else { + insets = widget.pinBoxOuterPadding; + } + return Container( + key: ValueKey("container$i"), + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 1.0), + margin: insets, + child: _animatedTextBox(strList[i], i), + decoration: BoxDecoration( + border: Border.all( + color: borderColor, + width: widget.pinBoxBorderWidth, + ), + color: pinBoxColor, + borderRadius: BorderRadius.circular(widget.pinBoxRadius), + ), + width: widget.pinBoxWidth, + height: widget.pinBoxHeight, + ); + } + + Widget _animatedTextBox(String text, int i) { + if (widget.pinTextAnimatedSwitcherTransition != null) { + return AnimatedSwitcher( + duration: widget.pinTextAnimatedSwitcherDuration, + transitionBuilder: widget.pinTextAnimatedSwitcherTransition ?? + (Widget child, Animation animation) { + return child; + }, + child: Text( + text, + key: ValueKey("$text$i"), + style: widget.pinTextStyle, + ), + ); + } else { + return Text( + text, + key: ValueKey("${strList[i]}$i"), + style: widget.pinTextStyle, + ); + } + } +} diff --git a/lib/widgets/radio/show_radio.dart b/lib/widgets/radio/show_radio.dart new file mode 100644 index 0000000..9a5a4b8 --- /dev/null +++ b/lib/widgets/radio/show_radio.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; + +class ShowRadio extends StatelessWidget { + String title, value, groupValue; + Color selectedColor; + + ShowRadio({ + required this.title, + required this.value, + required this.groupValue, + this.selectedColor = Colors.black, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: Colors.transparent, + border: Border.all(color: MyColors.grey98Color, width: 0.5), + borderRadius: BorderRadius.all(Radius.circular(100)), + ), + padding: EdgeInsets.all(4), + child: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + color: value == groupValue ? selectedColor : Colors.transparent, + borderRadius: BorderRadius.all(Radius.circular(100)), + ), + ), + ), + 12.width, + title.toText12(maxLine: 2) + ], + ); + } +} diff --git a/lib/widgets/shimmer/dashboard_shimmer_widget.dart b/lib/widgets/shimmer/dashboard_shimmer_widget.dart new file mode 100644 index 0000000..369b808 --- /dev/null +++ b/lib/widgets/shimmer/dashboard_shimmer_widget.dart @@ -0,0 +1,256 @@ +import 'package:easy_localization/src/public_ext.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +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:shimmer/shimmer.dart'; + +class GetAttendanceTrackingShimmer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + height: double.infinity, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: const Color(0xff000000).withOpacity(.05), + blurRadius: 26, + offset: const Offset(0, -3), + ), + ], + ), + child: Stack( + alignment: Alignment.center, + children: [ + // SvgPicture.asset("assets/images/"), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.markAttendance.tr().toText14(color: Colors.white, isBold: true).toShimmer(), + 16.height, + "07:55:12".toText10(color: Colors.white, isBold: true).toShimmer(), + 3.height, + LocaleKeys.timeLeftToday.tr().toText10(color: Colors.white).toShimmer(), + 9.height, + const ClipRRect( + borderRadius: BorderRadius.all( + Radius.circular(20), + ), + child: LinearProgressIndicator( + value: 0.7, + minHeight: 8, + valueColor: const AlwaysStoppedAnimation(Colors.white), + backgroundColor: const Color(0xff196D73), + ), + ).toShimmer(), + ], + ).paddingOnly(top: 12, right: 15, left: 12), + ), + Row( + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.checkIn.tr().toText12(color: Colors.white).toShimmer(), + ], + ).paddingOnly(left: 12), + ), + Container( + width: 45, + height: 45, + // color: Colors.blue, + padding: const EdgeInsets.only(left: 14, right: 14), + ).toShimmer(), + ], + ), + ], + ), + ], + ), + ); + } +} + +class MenuShimmer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: const Color(0xff000000).withOpacity(.05), + blurRadius: 26, + offset: const Offset(0, -3), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.workList.tr().toText12(color: Colors.white).toShimmer(), + Row( + children: [ + Expanded( + flex: 3, + child: 123.toString().toText10(color: Colors.white, isBold: true).toShimmer(), + ), + 12.width, + SvgPicture.asset("assets/images/arrow_next.svg", color: Colors.white).toShimmer() + ], + ) + ], + ).paddingOnly(left: 10, right: 10, bottom: 6, top: 6), + ); + } +} + +class ServicesHeaderShimmer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + "Other".tr().toText10().toShimmer(), + 6.height, + LocaleKeys.services.tr().toText12(isBold: true).toShimmer(), + ], + ), + ), + LocaleKeys.viewAllServices.tr().toText12(isUnderLine: true).toShimmer(), + ], + ); + } +} + +class ServicesMenuShimmer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: const Color(0xff000000).withOpacity(.05), + blurRadius: 26, + offset: const Offset(0, -3), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset("assets/images/monthly_attendance.svg").toShimmer(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Attendan".toText11(isBold: false).toShimmer(), + 5.height, + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: LocaleKeys.attendance.tr().toText11(isBold: false).toShimmer(), + ), + 6.width, + SvgPicture.asset("assets/images/arrow_next.svg").paddingOnly(bottom: 4).toShimmer() + ], + ), + ], + ) + ], + ).paddingOnly(left: 10, right: 10, bottom: 10, top: 12), + ); + } +} + +class ChatHomeShimmer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Shimmer.fromColors( + baseColor: Colors.white, + highlightColor: Colors.grey.shade100, + child: ListView.builder( + itemBuilder: (_, __) => Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 48.0, + height: 48.0, + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(40))), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8.0), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + height: 8.0, + color: Colors.white, + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 2.0), + ), + Container( + width: double.infinity, + height: 8.0, + color: Colors.white, + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 2.0), + ), + Container( + width: 40.0, + height: 8.0, + color: Colors.white, + ), + ], + ), + ) + ], + ), + ), + itemCount: 6, + ), + ), + ), + ], + )); + } +} diff --git a/lib/widgets/swipe/nfc_reader_sheet.dart b/lib/widgets/swipe/nfc_reader_sheet.dart new file mode 100644 index 0000000..3dc357d --- /dev/null +++ b/lib/widgets/swipe/nfc_reader_sheet.dart @@ -0,0 +1,194 @@ +// import 'dart:async'; +// +// import 'package:flutter/material.dart'; +// +// +// void showNfcReader(BuildContext context, {Function onNcfScan}) { +// showModalBottomSheet( +// context: context, +// enableDrag: false, +// isDismissible: false, +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)), +// ), +// backgroundColor: Colors.white, +// builder: (context) { +// return NfcLayout( +// onNcfScan: onNcfScan, +// ); +// }); +// } +// +// class NfcLayout extends StatefulWidget { +// Function onNcfScan; +// +// NfcLayout({this.onNcfScan}); +// +// @override +// _NfcLayoutState createState() => _NfcLayoutState(); +// } +// +// class _NfcLayoutState extends State { +// StreamSubscription _stream; +// bool _reading = false; +// Widget mainWidget; +// String nfcId; +// +// @override +// void initState() { +// super.initState(); +// +// setState(() { +// // _reading = true; +// // Start reading using NFC.readNDEF() +// _stream = NFC.readNDEF(once: false, throwOnUserCancel: false, readerMode: NFCDispatchReaderMode()).listen((NDEFMessage message) { +// setState(() { +// _reading = true; +// mainWidget = doneNfc(); +// }); +// Future.delayed(const Duration(milliseconds: 500), () { +// _stream?.cancel(); +// widget.onNcfScan(nfcId); +// Navigator.pop(context); +// }); +// print("read NDEF id: ${message.id}"); +// print("NFC Record " + message.payload); +// print("NFC Record Lenght " + message.records.length.toString()); +// print("NFC Record " + message.records.first.id); +// print("NFC Record " + message.records.first.payload); +// print("NFC Record " + message.records.first.data); +// print("NFC Record " + message.records.first.type); +// // widget.onNcfScan(message.id); +// nfcId = message.id; +// }, onError: (e) { +// // Check error handling guide below +// }); +// }); +// } +// +// @override +// Widget build(BuildContext context) { +// (mainWidget == null && !_reading) ? mainWidget = scanNfc() : mainWidget = doneNfc(); +// return AnimatedSwitcher(duration: Duration(milliseconds: 500), child: mainWidget); +// } +// +// Widget scanNfc() { +// return Container( +// key: ValueKey(1), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// SizedBox( +// height: 30, +// ), +// Text( +// "Ready To Scan", +// style: TextStyle( +// fontWeight: FontWeight.bold, +// fontSize: 24, +// ), +// ), +// SizedBox( +// height: 30, +// ), +// Image.asset( +// "assets/images/nfc/ic_nfc.png", +// height: MediaQuery.of(context).size.width / 3, +// ), +// SizedBox( +// height: 30, +// ), +// Text( +// "Approach an NFC Tag", +// style: TextStyle( +// fontSize: 18, +// ), +// ), +// SizedBox( +// height: 30, +// ), +// ButtonTheme( +// minWidth: MediaQuery.of(context).size.width / 1.2, +// height: 45.0, +// buttonColor: Colors.grey[300], +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.circular(6), +// ), +// child: RaisedButton( +// onPressed: () { +// _stream?.cancel(); +// Navigator.pop(context); +// }, +// elevation: 0, +// child: Text("CANCEL"), +// ), +// ), +// SizedBox( +// height: 30, +// ), +// ], +// ), +// ); +// } +// +// Widget doneNfc() { +// return Container( +// key: ValueKey(2), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// SizedBox( +// height: 30, +// ), +// Text( +// "Successfully Scanned", +// style: TextStyle( +// fontWeight: FontWeight.bold, +// fontSize: 24, +// ), +// ), +// SizedBox( +// height: 30, +// ), +// Image.asset( +// "assets/images/nfc/ic_done.png", +// height: MediaQuery.of(context).size.width / 3, +// ), +// SizedBox( +// height: 30, +// ), +// Text( +// "Approach an NFC Tag", +// style: TextStyle( +// fontSize: 18, +// ), +// ), +// SizedBox( +// height: 30, +// ), +// ButtonTheme( +// minWidth: MediaQuery.of(context).size.width / 1.2, +// height: 45.0, +// buttonColor: Colors.grey[300], +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.circular(6), +// ), +// child: RaisedButton( +// // onPressed: () { +// // _stream?.cancel(); +// // widget.onNcfScan(nfcId); +// // Navigator.pop(context); +// // }, +// onPressed: null, +// elevation: 0, +// child: Text("DONE"), +// ), +// ), +// SizedBox( +// height: 30, +// ), +// ], +// ), +// ); +// } +// }